有了开发工具的帮助,就可以开始学习ffmpeg命令行工具的源代码了。大致分析ffmpeg命令行工具的源代码,可以发现整个过程可以分成如下几个大的步骤:
一、日志级别处理
ffmpeg定义了几个日志级别,这里可以对参数中关于设置日志级别的参数进行解析,并设置。具体的日志级别定义可以查询官方文档。解析后在程序中可以使用av_log方法打印程序信息。
二、参数保存结构
参数解析的功能是把从命令行传入的参数解析成程序定义的结构体,方便后续的操作。结合之前配置的开发环境,用下面这个命令还学习代码:
gdbserver :1234 ./test -re -i /home/pi/Birds.mp4 -c copy -f flv rtmp://192.168.1.202:1985/myapp/demo
这个命令是将树莓派中的一个mp4文件推流至RTMP服务器,可以通过ffplay或者vlc播放。
- 命令行传入的参数
main函数接收两个参数:
int main(int argc, char **argv)
//argc是参数的总个数,这里是9
//argv是个指针,指向第一个参数
//argv[0] = '.test'
//argv[1] = '-re'
//argv[2] = '-i'
//argv[3] = '/home/pi/Birds.mp4'
//......
- 参数的结构体
这里整理了一下程序中定义的结构体及关系
从基础的结构体开始理解:
Option
通过debug来看下一下解析后的option参数结构,如下图
在ffmpeg_opt.c中定义了所有OptionDef的常量数组,通过输入的参数在数组中匹配相应的OptionDef值。key是Option的名称,val则是参数中指定的值,如果没有默认设置为“1”。以下是这两个结构的源代码:
typedef struct OptionDef {
const char *name;
int flags;
union {
void *dst_ptr;
int (*func_arg)(void *, const char *, const char *);
size_t off;
} u;
const char *help;
const char *argname;
} OptionDef;
typedef struct Option {
const OptionDef *opt;
const char *key;
const char *val;
} Option;
OptionGroup
OptionGroup共定义了三组,从上一篇的命令参数解析可以看出是:global(全局参数)、input(输入参数)、output(输出参数)
input解析后的OptionGroup如下图:
从group_def可以看出,这组参数是input,参数的分割符为“i”,arg是“i”指定的参数值:一个mp4文件的地址,opts是针对input的一些参数,从命令行中可以看出是“-re”。nb_opts的是值为1,代表opts中共有一个参数。opts指向的是第一个参数的指针。组中还有其他的一些参数如:sws_dict等,这些通过修改代码config.h中的值设置进来,准确的方法是在编译的时候指定这些变量。
output的Option参数结构与input相似:
这部分的源代码如下:
typedef struct OptionGroupDef {
/**< group name */
const char *name;
/**
* Option to be used as group separator. Can be NULL for groups which
* are terminated by a non-option argument (e.g. ffmpeg output files)
*/
const char *sep;
/**
* Option flags that must be set on each option that is
* applied to this group
*/
int flags;
} OptionGroupDef;
typedef struct OptionGroup {
const OptionGroupDef *group_def;
const char *arg;
Option *opts;
int nb_opts;
AVDictionary *codec_opts;
AVDictionary *format_opts;
AVDictionary *resample_opts;
AVDictionary *sws_dict;
AVDictionary *swr_opts;
} OptionGroup;
AVDictionary
这个是ffmpeg定一个的一个key-value格式的结构体,并提供三个api来使用这个结构体:
av_dict_set() // 添加一个键值对
v_dict_get() //通过key获取value
av_dict_free() //释放内存空间
一个key可以有多个value,可以在set的时候指定是替换还是添加,count代表value的个数。
struct AVDictionary {
int count;
AVDictionaryEntry *elems;
};
struct AVDictionaryEntry {
char *key;
char *value;
} AVDictionaryEntry;
GroupOptionList
这个结构体中存放就是global、input和output三个OptionGroup中的一个或者多个。
typedef struct OptionGroupList {
const OptionGroupDef *group_def;
OptionGroup *groups;
int nb_groups;
} OptionGroupList;
OptionParseContext
命令行参数解析的上下文结构体,解析后参数内容被解析到这个结构体,并传递给后面的处理程序
从变量命名上可以看出含义,cur_group是在程序解析时指定当前正在解析的参数组
typedef struct OptionParseContext {
OptionGroup global_opts;
OptionGroupList *groups;
int nb_groups;
/* parsing state */
OptionGroup cur_group;
} OptionParseContext;
理解主要的结构体可以帮助我们后续更好的理解解析的过程
三、参数解析过程
示例中的参数被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;
}