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());//获取当前启动线程的looperfatalError = 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
@Overridepublic 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}...}@Overridepublic 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方法。

    @Overridepublic 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的dataSpeclong length = dataSource.open(dataSpec);//打开读取的流,返回实际数据的长度if (length != C.LENGTH_UNSET) {length += position;//获取加载后的长度onLengthKnown();}...progressiveMediaExtractor.init(//初始化ProgressiveMediaExtractorextractorDataSource,//传入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*1024if (currentInputPosition > position + continueLoadingCheckIntervalBytes) {position = currentInputPosition;loadCondition.close();//阻塞下次加载handler.post(onContinueLoadingRequestedRunnable);//通过回调询问上层是否需要继续加载}}} finally {if (result == Extractor.RESULT_SEEK) {//如果解析器返回RESULT_SEEKresult = 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

  @Overridepublic 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}@Overridepublic 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作者山雨楼原创文章
转载请注明出处
原创不易,觉得有用的话,收藏转发点赞支持

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

map和set的简易封装(纯代码)

RBTree.h #pragma once#include<iostream> #include<vector> using namespace std;enum colar { red,black };template<class T>//有效参数就一个 struct RBTreeNode {RBTreeNode(const T& data):_left(nullptr), _right(nullptr), _parent(nullptr)…

黑马点评回顾 redis实现共享session

文章目录 传统session缺点整体访问流程代码实现生成验证码登录 问题具体思路 传统session缺点 传统单体项目一般是把session存入tomcat&#xff0c;但是每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat&#xff0c;并且把自己的信息存放到第一台服务器…

免费的快速手机文件解压APP,快冲

各位小伙伴们大家好&#xff0c;今天我要介绍一款手机上必备的神奇工具&#xff01;你有没有经常遇到需要解压文件情况呢&#xff1f;还在为不知道用哪个软件而烦恼吗&#xff1f;别担心&#xff0c;我给你带来了解决方案 &#xff0c;就是这一款免费的解压精灵。 解压精灵是一…

【Nginx】使用nginx进行反向代理与负载均衡

使用场景 反向代理&#xff1a;一个网站由许多服务器承载的&#xff0c;网站只暴露一个域名&#xff0c;那么这个域名指向一个代理服务器ip&#xff0c;然后由这台代理服务器转发请求到网站负载的多台服务器中的一台处理。这就需要用到Nginx的反向代理实现了 负载均衡&#xf…

怎么去掉邮件内容中的回车符

上图是Outlook 截图&#xff0c;可见1指向的总有回车符&#xff1b; 故障原因&#xff1a; 不小心误按了箭头4这个选项&#xff1b; 解决方法&#xff1a; 点击2箭头确保tab展开&#xff1b; 点击3以找到箭头4. 取消勾选或者多次点击&#xff0c;即可解决。

单区域OSPF配置

配置命令步骤&#xff1a; 1.使用router ospf 进程ID编号 启用OSPF路由 2.使用network 直连网络地址 反掩码 area 0 将其归于区域0 注意&#xff1a;1.进程ID编号可任意&#xff08;1-65535&#xff09;2.反掩码用4个255相减得到 如下图&#xff0c;根据给出要求配置OSPF单区…

HT8313 D/AB切换 音频功率放大器

HT8313具有AB类和D类的自Y切换功能&#xff0c;在受到D类功放EMI干扰困扰时&#xff0c;可随时切换至AB类音频功放模式&#xff08;此时电荷泵升压功能关闭&#xff09;。 HT8313内部固定28dB增益&#xff0c;内置的关断功能使待机电流Z小化&#xff0c;还集成了输出端过流保护…

JavaEE进阶学习:Spring核心和设计思想

Spring 是什么 我们通常所说的 Spring 指的是 Spring Framework&#xff08;Spring 框架&#xff09;&#xff0c;它是⼀个开源框架&#xff0c;有着活跃而庞大的社区&#xff0c;这就是它之所以能长久不衰的原因。Spring 支持广泛的应用场景&#xff0c;它可以让 Java 企业级…

PP-ChatOCRv2、PP-TSv2、大模型半监督学习工具...PaddleX新特性等你来pick!

小A是一名刚刚毕业的算法工程师&#xff0c;有一天&#xff0c;他被老板安排了一个活&#xff0c;要对一批合同扫描件进行自动化信息抽取&#xff0c;输出结构化的分析报表。OCR问题不大&#xff0c;但是怎么进行批量的结构化信息抽取呢&#xff1f;小A陷入了苦苦思索… 小B是…

