4. MediaCodec核心流程
MediaCodec 是 Android 平台上进行音视频硬编解码的核心 API。它的使用方式与普通对象不同:它并不是“调用一个方法就返回结果”的同步组件,而是围绕输入缓冲区与输出缓冲区两条队列运转的状态机。理解 MediaCodec 的关键,是把它的生命周期拆成五个动作:创建、配置、启动、缓冲区循环、停止与释放。只要这五步的顺序和参数正确,编解码流程就能稳定运行;一旦顺序错乱或格式描述不匹配,就会出现 IllegalStateException、花屏、绿屏或直接无输出。
一、核心原理:MediaCodec 是一个异步缓冲区状态机
MediaCodec 内部维护两个缓冲区队列:输入队列存放待解码的压缩数据或待编码的原始帧,输出队列存放编解码完成的数据。应用层通过 dequeueInputBuffer() 获取一个空闲输入缓冲区的索引,填充数据后 queueInputBuffer() 交还给编解码器;再通过 dequeueOutputBuffer() 获取已完成的输出缓冲区索引,处理完后 releaseOutputBuffer() 释放。整个过程由 MediaCodec.BufferInfo 携带时间戳、标志位和偏移量信息。
MediaCodec 的状态迁移非常严格:Uninitialized → Configured → Executing → Flushed/Stopped → Released。只有在 Configured 状态才能调用 start(),只有在 Executing 状态才能操作缓冲区,停止后必须重新配置才能再次启动。这个状态约束是很多崩溃问题的根源。
二、方法流程:五步走完一次完整编解码
第一步是创建。推荐使用 MediaCodec.createDecoderByType(mime) 或 createEncoderByType(mime),也可以先通过 MediaCodecList 查询可用编解码器名称,再用 createByCodecName() 精确指定。创建后对象处于 Uninitialized 状态。
第二步是配置。调用 configure(format, surface, crypto, flags),其中 format 是 MediaFormat,必须包含 mime、宽高或采样率等关键参数。解码时若使用 Surface 输出,则传入 Surface;编码时通常传 null。配置完成后进入 Configured 状态。
第三步是启动。调用 start() 后进入 Executing 状态,此时输入输出缓冲区队列才真正可用。启动前不能调用任何 dequeue 方法。
第四步是缓冲区循环。典型同步模式代码如下:
MediaCodec codec = MediaCodec.createDecoderByType("video/avc");
codec.configure(format, surface, null, 0);
codec.start();
while (!isEos) {
int inIndex = codec.dequeueInputBuffer(10000);
if (inIndex >= 0) {
ByteBuffer buffer = codec.getInputBuffer(inIndex);
buffer.clear();
buffer.put(sampleData);
codec.queueInputBuffer(inIndex, 0, sampleSize, ptsUs, flags);
}
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
int outIndex = codec.dequeueOutputBuffer(info, 10000);
if (outIndex >= 0) {
codec.releaseOutputBuffer(outIndex, true);
} else if (outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat newFormat = codec.getOutputFormat();
}
}
codec.stop();
codec.release();
第五步是停止与释放。先 stop() 回到 Uninitialized 附近状态,再 release() 彻底释放底层资源。若只是 seek 或切换码流,可以调用 flush() 清空队列而不释放编解码器。
三、格式描述:MediaFormat 是配置与输出的契约
MediaFormat 在配置阶段是“输入契约”,在输出阶段是“实际结果”。解码器输出时可能通过 INFO_OUTPUT_FORMAT_CHANGED 通知实际格式,例如 SPS/PPS 解析出的真实宽高、色彩格式。编码器输出时也会返回包含 csd-0、csd-1 的格式,这些 codec-specific data 必须写入封装容器,否则后续播放器无法解码。
| 阶段 | 关键 API | 状态变化 | 常见错误 |
|---|---|---|---|
| 创建 | createDecoderByType / createByCodecName | Uninitialized | mime 拼写错误导致返回 null |
| 配置 | configure(format, surface, ...) | Configured | 缺少 width/height 或 sample-rate |
| 启动 | start() | Executing | 未配置就 start 抛 IllegalStateException |
| 输入 | dequeueInputBuffer / queueInputBuffer | Executing | pts 单位错误、flags 未设关键帧 |
| 输出 | dequeueOutputBuffer / releaseOutputBuffer | Executing | 忘记 release 导致缓冲区耗尽 |
| 停止 | stop() / flush() / release() | Stopped / Released | stop 后继续 dequeue 抛异常 |
四、案例与场景:一次 H.264 解码的完整参数
假设从 MP4 中提取出一段 H.264 码流,需要解码到 Surface 上显示。配置参数如下:
MediaFormat format = MediaFormat.createVideoFormat("video/avc", 1920, 1080);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 1024 * 1024);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
codec.configure(format, surface, null, 0);
输入时,每个 sample 需要携带正确的 pts(微秒)和 flags。关键帧要设置 BUFFER_FLAG_KEY_FRAME,结束时要发送 BUFFER_FLAG_END_OF_STREAM。输出时若 info.size == 0 且带有 EOS 标志,说明流结束。若收到 INFO_OUTPUT_BUFFERS_CHANGED,在旧版本中需要重新获取输出缓冲区数组。
五、误区与复盘
- 误区一:创建后直接 start。 必须先 configure,否则抛 IllegalStateException。
- 误区二:输出缓冲区不 release。 输出队列很快耗尽,dequeueOutputBuffer 一直返回 -1。
- 误区三:忽略 INFO_OUTPUT_FORMAT_CHANGED。 真实宽高或 csd 数据可能变化,导致花屏或无法封装。
- 误区四:stop 后继续使用旧缓冲区。 stop 会清空队列,必须重新 start 或重新 configure。
- 误区五:pts 单位混用。 MediaCodec 使用微秒,若传入毫秒会导致音视频不同步。
总结来说,MediaCodec 的核心流程可以浓缩为:创建 → 配置 → 启动 → 输入输出循环 → 停止释放。其中 configure 决定能力边界,缓冲区管理决定运行稳定性,MediaFormat 决定数据契约,而状态机约束决定调用顺序。把这四点同时守住,MediaCodec 与 Codec2 的适配就有了可靠基础。