错误处理与日志:FFmpeg错误码处理、自定义日志系统、异常恢复机制
做音视频开发,说白了就是跟各种不确定因素打交道。文件格式不对、网络断了、编码器罢工……这些破事儿天天见。我刚开始写FFmpeg程序那会儿,最烦的就是程序莫名其妙崩溃,连个提示都没有。后来我学乖了——错误处理和日志系统,必须一开始就设计好。
FFmpeg的错误码体系
FFmpeg的错误码设计得挺有意思。它不像Windows那种HRESULT,也不像POSIX的errno。它用负值表示错误,0表示成功,正值表示非错误状态(比如文件结束)。
核心原则:所有FFmpeg API返回的负数,都是错误。别忽略它们。
常见的错误码长这样:
| 错误码 | 宏定义 | 含义 |
|---|---|---|
| -1 | AVERROR_BSF_NOT_FOUND | 找不到比特流过滤器 |
| -2 | AVERROR_BUG | 内部bug |
| -5 | AVERROR_IO | 输入输出错误 |
| -12 | AVERROR_ENOMEM | 内存不足 |
| -22 | AVERROR(EINVAL) | 无效参数 |
| -541478725 | AVERROR_EOF | 文件结束(注意这是负值) |
你可能会问:为什么EOF是这么个奇怪的值?嗯,FFmpeg内部用AVERROR_EOF表示流结束,它等于-(int)MKTAG('E','O','F',' ')。说白了就是把"EOF "四个字符拼成一个int再取负。这种做法在多媒体领域挺常见的。
错误码转换与打印
FFmpeg提供了两个好用的工具函数:
// 把错误码转成可读字符串
char errbuf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(ret, errbuf, sizeof(errbuf));
printf("错误: %s\n", errbuf);
// 或者直接用这个宏
fprintf(stderr, "打开文件失败: %s\n", av_err2str(ret));
我的习惯:项目中我从来不用av_err2str,因为它返回的是静态缓冲区,多线程下会出问题。我每次都老老实实用av_strerror,自己传缓冲区进去。
自定义日志系统
FFmpeg自带的日志系统其实够用,但说实话,它的输出格式太死板了。我更喜欢自己封装一层。
先看看FFmpeg的日志回调怎么用:
static void my_log_callback(void* ptr, int level,
const char* fmt, va_list vl) {
// 过滤掉太啰嗦的日志
if (level > AV_LOG_WARNING) return;
// 获取当前时间
time_t now = time(nullptr);
char timebuf[32];
strftime(timebuf, sizeof(timebuf),
"%Y-%m-%d %H:%M:%S", localtime(&now));
// 获取模块名
const char* module = "unknown";
if (ptr) {
AVClass* avclass = *(AVClass**)ptr;
if (avclass) module = avclass->item_name(ptr);
}
// 输出到文件
FILE* logfile = get_log_file();
fprintf(logfile, "[%s] [%s] [%s] ",
timebuf, get_level_str(level), module);
vfprintf(logfile, fmt, vl);
fflush(logfile);
}
// 注册回调
av_log_set_callback(my_log_callback);
这段代码看起来简单,但有几个坑:
- 线程安全:日志回调可能从多个线程同时调用,记得加锁
- 性能:AV_LOG_DEBUG级别的日志量巨大,生产环境要过滤掉
- 内存:回调里别做复杂操作,比如分配内存,容易死锁
我曾经踩过的坑:有一次我在日志回调里调用了avformat_open_input,结果死锁了。因为avformat_open_input内部也会打日志,又回调回来,形成了递归。所以记住——日志回调里只做最简单的格式化输出,别调用任何FFmpeg API。
日志级别管理
FFmpeg定义了8个日志级别,从最严重的到最啰嗦的:
| 级别 | 宏 | 说明 |
|---|---|---|
| 0 | AV_LOG_PANIC | 系统崩溃,马上要挂了 |
| 1 | AV_LOG_FATAL | 致命错误,无法继续 |
| 2 | AV_LOG_ERROR | 一般错误 |
| 3 | AV_LOG_WARNING | 警告,但还能继续 |
| 4 | AV_LOG_INFO | 普通信息 |
| 5 | AV_LOG_VERBOSE | 详细信息 |
| 6 | AV_LOG_DEBUG | 调试信息 |
| 7 | AV_LOG_TRACE | 最啰嗦的跟踪信息 |
我个人习惯:线上环境开到AV_LOG_WARNING,开发环境开到AV_LOG_DEBUG。调试棘手问题时才开AV_LOG_TRACE,但那个输出量……你想想看,一秒能刷几万行。
异常恢复机制
音视频处理里,异常恢复是个大学问。程序不能一遇到错误就崩溃,得想办法优雅地处理。
我总结了一套"三层恢复"策略:
- 局部恢复:单个操作失败,重试或跳过
- 流级别恢复:某个流出问题,尝试重新初始化
- 全局恢复:整个处理链出问题,回退到安全状态
举个例子,解码时遇到坏帧:
int decode_frame(AVCodecContext* ctx, AVFrame* frame,
AVPacket* pkt) {
int ret = avcodec_send_packet(ctx, pkt);
if (ret < 0) {
// 局部恢复:清空解码器状态,跳过这帧
avcodec_flush_buffers(ctx);
av_log(NULL, AV_LOG_WARNING,
"跳过损坏的帧,错误码: %s\n",
av_err2str(ret));
return 0; // 返回0表示跳过,不中断流程
}
ret = avcodec_receive_frame(ctx, frame);
if (ret == AVERROR(EAGAIN)) {
// 需要更多数据,正常情况
return 1;
} else if (ret == AVERROR_EOF) {
// 流结束
return -1;
} else if (ret < 0) {
// 流级别恢复:尝试重新打开解码器
av_log(NULL, AV_LOG_ERROR,
"解码器异常,尝试恢复...\n");
return recover_decoder(ctx);
}
return 0;
}
关键点:avcodec_flush_buffers是个好东西。它清空解码器内部状态,但不销毁解码器。遇到坏帧时调用它,解码器就能继续处理后续正常帧。这比销毁重建解码器快得多。
知识体系总览
下面这张图概括了本章的核心内容:
实战中的避坑指南
最后分享几个我实际项目中遇到的坑:
- 别在信号处理函数里打日志:我曾经在SIGSEGV的处理函数里写日志,结果死锁了。信号处理函数能调用的函数非常有限,printf都不安全。
- 日志文件要轮转:一个日志文件写几个GB,打开都费劲。我一般按天轮转,或者按100MB轮转。
- 错误码要记录上下文:光记录"avformat_open_input返回-5"没用。你得同时记录文件名、当前时间、调用栈。不然查问题的时候跟大海捞针一样。
- 异常恢复要有次数限制:别无限重试。我一般设置3次重试上限,超过就放弃,记录错误并通知上层。
一个小技巧:在日志里加上进程ID和线程ID。线上环境多进程多线程跑着,没有这两个ID,你根本分不清日志是谁打的。我习惯用getpid()和pthread_self()拼到日志前缀里。
好了,错误处理和日志这块儿就聊到这儿。记住一句话:好的错误处理不是让程序不崩溃,而是崩溃了你能快速知道为什么。把日志系统设计好,把恢复策略想清楚,你的程序才能扛得住生产环境的毒打。