7. NuPlayer的解码器管理:Decoder与DecoderBase、Decoder的创建与销毁、编解码器错误处理

解码器管理,说白了就是NuPlayer的"心脏"。没有它,你播的视频就是一堆乱码。我在做Android多媒体开发那几年,跟解码器打过无数次交道,踩过的坑能写满一页A4纸。今天咱们就把它彻底聊透。

7.1 Decoder与DecoderBase:一对父子档

NuPlayer里,解码器不是凭空冒出来的。它有一套清晰的继承体系。顶层是DecoderBase,一个抽象基类。它定义了所有解码器的通用行为。比如启动、停止、刷新、错误处理这些骨架方法。

真正的干活角色是Decoder,它继承自DecoderBase。你想想看,为什么非要拆成两个类?

我个人习惯把DecoderBase理解为"协议",Decoder是"实现"。这样设计的好处是:未来如果Google想支持新的解码后端(比如硬件解码器),只需要再写一个子类,基类代码完全不用动。嗯,这就是面向接口编程的典型应用。

核心关系图:

DecoderBase 抽象基类 Decoder 具体实现类 继承 NuPlayer::Decoder 内部类 DecoderBase::Looper 消息循环 包含 组合关系

你看这个图就清楚了。DecoderBase是骨架,Decoder是血肉。而NuPlayer::DecoderDecoder的进一步封装,它内部还跑着一个Looper来做异步消息处理。

7.2 解码器的创建与销毁

创建解码器,不是new一个对象就完事了。它背后有一套复杂的协商流程。

我举个例子。NuPlayer在播放视频时,会先拿到MediaCodec的实例。这个实例怎么来的?

// 伪代码,展示核心逻辑
sp<MediaCodec> codec = MediaCodec::CreateByType(
    looper, mimeType, false /* encoder */);

if (codec == nullptr) {
    // 软解兜底
    codec = MediaCodec::CreateByComponentName(
        looper, "OMX.google.h264.decoder");
}

这里有个细节:CreateByType会去查询系统里所有能处理这个mime类型的解码器。优先选硬件解码器。如果硬件不支持,或者分配失败,就回退到软件解码器。

避坑指南:我曾经遇到过一个诡异的问题——某些低端芯片上,硬件解码器创建成功了,但解码出来的画面全是绿色条纹。后来发现是硬件解码器对分辨率有限制,超过1080P就会出问题。解决方案就是在创建后主动查询getCapabilities,确认当前分辨率在支持范围内。

销毁解码器,同样不能马虎。直接delete会出大问题。因为解码器内部有异步线程在跑,有待处理的buffer队列。正确的销毁流程是:

  1. 先调用stop(),停止解码循环
  2. 再调用release(),释放底层资源
  3. 最后才析构对象

你想想看,如果顺序搞反了,底层OMX组件还在处理数据,上层对象突然没了——轻则crash,重则整个mediaserver进程挂掉。

7.3 编解码器错误处理

解码器在运行过程中,什么妖蛾子都可能出现。我总结了几类常见错误:

错误类型 错误码 典型原因 处理策略
资源不足 ERROR_IO 内存不够、OMX组件耗尽 释放其他解码器,重试
格式不支持 ERROR_UNSUPPORTED 编码参数超出硬件能力 回退到软解
数据异常 ERROR_MALFORMED 码流损坏、关键帧丢失 刷新解码器,跳过当前帧
超时 ERROR_TIMED_OUT 解码器卡死、死锁 强制重启解码器

NuPlayer的错误处理机制,核心在DecoderBase::onError()方法里。它会向NuPlayer发送一个kWhatError消息。NuPlayer收到后,会决定是尝试恢复,还是直接通知上层播放失败。

注意:千万不要在解码器错误回调里直接做耗时操作。我曾经见过一个同事在onError里写日志写到SD卡,结果I/O阻塞导致解码器线程卡死,整个播放器都hang住了。正确的做法是:把错误信息post到主线程,让主线程去处理。

还有一个容易被忽略的点:解码器错误恢复。不是所有错误都能恢复的。比如ERROR_IO这种资源型错误,重试几次可能就好了。但ERROR_MALFORMED这种数据型错误,重试多少次都没用,因为码流本身就是坏的。

我个人习惯的做法是:

  • 第一次出错:尝试刷新解码器,重新配置
  • 第二次出错:尝试重建解码器(销毁再创建)
  • 第三次出错:直接上报失败,不再挣扎

这个"三次原则"是我在实际项目中摸索出来的。太少了容易误判,太多了浪费性能。

7.4 解码器状态机

解码器内部是一个严格的状态机。不理解状态机,你就没法真正掌控解码器。

UNINITIALIZED INITIALIZED CONFIGURED FLUSHING RUNNING ERROR RELEASED configure start flush flush完成 出错 release

状态机里有个关键点:FLUSHING状态。为什么需要这个状态?因为解码器内部有buffer队列,你直接stop的话,队列里的数据就丢了。Flush的作用是:优雅地清空所有待处理数据,确保不会出现"半截子"帧。

嗯,这里要注意。Flush操作是同步的,它会阻塞直到所有buffer都回来。如果解码器卡住了,Flush也会卡住。所以实际代码里,Flush通常带超时机制。

核心总结:解码器管理,说白了就是三件事——创建时选对解码器,运行时管好状态机,出错时优雅恢复。这三件事做好了,播放器就稳了八成。


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