3. MediaCodec 核心状态机:未初始化、配置、运行、停止、释放

3.1 状态机原理

MediaCodec 内部维护一个严格的状态机,应用程序的所有 API 调用必须在正确的状态下执行,否则抛出 IllegalStateException。正确理解状态转换是编写稳定编解码管道的前提。状态机包含 5 个核心状态:Uninitialized(未初始化)、Configured(已配置)、Running(运行中)、Stopped(已停止)、Released(已释放)。每个状态都对应一组允许的操作,状态切换由明确的 API 调用触发。

3.2 状态定义与转换条件

状态含义入口转换出口转换
UninitializedCodec 对象已创建,但未配置任何参数MediaCodec.createByCodecName()configure() → Configured
Configured编解码参数(格式、flag)已设置,等待 startconfigure()start() → Running
release() → Released
Running正在处理输入/输出缓冲,可执行 dequeueInputBuffer、queueInputBuffer、dequeueOutputBuffer、releaseOutputBufferstart()stop() → Stopped
release() → Released
Stopped编解码停止,可重置或释放stop()start() → Running
release() → Released
reset() → Uninitialized
Released资源已释放,不能再进行任何操作release()无出口(终结态)

提示: 从 Stopped 可以通过 reset() 回到 Uninitialized,从而实现复用同一个 Codec 实例而不必重新创建对象,这在短切片处理场景中可显著减少对象创建开销。

3.3 状态转换触发条件详细说明

3.3.1 Uninitialized → Configured

  • 调用 configure(format, surface, crypto, flags) 后,MediaCodec 解析 MediaFormat 并配置内部底层组件。若 surface 为 null,则为纯 Buffer 模式;否则为 Surface 输入/输出模式。
  • 配置失败(格式不支持等)会抛出异常,状态仍保持 Uninitialized。

3.3.2 Configured → Running

  • 调用 start() 后,Codec 分配内部缓冲队列并启动编解码线程。此时输入输出端口就绪,可开始数据交互。
  • 若 start() 时底层资源不足,状态会回退到 Uninitialized(实际行为取决于厂商实现,建议捕获异常后手动 release 重建)。

3.3.3 Running ↔ Stopped

  • stop() 强制停止编解码,清空所有排队的缓冲,输出端不再产生新数据。可再次 start() 恢复运行。
  • 注意:stop() 后 必须 重新 start() 才能继续生产数据,不能再次 configure()(除非先 reset())。

3.3.4 Stopped → Released / Uninitialized

  • release() 彻底销毁 Codec,释放所有 native 资源。不可逆。
  • reset() 使 Codec 回到 Uninitialized 状态,之后可以重新 configure() 而无需新建对象。

3.4 标准 API 调用序列

一个典型生命周期代码片段:

MediaCodec codec = MediaCodec.createDecoderByType("video/avc");
// 状态: Uninitialized

MediaFormat format = MediaFormat.createVideoFormat("video/avc", 1920, 1080);
codec.configure(format, surface, null, 0);
// 状态: Configured

codec.start();
// 状态: Running

// 编解码循环...
while (!outputEndOfStream) {
    int inputIndex = codec.dequeueInputBuffer(10000);
    if (inputIndex >= 0) {
        ByteBuffer inputBuffer = codec.getInputBuffer(inputIndex);
        // 填充数据...
        codec.queueInputBuffer(inputIndex, 0, dataSize, pts, 0);
    }
    BufferInfo info = new BufferInfo();
    int outputIndex = codec.dequeueOutputBuffer(info, 10000);
    if (outputIndex >= 0) {
        // 渲染或处理输出
        codec.releaseOutputBuffer(outputIndex, true);
    }
}

codec.stop();
// 状态: Stopped

codec.reset();
// 状态: Uninitialized(可重用)
codec.configure(format2, null, null, 0);
// 或者直接释放:
codec.release();
// 状态: Released(终结)

3.5 调试方法:状态转换日志

启用 MediaCodec 内部日志(Android 10+):

adb shell setprop debug.media.codec.logging 1
adb logcat -s "ACodec" "MediaCodec"

日志输出示例:

ACodec  : [OMX.qcom.video.decoder.avc] state UNINITIALIZED -> CONFIGURING
ACodec  : [OMX.qcom.video.decoder.avc] state CONFIGURING -> CONFIGURED
ACodec  : [OMX.qcom.video.decoder.avc] state CONFIGURED -> EXECUTING
ACodec  : [OMX.qcom.video.decoder.avc] state EXECUTING -> FLUSHING
ACodec  : [OMX.qcom.video.decoder.avc] state FLUSHING -> EXECUTING
ACodec  : [OMX.qcom.video.decoder.avc] state EXECUTING -> FLUSHING
ACodec  : [OMX.qcom.video.decoder.avc] state FLUSHING -> STOPPING
ACodec  : [OMX.qcom.video.decoder.avc] state STOPPING -> STOPPED
ACodec  : [OMX.qcom.video.decoder.avc] state STOPPED -> UNINITIALIZED (reset)

注意:实际日志中的状态名(如 CONFIGURING、EXECUTING)与 API 层略有不同,但转换关系一致。

3.6 常见错误与避坑

错误 1:在 Running 状态下调用 configure() → 抛出 IllegalStateException: configure() is not allowed now

错误 2:在使用 Surface 输入模式后,忘记调用 signalEndOfInputStream() → Codec 卡住无法输出 EOS 帧。正确做法:在输入完成后调用 codec.signalEndOfInputStream(),状态仍保持 Running,但输出队列最终会产生带 BUFFER_FLAG_END_OF_STREAM 的输出。

错误 3:在 stop() 后未 reset() 就重新 configure() → 抛出异常。必须通过 reset() 回到 Uninitialized 再重新配置。

错误 4:多次调用 release() → 第二次调用会直接返回(无异常),但建议通过标志位避免重复释放。

3.7 性能与稳定性优化

  • 复用 Codec 实例: 若编码参数变化不大(如仅分辨率改变),先用 stop() + reset() 回到 Uninitialized,再用新 MediaFormat configure(),避免频繁创建/销毁底层 OMX 组件。
  • Surface 模式下的状态管理: 在 Surface 输入端,stop() 会触发 Surface 的 disconnect,需确保在 UI 线程重新连接 Surface(通常通过 surface.release() + 重建)。
  • 使用异步模式减少状态转换阻塞: 回调方式(setCallback())可以异步获取输入输出缓冲,避免主线程在 dequeueInputBuffer 上的长时间阻塞。
  • 监控状态转换时间: 在 start() 前后记录时间戳,若耗时超过 100ms 可能要检查 Codec 是否被其他进程占用,或者配置了过大的缓冲数。

3.8 MediaCodec 状态机转换图

MediaCodec 状态机转换图 Uninitialized (未初始化) Configured (已配置) Running (运行中) Stopped (已停止) Released (已释放) configure() start() stop() start() reset() release() release() release() 说明: 虚线箭头代表不推荐或一次性的释放路径;实线箭头为常用转换。reset() 可复用 Codec 对象。