29、错误排查:常见错误码解析、日志级别调整、调试技巧
说实话,FFmpeg 这玩意儿好用是好用,但报错信息有时候真能把人气笑。我刚开始用那会儿,经常对着满屏的红色输出发呆,完全不知道问题出在哪。后来摸爬滚打多了,才慢慢总结出一套排查套路。
这一章,我就把压箱底的调试经验拿出来。咱们不整虚的,直接看错误码、调日志、上技巧。
29.1 常见错误码:别怕,它们就那几种
FFmpeg 的错误码其实不多,翻来覆去就那几个。我列个表,你对照着看,基本能解决 90% 的问题。
| 错误码 | 数值 | 含义 | 我遇到过的场景 |
|---|---|---|---|
AVERROR_EOF |
-541478725 | 文件读取结束 | 读到一个破损的 MP4 尾部,直接返回这个码 |
AVERROR(EINVAL) |
-22 | 参数无效 | 给解码器传了个不支持的像素格式 |
AVERROR(ENOMEM) |
-12 | 内存不足 | 处理 4K 视频时没释放旧帧,内存爆了 |
AVERROR(EAGAIN) |
-11 | 资源暂时不可用 | 非阻塞模式下读网络流,数据还没到 |
AVERROR_BSF_NOT_FOUND |
-1179861752 | 找不到比特流过滤器 | 写错了过滤器名字,比如把 h264_mp4toannexb 拼错了 |
av_strerror() 用上。它能直接把错误码转成人类能看懂的文字。比如 av_strerror(ret, errbuf, sizeof(errbuf)),比对着数字猜强太多了。
29.2 日志级别调整:从「哑巴」到「话痨」
FFmpeg 默认的日志级别是 AV_LOG_INFO。说白了,它只告诉你「我干了什么」,不告诉你「我为什么干不了」。调试的时候,我们需要把级别调到 AV_LOG_DEBUG 甚至 AV_LOG_TRACE。
怎么调?就一行代码:
av_log_set_level(AV_LOG_DEBUG);
调完之后,输出会变得非常详细。每个包的 PTS、DTS,每个帧的宽高、格式,全都会打印出来。我曾经靠这个抓到一个「视频花屏」的 bug——原来是解码器返回的像素格式跟我预期的不一样,但 AV_LOG_INFO 级别下根本看不到这个信息。
日志级别从低到高是这样的:
AV_LOG_QUIET—— 啥也不输出,适合生产环境AV_LOG_PANIC—— 只有致命错误AV_LOG_FATAL—— 致命错误AV_LOG_ERROR—— 错误信息AV_LOG_WARNING—— 警告信息AV_LOG_INFO—— 常规信息(默认)AV_LOG_VERBOSE—— 详细信息AV_LOG_DEBUG—— 调试信息AV_LOG_TRACE—— 最详细,连函数调用栈都给你打出来
AV_LOG_TRACE。我试过一次,处理一个 10 秒的视频,日志文件直接写了 2GB。嗯,磁盘满了,程序挂了。
29.3 调试技巧:三板斧
光看日志还不够,有时候得动手。我总结了三个最实用的技巧,你试试看。
第一板斧:用 av_hex_dump 看裸数据
有时候你怀疑数据有问题,但不知道具体哪里不对。这时候 av_hex_dump() 就派上用场了。它能直接把内存里的二进制数据打印成十六进制,方便你对照协议规范。
// 打印一个 AVPacket 的前 64 字节
av_hex_dump(stdout, pkt->data, FFMIN(pkt->size, 64));
我记得有一次,客户说视频流有马赛克。我打印了 H.264 的 NAL Unit 头部,发现 nal_unit_type 字段的值不对——原来是编码器在某个 GOP 边界处丢了关键帧的起始码。用 av_hex_dump 一眼就看出来了。
第二板斧:自定义回调函数
默认的日志输出是往 stderr 写的。但你可以自己接管这个输出,把日志写到文件里,或者加上时间戳、线程 ID。
void my_log_callback(void *ptr, int level, const char *fmt, va_list vl) {
// 只处理 DEBUG 级别以上的日志
if (level > AV_LOG_DEBUG) return;
// 加上时间戳
time_t now = time(NULL);
fprintf(log_file, "[%ld] [%d] ", now, level);
vfprintf(log_file, fmt, vl);
fflush(log_file);
}
// 注册回调
av_log_set_callback(my_log_callback);
这个技巧特别适合排查多线程问题。你想想看,如果两个线程同时往 stderr 写日志,输出会混在一起,根本分不清谁是谁。加上线程 ID 之后,一目了然。
第三板斧:断点 + 源码
如果日志和 hex dump 都搞不定,那就得上源码了。FFmpeg 的源码虽然大,但关键路径其实很清晰。我常用的断点位置:
avcodec_send_packet—— 看看输入包是不是对的avcodec_receive_frame—— 看看解码器吐出来的帧对不对avformat_write_header—— 看看封装器初始化有没有问题
我曾经遇到一个诡异的问题:同一个 MP4 文件,用 ffplay 能播,但用我自己写的程序就报 AVERROR_INVALIDDATA。后来在 avformat_find_stream_info 里下了断点,发现我的程序在读取 moov box 时,因为缓冲区太小,只读了一半。嗯,问题出在我自己写的 IO 回调上,跟 FFmpeg 没关系。
29.4 知识体系:一张图看懂调试流程
我把上面这些内容串了一下,画了张图。你调试的时候,按这个流程走,基本不会漏掉什么。
29.5 避坑指南
最后,分享几个我踩过的坑,你遇到了直接绕过去。
- 别信默认日志级别。 我曾经花了一整天查一个「解码失败」的问题,最后发现是
av_log_set_level没调,导致关键警告被过滤了。调成AV_LOG_DEBUG之后,错误原因直接打印出来了。 - av_strerror 一定要用。 别自己猜错误码。我见过有人把
-22当成「文件不存在」,其实人家是「参数无效」。 - 多线程下加锁。 自定义日志回调里如果有共享资源,记得加锁。不然两个线程同时写日志,输出会乱掉。
- 别在生产环境开 TRACE。 嗯,这个我说过了,但值得再说一遍。磁盘写满的滋味不好受。
好了,这一章的内容就到这。调试这东西,说白了就是「多看、多试、多想」。你多碰几次壁,自然就熟练了。