目录概览
- 0.参数配置对象流程图
-
- 0.1 用到的设计模式
-
- 0.1.1 装饰器模式
- 0.1.2 模板模式
- 0.1.3 组合模式
- 0.2 与朴素思想的对比
- 1.参数传递部分
-
- 1.1 AVDictionary字典容器类
-
- 1.1.1 类定义及类图
- 1.1.2 构造函数
- 1.1.3 析构函数
- 1.1.4 设置/读取等配置参数
- 1.2 实例
- 2.参数配置生效部分
-
- 2.1 可配参数业务类
-
- 2.1.1 类定义及类图
- 2.1.2 相关操作函数
-
- 2.1.2.1 av_opt_set_defaults
- 2.1.2.2 av_opt_next
- 2.1.2.3 av_opt_set_dict
- 2.2 参数过滤模块
-
- 2.2.1 AVClass装饰器类
-
- 2.2.1.1 类定义及类图
- 2.2.2 AVOption类
-
- 2.2.2.1 类定义
- 2.2.2.2 查看支持的参数配置
- 2.3 实例
-
- 2.3.1 参数传递
- 2.3.2 拷贝到新字典类对象
- 2.3.3 第1个可配参数业务类对象
- 2.3.3 第2个可配参数业务类对象
- 2.3.4 一个内存泄漏的点
- 3.参数支持表有没有共性的表?
- 4.参数在哪里使用?
- 5.参数解析
-
- 5.1 标志类 AV_OPT_TYPE_FLAGS
- 5.2 其他类型
- 6.AVClass类对象绑定机制
- 7.小结
- 8.参考博客
ffmpeg支持很多参数配置——拉流、编码、解码等参数配置——那么庞大繁杂的配置项,如果是你,该如何实现呢?
其实看过一点点源码(不用全部)后发现,就是它的实现也是遵循一个朴素的思想——所谓“大道至简”,“万变不离其宗”——就算再多的参数,按照我们简单的思想,最开始的思维,最直接的思维,如何实现?目的很简单——把一个个的输入参数保存到对象的成员变量里或者变量里,然后运行时直接使用——这是一个非常简单、朴素的思想。如下图
但是实现手段可以千变万化——fffmpeg的实现也是这样的,同样的目的,只是经历的实现过程比较“千变万化”、比较“繁杂”、比较“迷人眼”而已。
看下它复杂实现的对象流程图——这属于总—分—分的描述写法了,先结论,再原因。
0.参数配置对象流程图
大致流程如上图,可以看到ffmpeg把输入参数统一抽象成键值对放到字典容器类对象里,且键和值都用字符串表示。内部生效时再转换成对应格式,然后映射到具体业务对象的成员变量里。
从他上图可知,和我们最初的梦想一样,都是把参数配置到一个对象的成员变量里。
为了实现ffmpeg的参数配置体系/机制,ffmpeg抽象了如上图5类(细分):AVDictionary字典容器类,AVDictionaryEntry字典实体类,参数支持表AVOption类,参数配置装饰器AVClass类,继承AVClass *class的可配参数业务类(比如AVCodecContext/RTSPState等类);
这5大类,其中AVDictionary字典容器类,AVDictionaryEntry字典实体类作为参数传递的载体。后面3类是参数配置生效的类。
前面4类是基础、工具类、公共模块,供其他模块使用,所以放到了工具箱目录——libavutil目录下。
第5类是需要开放参数配置的业务类,在业务功能模块里定义(比较灵活,谁需要谁装配),所以就不放到工具类了——第一个成员必须是AVClass *类型的,因为ffmpeg配置参数的实现是建立在它是这样的位置的假设的,不能随意改,不然得改源码。
还有个重要的AVOption类的成员offset,这个偏移相对的是谁?如上图offset虚线箭头指向——就是AVClass *所在宿主类对象地址——可配参数实体业务类对象的地址。——当然可以引入linux内核第一宏就不用把AVClass放到第一个成员了,但是要改源码了。
0.1 用到的设计模式
0.1.1 装饰器模式
ffmpeg将AVDictionary字典容器类对象里的参数映射到可配参数业务类(自己起的名字)这一过程中增加了参数支持配置表AVOption类,而AVOption类是被AVClass类管理的——AVClass类是个啥东西?我觉得称之为装饰器类,因为这用到了设计模式的装饰器设计模式——谁想增加参数可配置的功能,谁就戴上AVClass类就行了。装饰器就是谁想有什么能力就去戴上那个能力就行了。
因此,AVClass类是可配置参数能力的装饰器,这样不同需要配置参数的业务类可以定制化了。
0.1.2 模板模式
ffmpeg的这套参数配置机制,也形成了共性的代码,比如配置参数的接口,只需提供参数配置业务类对象的地址和字典类对象地址就行了,因为制定了统一的规则,所以这些代码一样,这块就不管你是谁,都能有共同的流程——这就是模板一样。称之为模板模式。
0.1.3 组合模式
这个体现在AVClass类。——AVClass类里可以包含AVClass类,这用到了第三个设计模式——组合模式。功能更强大。
0.2 与朴素思想的对比
下图是ffmpeg与朴素思想进行对比,它的实现只是朴素思想实现的演化或者复杂化——但万变不离其宗,也就是说思想上是一样的。
上图左边虚框里,是第一步,保存参数配置到字典容器里(下面会有详解)——相当于寄存器(或者寄存地)。
上图右边虚框里,是第二步,将参数配置落地——把字典容器里的参数设置到可配参数业务类对象里对应的参数成员里——最终落脚地,参数去的目的地。
从这看出,ffmpeg也是万变不离其中,和我们最初的梦想一样,都是把参数保存到全局变量或者对象的成员变量里面去,等运行的时候直接拿来使用。
初心不改,只是过程复杂。——或者说本质原理是极其简单的,一点也不复杂——复杂的是实现手段(一堆弯弯绕绕)。
对比朴素、简单的思想来说,为啥变成了这么多类呢?输入参数搞成字典类,保存参数的变量搞成了可配参数业务类——由参数配置装饰器类AVClass组合而成,参数配置器类AVClass主要管理参数支持表类AVOption。搞的这么弯弯绕绕,这样耦合性降低,同时增加了一个参数过滤的过程,不支持的参数不会配置。这样具有灵活性,各个参数配置业务类可以定制化参数支持表。
假想的进化演进之路(可略过,个人狂想)
ffmpeg的参数配置机制,是怎么从简单到复杂一点点进化成现在的模样的?
先说如果按照简单的思维,其中一种进化路线是这样的:
把所有参数弄到一个if判断函数里,然后各个接受参数配置的对象,轮流调用该函数一遍,是自己支持的参数就拿走保存到各自的参数成员变量中。——这就要求各类写下自己的参数过滤函数。但是每次都遍历所有参数表也太难了,能不能出现一种统一的参数保存地并且参数过滤后返回剩余的参数,这样其他类对象就能减少轮询次数,提高效率?于是字典类应运而生,它临时保存输入参数。然后配置的时候呢,又能把剩余的参数重新搞个字典对象返回回去——这就是字典类的功能,源自对它的需求。
解决了参数存放和遍历效率的问题,但还有个棘手的地方,我们需要每个配置参数的业务类都要有个判断支持参数的函数,这样也是一个进化演进方向,但是ffmpeg选择了抽象为配置表的形式——每个参数业务类都得自定义自己支持的参数表,
于是抽象出了AVOption类,包括了这个参数要去哪个地址的偏移offset——相对参数业务类的偏移。
这样有了字典类暂存参数,有了AVOption类表示支持的参数配置表格,还差啥?ffmpeg是要搞一个机制,我管你是什么参数可配置的业务类呢,你只要把你的地址告诉我,把你的参数表绑定好,接下来我自会把你支持的参数配置到你对应的成员变量里去。怎么实现这个机制呢?怎么才能不管你是谁呢?一种路是,你包含AVOption类,变成你的一个成员,然后参数配置过来的时候可以统一从你的地址找到AVOption类然后配置下去。但是这怎么确定你支持或不支持配置的呢?怎么办?在AVOption类里增加个标志?这就违反了软件设计5大原则的单一职责原则。那么好,方案一,不包含AVOption类了,而是包含AVOption类的指针作为成员变量,如果为NULL则不支持参数配置,非NULL为支持。
暂时是解决了,现在就剩一个问题了——知道配置参数业务类对象地址,怎么找到AVOption类的指针成员呢?方案一,把AVOption类的指针成员放在参数配置业务类的第一个成员,这样就好找了——这样就形成了装饰器模式。
这样都解决了。
但是ffmpeg的方案是另一个,再新增一个类——AVClass类——取代AVOption类的所在位置,然后它内部包含AVOption类的指针成员。具体的原因还得再探讨,所知道的是AVClass类里可以包含AVClass类,这用到了第三个设计模式——组合模式。功能更强大。
不管哪种方案,都得作为参数配置业务类对象的第一个成员,这样好找,处理代码好写。
1.参数传递部分
这一部分,ffmpeg把参数暂存到字典类中,涉及到两类,AVDictionary字典容器类和AVDictionaryEntry字典实体类。可以把这两类合并叫字典类。
为何要把输入参数统一抽象为键值对且都是字符串呢?这样对用户是很方便的,用户不用关心内部具体是什么格式,提供统一接口,方便进一步处理,类似数据处理的“归一化”——统一到同一个参考系下,减少用户的学习和使用成本。(但是,总感觉还是不够傻瓜式)
1.1 AVDictionary字典容器类
AVDictionary字典容器类——是个键值对容器——ffmpeg粗暴且低效地实现了python中的字典概念,或者cpp中的map容器概念。(所谓容器,可以简单理解为数组,实际上比数组复杂,反正就是按照一定规则存取东西的基础工具)。
它和AVDictionaryEntry字典实体类是什么关系?聚合关系(根据面向对象的思想)——具体见下面对象图。
1.1.1 类定义及类图
libavutil/dict.c中
//字典容器类定义,管理字典实体类,count是管理的个数。
struct AVDictionary {
int count;
AVDictionaryEntry *elems;
};
libavutil/dict.h中
//字典实体类,键值对的内存结构,也是存放地
typedef struct AVDictionaryEntry {
char *key;
char *value;
} AVDictionaryEntry;
//字典容器类对外的声明,好被别的模块拿去用
typedef struct AVDictionary AVDictionary;
从如上类图中,它粗暴低效的实现在于它在内存中搞了个指针数组elems,每个成员指向了一个存放键值对的内存地址(AVDictionaryEntry类对象的地址),每次新增都是调用realloc扩展指针数组elems指向的那块连续内存(而不是链表形式),每次查找都是循环遍历指针数组。
又粗暴又低效,不过能用。
1.1.2 构造函数
直接分配一块AVDictionary这么大的内存也行,但是建议调用ffmpeg实现的内存分配函数为好,因为有内存对齐,提升运行效率。
另外,ffmpeg中的av_dict_set对象方法也可以,如下。
里面包含了内存开辟。所以,直接使用即可。比如:
AVDictionary *opts = NULL;
av_dict_set(&opts, "timeout", "10000000", 0);
另外一个av_dict_copy也包含了构造函数。
int av_dict_copy(AVDictionary **dst, const AVDictionary *src, int flags)
{
AVDictionaryEntry *t = NULL;
while ((t = av_dict_get(src, "", t, AV_DICT_IGNORE_SUFFIX)))
{
int ret = av_dict_set(dst, t->key, t->value, flags);
if (ret < 0)
{
return ret;
}
}
return 0;
}
可以看到其实也是因为调用了av_dict_set函数,才具有构造功能。所以使用av_dict_copy时也可以这样:
AVDictionary *tmp = NULL;
av_dict_copy(&tmp, *options, 0);
这样就拷贝到tmp这个字典指向的对象了。
1.1.3 析构函数
av_dict_free(AVDictionary **dict);
1.1.4 设置/读取等配置参数
//设置键值对到字典类对象中——包含了构造。
int av_dict_set(AVDictionary **pm, const char *key, const char *value, int flags);
//获取字典类对象中的键值对。
AVDictionaryEntry *av_dict_get(const AVDictionary *m, const char *key,
const AVDictionaryEntry *prev, int flags);
//拷贝一个字典对象的键值对到另一个字典对象中(深拷贝),包含了构造函数。
int av_dict_copy(AVDictionary **dst, const AVDictionary *src, int flags);
按照oopc来说,它这些方法就是这个类的方法,模拟的面向对象的类方法定义,第一个形参可以看着是this指针。
1.2 实例
AVDictionary *opts = NULL;
av_dict_set(&opts, "timeout", "10000000", 0); //设置超时断开连接时间 us
av_dict_set(&opts, "buffer_size", "102400", 0); //设置缓存大小 byte
av_dict_set(&opts, "rtsp_transport", "tcp", 0); //设置rtsp以tcp/udp方式打开
av_dict_set(&opts, "threads", "0", 0); //设置自动开启线程数
av_dict_set(&opts, "probesize", "2097152", 0); //设置探测输入流数据包大小
av_dict_set(&opts, "max_delay", "1000", 0); //接收包间隔最大延迟 us
av_dict_set(&opts, "analyzeduration", "1000000", 0); //设置分析输入流所需时间 us
av_dict_set(&opts, "max_analyze_duration", "1000", 0); //设置分析输入流最大时长 us
这样呢,就把这些参数变成了键值对存放到了opts所指向的字典管理类对象中。那么接下来,ffmpeg就可以拿着opts去配置下去了。
到此,第0章的参数配置对象流程图中,参数配置传递完毕,接下来第2章所讲的就是参数配置到业务对象的成员变量中的“弯弯绕绕”“繁杂”的过程。
2.参数配置生效部分
该部分是参数配置最终到达的目的地——对应朴素思想中保存参数的对象成员变量或者变量那部分。
配置生效部分,主要分为2大块,一块是参数配置最终去的地方——可配参数业务类对象中各个参数成员。另一块是参数过滤模块。这块就是AVClass参数配置装饰器类和AVOption参数支持配置项类。
2.1 可配参数业务类
自己起的名字,ffmpeg支持的参数到底要配置到哪里呢?总得有个落脚点吧?于是可配参数业务类应运而生。
这一类,比如AVFormatContext/AVCodecContext/RTSPState等类为典型代表。
2.1.1 类定义及类图
这类是业务类,很灵活,不固定,但是这类的形式,只要把AVClass的指针成员放到第一个就行了,笼统的类图如下:
(其实这类似于c++的虚基类继承——所有可配参数类都继承自AVClass类这个纯虚基类)
可以看到,可配参数业务类是由AVClass类组成的,而AVClass类又由AVOption类组成。
如果想要拥有可配参数能力,那么就定义个这个业务的参数支持装饰器AVClass对象(实例化),否则就把成员class置为NULL。
比如rtsp的可配参数业务类对象是RTSPState这个全局变量(第1个成员必须是AVClass类的指针类型),可配参数装饰器AVClass类实例化是rtsp_demuxer_class,AVOption类实例化是ff_rtsp_options表格,然后它们绑定到一起,组成了rtsp可配参数业务类。如下图
2.1.2 相关操作函数
//从参数支持表格中获取默认参数,配置到最终目标对象——可配参数业务类——的对应参数成员变量里
void av_opt_set_defaults(void *s)
//把配置的参数设置到可配参数业务对象的成员变量里,
//obj就是可配置参数业务对象的地址(即this指针),
//比如AVCodecContext/AVFormatContext/RTSPState等的地址,也是参数最终到达的目标对象
int av_opt_set_dict(void *obj, AVDictionary **options)
//可配参数业务对象中,循环遍历获取AVOption表格中的一个个AVOption类成员的迭代器。
const AVOtion *av_opt_next(const void *obj, const AVOption *last)
有意思的是上面几个方法中的第一个参数obj/s其实是this指针——可配参数业务对象的地址。如下分别说呢:
2.1.2.1 av_opt_set_defaults
av_opt_set_defaults这个就是从AVClass类的成员参数支持表AVOption类中获取默认值,直接配置到目标对象——可配参数业务类对象——的对应的
参数成员里,没啥好说的。
2.1.2.2 av_opt_next
av_opt_next模拟了高级语言中的迭代器,把可配参数业务类对象里AVClass中的AVOption表格里的所有配置项遍历出来,使用例程如下:
AVOtion *opt = NULL;
while(opt = av_opt_next(obj, opt))
{
//循环遍历出一个个配置项,和c++/python等高级语言的迭代器是一模一样的,模拟了它们高级语言的特性
}
2.1.2.3 av_opt_set_dict
av_opt_set_dict的关键调用链如下:
av_opt_set_dict ⇒ av_opt_set_dict2 ⇒ av_opt_set⇒ av_opt_find2
具体:
int av_opt_set_dict(void *obj, AVDictionary **options)
{
return av_opt_set_dict2(obj, options, 0);
}
int av_opt_set_dict2(void *obj, AVDictionary **options, int search_flags)
{
AVDictionaryEntry *t = NULL;
AVDictionary *tmp = NULL;
int ret;
if (!options)
return 0;
while ((t = av_dict_get(*options, "", t, AV_DICT_IGNORE_SUFFIX)))
{
ret = av_opt_set(obj, t->key, t->value, search_flags);
if (ret == AVERROR_OPTION_NOT_FOUND)
{
ret = av_dict_set(&tmp, t->key, t->value, 0);
if (ret < 0)
{
av_log(obj, AV_LOG_ERROR, "Error setting option %s to value %s.\n", t->key, t->value);
av_dict_free(&tmp);
return ret;
}
}
}
av_dict_free(options);
*options = tmp;
return 0;
}
int av_opt_set(void *obj, const char *name, const char *val, int search_flags)
{
int ret = 0;
void *dst, *target_obj;
const AVOption *o = av_opt_find2(obj, name, NULL, 0, search_flags, &target_obj);
……
dst = ((uint8_t *)target_obj) + o->offset;
switch (o->type)
{
case AV_OPT_TYPE_BOOL:
return set_string_bool(obj, o, val, dst);
}
}
const AVOption *av_opt_find2(void *obj, const char *name, const char *unit,
int opt_flags, int search_flags, void **target_obj)
{
const AVClass *c;
const AVOption *o = NULL;
if(!obj)
return NULL;
c= *(AVClass**)obj;
if (!c)
return NULL;
if (search_flags & AV_OPT_SEARCH_CHILDREN)
{
if (search_flags & AV_OPT_SEARCH_FAKE_OBJ)
{
void *iter = NULL;
const AVClass *child;
while (child = av_opt_child_class_iterate(c, &iter))
if (o = av_opt_find2(&child, name, unit, opt_flags, search_flags, NULL))
return o;
}
else
{
void *child = NULL;
while (child = av_opt_child_next(obj, child))
if (o = av_opt_find2(child, name, unit, opt_flags, search_flags, target_obj))
return o;
}
}
while (o = av_opt_next(obj, o))
{
if (!strcmp(o->name, name) && ((o->flags & opt_flags) == opt_flags) &&
((!unit && o->type != AV_OPT_TYPE_CONST) ||
(unit &&am