解码错误处理:错误码解析、数据损坏恢复、解码器重启策略、日志与监控

做视频解码开发,最怕什么?

不是性能瓶颈,也不是兼容性问题。而是——解码器突然崩了,你连它为什么崩都不知道。

我早期做播放器的时候,就吃过这个亏。线上用户反馈播放卡死,我本地怎么都复现不了。后来加了详细的错误日志,才发现是网络差导致的数据流里混进了几个坏帧,解码器直接罢工了。

所以这一章,咱们就聊聊解码错误处理。说白了,就是怎么让解码器在遇到问题时,能优雅地告诉你「我出啥事了」,并且能自己缓过来。

18.1 错误码解析:读懂解码器的「黑话」

Android 的 MediaCodec 在出错时,会通过回调告诉你错误码。但这些错误码,说实话,文档里写得挺含糊的。

我习惯把常见的错误码整理成一张表,方便排查时快速定位。

错误码 常量名 含义 常见场景
-1 INFO_TRY_AGAIN_LATER 暂时没有数据 dequeueOutputBuffer 时缓冲区还没准备好
-1000 ERROR_KEY_FRAME_NOT_FOUND 找不到关键帧 跳转播放时,数据流里没有 IDR 帧
-1001 ERROR_CODEC_QUERY_ERROR 查询解码器信息失败 MediaCodecInfo 获取异常
-1002 ERROR_CODEC_INIT_ERROR 解码器初始化失败 配置参数不对,或硬件不支持
-1003 ERROR_CODEC_RELEASE_ERROR 释放解码器失败 资源泄漏或状态异常
-1004 ERROR_CODEC_UNSUPPORTED 不支持的编码格式 比如用硬解去解 HEVC,但芯片不支持
-1005 ERROR_CODEC_CRASH 解码器内部崩溃 数据损坏、驱动 bug 等
我的习惯: 每次收到错误回调,不要只打 log.e。我会把当前解码器的状态、输入队列长度、输出队列长度、当前 pts 都记录下来。这样排查问题时,信息量会大很多。

18.2 数据损坏恢复:别让一个坏帧毁了整个视频

数据损坏,是解码器崩溃的头号元凶。

你想想看,网络传输过程中,一个包丢了,或者一个字节被篡改了,解码器拿到的是残缺的数据。它尝试去解析,结果内部状态乱了,然后就崩了。

我遇到过最离谱的一次,是某个直播源在推流时,中间混了一段其他编码格式的数据。解码器直接报错,整个播放器卡死。用户骂声一片。

怎么处理?我的策略是分三步:

  1. 检测损坏:在喂数据给解码器之前,先做一层校验。比如检查 NAL 单元的长度是否合理,关键帧是否完整。
  2. 跳过坏帧:如果发现某个帧的数据明显有问题,直接丢弃,不要喂给解码器。宁可丢帧,也别让解码器崩。
  3. 刷新解码器:如果连续多个帧都损坏,或者解码器已经报错了,那就需要刷新解码器的状态。

核心原则: 解码器是脆弱的。你给它坏数据,它就死给你看。所以,数据校验要在解码器外面做。

代码上,我一般会在喂数据前加一个校验函数:

private boolean isFrameValid(ByteBuffer buffer, int size) {
    // 检查数据长度是否合理
    if (size <= 0 || size > MAX_FRAME_SIZE) {
        return false;
    }
    
    // 检查起始码
    byte[] header = new byte[4];
    buffer.position(0);
    buffer.get(header);
    buffer.rewind();
    
    // H.264 起始码通常是 0x00 0x00 0x00 0x01 或 0x00 0x00 0x01
    if (header[0] == 0x00 && header[1] == 0x00) {
        if (header[2] == 0x01 || (header[2] == 0x00 && header[3] == 0x01)) {
            return true;
        }
    }
    
    return false;
}
注意: 这个校验只是最基础的。实际项目中,你可能还需要检查 NAL 单元类型、SPS/PPS 是否完整等。别过度校验,否则会影响性能。

18.3 解码器重启策略:优雅地「重启大法」

解码器崩了怎么办?

最粗暴的方式:直接销毁,重新创建一个。但这样会有一个明显的卡顿,用户体验很差。

我建议的策略是「分级重启」:

  • 轻度恢复:只是丢了一两个帧,解码器还没崩。这时候调用 flush() 清空缓冲区,重新开始解码。这个操作很快,几乎无感。
  • 中度恢复:解码器报错了,但还没完全死。这时候先 stop(),再 start()。注意,stop() 之后要重新配置解码器。
  • 重度恢复:解码器彻底挂了,或者连续多次中度恢复都失败。这时候只能 release() 然后重新 createByCodecName()。这个操作最重,但也是最后的保障。

我曾经在一个项目中,把重启策略做成了自动化的。解码器每崩一次,就记录一次。如果 1 分钟内崩了 3 次以上,就降级到软解。嗯,这个策略后来救了不少低端机用户。

代码示例:

private void restartCodec(int level) {
    switch (level) {
        case RESTART_LIGHT:
            // 轻度:flush 即可
            mediaCodec.flush();
            break;
            
        case RESTART_MEDIUM:
            // 中度:stop + start
            mediaCodec.stop();
            mediaCodec.configure(format, surface, null, 0);
            mediaCodec.start();
            break;
            
        case RESTART_HEAVY:
            // 重度:重新创建
            mediaCodec.release();
            mediaCodec = MediaCodec.createByCodecName(codecName);
            mediaCodec.configure(format, surface, null, 0);
            mediaCodec.start();
            break;
    }
}
我的经验: 重启之后,别忘了把之前的关键帧数据重新喂一遍。否则解码器没有参考帧,出来的画面全是马赛克。

18.4 日志与监控:让问题无处遁形

日志这东西,平时觉得烦,出问题的时候才知道它的好。

我见过太多项目,线上出了问题,日志里只有一行「decode error」。这跟没写一样。

我个人的日志规范是这样的:

  • 每次喂数据:记录输入数据的 pts、大小、是否是关键帧。
  • 每次取数据:记录输出数据的 pts、格式变化、是否有异常。
  • 每次错误回调:记录错误码、当前解码器状态、队列长度、系统时间。
  • 每次重启:记录重启级别、重启耗时、重启次数。

监控方面,我建议关注这几个指标:

指标 说明 告警阈值
解码器崩溃次数 单位时间内解码器崩溃的次数 1 分钟内超过 3 次
丢帧率 丢弃的帧数 / 总帧数 超过 5%
重启耗时 从开始重启到恢复解码的时间 超过 500ms
解码延迟 从喂数据到拿到输出帧的时间 超过 100ms

把这些指标上报到后台,你就能看到全量的解码器健康状态。哪个机型容易崩,哪个码流容易出问题,一目了然。

注意: 日志别打太多。尤其是在低端机上,频繁的 log 会拖慢解码速度。我一般会在 debug 版本全量打,release 版本只打错误和关键信息。

知识体系总览

下面这张图,把解码错误处理的整体流程串起来了。你可以对照着看,心里有个谱。

解码错误处理流程 输入数据流 数据校验 解码器解码 校验失败 丢弃坏帧 输出渲染 错误处理 & 重启 日志 & 监控上报

嗯,这张图把整个流程串起来了。从数据进来,到校验、解码、出错处理,再到日志上报,每一步都不能少。

最后说一句:解码错误处理,不是等到出问题了才去想。而是在写代码的第一天,就把这些逻辑埋进去。等到线上真的出问题了,你会发现,之前的每一行代码都值了。


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