ExoPlayer架构详解与源码分析(8)——Loader

系列文章目录

ExoPlayer架构详解与源码分析(1)——前言
ExoPlayer架构详解与源码分析(2)——Player
ExoPlayer架构详解与源码分析(3)——Timeline
ExoPlayer架构详解与源码分析(4)——整体架构
ExoPlayer架构详解与源码分析(5)——MediaSource
ExoPlayer架构详解与源码分析(6)——MediaPeriod
ExoPlayer架构详解与源码分析(7)——SampleQueue
ExoPlayer架构详解与源码分析(8)——Loader


文章目录

  • 系列文章目录
  • 前言
  • ProgressiveMediaPeriod
  • Loader
  • ExtractingLoadable
  • BundledExtractorsAdapter
  • Extractor
  • 总结


前言

ProgressiveMediaPeriod的左半部分SampleQueue已经在上篇讲完,相对今天说的这部分还算简单,ProgressiveMediaPeriod右半部分主要为Loader,而Loader中及包含数据的获取也包含数据的解析,本篇主要分析Loader的整体机构和数据解析部分结构。

ProgressiveMediaPeriod

还是先预习下上篇的整体结构,本篇主要分析右半半部分的Loader:
在这里插入图片描述
图中Loader数据的加载主要靠DataSource,而解析部分主要为Executor

Loader

Loader本质上就是就是一个线程池,初始化时就创建了一个ExecutorService,启动时实例化出一个LoadTask放入线程池中执行。

  private final ExecutorService downloadExecutorService;
  public Loader(String threadNameSuffix) {
    this.downloadExecutorService =
        Util.newSingleThreadExecutor(THREAD_NAME_PREFIX + threadNameSuffix);
  }

  public <T extends Loadable> long startLoading(
      T loadable, Callback<T> callback, int defaultMinRetryCount) {
    Looper looper = Assertions.checkStateNotNull(Looper.myLooper());//获取当前启动线程的looper
    fatalError = null;
    long startTimeMs = SystemClock.elapsedRealtime();
    new LoadTask<>(looper, loadable, callback, defaultMinRetryCount, startTimeMs).start(0);
    return startTimeMs;
  }

LoadTask初始化时会传入当前线程的looper和callback,通过looper将后台线程的信息传递到启动线程的callback中执行,所以startLoading的线程必须要包含一个looper,callback也将在启动现场上调用。通常情况下启动线程就是内部播放线程,具体参照之前将的线程模型。

