30、综合项目实战(下):实现一个音视频转码与推流工具,集成滤镜、参数调优、进度反馈
上一章我们把工具的基础框架搭好了,能转码、能推流。但说实话,那只是个“能用”的版本。这一章,我们要把它打磨成“好用”的工具。我会带你加入滤镜处理、参数调优,还有用户最关心的——进度反馈。
嗯,先别急着写代码。我们得先理清楚,一个成熟的转码推流工具,到底需要哪些能力?
核心能力矩阵
- 滤镜处理:缩放、裁剪、水印、色彩调整
- 参数调优:码率控制、编码器预设、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 毫秒的间隔。另外,剩余时间可以用 (总帧数 - 已处理帧数) / (速度 × 帧率) 来估算,虽然不太准,但用户看了心里有底。
异常恢复:让工具更健壮
做音视频处理,异常情况太多了。网络断了、编码器崩了、磁盘满了……我早期做的工具,遇到异常就直接退出,用户反馈说“转了一小时,最后告诉我失败了,连个断点续传都没有”。
后来我加了三层防护:
- 断流重连:推流时检测网络状态,断开后自动重连,最多重试 3 次
- 转码 checkpoint:每处理 1000 帧记录一次位置,崩溃后可以从最近的位置恢复
- 资源监控:实时检查磁盘空间和内存使用,提前预警
// 断流重连逻辑
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% 是稳定性和用户体验。滤镜、参数调优、进度反馈、异常恢复,缺一不可。
好了,这一章的内容就到这里。代码示例我都跑过,你可以直接拿去用。如果遇到问题,记得先检查滤镜描述语法,再确认编码参数是否匹配。嗯,动手试试吧。