分块容器 ChunkHolder
参数
- 持有一个分块 Chunk
- 一个 boolean 代表该分块是否还有更多数据可以加载,代表流已到达末尾的标记。
- clear 清空容器中的分块数据
1 | package androidx.media3.exoplayer.source.chunk; |
核心加载策略源 ChunkSource
在 ExoPlayer 的分块加载系统中,ChunkSource 是逻辑复杂度最高、最核心的组件。它不负责实际的数据传输,而是充当“大脑”,基于网络状况、缓冲区余量和播放位置,动态决定“下一步该做什么”。
核心职能概述
ChunkSource 的主要职责可以概括为以下四个方面:
- 分块决策(Decision Making):通过
getNextChunk方法,决定下一个要加载的是初始化分块、媒体分块还是流结束信号。 - ABR 策略执行(Adaptive Bitrate Control):实时监测
queue(已缓冲队列)的长度。如果缓冲区过薄,切换至低码率保证流畅;如果缓冲区厚实,则请求高码率。 - 缓冲管理(Queue Management):评估已缓存的块是否过时。例如,当用户网络好转,
getPreferredQueueSize可能会建议删掉后端尚未播放的低画质块,替换为高清块。 - 跳转校准(Seek Calibration):处理
SeekParameters,确保用户点击进度条时,能够根据分块的关键帧分布,计算出最合理的加载起点。(这里联合 Dash 分块在播放器中的存储结构 TreeSet 去理解)。
重要数据结构与参数解析
在 ChunkSource 的方法调用中,以下数据结构决定了其决策的精确性:
List<? extends MediaChunk> queue(缓冲队列)
- 职能:这是
ChunkSource的“实时地图”。它包含了所有已下载完成、正在排队等待被渲染器消费的媒体分块。 - 关键作用:
- 连续性校验:通过查看
queue的最后一个分块序号,决定下一个请求的起始位置,防止数据断层。 - 缓冲区评估:通过
queue.size()计算当前“缓冲区厚度”,这是 ABR 算法切换画质的核心依据。
- 连续性校验:通过查看
ChunkHolder out(指令输出容器)
- 职能:如前所述,它是一个轻量级契约,负责将
getNextChunk的决策结果回传给ChunkSampleStream。 - 职能细分:携带具体的
Chunk实例(下载任务)或endOfStream标识(终止信号)。
LoadingInfo(加载上下文)
- 职能:包含当前加载请求发起时的全局信息,如当前的播放速度、预期的位置等。
- 作用:帮助
ChunkSource站在全局视角(而非单一分块视角)做出更符合用户当前观看行为的判断。
多轨道流管理器 ChunkSampleStream 和 EmbeddedSampleStream
流的类型
| 轨道类型常量 | 含义 |
|---|---|
TRACK_TYPE_UNKNOWN |
未知类型 |
TRACK_TYPE_DEFAULT |
默认类型 |
TRACK_TYPE_AUDIO |
音频轨道 |
TRACK_TYPE_VIDEO |
视频轨道 |
TRACK_TYPE_TEXT |
文本轨道 |
TRACK_TYPE_IMAGE |
图像轨道 |
TRACK_TYPE_METADATA |
元数据轨道 |
TRACK_TYPE_CAMERA_MOTION |
相机运动数据轨道 |
TRACK_TYPE_NONE |
无轨道类型 |
TRACK_TYPE_CUSTOM_BASE |
应用定义的自定义基值 |
关键参数
1 | /** 策略源,负责决策下一个该下载哪个分块 */ |
1 | /** 嵌入式(从属)轨道的类型数组 */ |
1 | /** 决策结果的临时持有者,用于与 chunkSource 交互获取下一任务 */ |
主轨道和嵌入轨道
在流媒体开发(尤其是 ExoPlayer 的语境)中,主轨道(Primary Track)和嵌入轨道(Embedded Track)的关系,本质上是“整体”与“部分”、“容器”与“附件”的关系。
通过下面的对比,你可以清晰地理解它们的角色:
主轨道 (Primary Track)
主轨道是媒体流的核心负载。在绝大多数分块流(如 DASH 或 HLS)中,主轨道通常就是视频轨。
- 地位:它是“家长”。所有的网络请求(HTTP Request)和数据下载都是由主轨道发起的。
- 职责:它不仅负责下载自己的视频数据,还顺带把同一个分块(Chunk)里的其他辅助数据一并下载下来。
- 物理形式:在传输层,它是一个完整的数据块(Chunk)。
嵌入轨道 (Embedded Track)
嵌入轨道是寄生在主轨道分块里的附加信息。常见的包括:
**闭路字幕 (Closed Captions)**:隐藏在视频采样数据中的 CEA-608/708 字幕。
**元数据 (Metadata)**:如相机运动参数、实时统计数据、或是某些特殊的定时 ID3 标签。
地位:它是“乘客”。它没有独立下载数据的能力,必须搭主轨道的“便车”。
物理形式:它在物理上并不存在独立的文件,而是通过解析主轨道的数据块,被“剥离”出来的逻辑流。
核心区别对比
| 特性 | 主轨道 (Primary Track) | 嵌入轨道 (Embedded Track) |
|---|---|---|
| 数据来源 | 直接发起网络下载 | 从主轨道下载的数据中二次解析 |
| 生命周期 | 独立存在,控制下载进度 | 依赖主轨道,主轨道停则它停 |
| 代码实现 | ChunkSampleStream |
EmbeddedSampleStream |
| 典型内容 | 视频、主音频 | 字幕、定时元数据、辅助轨道 |
形象化的例子:快递包裹
想象你网购了一台电脑显示器,包装箱里顺便塞了一本说明书。
- 主轨道 = 显示器:它是你买这个包裹的核心目的。快递员(网络加载线程)运送的是这个大箱子。
- 嵌入轨道 = 说明书:它被嵌入在显示器的包装箱里。它不需要单独的快递单号(独立 URL),只要显示器到了,说明书也就跟着到了。
- **解封装 (Extractor)**:当你拆开箱子(解析 Data Chunk),你把显示器连上主机,同时把说明书拿给用户看。
主轨道流 ChunkSampleStream
它是数据的入口。它拥有 ChunkSource,决定何时下载下一个分块(Chunk)。当一个包含多个轨道的媒体块被下载并解封装(Extract)后,主轨道数据会进入其自身内部的 SampleQueue。
嵌入轨道流 EmbeddedSampleStream
它不具备解封装能力。在父类解封装时,次要轨道的数据会被分流到父类维护的其他 SampleQueue 中。EmbeddedSampleStream 只是这些队列的一个访问窗口。
ChunkSampleStream 与 EmbeddedSampleStream 的关系
ChunkSampleStream 参数
1 | /** 主轨道类型(例如:C.TRACK_TYPE_VIDEO 或 C.TRACK_TYPE_AUDIO) */ |
EmbeddedSampleStream 参数
1 | /** 关联的父级媒体块样本流,负责实际的数据加载逻辑 */ |
整体与部分
从我列出的这些参数中,我们能清晰看出 ChunkSampleStream 和 EmbeddedSampleStream 之间整体与部分的关系
ChunkSampleStream 中持有主轨道类型和数据,以及多个嵌入轨道的类型和数据
EmbeddedSampleStream 中持有父级 ChunkSampleStream 类型的指针,以及父级嵌入轨道数据的指针,还有一个轨道在父级流中的索引位置
index 和 parent:
- **主轨道 (
parent)**:负责把大箱子搬进屋。它内部有多个抽屉(SampleQueue),它把显示器零件放 0 号抽屉,说明书放 1 号抽屉。 - **嵌入轨道 (
index)**:它只负责盯着 1 号抽屉。每当主轨道往里放一张纸,嵌入轨道就立刻取出来读。
这种设计的目的是为了减少网络连接数——不需要为每一行字幕都去发一次 HTTP 请求,直接塞在视频流里最高效。
Chunk 的整体性
物理上:它们是一个整体(效率优先)
在传输层和文件存储层,视频、音频和元数据是“捆绑”在一起的。
单一容器(Container): 媒体分块(如 .m4s 或 .ts 文件)通常是一个二进制文件。为了节省网络连接开销(HTTP 请求次数),编码器会将视频帧、音频帧和字幕数据按时间戳交错排列,打包成一个 Chunk。
单一请求: 播放器只需要发起一次网络请求(例如 GET chunk_1.m4s),就能把这个时间段内所有的媒体信息全部拿回来。
共享生命周期: 这个 Chunk 要么下载成功,所有轨道都有数据;要么下载失败,所有轨道都断流。这种“同生共死”的关系由 parent(ChunkSampleStream)统一管理。
Chunk 的独立性
逻辑上:它们是相互独立的(功能优先)
在播放器内部处理和解码层,每个轨道必须表现得像是单独存在一样。
- 独立的消费速度: 视频解码器、音频解码器和字幕渲染引擎的工作节奏是不一样的。视频可能在等待硬件解码,而字幕可能瞬间就处理完了。因此,每个轨道需要有自己的
SampleQueue(缓冲区)和读取指针。 - 独立的选择性: 用户可以关闭字幕,或者切换不同的音轨。在逻辑上,播放器需要能够“按需取货”,只读取某个特定的轨道,而忽略其他轨道。
- 独立的格式声明: 视频的格式信息(分辨率、编码)和嵌入轨道的格式信息(如元数据类型)完全不同。
EmbeddedSampleStream通过maybeNotifyDownstreamFormat确保每个轨道都能正确地向后级报告自己的身份。
EmbeddedSampleStream 只是容器
要理解为什么说 EmbeddedSampleStream “不具备解封装能力”且只是个“访问窗口”,我们需要从 ExoPlayer 的数据流水线(Pipeline) 架构来看。
为什么它不具备解封装能力?
在 ExoPlayer 中,“解封装能力”指的是 Extractor(解封装器) 的职责。
- 真正的执行者是父类: 当
ChunkSampleStream加载一个媒体块(MediaChunk)时,父类内部持有一个解封装器(如MatroskaExtractor或FragmentedMp4Extractor)。 - 一次拆解,多路输出: 当解封装器处理一个文件时,它会同时识别出视频轨、音频轨和元数据轨。解封装器会将这些样本(Samples)直接推送到各自对应的
SampleQueue中。 - 嵌入流只是“持有者”:
EmbeddedSampleStream只是在构造时,由父类分配给它一个已经创建好的SampleQueue的引用(指针)。它本身不参与将二进制数据转换为样本的过程,它只是静静地守着那个队列,等待父类把“洗好的菜”放进去。
| 特性 | 父类 ChunkSampleStream | 嵌入类 EmbeddedSampleStream |
|---|---|---|
| 数据源 | 网络/磁盘(通过 Extractor 解封装) | 父类分配的 SampleQueue |
| 队列管理 | 创建并管理多个 SampleQueue | 仅持有其中一个队列的引用 |
| 解封装逻辑 | 运行 Extractor,分发样本 | 无 |
| 角色比喻 | 厨师(负责切菜、炒菜、摆盘) | 传菜员(负责把对应的菜端给对应的客人) |
这种设计的妙处在于,无论一个分块里有多少个轨道,底层的 IO 加载和解封装逻辑只运行一次,极大地节省了 CPU 和内存开销。
深入理解嵌入轨道流持有主轨道流指针·
节省带宽与 IO (合并下载)
在 DASH 或 HLS 这种分块流中,视频轨道和嵌入轨道(如元数据、相机运动轨迹)通常被物理封装在同一个文件(Chunk)里。
- 如果没有 Parent:每个轨道都要独立发请求下载同一个文件,导致带宽翻倍,流量被极度浪费。
- 有了 Parent:由
parent发起一次 HTTP 请求,下载整个分块,解析后分发给不同的轨道。
状态的“一键同步” (重置与跳转)
当用户拖动进度条(Seek)时,所有轨道必须同时停下来并跳转。
- 如果没有 Parent:播放器必须逐个通知每一个嵌入流去重置,极易出现视频跳了但音频或字幕还在播旧数据的情况。
- 有了 Parent:只需重置
parent,所有EmbeddedSampleStream通过isPendingReset()瞬间感知并同步停止。
精准的生命周期控制
嵌入流往往是“顺带”存在的。
- 安全性:当
parent取消了某个正在下载的分块(比如因为网速慢切换到了低清晰度),parent会通过canceledMediaChunk标记一个边界。 - 作用:它告诉嵌入流:“虽然你现在还能读到缓存里的数据,但由于我已经切走轨道了,后面的数据不要再读了。”这种边界约束必须由掌控全局下载的
parent来下达。
readData 和 skipData 函数
1 |
|
skipData (跳过数据)
- 核心逻辑:根据目标时间戳
positionUs计算出可以跳过的样本数量,并移动读取指针。 - 特殊处理:它引入了对
canceledMediaChunk的判断。如果某个数据块(Chunk)因为加载策略(如下载太慢、切换画质)被取消了,这个函数会强制限制跳过的范围,严禁跳入无效的数据区。
readData (读取数据)
- 核心逻辑:从缓冲区(
SampleQueue)中提取真实的媒体数据或新的格式信息(Format)。 - 特殊处理:在真正读取数据前,它会先通过
maybeNotifyDownstreamFormat()检查并上报格式变更。同时,它也具备“拦截”功能:如果读取位置已经触及被取消的块,直接返回“无数据可读”,防止解码器读到垃圾数据。
与父类的区别
| 维度 | 父类 (ChunkSampleStream / Primary) | 本类 (EmbeddedSampleStream) |
|---|---|---|
| 数据来源 | 负责发起 HTTP 请求,加载整个 MediaChunk。 |
不加载数据,只从父类加载好的缓冲区中分一杯羹。 |
| 轨道地位 | 主轨道(通常是视频或音频)。 | 次要轨道(通常是嵌入在视频流里的字幕或元数据)。 |
| 错误处理 | maybeThrowError 会真实抛出网络异常。 |
maybeThrowError 是空的,它相信父类会处理好所有加载异常。 |
| 索引管理 | 管理 0 号轨道。 |
管理 1 + index 号轨道。 |
这些函数构成了 ChunkSampleStream 的核心逻辑。为了方便理解,可以将它们按照 生命周期管理、数据读取、加载控制、Seek/位置管理 以及 内部状态维护 这五个核心维度进行分类。 |
ChunkSampleStream 里面大概有什么
Chunk 纬度
在 ChunkSampleStream 的这些参数中,直接或间接与 Chunk(分块) 相关的成员可以归纳为以下几类,涵盖了从“决策”到“执行”,再到“存储”和“消费”的完整生命周期:
分块决策与获取 (Decision & Retrieval)
这组参数决定了“下一个分块是什么”以及“去哪里拿”。
chunkSource: 分块源。这是整个流程的“大脑”,负责根据当前的缓冲情况和网络带宽,决定下一个该下载哪个分块。nextChunkHolder: 分块持有者。一个临时的容器,chunkSource会将选中的分块信息填充到这个对象中,供加载器使用。
分块任务执行 (Execution)
这组参数关注“正在进行的”分块任务。
loadingChunk: 当前加载的分块。记录目前正在通过网络下载的那个Chunk对象。canceledMediaChunk: 被取消的分块。如果当前的加载任务被中止(例如因为用户跳转或切换了更清晰的轨道),该变量会临时持有该分块,用于后续的清理或重试逻辑。
分块存储与视图 (Storage & View)
分块下载完成后的存放地。
mediaChunks: 分块列表。一个内部的ArrayList,按顺序存储所有已下载完成、正等待被SampleQueue消费或正在输出的分块。readOnlyMediaChunks: 只读分块列表。提供给外部(如 UI 或监控层)查看当前缓冲区内分块状态的视图,保证了线程安全和封装性。
分块解析与输出 (Parsing & Output)
负责将分块文件“拆解”并路由到正确的轨道。
chunkOutput: 分块输出桥接器。它扮演了中介角色,将解复用器(Extractor)从分块中解析出的音视频数据,精准地分发到对应的SampleQueue中。
分块状态同步 (Synchronization & Indexing)
维护分块与播放进度、格式之间的对应关系。
nextNotifyPrimaryFormatMediaChunkIndex: 格式通知索引。由于不同的分块可能具有不同的分辨率或码率,该索引记录了下一个需要触发“格式变化”通知的分块位置,确保渲染器能及时感知到数据流的变化。
分块生命周期示意
- Step 1:
chunkSource选定分块并放入nextChunkHolder。 - Step 2:
loader开始加载loadingChunk。 - Step 3:
chunkOutput将数据从分块剥离,存入primarySampleQueue。 - Step 4: 下载完的分块进入
mediaChunks队列。 - Step 5: 消费完成后,分块从
mediaChunks移除。
Loading 纬度
加载执行与调度 (Execution & Scheduling)
loader: 核心执行器。内部封装了线程池,负责在后台线程执行loadingChunk的下载任务。chunkSource: 加载决策者。虽然它不执行下载,但它决定了加载的内容,是加载行为的“指挥官”。loadingChunk: 当前加载任务。指向当前正在被loader处理的Chunk实例。
状态监控与反馈 (State Monitoring & Feedback)
用于实时追踪加载进度,并通知外部组件(如 ExoPlayer 核心或 UI)。
loadingFinished: 加载完成标志。这是一个关键的状态位,当chunkSource告知没有更多分块可加载(流结束)且当前无待处理任务时,该值为true。callback: 加载进度回调。用于将“数据已加载”、“加载完成”或“由于某种原因加载停滞”的消息通知给上层的MediaPeriod。mediaSourceEventDispatcher: 事件广播器。负责发送更细粒度的事件,例如onLoadStarted(加载开始)、onLoadCompleted(加载成功)和onLoadCanceled(加载取消)。
加载异常与容错 (Error Handling & Resilience)
当网络抖动或请求失败时,负责处理“接下来该怎么办”的逻辑。
loadErrorHandlingPolicy: 错误策略管理。定义了加载失败后的行为:是立即重试、延迟 5 秒重试,还是直接抛出异常并停止加载。canceledMediaChunk: 取消状态记录。如果加载任务因为某种原因被手动取消(Cancel),此变量会记录该任务,以便后续判断是否需要重新触发加载。
加载控制 (Loading Control)
根据播放状态决定是否允许继续读取或加载数据。
suppressRead: 数据读取抑制。虽然它更偏向消费端,但在某些跳转或重新缓冲(Re-buffering)场景下,它会配合加载状态共同确保播放器不会在数据未就绪时尝试读取,从而影响整体加载策略。
加载状态机简述
- 准备阶段:
chunkSource通过nextChunkHolder提交任务。 - 启动阶段:
loader接管loadingChunk,mediaSourceEventDispatcher发出开始信号。 - 运行阶段:
callback不断汇报进度。 - 异常阶段: 若失败,
loadErrorHandlingPolicy介入决定是否重试。 - 结束阶段: 如果没有更多数据,
loadingFinished设为true。
ExoPlayer Chunk 加载系统:职责分类与交互指南
| 所属分类 | 核心类名 | 职责说明 |
|---|---|---|
| 核心控制层 | ChunkSampleStream |
指挥中心。对接播放器,协调加载策略、缓存管理和最终的样本输出。 |
ChunkSource |
策略源。接口类。决定当前应该加载哪个分块、何时进行码率切换。 | |
ChunkHolder |
决策载体。临时存放 ChunkSource 返回的分块对象或待处理的状态信息。 |
|
| 分块数据模型 | Chunk / MediaChunk |
基类。定义了分块的物理属性,如时间戳、数据规格和加载范围。 |
BaseMediaChunk |
实现基础。媒体分块的核心抽象,主要负责处理样本索引和存储逻辑。 | |
Container / SingleSample |
具体实现。分别对应封装在容器(如 MP4/TS)中的分块或独立的单样本分块。 | |
Initialization / Data |
特殊分块。用于初始化解码器配置或传输非媒体数据(如 DRM 密钥)。 | |
| 样本提取层 | ChunkExtractor |
解析接口。定义了将二进制数据流还原为音视频帧(Samples)的标准。 |
Bundled / MediaParser |
解析实现。前者使用 ExoPlayer 内置解析库,后者调用 Android 系统原生 API。 | |
BaseMediaChunkOutput |
中间件。将解析出的样本数据精准路由到正确的 SampleQueue 中。 |
|
| 辅助与元数据 | MediaChunkIterator |
预览工具。在未加载真实数据前,允许系统查看后续分块的排列和时间信息。 |
[!TIP]
设计模式应用:该系统是典型的策略模式(ChunkSource)与工厂模式(ChunkExtractor)的结合,保证了对不同流媒体协议(DASH, HLS, SmoothStreaming)的扩展性。
💡 建议
如果你是在做代码重构或性能调优,重点关注 ChunkSource 的码率切换算法以及 SampleQueue 的内存占用情况。这两个点通常是 Chunk 系统中最容易产生瓶颈的地方。