7、解码器状态管理:状态机详解
解码器的状态管理,说白了就是一套「交通规则」。
我刚开始做视频开发时,觉得这玩意儿不就是 open、decode、close 嘛,有什么好研究的?直到有一次线上崩溃,解码器在错误的状态下被调用了 release,直接导致整个播放器卡死。嗯,从那以后,我老老实实把状态机背得滚瓜烂熟。
7.1 状态机五态模型
Android MediaCodec 的状态机,一共五个状态。我习惯用一张图来记:
这张图我建议你存下来。每次写解码器代码前看一眼,能省不少调试时间。
7.2 状态流转详解
7.2.1 Uninitialized → Configured
解码器刚创建出来,就是 Uninitialized 状态。这时候它啥也干不了,就是个空壳子。
MediaCodec codec = MediaCodec.createDecoderByType("video/avc");
// 此时状态:Uninitialized
调用 configure() 之后,才会进入 Configured 状态。这里有个坑——configure 必须传入正确的 MediaFormat。我在项目中遇到过,有人传了空的 format,结果 configure 直接抛 IllegalStateException。
7.2.2 Configured → Executing
调用 start() 后,解码器进入 Executing 状态。这才是干活的状态。
说白了,Executing 就是解码器的「工作模式」。这时候你可以往里面塞数据,也能从里面拿数据。我习惯把 Executing 再拆成两个子阶段:
- 输入阶段:通过 dequeueInputBuffer 拿到空 buffer,塞入编码数据
- 输出阶段:通过 dequeueOutputBuffer 拿到解码后的 YUV 或 Surface 数据
这两个阶段是并行的,你想想看,解码器内部其实是个流水线。一边吃数据,一边吐数据。
7.2.3 Executing → Flushed
Flushed 是个「中间态」。调用 flush() 后,解码器会清空所有待处理的输入和输出 buffer。
为什么会需要这个状态?举个例子:
你在看直播,突然切换频道。如果不 flush,解码器里还残留着上一个频道的 I 帧、P 帧,新频道的数据进来后,画面会花掉。我曾经在切换频道时没做 flush,结果用户反馈「画面像打翻了调色盘」。
7.2.4 Executing → End-of-Stream
当你往输入队列里塞入一个带有 BUFFER_FLAG_END_OF_STREAM 标志的 buffer 后,解码器就知道:数据送完了。
这时候解码器会继续处理完内部缓存的数据,然后输出最后一个带有 EOS 标志的 output buffer。之后,解码器就停在 End-of-Stream 状态。
// 发送 EOS
ByteBuffer inputBuffer = codec.getInputBuffer(inputIndex);
// ... 填充数据 ...
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
info.flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
codec.queueInputBuffer(inputIndex, 0, dataSize, 0, info.flags);
嗯,这里要注意:EOS 之后不能再送数据了。如果你还想继续解码,必须调用 reset() 回到 Uninitialized 状态,重新 configure。
7.3 异常恢复策略
解码器这东西,说崩就崩。我遇到过的情况包括:
- 硬件解码器被其他应用占满,返回
ERROR_INSUFFICIENT_RESOURCE - 码流格式突然变化,解码器无法处理
- Surface 被销毁,解码器失去输出目标
遇到这些情况怎么办?我的策略是三步走:
- 捕获异常:MediaCodec.Callback 里的 onError 会告诉你出事了
- 安全释放:调用 release() 把当前解码器干掉
- 重新创建:new 一个新的解码器,重新走一遍 configure → start
核心原则:不要试图在异常状态下恢复解码器。直接销毁重建,是最稳的做法。
我曾经试过在 onError 里调用 flush() 想「救」回来,结果解码器直接卡死。后来查文档才发现,onError 之后解码器已经进入不可用状态,任何操作都是徒劳。
7.4 重置与释放
这两个操作容易搞混,我专门列个表:
| 操作 | 目标状态 | 资源释放 | 能否复用 |
|---|---|---|---|
| reset() | Uninitialized | 部分释放 | 可以,重新 configure |
| release() | Released(终结态) | 完全释放 | 不能,必须 new 新的 |
reset() 相当于「重启」。它会清空所有内部状态,但保留 MediaCodec 对象本身。适合在 seek 或切换流时使用。
release() 是「销毁」。调用后这个 MediaCodec 对象就废了,必须重新 create。适合在播放结束或退出页面时调用。
7.5 状态机实战要点
最后,分享几个我在实战中总结的要点:
- 状态检查:每次调用 API 前,先确认当前状态是否合法。比如在 Executing 状态下调用 configure() 会抛异常
- 线程安全:MediaCodec 不是线程安全的。我习惯把所有操作放在同一个线程里,或者加锁保护
- 生命周期绑定:把解码器的创建和释放跟 Activity/Fragment 的生命周期绑定。onResume 时创建,onPause 时释放
- 日志打点:在每次状态切换时打日志。出问题时,看日志就能定位到是哪一步出了问题
状态机这东西,说白了就是个「有限状态自动机」。你只要记住:每个状态能做什么、不能做什么,以及怎么切换到下一个状态,解码器就再也不会给你「惊喜」了。