ffmpeg复杂滤镜-filter_complex
本文 以 ffmpeg4.4 源码为准。
a.mp4下载链接:百度网盘,提取码:nl0s 。logo.jpg 地址:点击查看
命令如下:
ffmpeg.exe -i a.mp4 -i logo.jpg -filter_complex "[1:v]scale=176:144[logo];[0:v][logo]overlay=x=0:y=0" output.mp4 -y
上面命令实现的功能就是 把 "弦外之音" 的 logo 放在视频左上角。
ffmpeg 命令行有两种 filter 用法:
1,-vf
,普通滤镜, 在 《ffmpeg命令分析-vf》有过讲解。
什么是简单滤镜?只有一个输入流是简单滤镜
2,-filter_complex
,-lavfi
这两个命令参数是一样的,这是复杂滤镜,lavfi 是估计是 libavfilter 的缩写。
什么是复杂滤镜?有多个输入流的就是复杂滤镜,本文命令有2个输入流,属于复杂滤镜
复杂滤镜 就是本文的分析重点。
首先 filter_complex 在 ffmpeg_opt.c 的定义如下:
{ "filter_complex", HAS_ARG | OPT_EXPERT, { .func_arg = opt_filter_complex },"create a complex filtergraph", "graph_description" }
从定义可以看出 filter_complex 会调用 opt_filter_complex 函数。
opt_filter_complex 函数的定义如下:
static int opt_filter_complex(void *optctx, const char *opt, const char *arg)
{GROW_ARRAY(filtergraphs, nb_filtergraphs);if (!(filtergraphs[nb_filtergraphs - 1] = av_mallocz(sizeof(*filtergraphs[0]))))return AVERROR(ENOMEM);filtergraphs[nb_filtergraphs - 1]->index = nb_filtergraphs - 1;filtergraphs[nb_filtergraphs - 1]->graph_desc = av_strdup(arg);if (!filtergraphs[nb_filtergraphs - 1]->graph_desc)return AVERROR(ENOMEM);
input_stream_potentially_available = 1;
return 0;
}
从上面的代码可以看出, opt_filter_complex 做的事情非常简单,就是 malloc 一个 struct FilterGraph
,然后放进行 全局变量 filtergraphs 里面。
-filter_complex 后面的参数字符串 "[1:v]scale=176:144[logo];[0:v][logo]overlay=x=0:y=0"
就被放进行 graph_desc 进行保存。
ffmpeg 会通过 graph_desc 这个参数判断这个 FilterGraph 是不是一个复杂 FilterGraph ,通过 filtergraph_is_simple()
函数实现。
在之前文章 《ffmpeg源码分析-open_output_file》里,我们知道 init_simple_filtergraph
函数 是在 open_output_file 里面执行的。
init_simple_filtergraph 是初始化 简单filter。init_complex_filters 是初始化 复杂filter,他们之间的调用流程如下:
流程图如下:
从上面流程图 可以看出 ,init_complex_filters 是在 init_simple_filtergraph 之前执行的,如果执行了 init_complex_filters 就不会执行 init_simple_filtergraph,只有一个会执行,例如 视频 滤镜用了 init_complex_filters,就不会执行 init_simple_filtergraph,但是本命令中 音频 会用 init_simple_filtergraph 初始化滤镜。
下面仔细分析 init_complex_filters 的逻辑。
init_complex_filters 函数代码如下:
static int init_complex_filters(void)
{int i, ret = 0;
for (i = 0; i < nb_filtergraphs; i++) {ret = init_complex_filtergraph(filtergraphs[i]);if (ret < 0)return ret;}return 0;
}
这个函数比较简单,就是循环执行 init_complex_filtergraph,本文命令只有一个 复杂 filter,所以只能循环一次,这里的循环其实为了处理那种很复杂的filter的。
。
接着分析 init_complex_filtergraph 函数的逻辑,重点如下:
从上图可以看到,init_complex_filtergraph() 函数里面 调 avfilter_graph_parse2() 来解析 "[1:v]scale=176:144[logo];[0:v][logo]overlay=x=0:y=0"
。
这里注意 第三个参数 inputs 变量是一个 struct AVFilterInOut
数组 ,从 debug 器可以看出 inputs 数组有两个值,1:v
跟 0:v
,跟命令行参数是对得上的。
代表这个 filter-graph 有两个输入流,这里跟以往的文章分析不同,以前的filter文章分析都只讲了一个输入流的情况。
接着 分析 init_input_filter() 函数里面做了什么事情,流程图如下:
init_input_filter 的代码有点长,只贴部分重点代码进行讲解。
从流程图跟代码中可以分析出来,init_input_filter 函数前半部分都是为了找出 ist,ist 是一个 struct InputStream
,放在全局变量 input_streams 里面。
重点代码如下:
//找出 文件 ctx
s = input_files[file_idx]->ctx;
for (i = 0; i < s->nb_streams; i++) {enum AVMediaType stream_type = s->streams[i]->codecpar->codec_type;if (stream_type != type &&!(stream_type == AVMEDIA_TYPE_SUBTITLE &&type == AVMEDIA_TYPE_VIDEO /* sub2video hack */))continue;if (check_stream_specifier(s, s->streams[i], *p == ':' ? p + 1 : p) == 1) {//找出 指定流,重点st = s->streams[i];break;}
}
//找出 InputStream
ist = input_streams[input_files[file_idx]->ist_index + st->index];
init_input_filter 函数一开始 就会把 in->name 进行提取,本命令中 in->name 是 1:v
,它的逻辑会把 1 提取出来 赋值给 file_idx ,因为下标是0开始的,所以这里的 1 是指定第二个文件,然后把 ":v" 也提取出来,用 check_stream_specifier 来获取到指定的流,v 代表是视频流,取文件的第一个视频流 赋值给 st,如果文件有多个视频流,只取第一个视频流,其他不管。
所以 命令行参数 中的 [1:v] ,就是指定第二个输入文件 的 第一个视频流 。
最后通过 st = input_streams[input_files[file_idx]->ist_index + st->index]
获取的 InputStream。
继续分析,后面就是添加以及初始化 fg->inputs,代码如下:
GROW_ARRAY(fg->inputs, fg->nb_inputs);
if (!(fg->inputs[fg->nb_inputs - 1] = av_mallocz(sizeof(*fg->inputs[0]))))exit_program(1);
//重点代码
fg->inputs[fg->nb_inputs - 1]->ist = ist;
fg->inputs[fg->nb_inputs - 1]->graph = fg;
fg->inputs[fg->nb_inputs - 1]->format = -1;
fg->inputs[fg->nb_inputs - 1]->type = ist->st->codecpar->codec_type;
fg->inputs[fg->nb_inputs - 1]->name = describe_filter_link(fg, in, 1);
fg->inputs[fg->nb_inputs - 1]->frame_queue = av_fifo_alloc(8 * sizeof(AVFrame*));
if (!fg->inputs[fg->nb_inputs - 1]->frame_queue)exit_program(1);
上面这段代码有三个重点:
1,这个 filter 有两个输入流,但是并没有先后顺序的区分,没有字段存储哪个是0,哪个是1。
2,fg->inputs[fg->nb_inputs - 1]->ist = ist;
这句是重点代码。这里关联了 InputFilter 跟 InputStream。
3,strcut InputFilter 里面的 frame_queue 是一个 AVFrame 的临时存储区,为什么要临时存储,是因为 FilterGraph 里面的所有 InputFilter 都初始化完成才能 往 某个filter 里面写 AVframe ,ffmpeg 是这样判断 InputFilter是否初始化完成的,InputFilter::format 不等于 -1 就是 初始化完成了。具体实现在 ifilter_has_all_input_formats() 函数里。如果 A InputFilter 初始化完成了,B InputFilter 没初始化完成,就不会往 A 的 InputFilter::filter 写数据,而是先写到 A 的 InputFilter::frame_queue,后面再从 InputFilter::frame_queue 里拿出来,写到 InputFilter::filter。部分代码如下:
//ffmpeg.c 2213行
if (!ifilter_has_all_input_formats(fg)) {AVFrame *tmp = av_frame_clone(frame);if (!tmp)return AVERROR(ENOMEM);av_frame_unref(frame);
if (!av_fifo_space(ifilter->frame_queue)) {ret = av_fifo_realloc2(ifilter->frame_queue, 2 * av_fifo_size(ifilter->frame_queue));if (ret < 0) {av_frame_free(&tmp);return ret;}}av_fifo_generic_write(ifilter->frame_queue, &tmp, sizeof(tmp), NULL);return 0;
}
PS:上面这段代码的命令场景我也没有,具体什么样的命令会跑上面这种临时存储逻辑,埋个坑,后续填,有朋友知道的,可以在文章评价留意。
init_input_filter 函数含有一个重点,最后两句代码如下:
GROW_ARRAY(ist->filters, ist->nb_filters); ist->filters[ist->nb_filters - 1] = fg->inputs[fg->nb_inputs - 1];
这里是把 InputStream 里面的 filters 同步了,为什么这样做我也不太清楚,想记一下,应该有地方用到。
至此,init_input_filter 函数分析完毕,接着分析上层函数 init_complex_filtergraph 后面的逻辑,代码如下:
for (cur = outputs; cur;) {GROW_ARRAY(fg->outputs, fg->nb_outputs);fg->outputs[fg->nb_outputs - 1] = av_mallocz(sizeof(*fg->outputs[0]));if (!fg->outputs[fg->nb_outputs - 1])exit_program(1);
fg->outputs[fg->nb_outputs - 1]->graph = fg;fg->outputs[fg->nb_outputs - 1]->out_tmp = cur;fg->outputs[fg->nb_outputs - 1]->type = avfilter_pad_get_type(cur->filter_ctx->output_pads,cur->pad_idx);fg->outputs[fg->nb_outputs - 1]->name = describe_filter_link(fg, cur, 0);cur = cur->next;fg->outputs[fg->nb_outputs - 1]->out_tmp->next = NULL;
}
上面的代码实际上 就是操作处理 fg->outputs,本文的命令 outputs 数组只有一个值,所以只会循环一次。这段代码比较易懂,不需要做太多分析。
至此,init_complex_filtergraph 函数分析完毕。
从之前的流程图可以看出,init_complex_filters 是在 init_simple_filtergraph 前面调用的,这里要着重讲解一下 init_complex_filters 跟 init_simple_filtergraph 的区别。
init_complex_filters 是用来初始化复杂滤镜的,什么是复杂滤镜?有多个输入流的就是复杂滤镜。
init_simple_filtergraph 是用来初始化简单滤镜的,什么是简单滤镜,只有一个输入流就是简单滤镜。
对于视频而言,如果用了 init_complex_filters 来初始化滤镜,代码 就不会执行 init_simple_filtergraph ,两者只有一个执行。
例如,本命令中,视频滤镜是用 init_complex_filters 实现,音频滤镜是调 init_simple_filtergraph 实现,代码如下:
/* create streams for all unlabeled output pads */
for (i = 0; i < nb_filtergraphs; i++) {FilterGraph *fg = filtergraphs[i];for (j = 0; j < fg->nb_outputs; j++) {OutputFilter *ofilter = fg->outputs[j];
if (!ofilter->out_tmp || ofilter->out_tmp->name)continue;
switch (ofilter->type) {case AVMEDIA_TYPE_VIDEO: o->video_disable = 1; break;case AVMEDIA_TYPE_AUDIO: o->audio_disable = 1; break;case AVMEDIA_TYPE_SUBTITLE: o->subtitle_disable = 1; break;}init_output_filter(ofilter, o, oc);}
}
上面的代码会把 o->video_disable 设为 1,导致 init_simple_filtergraph 没有执行。
这里说个重点,因为 视频输出流 对应多个输入流,如下图,所以 ost->source_index
会在 init_output_filter 函数里面设置为 -1,因为不是单个输入流。
一开始解析复杂滤镜参数的时候,已经往 全局变量 filtergraphs 数组 插入了一个 FilterGraph ,然后在 open_output_file 函数里面处理音频时,执行了 init_simple_filtergraph ,又插入了一个 FilterGraph 。所以现在数据结构如下图所示:
PS:init_simple_filtergraph 在《ffmpeg源码分析-open_output_file》有讲解。本命令里,音频滤镜是一个空的FilterGraph。
注意上面的结构图,复杂 filter 是有 nb_input 等于 2,代表有两个输入流的。
执行 init_complex_filters 跟 init_simple_filtergraph 初始化 简单跟复杂的 filtergraph 之后, 后面会执行 configure_filtergraph() 函数,下面就来分析 configure_filtergraph 在本命令中的逻辑,代码如下图所示:重点已圈出。
。
if (simple) {//简单 filter 处理 省略
} else {fg->graph->nb_threads = filter_complex_nbthreads;
}
从上的代码可以看出, configure_filtergraph 入口一开始就是对 简单跟复杂 filter 的区别处理,在这里复杂滤镜 的逻辑比较简单。
然后就又执行了 avfilter_graph_parse2 ,我为什么说 “又”,大家注意看,在开始的时候 init_complex_filtergraph 函数里面已经执行过一次 avfilter_graph_parse2 。对于复杂 filter 而已,这个概念是重中之重。
复杂滤镜 之所以 会执行两次 avfilter_graph_parse2 ,不是因为写错代码,而是有必要的。
第一次 avfilter_graph_parse2 是为了弄出来 FilterGraph::InputFilter 跟 FilterGraph::OutputFilter,把这两个东西弄好。
第二次 avfilter_graph_parse2 是为了 给后面的 configure_input_filter (第三个红圈)用。
第二次 avfilter_graph_parse2 是 简单滤镜 跟 复杂滤镜通用的,所以他执行两次,实际上是为了通用,我们如果调 API 函数,即使是复杂滤镜,也可以只调一次avfilter_graph_parse2 搞定。
还有一个重点,代码如下:
for (cur = inputs, i = 0; cur; cur = cur->next, i++)if ((ret = configure_input_filter(fg, fg->inputs[i], cur)) < 0) {avfilter_inout_free(&inputs);avfilter_inout_free(&outputs);goto fail;}
上面的 for 循环会循环两次,他没有用 下标之类的定位,是因为 两次 avfilter_graph_parse2 返回的 inputs 数组顺序都是一样的。
configure_input_filter 函数的重点代码如下:
if ((ret = avfilter_graph_create_filter(&ifilter->filter, buffer_filt, name,args.str, NULL, fg->graph)) < 0)goto fail;
把 ifilter->filter 初始化为 buffer filter (入口 filter),然后插入一些默认的 filter,例如 trim filter,最后 关联 到传进来的 cur 变量。
configure_filtergraph 函数分析完毕。
可以看到 复杂滤镜 在 ffmpeg.c 的实现确实比较复杂。但是在 API 函数的角度看来,复杂滤镜 跟 简单滤镜使用的 api 函数是一样的,都是用 avfilter_graph_parse2 。
复杂滤镜里面的复杂性是为了 命令行参数更 易用一些。
复杂滤镜 第一次调 avfilter_graph_parse2 是为了处理好 filter 跟 stream 的关联,在程序里写明,某个 InputStream 需要发往 某个 InputFilter 。
第二次调 avfilter_graph_parse2 才真正开始关联 InputFilter 跟 OutputFilter ,因为中间可能需要插入一些其他 filter,例如 trim filter,rotate filter 等等。
如果 自己调 api 函数, 我们代码里,哪个 InputStream 需要发往 哪个 InputFilter 已经确定了,不需要通过命令参数改变,就可以调一次 avfilter_graph_parse2 ,就搞定了,然后从不同的流读AVFrame,然后 往不同的 buffer filter发送 AVFrame 即可。
由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。
ffmpeg复杂滤镜-filter_complex相关推荐
- ffmpeg利用滤镜合并两个视频,一左一右
ffmpeg关于视频合并的例子,命令行的一堆,代码的感觉不多,命令行跟代码感觉还是有些差距,代码上要求还是高一些,关于滤镜的命令行,读者可以看我的一篇博客 ffmpeg利用滤镜进行视频混合(命令行) ...
- ffmpeg常用滤镜命令
FFmpeg添加了很多滤镜,查看哪些滤镜有效可用命令: # ./ffmpeg -filters. 1. FFmpeg滤镜文档 更多的信息和每个滤镜的使用示例可查看FFmpeg的滤镜文档: h ...
- LiveVideoStackCon讲师热身分享 ( 八 ) —— FFmpeg的滤镜在视频编辑场景中的应用
LiveVideoStackCon 2018音视频技术大会是每年的多媒体技术人的盛宴,为了让参会者与大会讲师更多互动交流,我们推出了LiveVideoStackCon讲师热身分享第一季,在每周四晚19 ...
- ffmpeg delogo滤镜去除图片水印
之前本人写过ffmpeg movie滤镜添加图片水印,ffmpeg 非movie滤镜添加图片水印 今天用delogo滤镜去掉图片水印,ffmpeg命令行如下: ffmpeg -i in-compute ...
- ffmpeg利用滤镜合并四个视频,左一右三
今天利用ffmpeg的滤镜功能合并,左一右三方式,如下所示: 读者需要先对滤镜的描述字符串有所了解,读者可以参看我写的一篇博客: ffmpeg利用滤镜进行视频混合(命令行) 四个文件都是1920x10 ...
- ffmpeg利用滤镜合并两个视频,一左一右---avfilter_link实现
之前写过一篇博客ffmpeg利用滤镜合并两个视频,一左一右 用的是滤镜字符串解析avfilter_graph_parse_ptr实现滤镜的连接,查看了avfilter_graph_parse_ptr的 ...
- ffmpeg音频滤镜
音频滤镜 分离声道 1 转码(源文件没问题可以省略) ffmpeg -i jy.ts -vcodec h264 -acodec aac jy1.ts 2 取一个声道 ffmpeg -i jy1.t ...
- Android直播开发之旅(18):FFmpeg中滤镜(filter)的工作原理
文章目录 1. 什么是滤镜 1.1 简单滤镜(滤镜链) 1.2 复杂滤镜(滤镜图) 2. 滤镜API介绍与使用 2.1 滤镜API介绍 2.1.1 结构体 2.1.2 功能函数 2.2 滤镜API的使 ...
- ffmpeg视频滤镜
过滤器分类 1.1简单过滤器 在简单过滤器中,只包含一个输入和一个输出,并且输入输出是同一类型.在下面的处理过程中,仅仅是在解码和编码之间加上一个额外的过滤步骤. 简单过滤器由per-stream-f ...
- ffmpeg volume滤镜改变音量
ffmpeg可以改变音量,用的是volume滤镜,volume滤镜支持两种方式改变音量 第一种,直接提高分贝方式,比如下面是音量提高10分贝 ffmpeg -i huoluan3_audio.mp4 ...
最新文章
- linux命令--提升
- 在 iOS 11 中使用 Core Bluetooth
- 2019年中国十大人才发展趋势
- 完美解决Ubuntu16.04虚拟机窗口自适应问题
- Linux macos 常用终端操作
- 软件工程实践2017第一次作业
- C# 实现 rtc_通过Xlua实现unity热更新的一个小例子
- SHELL编程命令大全
- 《电磁学》学习笔记4——磁场高斯定理、安培环路定理、电动势
- Photoshop CS6下载包下载 及破解安装教程
- Linux本地信息收集
- Win10Edge护眼色设置
- 大专计算机档案,大专档案自我鉴定(精选5篇)
- 建站提示:B2C网站建设的注意事项
- 免费MindManager2021最新版本地址win/mac思维导图工具 新增功能
- 全息网御上榜《CCSIP 2022中国网络安全产业全景图》
- Linux笔记之Docker安装,基于Debian 11(bullseye)
- aac音频怎么转mp3,这几个方法很简便
- 【前端面试必读】实现图片16:9
- 计算机硬盘显示隐藏,隐藏与显示硬盘盘符的最简单的方法
热门文章
- Python数据类型函数
- 超实用的自我规划模型 | 进击
- 由12306.cn谈谈网站性能技术
- Android屏幕区域划分及尺寸获取
- 计算机中利用的物理原理,现代电脑技术中物理原理.doc
- Windows10创建系统还原点
- Android 源码编译详解【一】:服务器硬件配置及机型推荐-2016/06
- c语言大作业实现程序功能描述,C语言程序设计大作业——员工管理系统(代码超详细内含实验报告)...
- Gram Matrices理解
- UART数据发送和接收(Verilog)