30、综合项目实战(下):实现一个音视频转码与推流工具,集成滤镜、参数调优、进度反馈

上一章我们把工具的基础框架搭好了,能转码、能推流。但说实话,那只是个“能用”的版本。这一章,我们要把它打磨成“好用”的工具。我会带你加入滤镜处理、参数调优,还有用户最关心的——进度反馈。

嗯,先别急着写代码。我们得先理清楚,一个成熟的转码推流工具,到底需要哪些能力?

核心能力矩阵

  • 滤镜处理:缩放、裁剪、水印、色彩调整
  • 参数调优:码率控制、编码器预设、GOP 大小
  • 进度反馈:实时百分比、处理速度、剩余时间
  • 异常恢复:断流重连、转码失败回滚
音视频转码推流工具架构 输入源 文件 / 摄像头 / 网络流 解封装 (Demuxer) 解码 (Decoder) 滤镜处理 缩放 / 裁剪 / 水印 编码调优 码率 / 预设 / GOP 进度反馈 百分比 / 速度 / 剩余时间 输出:本地文件 / 推流地址

滤镜处理:不只是加个水印那么简单

滤镜处理,说白了就是在编码之前对原始帧做一次“美容”。我个人习惯把滤镜分为三类:几何变换(缩放、裁剪)、像素操作(亮度、对比度)、叠加效果(水印、字幕)。

在 FFmpeg 里,滤镜通过 filter graph 来组织。我刚开始接触时觉得这玩意儿很绕,后来发现其实就是个有向无环图。每个节点是一个滤镜,边是数据流。

// 构建滤镜链:缩放 + 水印
const char* filter_desc = "scale=1280:720, drawtext=text='Hello':x=10:y=10:fontsize=24:fontcolor=white";

AVFilterGraph* filter_graph = avfilter_graph_alloc();
AVFilterContext* buffersrc_ctx = nullptr;
AVFilterContext* buffersink_ctx = nullptr;

// 创建输入输出
avfilter_graph_create_filter(&buffersrc_ctx, avfilter_get_by_name("buffer"), "in",
                             nullptr, nullptr, filter_graph);
avfilter_graph_create_filter(&buffersink_ctx, avfilter_get_by_name("buffersink"), "out",
                             nullptr, nullptr, filter_graph);

// 解析滤镜描述
AVFilterInOut* outputs = avfilter_inout_alloc();
AVFilterInOut* inputs  = avfilter_inout_alloc();
outputs->name       = av_strdup("in");
outputs->filter_ctx = buffersrc_ctx;
outputs->pad_idx    = 0;
outputs->next       = nullptr;
inputs->name        = av_strdup("out");
inputs->filter_ctx  = buffersink_ctx;
inputs->pad_idx     = 0;
inputs->next        = nullptr;

avfilter_graph_parse_ptr(filter_graph, filter_desc, &inputs, &outputs, nullptr);
avfilter_graph_config(filter_graph, nullptr);

避坑指南:我曾经在 drawtext 滤镜上栽过跟头——字体文件路径写错了,结果水印死活出不来。记住,drawtext 的 fontfile 参数必须传绝对路径,或者把字体文件放在 FFmpeg 能找到的地方。

参数调优:找到质量与性能的平衡点

编码参数调优,是区分“能用”和“好用”的关键。你想想看,同样的源文件,有人转出来画质好、体积小,有人转出来又大又糊。区别就在参数上。

我个人最常用的调优参数有这几个:

参数 作用 推荐值 我的经验
-crf 恒定质量因子 18-28 直播用 23,存档用 18
-preset 编码速度预设 ultrafast ~ veryslow 实时推流用 veryfast,离线用 medium
-g GOP 大小 帧率 × 2~4 30fps 视频设 60~120
-b:v 视频码率 根据分辨率定 1080p 建议 4~8 Mbps
-maxrate 最大码率 1.5× 目标码率 防止码率尖峰导致卡顿

嗯,这里要注意:CRF 和码率控制是互斥的。用了 CRF 就别再设 -b:v,否则后者会覆盖前者。我见过不少新手在这上面踩坑。