//@LoadTask.java
@Override
    public void run() {
      try {
        boolean shouldLoad;
        synchronized (this) {
          shouldLoad = !canceled;
          executorThread = Thread.currentThread();
        }
        if (shouldLoad) {
          TraceUtil.beginSection("load:" + loadable.getClass().getSimpleName());
          try {
            loadable.load();//执行loadable
          } finally {
            TraceUtil.endSection();
          }
        }
        synchronized (this) {
          executorThread = null;
          // Clear the interrupted flag if set, to avoid it leaking into a subsequent task.
          Thread.interrupted();
        }
        if (!released) {
          sendEmptyMessage(MSG_FINISH);//将执行结果通过handler发给启动线程looper
        }
     ...
    }


    @Override
    public void handleMessage(Message msg) {
      if (released) {
        return;
      }
      if (msg.what == MSG_START) {
        execute();
        return;
      }
      if (msg.what == MSG_FATAL_ERROR) {
        throw (Error) msg.obj;
      }
      finish();
      long nowMs = SystemClock.elapsedRealtime();
      long durationMs = nowMs - startTimeMs;
      Loader.Callback<T> callback = Assertions.checkNotNull(this.callback);
      if (canceled) {
        callback.onLoadCanceled(loadable, nowMs, durationMs, false);
        return;
      }
      switch (msg.what) {
        case MSG_FINISH:
          try {
            callback.onLoadCompleted(loadable, nowMs, durationMs);//执行完毕,在启动线程上调用callback
       ...
      }
    }

可以看到最终是调用了loadable.load方法
这个loadable定义在ProgressiveMediaPeriod的ExtractingLoadable中

ExtractingLoadable

ExtractingLoadable主要包含2个东西DataSource和ProgressiveMediaExtractor,DataSource负责从媒体数据源获取数据,而ProgressiveMediaExtractor则负责将获取的数据解析出Format和Metadata等sample到SampleQueue中,由此完成数据的加载
看下最核心的load方法。

    @Override
    public void load() throws IOException {
      int result = Extractor.RESULT_CONTINUE;
      while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {//循环多次读取和sample数据
        try {
          long position = positionHolder.position;//获取当前的读取位置
          dataSpec = buildDataSpec(position);//构建dataSource的dataSpec
          long length = dataSource.open(dataSpec);//打开读取的流,返回实际数据的长度
          if (length != C.LENGTH_UNSET) {
            length += position;//获取加载后的长度
            onLengthKnown();
          }
          ...
          progressiveMediaExtractor.init(//初始化ProgressiveMediaExtractor
              extractorDataSource,//传入dataSource,此时的dataSource已经open可以直接通过调用dataSource的read获取数据
              uri,
              dataSource.getResponseHeaders(),
              position,//当前读取位置
              length,//流加载后的总长度
              extractorOutput);//传入output,最终会关联输出到SampleQueue中

          ...
          while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
            try {
            //当未open时阻塞当前执行,主要作用是当上层正在决定是否要继续加载时阻塞住下一次加载,
            //当上层决定完可以继续加载时loadCondition.open()
            //这里的上层决定一般由LoadControl做出,后面会讲到
              loadCondition.block();
            } catch (InterruptedException e) {
            //如果不继续加载线程会等待,直到调用Loader的cancel方法会executorThread.interrupt();结束当前线程
              throw new InterruptedIOException();
            }
            result = progressiveMediaExtractor.read(positionHolder);//读取并解析数据到,传入positionHolder用于更新下一次读取位置
            long currentInputPosition = progressiveMediaExtractor.getCurrentInputPosition();//获取当前已经读取到的位置
            //如果当前进度达到必要去继续加载检查的阈值时调用上层加载检查判断是否下次加载,这个阈值有个默认值为1024*1024
            if (currentInputPosition > position + continueLoadingCheckIntervalBytes) {
              position = currentInputPosition;
              loadCondition.close();//阻塞下次加载
              handler.post(onContinueLoadingRequestedRunnable);//通过回调询问上层是否需要继续加载
            }
          }
        } finally {
          if (result == Extractor.RESULT_SEEK) {//如果解析器返回RESULT_SEEK
            result = Extractor.RESULT_CONTINUE;//就会再次open数据源,此时的Uri还是同一个,但是position可能已经在解析器中重新指定
          } else if (progressiveMediaExtractor.getCurrentInputPosition() != C.INDEX_UNSET) {
            positionHolder.position = progressiveMediaExtractor.getCurrentInputPosition();
          }
          DataSourceUtil.closeQuietly(dataSource);//关闭当前流
        }
      }
    }

这里有2个While循环,第一个While循环说明同一个数据源可能会从不同的位置被打开多次,当内循环中解析数据需要SEEK跳过一段数据时就会,返回RESULT_SEEK,这个时候跳出内循环,再次执行外循环。下面会讲到什么时候需要SEEK数据,另外我们注意到Loader从打开数据加载操作1M(默认值)数据时就会阻塞住内循环,停止向SampleQueue中写入数据,而是向上层讯问是否需要继续加载数据,如果上层决定继续加载更多数据,就会调用ProgressiveMediaPeriod continueLoading解开阻塞的锁继续加载。因为我们知道数据是加载到内存中的,如果无限制的加载肯定是不行的,需要有节制的加载和释放数据。
接下看下ProgressiveMediaExtractor的实现BundledExtractorsAdapter。

BundledExtractorsAdapter

这个类相当于包裹了一个Extractor,最终读取工作其实是转发给Extractor,初始化时确认当前流对应的Extractor

  @Override
  public void init(
      DataReader dataReader,
      Uri uri,
      Map<String, List<String>> responseHeaders,
      long position,
      long length,
      ExtractorOutput output)
      throws IOException {
      //包装输入流
    ExtractorInput extractorInput = new DefaultExtractorInput(dataReader, position, length);
    this.extractorInput = extractorInput;
    if (extractor != null) {
      return;
    }
    //extractorsFactory通过网络请求的返回的Content-Type和文件的后缀名获取按优先级创建一个Extractor列表
    Extractor[] extractors = extractorsFactory.createExtractors(uri, responseHeaders);
    if (extractors.length == 1) {
      this.extractor = extractors[0];
    } else {
      for (Extractor extractor : extractors) {
        try {
          //通过调用extractor方法获取流的头部信息最终确定当前流是否可以用这个extractor来解析,如TS就是通过判断0x47同步位来确定的
          if (extractor.sniff(extractorInput)) {
            this.extractor = extractor;
            break;
          }
        } catch (EOFException e) {
          // Do nothing.
        } finally {
          Assertions.checkState(this.extractor != null || extractorInput.getPosition() == position);
          extractorInput.resetPeekPosition();
        }
      }
      if (extractor == null) {//没有支持解析的extractor报错
        throw new UnrecognizedInputFormatException(
            "None of the available extractors ("
                + Util.getCommaDelimitedSimpleClassNames(extractors)
                + ") could read the stream.",
            Assertions.checkNotNull(uri));
      }
    }
    extractor.init(output);//初始化extractor
  }

  @Override
  public int read(PositionHolder positionHolder) throws IOException {
    return Assertions.checkNotNull(extractor)
        .read(Assertions.checkNotNull(extractorInput), positionHolder);//将数据的读取解析转发给extractor
  }

Extractor

主要将媒体数据从容器格式中解析出来,支持很多容器格式,每种都做了实现,有很多下图只列了一部分
在这里插入图片描述
FileTypes类里定义了这些格式,inferFileTypeFromUri可以看出他们与文件后缀之间的对应关系

  public static @FileTypes.Type int inferFileTypeFromUri(Uri uri) {
    @Nullable String filename = uri.getLastPathSegment();
    if (filename == null) {
      return FileTypes.UNKNOWN;
    } else if (filename.endsWith(EXTENSION_AC3) || filename.endsWith(EXTENSION_EC3)) {
      return FileTypes.AC3;
    } else if (filename.endsWith(EXTENSION_AC4)) {
      return FileTypes.AC4;
    } else if (filename.endsWith(EXTENSION_ADTS) || filename.endsWith(EXTENSION_AAC)) {
      return FileTypes.ADTS;
    } else if (filename.endsWith(EXTENSION_AMR)) {
      return FileTypes.AMR;
    } else if (filename.endsWith(EXTENSION_FLAC)) {
      return FileTypes.FLAC;
    } else if (filename.endsWith(EXTENSION_FLV)) {
      return FileTypes.FLV;
    } else if (filename.endsWith(EXTENSION_MID)
        || filename.endsWith(EXTENSION_MIDI)
        || filename.endsWith(EXTENSION_SMF)) {
      return FileTypes.MIDI;
    } else if (filename.startsWith(
            EXTENSION_PREFIX_MK,
            /* toffset= */ filename.length() - (EXTENSION_PREFIX_MK.length() + 1))
        || filename.endsWith(EXTENSION_WEBM)) {
      return FileTypes.MATROSKA;
    } else if (filename.endsWith(EXTENSION_MP3)) {
      return FileTypes.MP3;
    } else if (filename.endsWith(EXTENSION_MP4)
        || filename.startsWith(
            EXTENSION_PREFIX_M4,
            /* toffset= */ filename.length() - (EXTENSION_PREFIX_M4.length() + 1))
        || filename.startsWith(
            EXTENSION_PREFIX_MP4,
            /* toffset= */ filename.length() - (EXTENSION_PREFIX_MP4.length() + 1))
        || filename.startsWith(
            EXTENSION_PREFIX_CMF,
            /* toffset= */ filename.length() - (EXTENSION_PREFIX_CMF.length() + 1))) {
      return FileTypes.MP4;
    } else if (filename.startsWith(
            EXTENSION_PREFIX_OG,
            /* toffset= */ filename.length() - (EXTENSION_PREFIX_OG.length() + 1))
        || filename.endsWith(EXTENSION_OPUS)) {
      return FileTypes.OGG;
    } else if (filename.endsWith(EXTENSION_PS)
        || filename.endsWith(EXTENSION_MPEG)
        || filename.endsWith(EXTENSION_MPG)
        || filename.endsWith(EXTENSION_M2P)) {
      return FileTypes.PS;
    } else if (filename.endsWith(EXTENSION_TS)
        || filename.startsWith(
            EXTENSION_PREFIX_TS,
            /* toffset= */ filename.length() - (EXTENSION_PREFIX_TS.length() + 1))) {
      return FileTypes.TS;
    } else if (filename.endsWith(EXTENSION_WAV) || filename.endsWith(EXTENSION_WAVE)) {
      return FileTypes.WAV;
    } else if (filename.endsWith(EXTENSION_VTT) || filename.endsWith(EXTENSION_WEBVTT)) {
      return FileTypes.WEBVTT;
    } else if (filename.endsWith(EXTENSION_JPG) || filename.endsWith(EXTENSION_JPEG)) {
      return FileTypes.JPEG;
    } else if (filename.endsWith(EXTENSION_AVI)) {
      return FileTypes.AVI;
    } else {
      return FileTypes.UNKNOWN;
    }
  }

从这些代码中可以看出EXO所支持的媒体容器格式。
下篇我们以TS容器格式为例,讲解下TsExtractor,了解媒体数据如何从容器格式中解析出来,最终要交给Readerer渲染的。


总结

Loader主要作用就是将加载逻辑放入线程池中管理,然后通过ExtractingLoadable控制数据的加载,而数据主要由DataSource来获取,解析部分主要为Executor,Executor通过持有已经open的DataSource,不断从DataSource中read出数据用来解析媒体,整个过程都只有一个线程,即使到了数据加载部分也是在同一个线程中同步加载的,在这段同步操作中还将加载的控制权交给了上层组件。下篇将会把TsExtractor作为一个典型的解析器,来分析解析器的具体作用和执行过程。


版权声明 ©
本文为CSDN作者山雨楼原创文章
转载请注明出处
原创不易,觉得有用的话,收藏转发点赞支持

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/150995.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

DNS服务器典型配置

文章目录 安装主程序bind和安全插件bind-root修改主配置文件/etc/named.conf正向解析 安装主程序bind和安全插件bind-root yum install bind-chroot修改主配置文件/etc/named.conf vim /etc/named.conf将listen-on和allow-query的ip或域名换成any 表示为服务器所有的IP地址启…

多svn仓库一键更新脚本分享

之前分享过多git仓库一键更新脚本&#xff0c;本期就分享下svn仓库的一键更新脚本 1、首先需要设置svn为可执行命令行 打开SVN安装程序&#xff0c;选择modify&#xff0c;然后点击 command client tools&#xff0c;安装命令行工具 2、update脚本 echo 开始更新SVN目录&…

【Android 标题文字居中 快速实现】

背景&#xff1a; Android App系统默认setTitle左起展示(图左)&#xff0c;IOS App默认居中展示(图右)。现在美工设计 在Android中标题同样居中显示。 解决&#xff1a; 方案一&#xff1a;(传统方式,比较繁琐) 设置ToolBar样式&#xff0c;内嵌TextView来展示&#xff0c;具…

网络安全准入技术之MAC VLAN

网络准入控制作为主要保障企业网络基础设施的安全的措施&#xff0c;特别是对于中大型企业来说&#xff0c;终端类型多样数量激增、终端管理任务重难度大、成本高。 在这样的一个大背景下&#xff0c;拥有更灵活的动态识别、认证、访问控制等成为了企业网络安全的最核心诉求之…

保姆级教程——pytest【入门篇】

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

【libGDX】初识libGDX

1 前言 libGDX 是一个开源且跨平台的 Java 游戏开发框架&#xff0c;于 2010 年 3 月 11 日推出 0.1 版本&#xff0c;它通过 OpenGL ES 2.0/3.0 渲染图像&#xff0c;支持 Windows、Linux、macOS、Android、iOS、Web 等平台&#xff0c;提供了统一的 API&#xff0c;用户只需要…

数据结构—LinkedList与链表

目录 一、链表 1. 链表的概念及结构 1. 单向或者双向 2. 带头或者不带头 3. 循环或者非循环 二.LinkedList的使用 1.LinkedList概念及结构 2. LinkedList的构造 3. LinkedList的方法 三. ArrayList和LinkedList的区别 一、链表 1. 链表的概念及结构 链表是一种 物理…

开启创造力之门:掌握Vue中Slot插槽的使用技巧与灵感

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 ⭐ 专栏简介 &#x1f4d8; 文章引言 一、s…

【Ubuntu】设置永不息屏与安装 dconf-editor

方式一、GUI界面进行设置 No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 20.04.6 LTS Release: 20.04 Codename: focal打开 Ubuntu 桌面环境的设置菜单。你可以通过点击屏幕右上角的系统菜单&#xff0c;然后选择设置。在设置菜单中&#xff0c;…

弱类型和强类型自定义UDAF函数

目录 使用自带的avg函数弱类型自定义UDAF函数(AVG)强类型自定义UDAF函数(AVG) 弱类型&#xff1a;3.x过期 2.x有 强类型&#xff1a;3.x 2.x没有 使用自带的avg函数 import org.apache.spark.rdd.RDD import org.apache.spark.sql.{DataFrame, SparkSession}object UserDefine…

GD32_ADC采样+DMA多通道扫描传输

GD32_ADC采样DMA多通道扫描传输 文章目录 GD32_ADC采样DMA多通道扫描传输前言一、资源介绍二、原理1.ADC连续扫描模式2.DMA传输3.ADC内部通道 三、配置1.ADC配置2.DMA配置3.注意事项 四、计算1.分压转换2.数据转换 前言 <1>、硬件平台&#xff1a;可运行软件程序的GD32单…

【计算思维】少儿编程蓝桥杯青少组计算思维题考试真题及解析B

STEMA考试-计算思维-U8级(样题) 1.浩浩的左⼿边是&#xff08; &#xff09;。 A.兰兰 B.⻉⻉ C.⻘⻘ D.浩浩 2.2时30分&#xff0c;钟⾯上时针和分针形成的⻆是什么⻆&#xff1f;&#xff08; &#xff09; A.钝⻆ B.锐⻆ C.直⻆ D.平⻆ 3.下⾯是⼀年级同学最喜欢的《⻄游记》…

人工智能基础_机器学习037_多项式回归升维实战4_使用随机梯度下降模型_对天猫双十一销量数据进行预测_拟合---人工智能工作笔记0077

上一节我们使用线性回归模型最终拟合了双十一天猫销量数据,升维后的数据. 我们使用SGDRegressor的时候,随机梯度下降的时候,发现有问题, 对吧,怎么都不能拟合我们看看怎么回事现在 可以看到上面是之前的代码 上面是对数据的准备 这里我们还是修改,使用 poly=PolynomialFeatur…

nodejs+vue电影在线预定与管理系统的设计与实现-微信小程序-安卓-python-PHP-计算机毕业设计

通过软件的需求分析已经获得了系统的基本功能需求&#xff0c;根据需求&#xff0c;将电影在线预定与管理系统功能模块主要分为管理员模块。 我国各行各业的发展在信息化浪潮的推动下也在不断进步&#xff0c;尤其是电影产业&#xff0c;在人们生活水平提高的同时&#xff0c;从…

旅拍摄影技巧澳大利亚、韩国旅行攻略

欢迎关注「苏南下」 在这里分享我的旅行和影像创作心得 刚刚在腾讯内部做了一场摄影分享课&#xff1a; 《旅拍摄影技巧&澳大利亚、韩国旅行攻略》 分享了早前去两个国家的一些旅行见闻和摄影心得。我发现&#xff1a;把自己学会的东西整理出来&#xff0c;再告诉给别人这件…

探索人工智能领域——每日30个名词详解【day3】

目录 前言 正文 总结 &#x1f308;嗨&#xff01;我是Filotimo__&#x1f308;。很高兴与大家相识&#xff0c;希望我的博客能对你有所帮助。 &#x1f4a1;本文由Filotimo__✍️原创&#xff0c;首发于CSDN&#x1f4da;。 &#x1f4e3;如需转载&#xff0c;请事先与我联系以…

c语言从入门到实战——数组指针与函数指针

数组指针与函数指针 前言1. 字符指针变量2. 数组指针变量2.1 数组指针变量是什么&#xff1f;2.2 数组指针变量怎么初始化? 3. 二维数组传参的本质4. 函数指针变量4.1 函数指针变量的创建4.2 函数指针变量的使用4.3 两段有趣的代码4.3.1 typedef关键字 5. 函数指针数组6. 转移…

【华为HCIP | 华为数通工程师】ISIS 高频题(1)

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大三在校生&#xff0c;喜欢AI编程&#x1f38b; &#x1f43b;‍❄️个人主页&#x1f947;&#xff1a;落798. &#x1f43c;个人WeChat&#xff1a;hmmwx53 &#x1f54a;️系列专栏&#xff1a;&#x1f5bc;️…

机器人导航+OPENCV透视变换示例代码

透视变换又称四点变换&#xff0c;所以不能用于5边形这样的图形变换&#xff0c;不是真正的透视变换&#xff0c;但是这个方法可以把机器人看到的图像转换为俯视图&#xff0c;这样就可以建立地图&#xff0c;要不然怎么建立地图呢。 void CrelaxMyFriendDlg::OnBnClickedOk()…

【JavaSE语法】类和对象(二)

六、 封装 6.1 封装的概念 面向对象程序三大特性&#xff1a;封装、继承、多态。而类和对象阶段&#xff0c;主要研究的就是封装特性。 封装&#xff1a;将数据和操作数据的方法进行有机结合&#xff0c;隐藏对象的属性和实现细节&#xff0c;仅对外公开接口来和对象进行交互…