系列导读:主系列从”一帧像素怎么走完六层架构”切入,偏底层。但在开挖之前,得先让读者站在 App 工程师的视角,把 MediaCodec 的 Java API 摸清楚——状态机有哪些、常用 API 各自扮演什么角色、哪些坑踩了会直接 crash。

一句话理解 MediaCodec

MediaCodec 是一台带缓冲池的状态机

  • 状态机决定”你现在能做什么”——错了就抛 IllegalStateException
  • 缓冲池是 App 与底层 codec 之间的传送带——你租一个 buffer、填数据、还回去;底层处理完再租一个 buffer、拿数据、你还回去。

MediaCodec 状态机

用一张官方的图来展示 MediaCodec 的声明周期:

当通过任一工厂方法创建编解码器时,编解码器处于未初始化状态。首先需要通过 configure(…) 完成配置,该方法会将其切换至已配置状态;随后调用 start(),便可进入执行状态。在执行状态下,你就可以通过前文所述的缓冲区队列操作来进行数据处理。

执行状态包含三个子状态:刷新态(Flushed)、运行态(Running)和流结束态(End-of-Stream)。调用 start() 后,解码器会立即进入刷新子状态,此时它持有所有缓冲区。一旦取出第一个输入缓冲区,编解码器就会切换至运行子状态,这也是其最主要的工作状态。当你向输入缓冲区带入流结束标记并送入队列时,编解码器会切换到流结束子状态。在此状态下,编解码器不再接收新的输入缓冲区,但仍会持续生成输出缓冲区,直到输出端数据流收尾完成。对于解码器而言,只要处于执行状态下,随时都可以调用 flush() 回到刷新子状态。

调用 stop() 可将编解码器回归未初始化状态,之后可重新对其进行配置。使用完毕后,必须调用 release() 释放编解码器资源。

详细的内容可以查看MediaCodec#states一节。

完整状态图

stateDiagram-v2
    [*] --> Uninitialized: new / createByXxx()
    Uninitialized --> Configured: configure()
    Uninitialized --> Released: release()

    Configured --> Uninitialized: reset()
    Configured --> Released: release()
    Configured --> Executing: start()

    state Executing {
        [*] --> Flushed
        Flushed --> Running: queueInputBuffer()
        Running --> Running: queue/dequeue
        Running --> EndOfStream: queueInputBuffer(EOS)
        EndOfStream --> Flushed: flush()
        Running --> Flushed: flush()
        Flushed --> Flushed: flush()
    }

    Executing --> Uninitialized: stop()
    Executing --> Released: release()
    Executing --> Error: 任何调用失败

    Error --> Uninitialized: reset()
    Error --> Released: release()

    Released --> [*]

一个被忽略的细节:真正的状态机在 Native 层 (MediaCodec.cpp),Java 侧只是个”薄壳”。这也是为什么 MediaCodec.CodecException 会带上 errorCode——错误是从 native 甚至 HAL 层反射上来的。


一个最小闭环 MediaCodec 示例(解码 + 渲染到 Surface,同步模式)

先给个能跑的代码,脑子里有个整体印象再往下看,后续所有 native 的相关的解释都与下面的代码有关。

val extractor = MediaExtractor().apply { setDataSource(path) }
val trackIdx = (0 until extractor.trackCount).first {
extractor.getTrackFormat(it).getString(MediaFormat.KEY_MIME)!!.startsWith("video/")
}
extractor.selectTrack(trackIdx)
val format = extractor.getTrackFormat(trackIdx)

val codec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)!!).apply {
configure(format, outputSurface, null, 0) // 渲染到 Surface
start()
}

val info = MediaCodec.BufferInfo()
var inputDone = false
val startNs = System.nanoTime()

