在看源代码之前,先对命令行的参数有一个了解。程序的main函数中的参数就是命令行传入的,所以先了解命令参数的规则有助于以后的源代码分析。
这部分内容参考ffmpeg.org的文档部分,如果只是记录翻译内容达不到理解的目的。主要不是解释每个参数什么意思,而是理解输入参数的格式,代码中会映射成各种结构体。这里总结了对于这部分文档的理解,可能存在错误,欢迎指正。
一、基础命令格式
命令行的结构
文档中给出了通用格式:ffmpeg [global_options] {[input_file_options] -i input_url} … {[output_file_options] output_url} … 这个格式大致可以分成四个部分
- ffmpeg:这个是可执行程序本身,可以是绝对路径,如上一篇中的/opt/ffmpeg/_build/bin/ffmpeg
- [global_options]:这个是全局参数部分,如指定日志级别等
- {[input_file_options] -i input_url}:这部分是输入参数部分,也可以理解为视频源部分,这部分可以指定一个或多个视频源及参数,需要注意的是参数在前,-i 后面是视频源的路径
- {[output_file_options] output_url}:这部分是输入部分,指定输出的格式及其参数,同样可以指定一个或多个,参数在前,输出文件在后,但这里没有“-i”这样的标记参数
示例说明
用文档中的几个小示例来理解一下这个结构
ffmpeg -i input.avi -b:v 64k -bufsize 64k output.avi
- ffmpeg:可执行程序名称
- 全局变量:这里没有
- 输入部分:(没有参数)-i input.avi(输入的文件是当前目录下的input.avi文件)
- 输出部分:-b:v 64k -bufsize 64k(参数:视频比特率为64k,缓存大小64k)output.avi(输出文件是当前目录下的output.avi文件)
ffmpeg -r 1 -i input.m2v -r 24 output.avi
- ffmpeg:可执行程序名称
- 全局变量:这里没有
- 输入部分:-r 1(参数:设置帧率为1)-i input.avi(输入的文件是当前目录下的input.m2v文件)
- 输出部分:-r 24(参数:设置帧率为24)output.avi(输出文件是当前目录下的output.avi文件)
理解了参数的格式,如果想知道参数的含义直接查询文档就可以了
二、选择视频流
视频流选择分成两种方式:自动选择和手动选择。在命令行参数中通过指定 map 参数来区分,如果含有 map 参数则被解释为手动选择。这部分内容在官方文档里单独做了说明,结合官方的示例来理解一下。
- 自动选择
在没有指定 map 参数的时候,ffmpeg程序会检查输出格式,根据输出格式来选择输入文件中可用的视频、音频和字幕等。自动选择会依据一些默认的规则:
a. 视频选择分辨率最高的那个,如示例中B.mp4的stream 0
b. 音频选择声道最多的那个,如示例中B.mp4中的stream 3
c. 字幕选择第一个发现的字幕数据
d. 其他数据(除视频、音频和字幕)不能自动选择
示例说明
假设我们有三个不同格式的视频文件分别是:A.avi、B.mp4、C.mkv,不同的视频格式我们也可以理解为不同的容器,每个容器里可能包含:视频、音频、字母等数据,分别用stream0~n来表示:
input file 'A.avi'
stream 0: video 640x360 #一个分辨率为640 x 360的视频数据
stream 1: audio 2 channels #双声道音频数据
input file 'B.mp4'
stream 0: video 1920x1080 #一个分辨率为1920x1080的视频数据
stream 1: audio 2 channels #双声道音频数据
stream 2: subtitles (text) #文本字幕数据1(中文)
stream 3: audio 5.1 channels #5.1声道的音频数据
stream 4: subtitles (text) #文本字幕数据2(英文)
input file 'C.mkv'
stream 0: video 1280x720 #一个分辨率为1280x720的视频数据
stream 1: audio 2 channels #双声道音频数据
stream 2: subtitles (image) #图片字幕数据
命令1
ffmpeg -i A.avi -i B.mp4 out1.mkv out2.wav -map 1:a -c:a copy out3.mov
- 两个输入:A.avi、B.avi,三个输出:out1.mkv、out2.wav、out3.mov
- 前两个输出out1.mkv、out2.wav没有map参数,使用自动选择。
- out1.mkv是Matroska容器格式,可以包含视频、音频和字幕文件,因此根据上面说的默认规则:视频选择B.mp4中的stream 0,音频选择B.mp4中的stream 3,字幕选择B.mp4中的stream(因为A.avi中没有字幕数据)
- out2.wav是个音频文件,所以只包含音频,选择B.mp4中的stream 3
- out3.mov使用了map参数,因此根据参数指定,-map 1:a这个参数指定了B.mp4的音频。
- 前两个输出包含的流会被自动选择编码器。第三个输出指定音频编码为copy,也就是不进行编码。
三、过滤器
在编码之前,ffmpeg可以通过过滤器对原始的音视频数据进行处理,libavfilter库是对各种过滤器的实现。一次处理可以使用多个过滤器,这些过滤器可以以图的形式展示。ffmpeg中把过滤器图分成两类:简单和复杂。
简单过滤器
简单的过滤器是指只有一个输入和输出,并且都是相同类型的过滤器。如下图,编码和解码之间插入一个附加步骤来对处理帧数据:
_________ ______________
| | | |
| decoded | | encoded data |
| frames |\ _ | packets |
|_________| \ /||______________|
\ __________ /
simple _\|| | / encoder
filtergraph | filtered |/
| frames |
|__________|
简单过滤器通过vf和af参数分别指定视频和音频的过滤器,一个对视频的简单过滤其可以画成类似下面的示意图:
_______ _____________ _______ ________
| | | | | | | |
| input | ---> | deinterlace | ---> | scale | ---> | output |
|_______| |_____________| |_______| |________|
小提示:有些过滤器只是改变了帧的属性而没有改变帧的内容,例如:fps过滤只是改变帧的数量,没有改变内容
复杂过滤器
复杂过滤不能像简单过滤那样线性的处理数据。例如有多个输入或者输出,输出的流和输入的流类型不一样的时候,可以用下图表示:
_________
| |
| input 0 |\ __________
|_________| \ | |
\ _________ /| output 0 |
\ | | / |__________|
_________ \| complex | /
| | | |/
| input 1 |---->| filter |\
|_________| | | \ __________
/| graph | \ | |
/ | | \| output 1 |
_________ / |_________| |__________|
| | /
| input 2 |/
|_________|
复杂过滤器通过lavfi参数指定,需要注意的是这个参数是个全局参数,因为复杂过滤器不能明确的指定是哪个输入或者输出。例如overlay过滤器就是把两个视频合成为一个。
如果想了解具体参数的含义可以直接查询官方文档
解析流程
示例中的参数被split_commandline方法循环处理
数组下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
参数值 | ffmpeg | -re | -i | Bird.mp4 | -c | copy | -f | flv | rtmp://192.168.1.202:1935 |
主要的判断流程如图:
具体解析方法的源代码:
int split_commandline(OptionParseContext *octx, int argc, char *argv[], const OptionDef *options,
const OptionGroupDef *groups, int nb_groups) {
int optindex = 1;
int dashdash = -2;
init_parse_context(octx, groups, nb_groups);
while (optindex < argc) {
//opt[0]='-' opt[1]='r'
const char *opt = argv[optindex++], *arg;
const OptionDef *po;
int ret;
av_log(NULL, AV_LOG_DEBUG, "Reading option '%s' ...", opt);
if (opt[0] == '-' && opt[1] == '-' && !opt[2]) {
dashdash = optindex;
continue;
}
/* unnamed group separators, e.g. output filename */
if (opt[0] != '-' || !opt[1] || dashdash + 1 == optindex) {
finish_group(octx, 0, opt);
av_log(NULL, AV_LOG_DEBUG, " matched as %s.\n", groups[0].name);
continue;
}
opt++;
if ((ret = match_group_separator(groups, nb_groups, opt)) >= 0) {
do {
arg = argv[optindex++];
if (!arg) {
av_log(NULL, AV_LOG_ERROR, "Missing argument for option '%s'.\n", opt);
return AVERROR(EINVAL);
}
} while (0);
finish_group(octx, ret, arg);
av_log(NULL, AV_LOG_DEBUG, " matched as %s with argument '%s'.\n", groups[ret].name, arg);
continue;
}
/* normal options */
po = find_option(options, opt);
if (po->name) {
if (po->flags & OPT_EXIT) {
/* optional argument, e.g. -h */
arg = argv[optindex++];
} else if (po->flags & HAS_ARG) {
do {
arg = argv[optindex++];
if (!arg) {
av_log(NULL, AV_LOG_ERROR, "Missing argument for option '%s'.\n", opt);
return AVERROR(EINVAL);
}
} while (0);
} else {
arg = "1";
}
add_opt(octx, po, opt, arg);
av_log(NULL, AV_LOG_DEBUG, " matched as option '%s' (%s) with "
"argument '%s'.\n", po->name, po->help, arg);
continue;
}
/* AVOptions */
if (argv[optindex]) {
ret = opt_default(NULL, opt, argv[optindex]);
if (ret >= 0) {
av_log(NULL, AV_LOG_DEBUG, " matched as AVOption '%s' with argument '%s'.\n", opt, argv[optindex]);
optindex++;
continue;
} else if (ret != AVERROR_OPTION_NOT_FOUND) {
av_log(NULL, AV_LOG_ERROR, "Error parsing option '%s' with argument '%s'.\n", opt, argv[optindex]);
return ret;
}
}
/* boolean -nofoo options */
if (opt[0] == 'n' && opt[1] == 'o' &&
(po = find_option(options, opt + 2)) && po->name && po->flags & OPT_BOOL) {
add_opt(octx, po, opt, "0");
av_log(NULL, AV_LOG_DEBUG, " matched as option '%s' (%s) with argument 0.\n",
po->name, po->help);
continue;
}
av_log(NULL, AV_LOG_ERROR, "Unrecognized option '%s'.\n", opt);
return AVERROR_OPTION_NOT_FOUND;
}
return 0;
}