7、Stagefright 框架:OMX 组件、MediaExtractor 与 MediaSource、数据流处理

Stagefright 这个名字,听起来有点吓人,对吧?其实它就是个多媒体播放引擎。Android 从 2.3 开始就用它替代了老旧的 OpenCore。我刚开始接触它的时候,也觉得这玩意儿挺复杂的。但说白了,它就是个流水线:把媒体文件拆开,解码,然后送出去渲染。

今天我们就来聊聊这条流水线的核心三件套:OMX 组件MediaExtractorMediaSource。理解了它们,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 组件使用流程是这样的:

  1. 加载组件:通过 OMX_GetHandle 获取组件句柄。这时候组件处于 Loaded 状态。
  2. 设置参数:调用 OMX_SetParameter 设置输入输出格式、缓冲区数量等。
  3. 切换到 Idle:调用 OMX_SendCommand(OMX_CommandStateSet, OMX_StateIdle, ...)。此时组件会分配内部资源。
  4. 分配缓冲区:通过 OMX_AllocateBufferOMX_UseBuffer 给组件提供输入输出缓冲区。
  5. 切换到 Executing:调用 OMX_SendCommand(OMX_CommandStateSet, OMX_StateExecuting, ...)。这时候组件开始干活了。
  6. 处理数据:通过 OMX_EmptyThisBuffer 送数据进去,通过回调函数 OMX_FillBufferDone 拿解码后的数据出来。
  7. 清理:先切回 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 文件,可能包含一个视频轨道、一个音频轨道,甚至还有字幕轨道。

它的工作流程是这样的:

  1. 打开文件(或网络流)
  2. 解析文件头,找到所有轨道的信息
  3. 为每个轨道创建一个 MediaSource
  4. 提供接口让上层读取指定轨道的样本数据

说白了,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 数据流处理:从文件到屏幕

好了,现在我们把三个组件串起来,看看数据是怎么从文件流到屏幕的。

整个流程是这样的:

  1. MediaExtractor 打开文件,解析出轨道信息。
  2. 为每个轨道创建一个 MediaSource
  3. MediaSource 从文件中读取原始数据(比如 H.264 码流)。
  4. 数据被送到 OMX 组件(解码器)进行解码。
  5. 解码后的数据(YUV 或 RGB)被送到渲染器显示。

这里有个关键点:数据流是异步的。MediaSource 和 OMX 组件之间通过缓冲区队列通信。MediaSource 把数据塞进输入队列,OMX 组件解码后把结果放进输出队列。这样两边可以同时工作,互不阻塞。

避坑指南:我曾经遇到过一个性能问题:视频播放时一卡一卡的。后来发现是 MediaSource 的 read() 返回数据太慢,导致解码器经常饿着肚子干活。解决办法是增大缓冲区数量,让 MediaSource 提前多读几帧数据。嗯,这就是所谓的“预读”策略。

7.4.1 数据流处理流程图

下面这张图展示了 Stagefright 中数据流的核心路径:

Stagefright 数据流处理流程 媒体文件 MP4 / TS / AVI MediaExtractor 解析文件头 提取轨道信息 MediaSource 读取样本数据 提供 MediaBuffer OMX 解码器 (H.264/HEVC) 解码输出缓冲区 YUV / RGB 数据 渲染器 Surface / AudioTrack 缓冲区管理 实线:数据流 虚线:控制流

这张图展示了从文件到渲染的完整路径。注意看,数据是单向流动的,但控制信号(比如缓冲区管理)是双向的。这就是 Stagefright 的精髓:数据流 + 控制流分离

7.5 总结

好了,我们来捋一捋今天的内容:

  • OMX 组件:硬件解码器的标准接口,状态机驱动,注意参数设置时机。
  • MediaExtractor:文件拆解工,负责解析容器格式,提取轨道信息。
  • MediaSource:数据生产者,从 MediaExtractor 读取数据,提供给解码器。
  • 数据流处理:异步流水线,通过缓冲区队列连接各个组件。

说实话,Stagefright 这套设计虽然年代久远,但思路还是很清晰的。把复杂的多媒体处理拆成几个独立的模块,每个模块各司其职。你只要理解了这三个核心组件,再看 Stagefright 的源码就不会觉得晕头转向了。

下次我们聊聊更具体的东西——怎么自己写一个 MediaSource 来对接网络流。嗯,那才是真正有意思的地方。


公众号:蓝海资料掘金营,微信deep3321