错误处理与日志: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,但那个输出量……你想想看,一秒能刷几万行。

异常恢复机制

音视频处理里,异常恢复是个大学问。程序不能一遇到错误就崩溃,得想办法优雅地处理。

我总结了一套"三层恢复"策略:

  1. 局部恢复:单个操作失败,重试或跳过
  2. 流级别恢复:某个流出问题,尝试重新初始化
  3. 全局恢复:整个处理链出问题,回退到安全状态

举个例子,解码时遇到坏帧:

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是个好东西。它清空解码器内部状态,但不销毁解码器。遇到坏帧时调用它,解码器就能继续处理后续正常帧。这比销毁重建解码器快得多。

知识体系总览

下面这张图概括了本章的核心内容:

错误处理与日志系统 FFmpeg错误码处理 自定义日志系统 异常恢复机制 av_strerror / av_err2str 错误码转换与打印 常见错误码对照表 av_log_set_callback 日志级别管理 线程安全与性能 局部恢复:跳过坏帧 流级别:重新初始化 全局恢复:回退安全态 核心原则:不忽略任何错误码,日志要可追溯,恢复要分层 avcodec_flush_buffers 是快速恢复的关键

实战中的避坑指南

最后分享几个我实际项目中遇到的坑:

  • 别在信号处理函数里打日志:我曾经在SIGSEGV的处理函数里写日志,结果死锁了。信号处理函数能调用的函数非常有限,printf都不安全。
  • 日志文件要轮转:一个日志文件写几个GB,打开都费劲。我一般按天轮转,或者按100MB轮转。
  • 错误码要记录上下文:光记录"avformat_open_input返回-5"没用。你得同时记录文件名、当前时间、调用栈。不然查问题的时候跟大海捞针一样。
  • 异常恢复要有次数限制:别无限重试。我一般设置3次重试上限,超过就放弃,记录错误并通知上层。

一个小技巧:在日志里加上进程ID和线程ID。线上环境多进程多线程跑着,没有这两个ID,你根本分不清日志是谁打的。我习惯用getpid()pthread_self()拼到日志前缀里。

好了,错误处理和日志这块儿就聊到这儿。记住一句话:好的错误处理不是让程序不崩溃,而是崩溃了你能快速知道为什么。把日志系统设计好,把恢复策略想清楚,你的程序才能扛得住生产环境的毒打。

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