author: hjjdebug
date: 2025年 04月 17日 星期四 17:45:49 CST
description: lavfi 深度解析
文章目录
lavfi 深度解析
-f lavfi 是 FFmpeg 中一个特殊的输入格式选项, 全称 Libavfilter Input, 即滤镜输入.
核心功能是可以通过滤镜图(filtergraph)动态生成音视频测试源或处理已有数据流
用法: -f lavfi -i <filtergraph>格式,其中 <filtergraph> 为滤镜描述字符串
甲: 测试实例:
1.1凭空生成测试数据. 滤镜可以是数据源
lavfi 不依赖于实际文件或硬件设备,
而是通过 FFmpeg 内置的滤镜系统(libavfilter)
凭空生成音视频数据,例如颜色条、噪声、正弦波等测试信号。
ffplay -f lavfi -i “testsrc=duration=5:size=640x360” # 生成一个持续5秒、分辨率640x360的测试视频源
ffmpeg -f lavfi -i “smptebars=duration=10” output.mp4 # 生成10秒SMPTE色条视频
ffplay -f lavfi -i “sine=frequency=1000:duration=5” # 生成5秒1000Hz正弦波音频
1.2 动态处理数据, 如叠加,混合,调整等
ffmpeg -i input.mp4 -f lavfi -i “movie=logo.png[wm]; [in][wm]overlay=10:10[out]” output.mp4
此命令将 input.mp4 与 logo.png 叠加,生成带水印的视频
组合多个滤镜生成复杂效果(如分屏、画中画):
生成左右分屏的两个测试源
ffplay -f lavfi -i “testsrc=size=640x360[left]; testsrc2=size=640x360[right]; [left][right]hstack”
ffmpeg 4.4版本就有400多个滤镜, 下面列举几个常用滤镜.
1.3 视频处理常用滤镜
scale 调整分辨率,支持按比例或固定尺寸缩放(如 scale=1280:720)12。
crop 画面裁剪,可指定裁剪区域或固定宽高(如 crop=in_w:in_h-80 裁剪上下边缘)14。
overlay 叠加视频或图片,支持动态位置调整(如 overlay=W-w-10:H-h-10 叠加到右下角)13。
rotate / transpose 旋转或翻转视频(如 transpose=1 顺时针旋转 90 度)14。
drawtext 添加动态文本,支持字体、颜色及位置设置(如 drawtext=text=‘Title’:x=10:y=10)1。
fade 淡入淡出效果(如 fade=in:0:30 前 30 帧淡入)1。
yadif 去隔行扫描,提升画面流畅性(常用于处理老式视频源)2。
boxblur 盒式模糊效果,可调节模糊半径(如 boxblur=5:1)1。
1.4 音频处理常用滤镜
volume 调整音量增益(如 volume=2.0 音量翻倍)1。
aresample 音频重采样(如将 48kHz 转为 44.1kHz)1。
atempo 变速不变调(如 atempo=1.5 提速 50%)1。
silenceremove 移除静音片段(如裁剪音频开头/结尾的无声段)1。
dynaudnorm 动态音频归一化,避免音量突变(适用于直播或混合音轨)1。
acompressor 动态范围压缩(如降低音乐中高响度部分的峰值)6。
acrossfade
1.5 其他实用滤镜
movie 加载外部视频或图片(如叠加水印或画中画)2。
split + vstack/hstack 分割视频流并垂直或水平拼接(如制作分屏效果)4。
colorkey 基于颜色键实现透明区域(如绿幕抠图)1。
hue 调整色调与饱和度(如 hue=s=1.5 增加饱和度 50%)4。
乙: 原理说明
世界很精彩! 到底它是怎样运行的? 理解的东西才能记得住.
1. 一个虚拟设备 ff_lavfi_demuxer, 过滤器解复用器对象
跟踪代码研究.
-f lavfi 是说输入格式是 “lavfi”
AVInputFormat *iformat = av_find_input_format(“lavfi”);
会找到输入格式为 ff_lavfi_demuxer 对象
它不是一个普通的输入对象,而是一个inputdevice 对象,真身在文件libavdevice/lavfi.c中
我跟踪了一下它的查找过程,原来它在普通demuxer_list 中找了个遍找不到名称为"lavfi"的对象,
最后在在indev_list中找到了.
于是我们可以更准确的说, -f lavfi 对应着一个虚拟的设备,
用ffprobe -devices 也可以看到确实存在该设备
$ ffprobe -devices
D lavfi Libavfilter virtual input device
D 表示它支持Demuxing , 支持解引用的意思.就是支持把音视频数据分离,显示的意思.
找到了输入格式对象,当然也要看看它长什么样了. 有些概念还连不上也没关系,以后逐渐消化.
AVInputFormat ff_lavfi_demuxer = {
.name = “lavfi”,
.long_name = NULL_IF_CONFIG_SMALL(“Libavfilter virtual input device”),
.priv_data_size = sizeof(LavfiContext),
.read_header = lavfi_read_header, //*******
.read_packet = lavfi_read_packet, //*******
.read_close = lavfi_read_close,
.flags = AVFMT_NOFILE, //这个flags 说它跟文件没有关联,这样avformat_open_input->init_input就简单了
.priv_class = &lavfi_class, //*******
};
我们关注它3个方面,上边加*****部分:
首先,它包含一个私有类对象地址lavfi_class,私有类大小是sizeof(LavfiContext)
然后,定义了read_header 和 read_packet 函数
1.1 私有类对象 lavfi_class
把这个对象lavfi_class拷贝过来吧,为了一目了然性.这里面内涵还是很丰富.
#define OFFSET(x) offsetof(LavfiContext, x)
static const AVOption options[] = { //这时选项表,可以初始化,访问LavfiContext 成员
{ "graph", "set libavfilter graph", OFFSET(graph_str), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, DEC },
{ "graph_file", "set libavfilter graph filename", OFFSET(graph_filename), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, DEC },
{ "dumpgraph", "dump graph to stderr", OFFSET(dump_graph), AV_OPT_TYPE_STRING, { .str = NULL }, 0, 0, DEC },
{ NULL },
};
static const AVClass lavfi_class = { //这时一个AVClass 对象,能够描述LavfiContext 对象成员信息
.class_name = "lavfi indev",
.item_name = av_default_item_name,
.option = options,
.version = LIBAVUTIL_VERSION_INT,
.category = AV_CLASS_CATEGORY_DEVICE_INPUT,
};
LavfiContext 结构就不copy了,copy 很多东西也没啥意思,拣重点说吧.
一看定义,有过滤图和graph 和数据汇sinks
AVFilterGraph* graph;
AVFilterContext** sinks;
//但是下面这两个名称起得就要格外注意了,因为太像了,所以要注意先后顺序
int* sink_stream_map;
int* stream_sink_map;
有了lavfi_class对象,将来可以创建具体实例LavfiConttext 对象
例如avformat_open_input 时有如下代码: s-iformat此时就是ff_lavfi_demuxer指针,
由它可拿到lavfi_class 对象地址及私有类数据大小,就可以构建这个私有对象
if (s->iformat->priv_data_size > 0)
{
s->priv_data = av_mallocz(s->iformat->priv_data_size); //该私有数据就是LavfiContext 对象指针
if (s->iformat->priv_class)
{
*(const AVClass**)s->priv_data = s->iformat->priv_class;
av_opt_set_defaults(s->priv_data); //设置默认值
}
}
1.2 lavfi_read_header
下面看看lavfi_read_header 函数, 在一堆陌生的概念中前行, 也有了解它的冲动.
该函数会在打开文件时调用到. avformat_open_input->lavfi_read_header
这是个重点,采用代码标注之法,只拣重点标注.
static int lavfi_read_header(AVFormatContext* avctx)
{
AVFilterInOut *input_links = NULL, *output_links = NULL;
//获取私有数据做为上下文地址
LavfiContext* lavfi = avctx->priv_data;
//获取2个filter对象
const AVFilter *buffersink = avfilter_get_by_name("buffersink");
const AVFilter *abuffersink = avfilter_get_by_name("abuffersink");
//获取所有pixel formats 描述符
int* pix_fmts = create_all_formats(AV_PIX_FMT_NB); //关注该行代码
if (!lavfi->graph_str) lavfi->graph_str = av_strdup(avctx->url);
//分配过滤图
if (!(lavfi->graph = avfilter_graph_alloc()))
FAIL(AVERROR(ENOMEM));
//根据命令行字符串,在图上创建过滤器
if ((ret = avfilter_graph_parse_ptr(lavfi->graph, lavfi->graph_str,
&input_links, &output_links, avctx)) < 0) goto end;
if (input_links) //不能有浮空的输入脚
{
av_log(avctx, AV_LOG_ERROR, "Open inputs in the filtergraph are not acceptable\n");
FAIL(AVERROR(EINVAL));
}
/* count the outputs */ 计算接受脚个数
for (n = 0, inout = output_links; inout; n++, inout = inout->next) ;
lavfi->nb_sinks = n;
/* parse the output link names - they need to be of the form out0, out1, ...
* create a mapping between them and the streams */
分析接受脚名称,创建接受脚和流之间的映射.一个接受脚对应一个流,引脚名称已经包含了流索引
for (i = 0, inout = output_links; inout; i++, inout = inout->next)
{
int stream_idx = 0, suffix = 0, use_subcc = 0;
sscanf(inout->name, "out%n%d%n", &suffix, &stream_idx, &suffix); //关注该行代码见后
if (!suffix)
{ //不是out开头,suffix就会为0,出错
av_log(avctx, AV_LOG_ERROR, "Invalid outpad name '%s'\n", inout->name);
FAIL(AVERROR(EINVAL));
}
if (inout->name[suffix]) //是out开头,suffix后只能跟"+subcc",例如out3+subcc
{
if (!strcmp(inout->name + suffix, "+subcc"))
{
use_subcc = 1;
}
else
{ //否则出错
av_log(avctx, AV_LOG_ERROR,
"Invalid outpad suffix '%s'\n", inout->name);
FAIL(AVERROR(EINVAL));
}
}
//保存数值到数组, stream_index对应stream, i对应sink接受脚序号
lavfi->sink_stream_map[i] = stream_idx;
lavfi->stream_sink_map[stream_idx] = i;
lavfi->sink_stream_subcc_map[i] = !!use_subcc;
}
/* for each open output create a corresponding stream */
//对每一个接受脚,创建一个流,例如3个脚就创建3个流
for (i = 0, inout = output_links; inout; i++, inout = inout->next)
{
AVStream* st; //stream 存到 avctx 中了
if (!(st = avformat_new_stream(avctx, NULL))) FAIL(AVERROR(ENOMEM));
st->id = i;
}
/* create a sink_ctx for each output and connect them to the graph */
//继续...对每一个输出脚都创建一个AVFilterContext 对象(sinks)
lavfi->sinks = av_malloc_array(lavfi->nb_sinks, sizeof(AVFilterContext*));
for (i = 0, inout = output_links; inout; i++, inout = inout->next)
{
AVFilterContext* sink_ctx;
//这句话很长.实际上意思很简单.就是给了你模块的引脚数组指针和引脚索引,取该引脚类型
type = avfilter_pad_get_type(inout->filter_ctx->output_pads, inout->pad_idx);
if (type == AVMEDIA_TYPE_VIDEO && !buffersink || type == AVMEDIA_TYPE_AUDIO && !abuffersink)
{ //不是音频和视频就失败,失败了应该查模块引脚定义了.
av_log(avctx, AV_LOG_ERROR, "Missing required buffersink filter, aborting.\n");
FAIL(AVERROR_FILTER_NOT_FOUND);
}
if (type == AVMEDIA_TYPE_VIDEO)
{ //创建sink_ctx, 目地是将来从这个对象取数据. 其filter 是ff_vsink_buffer
ret = avfilter_graph_create_filter(&sink_ctx, buffersink,
inout->name, NULL,
NULL, lavfi->graph);
if (ret >= 0) //初始化该对象
ret = av_opt_set_int_list(sink_ctx, "pix_fmts", pix_fmts, AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN);
}
else if (type == AVMEDIA_TYPE_AUDIO) //引脚类型为AUDIO
{
enum AVSampleFormat sample_fmts[] = { AV_SAMPLE_FMT_U8,
AV_SAMPLE_FMT_S16,
AV_SAMPLE_FMT_S32,
AV_SAMPLE_FMT_FLT,
AV_SAMPLE_FMT_DBL, -1 };
//由 "abuffersink" 对象及名称创建audio context 对象(接受音频的对象)
ret = avfilter_graph_create_filter(&sink_ctx, abuffersink,
inout->name, NULL,
NULL, lavfi->graph);
if (ret >= 0)// 设置参数
ret = av_opt_set_int_list(sink_ctx, "sample_fmts", sample_fmts, AV_SAMPLE_FMT_NONE, AV_OPT_SEARCH_CHILDREN);
if (ret < 0)
goto end;
ret = av_opt_set_int(sink_ctx, "all_channel_counts", 1,
AV_OPT_SEARCH_CHILDREN);
if (ret < 0)
goto end;
}
lavfi->sinks[i] = sink_ctx;
//建立连接, sink_ctx 与 filter_ctx的某个引脚相连,像不像硬件的画图?
if ((ret = avfilter_link(inout->filter_ctx, inout->pad_idx, sink_ctx, 0)) < 0)
goto end;
}
/* configure the graph */ //配置过滤图,对图进行配置检查之意.
if ((ret = avfilter_graph_config(lavfi->graph, avctx)) < 0) goto end;
/* fill each stream with the information in the corresponding sink */
//由sink_ctx 填充流信息. 流的codecpar等等...
for (i = 0; i < lavfi->nb_sinks; i++)
{ //由顺序号到stream号,由stream号到ctx对象
AVFilterContext* sink_ctx = lavfi->sinks[lavfi->stream_sink_map[i]];
AVRational time_base = av_buffersink_get_time_base(sink_ctx);
AVStream* st = avctx->streams[i]; //获取对应stream
st->codecpar->codec_type = av_buffersink_get_type(sink_ctx);
avpriv_set_pts_info(st, 64, time_base.num, time_base.den);
if (av_buffersink_get_type(sink_ctx) == AVMEDIA_TYPE_VIDEO)
{ //从引脚ctx向codecpar copy 数据
st->codecpar->codec_id = AV_CODEC_ID_RAWVIDEO;
st->codecpar->format = av_buffersink_get_format(sink_ctx);
st->codecpar->width = av_buffersink_get_w(sink_ctx);
st->codecpar->height = av_buffersink_get_h(sink_ctx);
st->sample_aspect_ratio = st->codecpar->sample_aspect_ratio = av_buffersink_get_sample_aspect_ratio(sink_ctx);
avctx->probesize = FFMAX(avctx->probesize,
av_buffersink_get_w(sink_ctx) * av_buffersink_get_h(sink_ctx) *
av_get_padded_bits_per_pixel(av_pix_fmt_desc_get(av_buffersink_get_format(sink_ctx))) * 30);
}
else if (av_buffersink_get_type(sink_ctx) == AVMEDIA_TYPE_AUDIO)
{
st->codecpar->codec_id = av_get_pcm_codec(av_buffersink_get_format(sink_ctx), -1);
st->codecpar->channels = av_buffersink_get_channels(sink_ctx);
st->codecpar->format = av_buffersink_get_format(sink_ctx);
st->codecpar->sample_rate = av_buffersink_get_sample_rate(sink_ctx);
st->codecpar->channel_layout = av_buffersink_get_channel_layout(sink_ctx);
if (st->codecpar->codec_id == AV_CODEC_ID_NONE) //codec_id不能为空
av_log(avctx, AV_LOG_ERROR,
"Could not find PCM codec for sample format %s.\n",
av_get_sample_fmt_name(av_buffersink_get_format(sink_ctx)));
}
}
if ((ret = create_subcc_streams(avctx)) < 0) goto end; //关于字幕的初始化
end:
av_free(pix_fmts);
avfilter_inout_free(&input_links);
avfilter_inout_free(&output_links);
return ret;
}
重点中的重点再补充几句.
它会调用一个create_all_formats 函数,什么意思呢?
原来在libavutil/pixdesc.c中定义了一个198个 像素格式表述符表
static const AVPixFmtDescriptor av_pix_fmt_descriptors[AV_PIX_FMT_NB] = {
[AV_PIX_FMT_YUV420P] = {
.name = "yuv420p",
.nb_components = 3,
.log2_chroma_w = 1,
.log2_chroma_h = 1,
.comp = {
{ 0, 1, 0, 0, 8, 0, 7, 1 }, /* Y */
{ 1, 1, 0, 0, 8, 0, 7, 1 }, /* U */
{ 2, 1, 0, 0, 8, 0, 7, 1 }, /* V */
},
.flags = AV_PIX_FMT_FLAG_PLANAR,
},
[AV_PIX_FMT_YUYV422] = {
...
}
它要把非硬件加速的像素描述符都收集起来的意思. 就是软件支持的像素描述符都收集到fmts数组中,以-1结尾
for (j = 0, i = 0; i < 198; i++)
{
const AVPixFmtDescriptor* desc = av_pix_fmt_desc_get(i);
if (!(desc->flags & AV_PIX_FMT_FLAG_HWACCEL))
fmts[j++] = i;
}
fmts[j] = -1;
分析输入的字符串,在图上创建过滤器,其中lavfi->graph_str 就是命令行上传入的过滤器字符串
if ((ret = avfilter_graph_parse_ptr(lavfi->graph, lavfi->graph_str,
&input_links, &output_links, avctx))
说明这个函数需要另起一片博客,这就不详细说明了.
标注完了忽然明白, 这个lavfi_read_header 就是根据命令行提供的字符串进行了过滤器创建和连接
是打开文件的过程中调用的.
1.3 lavfi_read_packet
代码标注…
static int lavfi_read_packet(AVFormatContext* avctx, AVPacket* pkt)
{
LavfiContext* lavfi = avctx->priv_data;
AVFrame* frame = lavfi->decoded_frame;
AVDictionary* frame_metadata;
AVStream* st;
/* iterate through all the graph sinks. Select the sink with the
* minimum PTS */
//从所有的接受引脚中,选择时间最小的
for (i = 0; i < lavfi->nb_sinks; i++)
{
AVRational tb = av_buffersink_get_time_base(lavfi->sinks[i]);
if (lavfi->sink_eof[i]) continue;
//查询frame
int ret = av_buffersink_get_frame_flags(lavfi->sinks[i], frame, AV_BUFFERSINK_FLAG_PEEK);
double d = av_rescale_q_rnd(frame->pts, tb, AV_TIME_BASE_Q, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
av_frame_unref(frame);
if (d < min_pts)
{
min_pts = d;
min_pts_sink_idx = i; //把最小pts的sink做为sink序号
}
}
//由sink序号找到sink, 取该sink的frame, 该函数很关键,它取到了滤波器数据
av_buffersink_get_frame_flags(lavfi->sinks[min_pts_sink_idx], frame, 0);
//由sink序号找到流号
stream_idx = lavfi->sink_stream_map[min_pts_sink_idx];
//由流号找到流
st = avctx->streams[stream_idx];
if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{ //把视频frame,copy到pkt
size = av_image_get_buffer_size(frame->format, frame->width, frame->height, 1);
if ((ret = av_new_packet(pkt, size)) < 0) goto fail;
av_image_copy_to_buffer(pkt->data, size, (const uint8_t**)frame->data, frame->linesize,
frame->format, frame->width, frame->height, 1);
}
else if (st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
{//把音频frame,copy到pkt
size = frame->nb_samples * av_get_bytes_per_sample(frame->format) * frame->channels;
if ((ret = av_new_packet(pkt, size)) < 0) goto fail;
memcpy(pkt->data, frame->data[0], size);
}
pkt->stream_index = stream_idx;
pkt->pts = frame->pts;
pkt->pos = frame->pkt_pos;
av_frame_unref(frame);
return size;
fail:
av_frame_unref(frame);
return ret;
}
它把数据copy给了pkt, 但它的数据是从哪来的?
是av_buffersink_get_frame_flags(lavfi->sinks[min_pts_sink_idx], frame, 0);
它从sink_ctx中取到了frame, copy给pkt的,
而从sink_ctx中取到frame, 就不是lavfi.c 管的事了.
分层管理就是这个毛病,断层了,是别人管的事了,上边的都是框架,整了一圈还不知道是谁产生的数据.
其实不整也知道,真正造数的地方是你命令行上指定的filter
lavfi 就是一个虚拟的设备,上层调用它,它调用具体的filter
丙: 分析一个具体实例. 生成一个简单的屏幕全红的视频
$ffprobe -f lavfi color=c=red:s=640*480 -show_packets -of ini -read_intervals %+0.1
-show_packets: 才会读包,显示包
-of ini 指明输出格式ini, 可显示包的序号
-read_intervals 指明时间间隔,只显示0.1秒, 时间长了也没有用,就一个图片
播放它也可以
$ffplay -f lavfi color=c=red:s=640*480
重点研究一下它的数据是从哪里来的.
前边讲了一大堆才说明了 -f lavfi, 后面重点介绍 “color=c=red:s=640480"
回忆 "color=c=red:s=640480” 被当成输入文件名,因format 是lavfi,
在 avformat_open_input->read_header->lavfi_read_header被avfilter_graph_parse_ptr来分析
建立了过滤图和过滤器, 我们直接去看color过滤器吧. 那是一个真实的过滤器,博客太长只能后续
虚实结合才是一个完整的应用.
小结:
专业的说法是:-f lavfi 对应一个AVInputFormat 输入格式对象(或输入设备对象),该对象定义了读头和读包函数.其中用到了私有类对象. 该私有类对象包含了一个filter_graph 和AVFilterContext 数组sinks, sinks用来收集filter_graph上传来的数据.
通俗的解释是:虚拟设备 lavfi 是个领导者,它负责初始化,负责组织,创建下层对象和与上层打交道,当需要提供数据时,它交给下层去执行,然后把数据copy给上层.