7、Stagefright 框架:OMX 组件、MediaExtractor 与 MediaSource、数据流处理
Stagefright 这个名字,听起来有点吓人,对吧?其实它就是个多媒体播放引擎。Android 从 2.3 开始就用它替代了老旧的 OpenCore。我刚开始接触它的时候,也觉得这玩意儿挺复杂的。但说白了,它就是个流水线:把媒体文件拆开,解码,然后送出去渲染。
今天我们就来聊聊这条流水线的核心三件套:OMX 组件、MediaExtractor 和 MediaSource。理解了它们,Stagefright 的骨架你就拿下了。
7.1 OMX 组件:硬件解码的“黑盒子”
OMX,全称 OpenMAX IL(Integration Layer)。它是 Khronos 组织定义的一套标准接口。说白了,就是让 Android 和硬件解码器对话的“通用语言”。
为什么要搞这么个东西?你想想看,高通、联发科、海思,每家芯片的解码器都不一样。如果没有统一接口,Google 得给每款芯片写一套适配代码,那不得累死?OMX 就是来解决这个问题的。
核心概念:OMX 组件是一个“状态机”。它只有几个状态:Loaded、Idle、Executing、Pause。你只能按顺序切换状态,不能乱跳。比如从 Loaded 直接跳到 Executing,那是不允许的。
我在项目中遇到过一个问题:某个芯片的解码器在 Idle 状态下设置参数时,竟然返回了 OMX_ErrorNone,但实际参数根本没生效。后来查了芯片手册才发现,有些参数必须在 Loaded 状态下设置。嗯,这里要注意,参数设置时机是个坑。
7.1.1 OMX 组件的生命周期
一个典型的 OMX 组件使用流程是这样的:
- 加载组件:通过
OMX_GetHandle获取组件句柄。这时候组件处于 Loaded 状态。 - 设置参数:调用
OMX_SetParameter设置输入输出格式、缓冲区数量等。 - 切换到 Idle:调用
OMX_SendCommand(OMX_CommandStateSet, OMX_StateIdle, ...)。此时组件会分配内部资源。 - 分配缓冲区:通过
OMX_AllocateBuffer或OMX_UseBuffer给组件提供输入输出缓冲区。 - 切换到 Executing:调用
OMX_SendCommand(OMX_CommandStateSet, OMX_StateExecuting, ...)。这时候组件开始干活了。 - 处理数据:通过
OMX_EmptyThisBuffer送数据进去,通过回调函数OMX_FillBufferDone拿解码后的数据出来。 - 清理:先切回 Idle,再切回 Loaded,最后释放句柄。
// 伪代码示例:OMX 组件状态切换
OMX_HANDLETYPE hComponent;
OMX_GetHandle(&hComponent, "OMX.google.h264.decoder", ...);
// 设置解码格式
OMX_VIDEO_PARAM_PORTFORMATTYPE format;
format.nPortIndex = 0; // 输入端口
format.eCompressionFormat = OMX_VIDEO_CodingAVC;
OMX_SetParameter(hComponent, OMX_IndexParamVideoPortFormat, &format);
// 切换到 Idle
OMX_SendCommand(hComponent, OMX_CommandStateSet, OMX_StateIdle, NULL);
// 分配缓冲区(省略具体实现)
// ...
// 切换到 Executing
OMX_SendCommand(hComponent, OMX_CommandStateSet, OMX_StateExecuting, NULL);
// 开始解码
OMX_EmptyThisBuffer(hComponent, pInputBuffer);
// 在回调中接收输出
// OMX_FillBufferDone(...)
个人经验:我建议你在调试 OMX 组件时,先确认芯片厂商提供的 media_codecs.xml 文件。里面列出了所有支持的组件名称和性能参数。有一次我折腾了半天,结果发现是组件名写错了,真是哭笑不得。
7.2 MediaExtractor:文件拆解工
MediaExtractor 的作用很简单:把媒体文件拆成一个个轨道(Track)。比如一个 MP4 文件,可能包含一个视频轨道、一个音频轨道,甚至还有字幕轨道。
它的工作流程是这样的:
- 打开文件(或网络流)
- 解析文件头,找到所有轨道的信息
- 为每个轨道创建一个 MediaSource
- 提供接口让上层读取指定轨道的样本数据
说白了,MediaExtractor 就是个“拆包工”。它不关心数据怎么解码,只关心数据怎么从文件里拿出来。
7.2.1 MediaExtractor 的核心接口
| 接口 | 作用 |
|---|---|
setDataSource() |
设置数据源,可以是文件路径、文件描述符或网络 URL |
getTrackCount() |
返回轨道数量 |
getTrackFormat() |
获取指定轨道的格式信息(编码类型、分辨率、采样率等) |
selectTrack() |
选择要读取的轨道 |
readSampleData() |
读取当前轨道的下一个样本数据 |
advance() |
移动到下一个样本 |
// 伪代码:使用 MediaExtractor 读取视频轨道
MediaExtractor extractor;
extractor.setDataSource("/sdcard/test.mp4");
int trackCount = extractor.getTrackCount();
for (int i = 0; i < trackCount; i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
extractor.selectTrack(i);
break;
}
}
// 读取样本数据
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
while (extractor.readSampleData(buffer, 0) >= 0) {
info.size = extractor.readSampleData(buffer, 0);
info.presentationTimeUs = extractor.getSampleTime();
info.flags = extractor.getSampleFlags();
// 处理样本...
extractor.advance();
}
注意:MediaExtractor 不是线程安全的。如果你在多线程环境下使用,记得加锁。我曾经在项目里踩过这个坑,两个线程同时调用 advance(),结果数据全乱了。
7.3 MediaSource:数据生产者
MediaSource 是 Stagefright 内部的一个抽象类。它定义了如何从 MediaExtractor 中读取数据,并交给解码器处理。
你可以把 MediaSource 想象成一个“水龙头”。MediaExtractor 把水管接好,MediaSource 负责拧开水龙头,让数据流出来。
7.3.1 MediaSource 的核心方法
start():开始生产数据。通常在这里会启动一个线程,或者准备好缓冲区。read():读取一个样本数据。返回一个MediaBuffer,里面包含数据和时间戳。stop():停止生产数据。释放资源。pause():暂停生产。这个不是所有实现都支持。
在 Stagefright 中,MediaSource 最常见的实现是 MediaExtractor::Track。它直接从 MediaExtractor 中读取数据。但有时候,我们也会自己实现 MediaSource,比如从网络流中读取数据,或者对原始数据进行预处理。
关键点:MediaSource 的 read() 方法是阻塞的。如果数据还没准备好,它会一直等。这在设计流水线时要注意,避免死锁。
7.4 数据流处理:从文件到屏幕
好了,现在我们把三个组件串起来,看看数据是怎么从文件流到屏幕的。
整个流程是这样的:
- MediaExtractor 打开文件,解析出轨道信息。
- 为每个轨道创建一个 MediaSource。
- MediaSource 从文件中读取原始数据(比如 H.264 码流)。
- 数据被送到 OMX 组件(解码器)进行解码。
- 解码后的数据(YUV 或 RGB)被送到渲染器显示。
这里有个关键点:数据流是异步的。MediaSource 和 OMX 组件之间通过缓冲区队列通信。MediaSource 把数据塞进输入队列,OMX 组件解码后把结果放进输出队列。这样两边可以同时工作,互不阻塞。
避坑指南:我曾经遇到过一个性能问题:视频播放时一卡一卡的。后来发现是 MediaSource 的 read() 返回数据太慢,导致解码器经常饿着肚子干活。解决办法是增大缓冲区数量,让 MediaSource 提前多读几帧数据。嗯,这就是所谓的“预读”策略。
7.4.1 数据流处理流程图
下面这张图展示了 Stagefright 中数据流的核心路径:
这张图展示了从文件到渲染的完整路径。注意看,数据是单向流动的,但控制信号(比如缓冲区管理)是双向的。这就是 Stagefright 的精髓:数据流 + 控制流分离。
7.5 总结
好了,我们来捋一捋今天的内容:
- OMX 组件:硬件解码器的标准接口,状态机驱动,注意参数设置时机。
- MediaExtractor:文件拆解工,负责解析容器格式,提取轨道信息。
- MediaSource:数据生产者,从 MediaExtractor 读取数据,提供给解码器。
- 数据流处理:异步流水线,通过缓冲区队列连接各个组件。
说实话,Stagefright 这套设计虽然年代久远,但思路还是很清晰的。把复杂的多媒体处理拆成几个独立的模块,每个模块各司其职。你只要理解了这三个核心组件,再看 Stagefright 的源码就不会觉得晕头转向了。
下次我们聊聊更具体的东西——怎么自己写一个 MediaSource 来对接网络流。嗯,那才是真正有意思的地方。
公众号:蓝海资料掘金营,微信deep3321