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 知识体系:一张图看懂调试流程

我把上面这些内容串了一下,画了张图。你调试的时候,按这个流程走,基本不会漏掉什么。

FFmpeg 错误排查流程 错误发生 判断错误码(对照表格) 调日志级别到 DEBUG 用 av_hex_dump 看数据 下断点 + 读源码 av_log_set_callback 自定义日志输出 打印 NAL Unit / PES 头部 对照协议规范 avcodec_send_packet avcodec_receive_frame 定位并修复问题

29.5 避坑指南

最后,分享几个我踩过的坑,你遇到了直接绕过去。

  • 别信默认日志级别。 我曾经花了一整天查一个「解码失败」的问题,最后发现是 av_log_set_level 没调,导致关键警告被过滤了。调成 AV_LOG_DEBUG 之后,错误原因直接打印出来了。
  • av_strerror 一定要用。 别自己猜错误码。我见过有人把 -22 当成「文件不存在」,其实人家是「参数无效」。
  • 多线程下加锁。 自定义日志回调里如果有共享资源,记得加锁。不然两个线程同时写日志,输出会乱掉。
  • 别在生产环境开 TRACE。 嗯,这个我说过了,但值得再说一遍。磁盘写满的滋味不好受。

好了,这一章的内容就到这。调试这东西,说白了就是「多看、多试、多想」。你多碰几次壁,自然就熟练了。


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