当前位置: 首页 > news >正文

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
ExoPlayer架构详解与源码分析(9)——TsExtractor


文章目录

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

相关文章:

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

系列文章目录 ExoPlayer架构详解与源码分析&#xff08;1&#xff09;——前言 ExoPlayer架构详解与源码分析&#xff08;2&#xff09;——Player ExoPlayer架构详解与源码分析&#xff08;3&#xff09;——Timeline ExoPlayer架构详解与源码分析&#xff08;4&#xff09;—…...

ExoPlayer架构详解与源码分析(9)——TsExtractor

系列文章目录 ExoPlayer架构详解与源码分析&#xff08;1&#xff09;——前言 ExoPlayer架构详解与源码分析&#xff08;2&#xff09;——Player ExoPlayer架构详解与源码分析&#xff08;3&#xff09;——Timeline ExoPlayer架构详解与源码分析&#xff08;4&#xff09;—…...

【Python 千题 —— 基础篇】输出列表方差

题目描述 题目描述 输出列表的方差。题中有一个包含数字的列表 [10, 39, 13, 48, 32, 10, 9]&#xff0c;使用 for 循环获得这个列表中所有项的方差。 输入描述 无输入。 输出描述 输出列表的方差。 示例 示例 ① 输出&#xff1a; 列表的方差是&#xff1a;228.0代码…...

【Spring总结】基于配置的方式来写Spring

本篇文章是对这两天所学的内容做一个总结&#xff0c;涵盖我这两天写的所有笔记&#xff1a; 【Spring】 Spring中的IoC&#xff08;控制反转&#xff09;【Spring】Spring中的DI&#xff08;依赖注入&#xff09;Dependence Import【Spring】bean的基础配置【Spring】bean的实…...

Unity在Windows选项下没有Auto Streaming

Unity在Windows选项下没有Auto Streaming Unity Auto Streaming插件按网上说的不太好使最终解决方案 Unity Auto Streaming插件 我用的版本是个人版免费版&#xff0c;版本号是&#xff1a;2021.2.5f1c1&#xff0c;我的里边Windows下看不到Auto Streaming选项,就像下边这张图…...

下厨房网站月度最佳栏目菜谱数据获取及分析

目录 概要 源数据获取 写Python代码爬取数据 Scala介绍与数据处理 1.Sacla介绍...

【Java 进阶篇】深入理解 JQuery 事件绑定:标准方式

在前端开发中&#xff0c;处理用户与页面的交互是至关重要的一部分。JQuery作为一个广泛应用的JavaScript库&#xff0c;为我们提供了简便而强大的事件绑定机制&#xff0c;使得我们能够更加灵活地响应用户的行为。本篇博客将深入解析 JQuery 的标准事件绑定方式&#xff0c;为…...

某app c++层3处魔改md5详解

hello everybody,本期是安卓逆向so层魔改md5教学,干货满满,可以细细品味,重点介绍的是so层魔改md5的处理. 常见的魔改md5有: 1:明文加密前处理 2:改初始化魔数 3:改k表中的值 4:改循环左移的次数 本期遇到的是124.且循环左移的次数是动态的,需要前面的加密结果处理生成 目录…...

安装MongoDB

查看MongoDB版本可以执行如下命令 mongod --version 如果是Ubuntu&#xff0c;则直接安装 sudo apt-get install -y mongodb如果是其他&#xff0c;比如Amazon Linux2。 查看Linux系统发行版类型 grep ^NAME /etc/*release 如果是 Amazon Linux 2&#xff0c;则创建一个r…...

C++加持让python程序插上翅膀——利用pybind11进行c++和python联合编程示例

目录 0、前言1、安装 pybind11库c侧python侧 2、C引入bybind11vs增加相关依赖及设置cpp中添加头文件及导出模块cpp中添加numpy相关数据结构的接收和返回编译生成dll后改成导出模块同名文件的.pyd 3、python调用c4、C引入bybind11 0、前言 在当今的计算机视觉和机器学习领域&am…...

ubuntu20.04安装cv2

查看ubuntu的版本 cat /etc/lsb-release DISTRIB_IDUbuntu DISTRIB_RELEASE20.04 DISTRIB_CODENAMEfocal DISTRIB_DESCRIPTION"Ubuntu 20.04.3 LTS"更改镜像源 cp /etc/apt/sources.list /etc/apt/sources.list.bak cat > /etc/apt/sources.listdeb http://mirr…...

Android 13.0 recovery出厂时清理中字体大小的修改

1.前言 在13.0的系统rom定制化开发中,在recovery模块也是系统中比较重要的模块,比如恢复出厂设置,recovery ota升级, 清理缓存等等,在一些1080p的设备,但是density只是240这样的设备,会在恢复出厂设置的时候,显示的字体有点小, 产品要求需要将正在清理的字体调大点,这…...

spring+pom-注意多重依赖时的兼容问题[java.lang.NoSuchMethodError]

背景&#xff1a; 项目中同时引入了依赖A和依赖B&#xff0c;而这两个依赖都依赖于项目C&#xff0c;但它们指定的C版本不一致&#xff0c;导致运行时出现了错误。 报错如&#xff1a; java.lang.NoSuchMethodError 解决方案&#xff1a; 需要在项目pom文件中引入依赖C并指定需…...

Matalab插值详解和源码

转载&#xff1a;Matalab插值详解和源码 - 知乎 (zhihu.com) 插值法 插值法又称“内插法”&#xff0c;是利用函数f (x)在某区间中已知的若干点的函数值&#xff0c;作出适当的特定函数&#xff0c;在区间的其他点上用这特定函数的值作为函数f (x)的近似值&#xff0c;这种方…...

Flask 接口