Java获取Jar、War包路径,并生成可编辑修改的本地配置文件

前言 本地的可修改配置文件的编写理应是一个很常用的功能&#xff0c;但由于数据库的存在&#xff0c;它鲜少被提及&#xff0c;大多数我们直接存储到数据库中了。 以至于现今&#xff0c;除了没接触数据库的新手时常使用它以外&#xff0c;它没有太多的出场机会。 也因此&am…

Hive数据表操作--学习笔记

1&#xff0c;Hive数据表操作 1&#xff0c;建表语句和内外部表 ①创建内部表 create [external] table [if not exists] 表名( 字段名 字段类型 [comment 注释], 字段名 字段类型 [comment 注释], ... ) [row format delimited fields terminated by 指定分隔符];&#xff0…

后端接口性能优化分析-问题发现问题定义

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring源码、JUC源码&#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44d;三连支持&…

使用requests库设置no_proxy选项的方法

问题背景 在使用requests库进行HTTP请求时&#xff0c;如果需要使用爬虫IP服务器&#xff0c;可以通过设置proxies参数来实现。proxies参数是一个字典&#xff0c;其中包含了爬虫IP服务器的地址和端口号。然而&#xff0c;当前的requests库并不支持通过proxies参数来设置no_pr…

【GitLab】-HTTP 500 curl 22 The requested URL returned error: 500~SSH解决

写在前面 本文主要介绍通过SSH的方式拉取GitLab代码。 目录 写在前面一、场景描述二、具体步骤1.环境说明2.生成秘钥3.GitLab添加秘钥4.验证SSH方式4.更改原有HTTP方式为SSH 三、参考资料写在后面系列文章 一、场景描述 之前笔者是通过 HTTP Personal access token 的方式拉取…

持续集成指南:GitHubAction 自动构建+部署AspNetCore项目

前言 之前研究了使用 GitHub Action 自动构建和发布 nuget 包&#xff1a;开发现代化的.NetCore控制台程序&#xff1a;(4)使用GithubAction自动构建以及发布nuget包 现在更进一步&#xff0c;使用 GitHub Action 在其提供的 runner 里构建 docker 镜像&#xff0c;之后提交到阿…

6块钱改变世界,网易和拼多多踏入同一条河流?

年底将至&#xff0c;各种颁奖盛典星光熠熠。如果要给今年深蹲反弹中的互联网大厂颁奖&#xff0c;2023表现最突出的可能是师出同门的兄弟网易和拼多多。 从市场表现来看&#xff0c;两家企业录得今年互联网中概股最高涨幅&#xff0c;被称为“中概股之光”&#xff1a;2023年…

【Spring Cloud】黑马头条 用户服务创建、登录功能实现

点击去看上一篇 一、创建用户 model 1.创建用户数据库库 leadnews_user 核心表 ap_user 建库建表语句 这里一定要使用 navicat&#xff0c;执行SQL 文件&#xff0c;以防止 cmd 中的编码问题 先将 SQL 语句&#xff0c;保存在电脑中&#xff0c;再使用 navicat 打开 CREATE…

华为eNSP综合实验考试

VLAN信息表 设备名称 端口 链路类型 VLAN 参数 HZ-HZCampus-Agg01-S5731 GE0/0/1 Trunk PVID:1 Allow-pass&#xff1a;10 20 Eth-trunk1&#xff08;GE0/0/2,0/0/3,0/0/23&#xff09; Trunk PVID:1 Allow-pass&#xff1a;10 20 GE0/0/24 Access PVID&#xf…

(免费)双相情感障碍筛查MDQ 在线测试双向情感障碍

MDQ用于筛查双相障碍&#xff0c;主要包含13个关于双相障碍症状的是非问题&#xff0c;当前测试采用的量表为2010年杨海晨博士翻译版。该量表为目前世界范围内最常用的双相障碍筛查量表&#xff0c;目前在精神科门诊最为常用的量表之一。 双向情感障碍筛查量表&#xff0c;也叫…

【linux】查看CPU的使用率

命令1&#xff1a;top top 总体系统信息 uptime&#xff1a;系统的运行时间和平均负载。tasks&#xff1a;当前运行的进程和线程数目。CPU&#xff1a;总体 CPU 使用率和各个核心的使用情况。内存&#xff08;Memory&#xff09;&#xff1a;总体内存使用情况、可用内存和缓存…