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 生命周期与缓冲区流转 createByCodecName Uninitialized configure Configured start Executing stop / flush Stopped release Released 输入缓冲区队列 index 0 index 1 index 2 dequeueInputBuffer → 填充 → queueInputBuffer 输出缓冲区队列 index 0 index 1 index 2 dequeueOutputBuffer → 处理 → releaseOutputBuffer Codec 硬/软编解码 MediaFormat 格式描述(配置阶段必须匹配) 视频:mime / width height / frame-rate 音频:mime / sample-rate channel-count / bitrate 关键帧:KEY_FRAME CODEC_CONFIG 输出格式变化: OUTPUT_FORMAT_CHANGED

二、方法流程:五步走完一次完整编解码

第一步是创建。推荐使用 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 / createByCodecNameUninitializedmime 拼写错误导致返回 null
配置configure(format, surface, ...)Configured缺少 width/height 或 sample-rate
启动start()Executing未配置就 start 抛 IllegalStateException
输入dequeueInputBuffer / queueInputBufferExecutingpts 单位错误、flags 未设关键帧
输出dequeueOutputBuffer / releaseOutputBufferExecuting忘记 release 导致缓冲区耗尽
停止stop() / flush() / release()Stopped / Releasedstop 后继续 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,在旧版本中需要重新获取输出缓冲区数组。

在 Codec2 架构下,MediaCodec 的 API 行为保持一致,但底层组件从 ACodec 切换为 C2Component。适配时重点检查 configure 参数是否被 Codec2 支持,以及输出格式变化回调是否及时处理。

五、误区与复盘

  • 误区一:创建后直接 start。 必须先 configure,否则抛 IllegalStateException。
  • 误区二:输出缓冲区不 release。 输出队列很快耗尽,dequeueOutputBuffer 一直返回 -1。
  • 误区三:忽略 INFO_OUTPUT_FORMAT_CHANGED。 真实宽高或 csd 数据可能变化,导致花屏或无法封装。
  • 误区四:stop 后继续使用旧缓冲区。 stop 会清空队列,必须重新 start 或重新 configure。
  • 误区五:pts 单位混用。 MediaCodec 使用微秒,若传入毫秒会导致音视频不同步。
在排障时,优先打印 MediaFormat 的 toString()、每次 dequeue 的 index 与 BufferInfo,以及 flags 值。大多数“无输出”问题都能从这些日志中定位到是配置不匹配、缓冲区未释放,还是 EOS 未正确发送。

总结来说,MediaCodec 的核心流程可以浓缩为:创建 → 配置 → 启动 → 输入输出循环 → 停止释放。其中 configure 决定能力边界,缓冲区管理决定运行稳定性,MediaFormat 决定数据契约,而状态机约束决定调用顺序。把这四点同时守住,MediaCodec 与 Codec2 的适配就有了可靠基础。