目录 前言 代码实现 简单接口实现 执行其它程序接口 携带参数访问接口 前言 有时候会想着开个一个接口来访问试试&#xff0c;这里就给出一个基础接口代码示例 代码实现 导入Flask模块&#xff0c;没安装Flask 模块需要进行 安装&#xff1a;pip install flask 使用镜…...

Vue3 toRef函数和toRefs函数

当我们在setup 中的以读取对象属性单独交出去时&#xff0c;我们会发现这样会丢失响应式&#xff1a; setup() {let person reactive({name: "张三",age: 18,job: {type: "前端",salary:10}})return {name: person.name,age: person.age,type: person.jo…...

【论文阅读】(VAE-GAN)Autoencoding beyond pixels using a learned similarity metric

论文地址;[1512.09300] Autoencoding beyond pixels using a learned similarity metric (arxiv.org) / 一、Introduction 主要讲了深度学习中生成模型存在的问题&#xff0c;即常用的相似度度量方式&#xff08;使用元素误差度量&#xff09;对于学习良好的生成模型存在一定…...

verilog之wire vs reg区别

文章目录 一、wire vs reg二、实例一、wire vs reg wire线网: 仅支持组合逻辑建模必须由assign语句赋值不能在always块中驱动用于连接子模块的输出用于定义模块的输入端口reg寄存器: 可支持组合逻辑或时序逻辑建模必须在always块中赋值二、实例 wire [7:0] cnt; assign cnt …...

力扣面试经典150题详细解析

刷题的初心 众所周知&#xff0c;算法题对于面试大厂是必不可缺的一环&#xff0c;而且对于提高逻辑思维能力有着不小的提升。所以&#xff0c;对于程序员来讲&#xff0c;无论刚入行&#xff0c;还是从业多年&#xff0c;保持一个清醒的头脑&#xff0c;具备一个良好的设计思…...

【Java 进阶篇】唤醒好运:JQuery 抽奖案例详解

在现代社交网络和电商平台中&#xff0c;抽奖活动成为吸引用户、提升用户参与度的一种常见手段。通过精心设计的抽奖页面&#xff0c;不仅可以增加用户的互动体验&#xff0c;还能在一定程度上提高品牌的知名度。本篇博客将通过详细解析 JQuery 抽奖案例&#xff0c;带领你走进…...

Oracle查询表空间大小

1 查询数据库中所有的表空间以及表空间所占空间的大小 SELECTtablespace_name,sum( bytes ) / 1024 / 1024 FROMdba_data_files GROUP BYtablespace_name; 2 Oracle查询表空间大小及每个表所占空间的大小 SELECTtablespace_name,file_id,file_name,round( bytes / ( 1024 …...

无法与IP建立连接,未能下载VSCode服务器

如题&#xff0c;在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈&#xff0c;发现是VSCode版本自动更新惹的祸&#xff01;&#xff01;&#xff01; 在VSCode的帮助->关于这里发现前几天VSCode自动更新了&#xff0c;我的版本号变成了1.100.3 才导致了远程连接出…...

SCAU期末笔记 - 数据分析与数据挖掘题库解析

这门怎么题库答案不全啊日 来简单学一下子来 一、选择题&#xff08;可多选&#xff09; 将原始数据进行集成、变换、维度规约、数值规约是在以下哪个步骤的任务?(C) A. 频繁模式挖掘 B.分类和预测 C.数据预处理 D.数据流挖掘 A. 频繁模式挖掘&#xff1a;专注于发现数据中…...

376. Wiggle Subsequence

376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...

在Ubuntu24上采用Wine打开SourceInsight

1. 安装wine sudo apt install wine 2. 安装32位库支持,SourceInsight是32位程序 sudo dpkg --add-architecture i386 sudo apt update sudo apt install wine32:i386 3. 验证安装 wine --version 4. 安装必要的字体和库(解决显示问题) sudo apt install fonts-wqy…...

Java编程之桥接模式

定义 桥接模式&#xff08;Bridge Pattern&#xff09;属于结构型设计模式&#xff0c;它的核心意图是将抽象部分与实现部分分离&#xff0c;使它们可以独立地变化。这种模式通过组合关系来替代继承关系&#xff0c;从而降低了抽象和实现这两个可变维度之间的耦合度。 用例子…...

C#学习第29天:表达式树(Expression Trees)

目录 什么是表达式树&#xff1f; 核心概念 1.表达式树的构建 2. 表达式树与Lambda表达式 3.解析和访问表达式树 4.动态条件查询 表达式树的优势 1.动态构建查询 2.LINQ 提供程序支持&#xff1a; 3.性能优化 4.元数据处理 5.代码转换和重写 适用场景 代码复杂性…...

省略号和可变参数模板

本文主要介绍如何展开可变参数的参数包 1.C语言的va_list展开可变参数 #include <iostream> #include <cstdarg>void printNumbers(int count, ...) {// 声明va_list类型的变量va_list args;// 使用va_start将可变参数写入变量argsva_start(args, count);for (in…...

CSS3相关知识点

CSS3相关知识点 CSS3私有前缀私有前缀私有前缀存在的意义常见浏览器的私有前缀 CSS3基本语法CSS3 新增长度单位CSS3 新增颜色设置方式CSS3 新增选择器CSS3 新增盒模型相关属性box-sizing 怪异盒模型resize调整盒子大小box-shadow 盒子阴影opacity 不透明度 CSS3 新增背景属性ba…...

手动给中文分词和 直接用神经网络RNN做有什么区别

手动分词和基于神经网络&#xff08;如 RNN&#xff09;的自动分词在原理、实现方式和效果上有显著差异&#xff0c;以下是核心对比&#xff1a; 1. 实现原理对比 对比维度手动分词&#xff08;规则 / 词典驱动&#xff09;神经网络 RNN 分词&#xff08;数据驱动&#xff09…...