ffmpeg命令分析下【详细分析合集】

接上篇

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() 函数使用了一下,用于退出程序的。

猜你喜欢

转载自blog.csdn.net/qq_52703909/article/details/123758852