MediaCodec 全链路深度剖析(二):Java 层 API 与状态机
系列导读:主系列从”一帧像素怎么走完六层架构”切入,偏底层。但在开挖之前,得先让读者站在 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) } |
不到 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 枯竭卡死。