7、解码器状态管理:状态机详解

解码器的状态管理,说白了就是一套「交通规则」。

我刚开始做视频开发时,觉得这玩意儿不就是 open、decode、close 嘛,有什么好研究的?直到有一次线上崩溃,解码器在错误的状态下被调用了 release,直接导致整个播放器卡死。嗯,从那以后,我老老实实把状态机背得滚瓜烂熟。

7.1 状态机五态模型

Android MediaCodec 的状态机,一共五个状态。我习惯用一张图来记:

Uninitialized 未初始化 Configured 已配置 Executing 执行中 Flushed 已刷新 End-of-Stream 流结束 configure() start() flush() signal EOS reset() release() → Released(终结态)

这张图我建议你存下来。每次写解码器代码前看一眼,能省不少调试时间。

7.2 状态流转详解

7.2.1 Uninitialized → Configured

解码器刚创建出来,就是 Uninitialized 状态。这时候它啥也干不了,就是个空壳子。

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

调用 configure() 之后,才会进入 Configured 状态。这里有个坑——configure 必须传入正确的 MediaFormat。我在项目中遇到过,有人传了空的 format,结果 configure 直接抛 IllegalStateException。

注意:Configured 状态下,解码器还没有真正分配资源。只是把参数记下来了。真正的「大动作」在 start() 之后。

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,结果用户反馈「画面像打翻了调色盘」。

我的习惯:每次 seek 或切换流之后,先 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 被销毁,解码器失去输出目标

遇到这些情况怎么办?我的策略是三步走:

  1. 捕获异常:MediaCodec.Callback 里的 onError 会告诉你出事了
  2. 安全释放:调用 release() 把当前解码器干掉
  3. 重新创建:new 一个新的解码器,重新走一遍 configure → start

核心原则:不要试图在异常状态下恢复解码器。直接销毁重建,是最稳的做法。

我曾经试过在 onError 里调用 flush() 想「救」回来,结果解码器直接卡死。后来查文档才发现,onError 之后解码器已经进入不可用状态,任何操作都是徒劳。

7.4 重置与释放

这两个操作容易搞混,我专门列个表:

操作 目标状态 资源释放 能否复用
reset() Uninitialized 部分释放 可以,重新 configure
release() Released(终结态) 完全释放 不能,必须 new 新的

reset() 相当于「重启」。它会清空所有内部状态,但保留 MediaCodec 对象本身。适合在 seek 或切换流时使用。

release() 是「销毁」。调用后这个 MediaCodec 对象就废了,必须重新 create。适合在播放结束或退出页面时调用。

避坑指南:我曾经在 Activity 的 onDestroy 里忘了调用 release(),结果导致解码器泄漏。Android 的 MediaCodec 是有限资源,不释放的话,其他应用可能无法创建解码器。

7.5 状态机实战要点

最后,分享几个我在实战中总结的要点:

  • 状态检查:每次调用 API 前,先确认当前状态是否合法。比如在 Executing 状态下调用 configure() 会抛异常
  • 线程安全:MediaCodec 不是线程安全的。我习惯把所有操作放在同一个线程里,或者加锁保护
  • 生命周期绑定:把解码器的创建和释放跟 Activity/Fragment 的生命周期绑定。onResume 时创建,onPause 时释放
  • 日志打点:在每次状态切换时打日志。出问题时,看日志就能定位到是哪一步出了问题

状态机这东西,说白了就是个「有限状态自动机」。你只要记住:每个状态能做什么、不能做什么,以及怎么切换到下一个状态,解码器就再也不会给你「惊喜」了。

公众号:蓝海资料掘金营,微信deep3321