while (true) {
// 1. 喂输入
if (!inputDone) {
val inIdx = codec.dequeueInputBuffer(10_000)
if (inIdx >= 0) {
val buf = codec.getInputBuffer(inIdx)!!
val size = extractor.readSampleData(buf, 0)
if (size < 0) {
codec.queueInputBuffer(inIdx, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
inputDone = true
} else {
codec.queueInputBuffer(inIdx, 0, size, extractor.sampleTime, 0)
extractor.advance()
}
}
}

// 2. 取输出
when (val outIdx = codec.dequeueOutputBuffer(info, 10_000)) {
MediaCodec.INFO_TRY_AGAIN_LATER -> { /* 继续转 */ }
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
Log.d(TAG, "output format = ${codec.outputFormat}")
}
else -> if (outIdx >= 0) {
// 按 PTS 精确送显,实现音画同步
val renderAtNs = startNs + info.presentationTimeUs * 1000L
codec.releaseOutputBuffer(outIdx, renderAtNs)

if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) break
}
}
}

codec.stop(); codec.release()
extractor.release()

不到 40 行,就是一个能用的硬解播放内核。编码器的骨架和它镜像——把 extractor 换成 GPU 画 Surface,把 releaseOutputBuffer 换成 MediaMuxer 写流。


API 全景:一个生命周期流水线

把所有常用 API 按调用顺序串起来,就是下面这张流水线:

flowchart TB
    S1["1. 创建
createDecoderByType / createEncoderByType
createByCodecName"] S2["2. 注册回调(可选)
setCallback
异步模式必调,且必须在 configure 之前"] S3["3. 配置
configure(format, surface, crypto, flags)"] S4["4. 启动
start()"] subgraph LOOP["5. 运行循环"] direction LR IN["喂输入
dequeueInputBuffer

getInputBuffer

queueInputBuffer"] OUT["取输出
dequeueOutputBuffer

getOutputBuffer

releaseOutputBuffer"] MID["中途可调
flush · setParameters
getOutputFormat
signalEndOfInputStream"] IN -.并行.- OUT OUT -.-> MID end S6["6. 清理
stop() / reset() / release()"] S1 --> S2 --> S3 --> S4 --> LOOP --> S6 classDef phase fill:#E3F2FD,stroke:#1976D2,stroke-width:2px,color:#000 classDef ioBox fill:#FFF9C4,stroke:#F9A825,stroke-width:1.5px,color:#000 classDef midBox fill:#FFE0B2,stroke:#EF6C00,stroke-width:1.5px,color:#000 classDef endBox fill:#FFCDD2,stroke:#C62828,stroke-width:2px,color:#000 class S1,S2,S3,S4 phase class IN,OUT ioBox class MID midBox class S6 endBox

Flush / Stop / Reset / Release

API 丢 buffer 保留 configure 需要重新 configure 之后还能用
flush() ✅ 直接 queue
stop() ✅ configure + start
reset() ✅ configure + start(Error 可调)
release() ❌ 对象已死

同步 vs 异步模式

同步模式 异步模式
喂/取 buffer dequeueInputBuffer / dequeueOutputBuffer 轮询 onInputBufferAvailable / onOutputBufferAvailable 回调
错误处理 catch CodecException onError(codec, e) 回调
何时选 串行逻辑、能接受阻塞循环 低延迟播放、编码器管线、UI 友好
共同点 两种都要处理 INFO_OUTPUT_FORMAT_CHANGED(异步里是 onOutputFormatChanged

规则很硬:setCallback 之后,同步的 dequeueXxx 就禁用了,调一次抛一次 IllegalStateException。

总结

回到开篇那句话——MediaCodec 是一台带缓冲池的状态机

状态机决定你”现在能调什么”:configure → start → queue/dequeue → stop/release 是主干,flush 在 Executing 内部腾挪,reset/release 负责兜底和终结。调错顺序 → IllegalStateException
缓冲池决定你”怎么交换数据”:dequeue → get → queue/release 是三步闭环,每租必还,PTS 单调,EOS 用 flag 而不是 API。忘了 release → buffer 枯竭卡死。