接上篇
ffmpeg命令分析【内容包括】-vf/ac/b:v/r/re/segment/t/ss/output_ts_offset/vn/acc/print/yuv420p/yuv封装mp4/FFmpeg硬件加速/pix_fmt/拉取TCP流/tee输出多路流/ffmpeg复杂滤镜filter_complex/map_channel/vframe
16、ffmpeg命令分析-pix_fmt
16、ffmpeg命令分析-pix_fmt
本系列 以 ffmpeg4.2 源码为准,下载地址:链接:百度网盘 提取码:g3k8
本系列主要分析各种 ffmpeg 命令参数 在代码里是如何实现的。a.mp4下载链接:百度网盘,提取码:nl0s 。
命令如下:
ffmpeg -vcodec h264 -i a.mp4 -pix_fmt nv12 -vcodec h264_mf -acodec copy a_h264_nvenc.mp4 -y
上面的命令是解码,然后编码,本文不关注编解码,主要分析 -pix_fmt nv12 参数,由于一些硬件编码器只支持特定的像素格式,所以需要 --pix_fmt 指定编码器的 pix_fmt,代码如下:
MATCH_PER_STREAM_OPT(frame_pix_fmts, str, frame_pix_fmt, oc, st);
//省略代码。
if (frame_pix_fmt && (video_enc->pix_fmt = av_get_pix_fmt(frame_pix_fmt)) == AV_PIX_FMT_NONE) {
av_log(NULL, AV_LOG_FATAL, "Unknown pixel format requested: %s.\n", frame_pix_fmt);
exit_program(1);
}
相关视频推荐
FFmpeg最佳学习方法,只讲一次!/FFmpeg/webRTC/rtmp/hls/rtsp/ffplay/srs
重点代码是 video_enc->pix_fmt = av_get_pix_fmt(frame_pix_fmt),命令行的 pix_fmt 最后解析给编码器的 pix_fmt 。
这里,还有一个重点,如果解码器输出的 pix_fmt 跟 video_enc->pix_fmt 不一致,就会创建 filter,进行格式转换。把像素格式转换好之后再丢给编码器。
代码如下:
configure_output_video_filter() 函数代码
if ((pix_fmts = choose_pix_fmts(ofilter))) {
AVFilterContext *filter;
snprintf(name, sizeof(name), "format_out_%d_%d",
ost->file_index, ost->index);
ret = avfilter_graph_create_filter(&filter,
avfilter_get_by_name("format"),
"format", pix_fmts, NULL, fg->graph);
av_freep(&pix_fmts);
if (ret < 0)
return ret;
if ((ret = avfilter_link(last_filter, pad_idx, filter, 0)) < 0)
return ret;
last_filter = filter;
pad_idx = 0;
}
17、ffmpeg命令分析-拉取TCP流
TCP推流命令如下:
ffmpeg.exe -re -i a.mp4 -c copy -f flv tcp://127.0.0.1:1234/live/stream
TCP拉流命令如下:
ffmpeg.exe -listen 1 -i tcp://127.0.0.1:1234/live/stream -c copy -f flv output.flv -y
ffmpeg 除了支持 RTMP,HTTP 等高层的协议,也支持直接拉取 TCP 流。本文主要分析 TCP 拉流的逻辑,推流不管。
-i tcp://127.0.0.1:1234/live/stream 这个参数其实没有太多需要分析的地方,在API调用层,跟 -i 本地文件是一样的,就是传递个字符串进去 avforamt_open_input 函数里面。avforamt_open_input 内部根据字符串类型 做不同的解封装处理,如图:
主要分析 -listen 1
会影响哪些逻辑。 listen 的定义是在 libavformat/tcp.c
里面,如下:
static const AVOption options[] = {
{ "listen","Listen for incoming connections", OFFSET(listen),AV_OPT_TYPE_INT, { .i64 = 0 },0, 2,.flags = D|E }
省略代码...
};
因为 listen 定义是在 libavformat 目录里面的,所以这个 -listen 的解析,是走的 opt_default() -> avformat_get_class() ,最后把 listen 解析到 o->g->format_opts ,然后丢进去 avformat_open_input() 函数,具体分析可以看专栏《FFmpeg源码分析-参数解析篇》
所以 -listen 1 对 ffmpeg.c 的代码逻辑并没有什么影响,他只是 tcp format 的一个 option 参数,传递进去 avformat_open_input() 函数,内部已经封装好,API调用层无感知。
分享一个音视频高级开发交流群,需要C/C++ Linux服务器架构师学习资料加企鹅群:788280672获取。资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等),免费分享。
18、ffmpeg命令分析-tee输出多路流
早期 FFmpeg 在 转 码 后 输出 直播 流 时并 不支持 编码 一次 之后 同时 输出 多路 直播 流, 需要 使用 管道 方式 进行 输出, 而在 新版本 的 FFmpeg 中 已经 支持 tee 文件 封装 及 协议 输出, 可以 使用 tee 进行 多路 流 输出, 本节 将 主要 讲解 管道 方式 输出 多路 流 与 tee 协议 输出 方式 输出 多路 流。
本文主要讲解 tee 方式 输出多路流 在 ffmpeg.c 里面的逻辑实现,命令如下:
ffmpeg.exe -re -i a.mp4 -vcodec h264_mf -acodec aac -map 0 -f tee "[f=flv]tcp://127.0.0.1:1234/live/stream | [f=flv]rtmp://192.168.0.122/live/livestream"
上面的命令 音频编码 为 AAC,视频编码为 H264,转成 flv 的封装,然后推了两路流。
1,tcp 流,把 flv 的数据放在 tcp 包里面进行传输。
2,rtmp 流,把flv 的数据放在 rtmp 上层进行传输。
这两路流的服务器请自行搭建。
首先分析 -map 0 参数在 ffmpeg.c 里面的逻辑,map 的定义在 ffmpeg_opt.c 里面。
{ "map", HAS_ARG | OPT_EXPERT | OPT_PERFILE |
OPT_OUTPUT, { .func_arg = opt_map },
"set input stream mapping",
"[-]input_file_id[:stream_specifier][,sync_file_id[:stream_specifier]]" },
"[-]input_file_id[:stream_specifier][,sync_file_id[:stream_specifier]]" 这句注释,我也没看出这个 map 的具体用法,所以直接分析代码逻辑,从代码逻辑推导出 map的 具体用法。
从代码可以看出 map 会调用 opt_map 函数。opt_map 里面会 操作这两个变量 o->nb_stream_maps 跟 o->stream_maps, 这里面有一个新的结构 StreamMap,如下:
typedef struct StreamMap {
int disabled; /* 1 is this mapping is disabled by a negative map */
int file_index;
int stream_index;
int sync_file_index;
int sync_stream_index;
char *linklabel; /* name of an output link, for mapping lavfi outputs */
} StreamMap;
opt_map函数 的重点如下,已经圈出来了:
o->nb_stream_maps
会对哪些逻辑产生影响呢?请继续往下看,
从上图可以看到,在 open_output_file 函数里面使用了 nb_stream_maps 这个变量,如果这个 nb_stream_maps 是 0,就会执行以下逻辑:
1,从输入文件 选出分辨率最高的视频流 作为 视频输出流的 输入。
2,从输入文件 选出声道数最多的音频流 作为 音频输出流的 输入。
由于 我们命令行指定 了 -map 0,所以 nb_stream_maps 等于 2,因为 a.mp4 里面有两个流。所以不会走上面的逻辑。而是走下面的 else{}.
open_output_file 里面的重点逻辑如下:
如上图所示,本文的命令会跑进去红笔圈出来的逻辑。
-map 参数 分析完毕,这个 map 选项 是对多个输入输出流 做指定的,指定哪个输入流对应哪个输出流。
-map 0 后面带一个 0 ,实现的功能就是 把输入文件的所有流都输出给输出文件,不选最好的流进行输出,所有流都输出。
接下来 分析 -f tee 的实现,-f 的定义在 ffmpeg_opt.c 里面。
{ "f", HAS_ARG | OPT_STRING | OPT_OFFSET |
OPT_INPUT | OPT_OUTPUT, { .off = OFFSET(format) },
"force format", "fmt" },
如果你看过之前的博客文章,就知道 f 是强制指定封装格式,所以 tee 肯定是一个类似 flv 之类的伪封装格式。
首先 -f 的解析是这样的,-f tee 最后会以 key=value 的方式,也就是 f=tee 的方式丢进去 o->g->format_opts,然后给 avformat_open_input 函数用,如下:
err = avformat_open_input(&ic, filename, file_iformat, &o->g->format_opts);
所以 -f tee 跟 -f flv 是类似的,在 api 层使用没有太大的区别。
这样一看 就知道 tee 肯定是 在 libavformat 目录下有个 c文件的实现,果然,libavformat\tee.c 文件存在,部分代码如下:
static const AVClass tee_muxer_class = {
.class_name = "Tee muxer",
.item_name = av_default_item_name,
.option = options,
.version = LIBAVUTIL_VERSION_INT,
};
这是一个 伪封装格式,并不是真正意义的音视频封装格式,只是为了 api 函数的通用性设计出来的。
tee 还有另一种写法,命令如下:
ffmpeg.exe -re -i a.mp4 -vcodec h264_mf -acodec aac -map 0 -f flv "tee:tcp://127.0.0.1:1234/live/stream|rtmp://192.168.0.122/live/livestream"
这种写法 tee 看起是一种协议,而不是一种封装,实际上是一样的,这也是一个伪协议。
19、ffmpeg复杂滤镜-filter_complex
命令如下:
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 。所以现在数据结构如下图所示:
本命令里,音频滤镜是一个空的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 即可。
20、ffmpeg命令分析-map_channel
命令如下:
ffmpeg.exe -i a.mp4 -map_channel 0.1.0 left.aac -map_channel 0.1.1 right.aac
上面的命令 是把 a.mp4 里面的左右声道分布存储在 left.aac ,right.aac 里面。
下面主要分析 一下 -map_channel 0.1.0 参数的作用。
在 ffmpeg_opt.c 的定义如下:
{ "map_channel", HAS_ARG | OPT_EXPERT | OPT_PERFILE | OPT_OUTPUT, { .func_arg = opt_map_channel },
"map an audio channel from one stream to another", "file.stream.channel[:syncfile.syncstream]" },
从定义得知,0.1.0 的意思如下:
0 代表第一个输入文件。
1 代表输入文件的第二个流。
0 代表流的第一个声道。
从上面定义可以看出,-map_channel 会调用 opt_map_channel() 函数,这个函数的逻辑比较简单,不进行分析了,自动查看即可。
opt_map_channel() 里面做的是就是 操作两个变量,o->audio_channel_maps 跟 o->nb_audio_channel_maps,定义如下:
typedef struct {
int file_idx, stream_idx, channel_idx; // input
int ofile_idx, ostream_idx; // output
} AudioChannelMap;
下面继续分析 ,audio_channel_maps 变量在什么地方用到。全局搜索可知,audio_channel_maps 在 new_audio_stream() 函数里面使用了。
new_audio_stream() 函数相对来说也是比较简单,主要就是把 o->audio_channel_maps 转移给 ost->audio_channels_map,重点代码如下:
//ffmpeg_opt.c 1964行
ost->audio_channels_map[ost->audio_channels_mapped++] = map->channel_idx;
ost->audio_channels_map 的作用是这样的,音频流(输出)的声道 默认是 取 的 输入音频流的全部声道,这个 ost->audio_channels_map 就是表明,我这个输出流 只取 输入流的哪些声道。
下面继续分析 ost->audio_channels_map 在哪些地方有被使用。全局搜索可知,ost->audio_channels_map 主要在 ffmpeg_filter.c 文件的 configure_output_audio_filter() 函数里面使用,重点代码如下:
#define AUTO_INSERT_FILTER(opt_name, filter_name, arg) do { \
AVFilterContext *filt_ctx; \
\
av_log(NULL, AV_LOG_INFO, opt_name " is forwarded to lavfi " \
"similarly to -af " filter_name "=%s.\n", arg); \
\
ret = avfilter_graph_create_filter(&filt_ctx, \
avfilter_get_by_name(filter_name), \
filter_name, arg, NULL, fg->graph); \
if (ret < 0) \
return ret; \
\
ret = avfilter_link(last_filter, pad_idx, filt_ctx, 0); \
if (ret < 0) \
return ret; \
\
last_filter = filt_ctx; \
pad_idx = 0; \
} while (0)
//ffmpeg_filter.c 567行
if (ost->audio_channels_mapped) {
int i;
AVBPrint pan_buf;
av_bprint_init(&pan_buf, 256, 8192);
av_bprintf(&pan_buf, "0x%"PRIx64,
av_get_default_channel_layout(ost->audio_channels_mapped));
for (i = 0; i < ost->audio_channels_mapped; i++)
if (ost->audio_channels_map[i] != -1)
av_bprintf(&pan_buf, "|c%d=c%d", i, ost->audio_channels_map[i]);
AUTO_INSERT_FILTER("-map_channel", "pan", pan_buf.str);
av_bprint_finalize(&pan_buf, NULL);
}
从上面代码可以看出 调用了 pan filter
来实现这个这个 channel map 的功能。从代码的 log 可以看出,-map_channel 0.1.0
是一种过时的写法,主要为了与 lavfi
进行兼容,新的命令行语法如下:
ffmpeg.exe -i a.mp4 -af pan="0x4|c0=c0" left.aac -af pan="0x4|c0=c1" right.aac -y
上面的 0x4 声道布局的描述,是 AV_CH_FRONT_CENTER 的宏定义,如下:
#define AV_CH_FRONT_CENTER 0x00000004
声道数 对应 的声道布局 channel_layout_map 的定义在 源码 libavutil\channel_layout.c
里面,代码如下:
static const struct {
const char *name;
int nb_channels;
uint64_t layout;
} channel_layout_map[] = {
{ "mono", 1, AV_CH_LAYOUT_MONO },
{ "stereo", 2, AV_CH_LAYOUT_STEREO },
{ "2.1", 3, AV_CH_LAYOUT_2POINT1 },
{ "3.0", 3, AV_CH_LAYOUT_SURROUND },
{ "3.0(back)", 3, AV_CH_LAYOUT_2_1 },
{ "4.0", 4, AV_CH_LAYOUT_4POINT0 },
{ "quad", 4, AV_CH_LAYOUT_QUAD },
{ "quad(side)", 4, AV_CH_LAYOUT_2_2 },
{ "3.1", 4, AV_CH_LAYOUT_3POINT1 },
{ "5.0", 5, AV_CH_LAYOUT_5POINT0_BACK },
{ "5.0(side)", 5, AV_CH_LAYOUT_5POINT0 },
{ "4.1", 5, AV_CH_LAYOUT_4POINT1 },
{ "5.1", 6, AV_CH_LAYOUT_5POINT1_BACK },
{ "5.1(side)", 6, AV_CH_LAYOUT_5POINT1 },
{ "6.0", 6, AV_CH_LAYOUT_6POINT0 },
{ "6.0(front)", 6, AV_CH_LAYOUT_6POINT0_FRONT },
{ "hexagonal", 6, AV_CH_LAYOUT_HEXAGONAL },
{ "6.1", 7, AV_CH_LAYOUT_6POINT1 },
{ "6.1(back)", 7, AV_CH_LAYOUT_6POINT1_BACK },
{ "6.1(front)", 7, AV_CH_LAYOUT_6POINT1_FRONT },
{ "7.0", 7, AV_CH_LAYOUT_7POINT0 },
{ "7.0(front)", 7, AV_CH_LAYOUT_7POINT0_FRONT },
{ "7.1", 8, AV_CH_LAYOUT_7POINT1 },
{ "7.1(wide)", 8, AV_CH_LAYOUT_7POINT1_WIDE_BACK },
{ "7.1(wide-side)", 8, AV_CH_LAYOUT_7POINT1_WIDE },
{ "octagonal", 8, AV_CH_LAYOUT_OCTAGONAL },
{ "hexadecagonal", 16, AV_CH_LAYOUT_HEXADECAGONAL },
{ "downmix", 2, AV_CH_LAYOUT_STEREO_DOWNMIX, },
{ "22.2", 24, AV_CH_LAYOUT_22POINT2, },
};
map_channel 分析完毕,总结:通过 pan filter 实现。
21、ffmpeg命令分析-vframe
命令如下:
ffmpeg.exe -i a.mp4 -ss 00:00:7.435 -vframes 1 out.png
上面的命令是 偏移到 7 秒左右的地方,截图 一张图片来进行保存。
-ss 在 《ffmpeg命令分析-ss》里面已经讲过,本文不再分析,本文主要讲解 -vframes 参数在 ffmpeg 工程中的实现。
vframes 的定义如下:
{ "vframes", OPT_VIDEO | HAS_ARG | OPT_PERFILE | OPT_OUTPUT, { .func_arg = opt_video_frames },
"set the number of video frames to output", "number" },
会调用 opt_video_frames
函数,opt_video_frames
函数如下:
static int opt_video_frames(void *optctx, const char *opt, const char *arg)
{
OptionsContext *o = optctx;
return parse_option(o, "frames:v", arg, options);
}
从上面代码可以看出,-vframes
实际上 是 -frames:v
的另一种写法,所以真正的 定义是 -frames
,定义如下:
{ "frames", OPT_INT64 | HAS_ARG | OPT_SPEC | OPT_OUTPUT, { .off = OFFSET(max_frames) },
"set the number of frames to output", "number" },
从上面的代码可以看出,frames 最后就解析到 max_frames ,OFFSET 是一个宏,定位到 OptionsContext 的偏移位置。max_frames 实际上是 OptionsContext 里面的 max_frames 。
下面就看看 哪些地方使用了 max_frames 这个变量。
通过全局搜索 max_frames 可知,new_output_stream() 函数里面有使用,代码如下:
#ffmpeg_opt.c 1537行
ost->max_frames = INT64_MAX;
MATCH_PER_STREAM_OPT(max_frames, i64, ost->max_frames, oc, st);
从上面代码可以看出,命令行中的 max_frames 最后会赋值给 ost->max_frames,ost 是一个 struct OutputStream 结构。
继续分析,ost->max_frames 又在哪里被使用了呢?答案如下:
在本文命令下,在 do_video_out() 函数中使用了 ost->max_frames,代码如下:
//ffmpeg.c 1262行
nb_frames = FFMIN(nb_frames, ost->max_frames - ost->frame_number);
nb0_frames = FFMIN(nb0_frames, nb_frames);
从上面两句代码可以看出,输出流的已经丢给编码器的 frame_number 如果等于大于 ost->max_frames,nb_frames 变量 就会是0。
nb_frames 变成 0 就会导致 do_video_out 后面的 for (i = 0; i < nb_frames; i++) {} 循环不会执行,也就不会再有 AVFrame 输入给编码器。
这样就实现了 最大帧的功能。
最后 ost->max_frames 还在 need_output() 函数使用了一下,用于退出程序的。