解码错误处理:错误码解析、数据损坏恢复、解码器重启策略、日志与监控
做视频解码开发,最怕什么?
不是性能瓶颈,也不是兼容性问题。而是——解码器突然崩了,你连它为什么崩都不知道。
我早期做播放器的时候,就吃过这个亏。线上用户反馈播放卡死,我本地怎么都复现不了。后来加了详细的错误日志,才发现是网络差导致的数据流里混进了几个坏帧,解码器直接罢工了。
所以这一章,咱们就聊聊解码错误处理。说白了,就是怎么让解码器在遇到问题时,能优雅地告诉你「我出啥事了」,并且能自己缓过来。
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 等 |
18.2 数据损坏恢复:别让一个坏帧毁了整个视频
数据损坏,是解码器崩溃的头号元凶。
你想想看,网络传输过程中,一个包丢了,或者一个字节被篡改了,解码器拿到的是残缺的数据。它尝试去解析,结果内部状态乱了,然后就崩了。
我遇到过最离谱的一次,是某个直播源在推流时,中间混了一段其他编码格式的数据。解码器直接报错,整个播放器卡死。用户骂声一片。
怎么处理?我的策略是分三步:
- 检测损坏:在喂数据给解码器之前,先做一层校验。比如检查 NAL 单元的长度是否合理,关键帧是否完整。
- 跳过坏帧:如果发现某个帧的数据明显有问题,直接丢弃,不要喂给解码器。宁可丢帧,也别让解码器崩。
- 刷新解码器:如果连续多个帧都损坏,或者解码器已经报错了,那就需要刷新解码器的状态。
核心原则: 解码器是脆弱的。你给它坏数据,它就死给你看。所以,数据校验要在解码器外面做。
代码上,我一般会在喂数据前加一个校验函数:
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;
}
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 |
把这些指标上报到后台,你就能看到全量的解码器健康状态。哪个机型容易崩,哪个码流容易出问题,一目了然。
知识体系总览
下面这张图,把解码错误处理的整体流程串起来了。你可以对照着看,心里有个谱。
嗯,这张图把整个流程串起来了。从数据进来,到校验、解码、出错处理,再到日志上报,每一步都不能少。
最后说一句:解码错误处理,不是等到出问题了才去想。而是在写代码的第一天,就把这些逻辑埋进去。等到线上真的出问题了,你会发现,之前的每一行代码都值了。
公众号:蓝海资料掘金营,微信deep3321