// 动态设置编码参数
AVDictionary* codec_opts = nullptr;
av_dict_set(&codec_opts, "crf", "23", 0);
av_dict_set(&codec_opts, "preset", "veryfast", 0);
av_dict_set(&codec_opts, "g", "60", 0);
av_dict_set(&codec_opts, "maxrate", "6000k", 0);
av_dict_set(&codec_opts, "bufsize", "8000k", 0);

// 打开编码器时传入
avcodec_open2(enc_ctx, codec, &codec_opts);

注意:不同编码器支持的参数不一样。比如 H.264 的 preset 在 H.265 里可能不适用。建议根据实际使用的编码器,查阅 FFmpeg 文档确认参数是否有效。

进度反馈:让用户不再焦虑

做转码工具,最怕用户等得不耐烦直接关掉。进度反馈不是锦上添花,而是刚需。我习惯用回调函数的方式,把进度信息抛给上层 UI。

怎么算进度?其实很简单:已经处理的帧数 / 总帧数。但总帧数怎么拿?对于视频文件,可以从 AVStream->nb_frames 获取。不过有些容器格式不记录这个值,那就得用 duration * fps 估算。

// 进度回调结构体
struct ProgressInfo {
    int64_t total_frames;      // 总帧数
    int64_t processed_frames;  // 已处理帧数
    double  speed;             // 处理速度 (倍速)
    int     percent;           // 百分比 0-100
    std::string status;        // 当前状态描述
};

// 回调函数类型
using ProgressCallback = std::function<void(const ProgressInfo&)>;

// 在编码循环中调用
void encode_loop(AVCodecContext* enc_ctx, AVFormatContext* ofmt_ctx,
                 ProgressCallback on_progress) {
    int64_t total = get_total_frames(ofmt_ctx);
    int64_t processed = 0;
    auto start_time = std::chrono::steady_clock::now();

    while (/* 还有帧 */) {
        // ... 编码一帧 ...
        processed++;

        // 每 30 帧更新一次进度,避免频繁回调
        if (processed % 30 == 0) {
            auto now = std::chrono::steady_clock::now();
            double elapsed = std::chrono::duration<double>(now - start_time).count();
            double speed = processed / elapsed / get_fps();  // 倍速

            ProgressInfo info;
            info.total_frames = total;
            info.processed_frames = processed;
            info.speed = speed;
            info.percent = static_cast<int>(processed * 100.0 / total);
            info.status = "正在编码...";

            on_progress(info);
        }
    }
}

小技巧:进度回调不要每帧都触发,否则 UI 线程会被刷爆。我一般设 30 帧或 100 毫秒的间隔。另外,剩余时间可以用 (总帧数 - 已处理帧数) / (速度 × 帧率) 来估算,虽然不太准,但用户看了心里有底。

异常恢复:让工具更健壮

做音视频处理,异常情况太多了。网络断了、编码器崩了、磁盘满了……我早期做的工具,遇到异常就直接退出,用户反馈说“转了一小时,最后告诉我失败了,连个断点续传都没有”。

后来我加了三层防护:

  1. 断流重连:推流时检测网络状态,断开后自动重连,最多重试 3 次
  2. 转码 checkpoint:每处理 1000 帧记录一次位置,崩溃后可以从最近的位置恢复
  3. 资源监控:实时检查磁盘空间和内存使用,提前预警
// 断流重连逻辑
int push_stream(AVFormatContext* ofmt_ctx, const char* url) {
    int retry_count = 0;
    const int max_retries = 3;

    while (retry_count < max_retries) {
        int ret = avio_open2(ofmt_ctx->pb, url, AVIO_FLAG_WRITE,
                             nullptr, nullptr);
        if (ret >= 0) {
            return 0;  // 成功
        }

        retry_count++;
        fprintf(stderr, "推流失败,第 %d 次重试...\n", retry_count);
        std::this_thread::sleep_for(std::chrono::seconds(2));
    }

    return -1;  // 重试耗尽
}

核心要点:一个成熟的音视频工具,功能只占 50%,另外 50% 是稳定性和用户体验。滤镜、参数调优、进度反馈、异常恢复,缺一不可。

好了,这一章的内容就到这里。代码示例我都跑过,你可以直接拿去用。如果遇到问题,记得先检查滤镜描述语法,再确认编码参数是否匹配。嗯,动手试试吧。

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