觉得有用,就请您帮忙点赞转发收藏吧,您的鼓励是我创作的动力,多谢看官。
由于能力水平有限,文中的错误或不严谨的地方在所难免,还请批评指正。
目录
[1. 先建立直觉:滤镜图就是一条"管道"](#1. 先建立直觉:滤镜图就是一条"管道")
[2. 滤镜系统的四大件](#2. 滤镜系统的四大件)
[3. 搭一条最简单的视频滤镜链](#3. 搭一条最简单的视频滤镜链)
[3.1 创建滤镜图](#3.1 创建滤镜图)
[3.2 创建输入端(buffer)](#3.2 创建输入端(buffer))
[3.3 创建输出端(buffersink)](#3.3 创建输出端(buffersink))
[3.4 插入中间的滤镜(scale)](#3.4 插入中间的滤镜(scale))
[3.5 连接滤镜](#3.5 连接滤镜)
[3.6 配置并校验整张图](#3.6 配置并校验整张图)
[4. 把帧送进去、取出来](#4. 把帧送进去、取出来)
[4.1 送帧](#4.1 送帧)
[4.2 取帧](#4.2 取帧)
[5. 用字符串语法搭图(90% 场景推荐)](#5. 用字符串语法搭图(90% 场景推荐))
[6. 音频滤镜:跟视频几乎一模一样](#6. 音频滤镜:跟视频几乎一模一样)
[6.1 音频 abuffer 的初始化](#6.1 音频 abuffer 的初始化)
[6.2 音量滤镜](#6.2 音量滤镜)
[6.3 变速不变调(播放大杀器)](#6.3 变速不变调(播放大杀器))
[7. FFmpeg 6 里滤镜的几个实用变化](#7. FFmpeg 6 里滤镜的几个实用变化)
[7.1 音频 channel layout 统一用 AVChannelLayout](#7.1 音频 channel layout 统一用 AVChannelLayout)
[7.2 buffersink 支持动态格式查询](#7.2 buffersink 支持动态格式查询)
[7.3 硬件帧滤镜(hwupload/hwdownload)](#7.3 硬件帧滤镜(hwupload/hwdownload))
[8. 一个完整的视频滤镜示例(可直接跑骨架)](#8. 一个完整的视频滤镜示例(可直接跑骨架))
[9. 常见坑速查表](#9. 常见坑速查表)
[10. 滤镜 + seek 的正确姿势](#10. 滤镜 + seek 的正确姿势)
[11. 脑内流程图](#11. 脑内流程图)
如果你写过 FFmpeg 的解码和编码,大概率会觉得 API 还算规整:Packet 进,Frame 出,send/receive 一套走天下。但第一次翻开 libavfilter 的头文件,很多人会愣住------这边的概念密度突然翻倍了。
滤镜(filter)不是"调一个函数处理一帧"那么简单,而是一个微型的有向图系统。 今天按博客风格,把 FFmpeg 6 里视频/音频滤镜的核心函数和套路捋一遍,尽量让你看完能写出一个能跑的 filter 链。
1. 先建立直觉:滤镜图就是一条"管道"
FFmpeg 的滤镜系统可以理解成这样:
buffer → scale → overlay → rotate → buffersink
↑ ↑
AVFrame 从这里灌入 AVFrame 从这里取出
-
buffer:图的源头 ,你负责把解码好的AVFrame塞进来 -
buffersink:图的终点 ,你从这里把处理完的AVFrame取走 -
中间每一个节点(scale、overlay、rotate)都是一个
AVFilter
类比:就像 Shell 里的管道
cat input.yuv | ffmpeg -vf "scale=1280:720,rotate=45" > output.yuv,只不过你在 C 代码里手工搭这条管道。
2. 滤镜系统的四大件
| 概念 | 结构体 | 作用 |
|---|---|---|
| 滤镜 | AVFilter |
一个算法的描述(如 scale、overlay) |
| 滤镜上下文 | AVFilterContext |
一个滤镜的实例,带私有状态 |
| 滤镜图 | AVFilterGraph |
所有滤镜和连接的容器 |
| 滤镜链描述 | 字符串(如 "scale=1280:720") |
人类可读的配置,传给 avfilter_graph_parse_ptr |
记住一句话:AVFilter是类,AVFilterContext是对象,AVFilterGraph是对象管理器。
3. 搭一条最简单的视频滤镜链
3.1 创建滤镜图
AVFilterGraph *graph = avfilter_graph_alloc();
// 可选:降低日志噪音
graph->nb_threads = 0; // 让 FFmpeg 自动选线程数
3.2 创建输入端(buffer)
const AVFilter *buf_src = avfilter_get_by_name("buffer");
AVFilterContext *buf_src_ctx;
char args[512];
snprintf(args, sizeof(args),
"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
dec_ctx->width, dec_ctx->height,
dec_ctx->pix_fmt,
1, 25, // time_base,随便填个合理的
1, 1); // sar
avfilter_graph_create_filter(&buf_src_ctx, buf_src, "in",
args, NULL, graph);
buffer滤镜的作用是:告诉滤镜图,进来的帧长什么样。它不像别的滤镜那样"处理"帧,它只是个入口。
3.3 创建输出端(buffersink)
const AVFilter *buf_sink = avfilter_get_by_name("buffersink");
AVFilterContext *buf_sink_ctx;
avfilter_graph_create_filter(&buf_sink_ctx, buf_sink, "out",
NULL, NULL, graph);
buffersink是出口,你不给它配参数,它只负责把处理完的帧递给你。
3.4 插入中间的滤镜(scale)
const AVFilter *scale = avfilter_get_by_name("scale");
AVFilterContext *scale_ctx;
char scale_args[64];
snprintf(scale_args, sizeof(scale_args), "w=1280:h=720");
avfilter_graph_create_filter(&scale_ctx, scale, "scale",
scale_args, NULL, graph);
3.5 连接滤镜
// buffer → scale
avfilter_link(buf_src_ctx, 0, scale_ctx, 0);
// scale → buffersink
avfilter_link(scale_ctx, 0, buf_sink_ctx, 0);
avfilter_link(src, src_pad, dst, dst_pad)的 pad 索引通常是 0(大多数滤镜只有一个输入一个输出)。
3.6 配置并校验整张图
avfilter_graph_config(graph, NULL);
这一步会:
-
推导各滤镜的实际参数(比如 scale 的输出 pix_fmt)
-
分配内部缓冲区
-
检查连接合法性
失败的常见原因:格式不兼容、参数非法、图中有环。
4. 把帧送进去、取出来
4.1 送帧
// decoded_frame 是 avcodec_receive_frame 得到的
av_buffersrc_add_frame_flags(buf_src_ctx, decoded_frame,
AV_BUFFERSRC_FLAG_KEEP_REF);
flags 常用选项:
| Flag | 含义 |
|---|---|
AV_BUFFERSRC_FLAG_KEEP_REF |
不窃取 frame 的引用计数,调用方负责 unref |
0 |
默认行为,滤镜图接管 frame,你不该再碰它 |
AV_BUFFERSRC_FLAG_NO_CHECK_FORMAT |
跳过格式检查(慎用) |
4.2 取帧
AVFrame *filtered = av_frame_alloc();
int ret = av_buffersink_get_frame(buf_sink_ctx, filtered);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
av_frame_free(&filtered);
// EAGAIN = 还没输出,继续送帧
// EOF = 没更多输出了
} else if (ret >= 0) {
// filtered 就是处理完的帧
// 用完记得 av_frame_unref(filtered)
}
跟编解码器的 send/receive 很像对吧?滤镜系统也是个状态机:送进去不一定马上出,得持续抽。
5. 用字符串语法搭图(90% 场景推荐)
上面那种手工 create_filter + link的方式太啰嗦了。FFmpeg 提供了字符串解析 的方式,跟命令行 -vf一样的语法:
AVFilterGraph *graph = avfilter_graph_alloc();
const char *filters_desc = "scale=1280:720,transpose=1";
// 或更复杂的:
// "crop=iw/2:ih/2,scale=640:480,hflip"
AVFilterInOut *inputs = NULL;
AVFilterInOut *outputs = NULL;
// 创建输入/输出端点
avfilter_inout_alloc(&inputs);
avfilter_inout_alloc(&outputs);
inputs->name = av_strdup("in");
inputs->filter_ctx = buf_src_ctx;
inputs->pad_idx = 0;
inputs->next = NULL;
outputs->name = av_strdup("out");
outputs->filter_ctx = buf_sink_ctx;
outputs->pad_idx = 0;
outputs->next = NULL;
// 解析字符串并自动创建中间滤镜、自动连接
avfilter_graph_parse_ptr(graph, filters_desc, &inputs, &outputs, NULL);
// 配置
avfilter_graph_config(graph, NULL);
// 清理
avfilter_inout_free(&inputs);
avfilter_inout_free(&outputs);
这几乎是生产代码的标配写法,比手工 link 少一半代码量,还不容易连错。
6. 音频滤镜:跟视频几乎一模一样
音频滤镜的 API 结构和视频完全一致,区别在于:
| 视频 | 音频 |
|---|---|
buffer |
abuffer |
buffersink |
abuffersink |
| 参数:width/height/pix_fmt | 参数:sample_rate/ch_layout/sample_fmt |
| scale | aresample / volume / atempo |
6.1 音频 abuffer 的初始化
const AVFilter *abuffer = avfilter_get_by_name("abuffer");
AVFilterContext *abuffer_ctx;
char abuf_args[256];
snprintf(abuf_args, sizeof(abuf_args),
"time_base=1/44100:"
"sample_rate=44100:"
"sample_fmt=%s:"
"channel_layout=%lld",
av_get_sample_fmt_name(AV_SAMPLE_FMT_S16),
AV_CH_LAYOUT_STEREO);
avfilter_graph_create_filter(&abuffer_ctx, abuffer, "in",
abuf_args, NULL, graph);
6.2 音量滤镜
const AVFilter *volume = avfilter_get_by_name("volume");
AVFilterContext *vol_ctx;
avfilter_graph_create_filter(&vol_ctx, volume, "volume",
"volume=0.5", NULL, graph); // 50% 音量
6.3 变速不变调(播放大杀器)
const AVFilter *atempo = avfilter_get_by_name("atempo");
AVFilterContext *atempo_ctx;
avfilter_graph_create_filter(&atempo_ctx, atempo, "atempo",
"tempo=1.5", NULL, graph); // 1.5x 速度
7. FFmpeg 6 里滤镜的几个实用变化
7.1 音频 channel layout 统一用 AVChannelLayout
跟前面聊的 avcodec变化一致,滤镜里也不再推荐用 channel_layout的 uint64 mask:
// 旧(deprecated)
"channel_layout=3" // stereo
// 新(FFmpeg 6)
"channel_layout=stereo"
// 或更复杂的
"channel_layout=5.1"
7.2 buffersink 支持动态格式查询
enum AVPixelFormat pix_fmt;
int ret = av_opt_get_int(buf_sink_ctx, "pix_fmt", AV_OPT_SEARCH_CHILDREN, &pix_fmt);
// 或音频
int sample_rate;
av_opt_get_int(buf_sink_ctx, "sample_rate", AV_OPT_SEARCH_CHILDREN, &sample_rate);
这在你不知道滤镜链最终输出什么格式时特别有用(比如 scale 可能输出 NV12 或 YUV420P,取决于显卡和 flags)。
7.3 硬件帧滤镜(hwupload/hwdownload)
FFmpeg 6 对硬件帧的支持更完整。如果你在用 DXVA2 解码,想把帧送到滤镜链处理,需要过桥:
// DXVA2 解码出的帧 → hwdownload → 软件滤镜 → hwupload → DXVA2 渲染
const char *filters =
"hwdownload,"
"format=nv12,"
"scale=1280:720,"
"hwupload";
// hwupload 会自动选择当前硬件上下文
⚠️ 硬件帧不能直接进普通滤镜(scale/rotate 等大多是 CPU 实现),必须先
hwdownload落到系统内存,处理完再hwupload回去。这一来一回有拷贝开销,是性能热点。
8. 一个完整的视频滤镜示例(可直接跑骨架)
int setup_video_filter(AVCodecContext *dec_ctx, AVFilterGraph **out_graph,
AVFilterContext **out_src, AVFilterContext **out_sink)
{
AVFilterGraph *graph = avfilter_graph_alloc();
// buffer 源
const AVFilter *buf_src = avfilter_get_by_name("buffer");
AVFilterContext *src_ctx;
char args[512];
snprintf(args, sizeof(args),
"video_size=%dx%d:pix_fmt=%d:time_base=1/25:sar=1/1",
dec_ctx->width, dec_ctx->height, dec_ctx->pix_fmt);
avfilter_graph_create_filter(&src_ctx, buf_src, "in", args, NULL, graph);
// buffersink 汇
const AVFilter *buf_sink = avfilter_get_by_name("buffersink");
AVFilterContext *sink_ctx;
avfilter_graph_create_filter(&sink_ctx, buf_sink, "out", NULL, NULL, graph);
// 滤镜链:裁剪中心 + 缩放 + 水平翻转
const char *desc = "crop=iw/2:ih:iw/4:0,scale=1280:720,hflip";
AVFilterInOut *in = avfilter_inout_alloc();
AVFilterInOut *out = avfilter_inout_alloc();
in->name = av_strdup("in"); in->filter_ctx = src_ctx; in->pad_idx = 0;
out->name = av_strdup("out"); out->filter_ctx = sink_ctx; out->pad_idx = 0;
avfilter_graph_parse_ptr(graph, desc, &in, &out, NULL);
avfilter_graph_config(graph, NULL);
avfilter_inout_free(&in);
avfilter_inout_free(&out);
*out_graph = graph;
*out_src = src_ctx;
*out_sink = sink_ctx;
return 0;
}
// 使用
AVFrame *decoded = ...; // 从解码器得到
av_buffersrc_add_frame_flags(src_ctx, decoded, AV_BUFFERSRC_FLAG_KEEP_REF);
AVFrame *filtered = av_frame_alloc();
while (av_buffersink_get_frame(sink_ctx, filtered) >= 0) {
// filtered 就是处理完的帧,送去渲染或编码
av_frame_unref(filtered);
}
av_frame_free(&filtered);
9. 常见坑速查表
| 现象 | 原因 | 解决 |
|---|---|---|
avfilter_graph_config返回 -22 |
格式不兼容或参数非法 | 检查 buffer 输入的 pix_fmt/sample_fmt 是否跟解码器输出一致 |
| 滤镜链没输出(一直 EAGAIN) | 帧的 PTS 有问题 | 确保送入的 frame->pts 合理,或试试 setpts=PTS-STARTPTS |
| 内存暴涨 | 忘了 av_frame_unref |
每次取完 filtered frame 必须 unref |
| 硬件解码 + 滤镜黑屏 | 没加 hwdownload |
硬件帧必须先 download 到内存才能进软件滤镜 |
| scale 输出格式不确定 | buffersink 没查询实际格式 | 用 av_opt_get_int查输出 pix_fmt |
| 音频变速爆音 | 用了错误的滤镜 | 用 atempo而不是改 sample_rate |
| seek 后滤镜画面异常 | 没 flush 滤镜图 | seek 时 avfilter_graph_free(&graph);重建,或在 buffer 端送 NULL 帧触发 drain |
10. 滤镜 + seek 的正确姿势
seek 后滤镜图必须重建,原因是:
-
滤镜内部有状态(如 deinterlace、fps、atempo)
-
PTS 序列跳变会让滤镜混乱
-
最简单可靠的做法是 seek 时整张图 free 再重建
// seek 前
avfilter_graph_free(&graph);// seek 后
setup_video_filter(dec_ctx, &graph, &src_ctx, &sink_ctx);
是的,重建滤镜图有开销,但正确性优先于性能。如果你追求极致性能,可以在特定滤镜组合下只 flush 不重建,但这需要逐个滤镜确认其 reset 行为,不推荐新手尝试。
11. 脑内流程图
AVFrame (解码器)
│ av_buffersrc_add_frame
▼
[buffer] → [scale] → [rotate] → [buffersink]
│ av_buffersink_get_frame
▼
AVFrame (渲染/编码)
音频版只是把 box 名字换了:
AVFrame (解码器)
│
▼
[abuffer] → [volume] → [aresample] → [abuffersink]
│
▼
PCM 数据
写在最后
滤镜系统是 FFmpeg 里抽象程度最高的模块,但一旦理解了"图 + 状态机"这个模型,就会发现它其实比编解码还规整------因为所有滤镜都遵循同一套输入输出契约。
对音视频播放器来说,滤镜的典型用途是:
-
视频:缩放(适配渲染窗口)、旋转(手机竖拍)、裁剪、镜像、色彩调整
-
音频:音量、均衡器、变速不变调、声道重映射
-
后期:水印(overlay)、字幕渲染(subtitles)、截图(thumbnail)