ffmpeg面向对象——参数配置秘密探索及其设计模式
ffmpeg支持很多参数配置——拉流配置推流配置等等——那么庞大繁杂的配置项,如果是你,该如何实现呢?
其实看过一点点源码(不用全部)后发现,就是它的实现也是遵循一个朴素的思想——所谓“大道至简”,“万变不离其宗”——就算再多的参数,按照我们简单的思想,最开始的思维,最直接的思维,如何实现?目的很简单——把一个个的参数映射到对象的成员变量里或者全局变量里。这是一个非常简单的思想,及其朴素的思想——但是实现手段可以千变万化——fffmpeg的实现也是这样的,同样的目的,只是经历的实现过程比较“千变万化”、比较“繁杂”、比较“迷人眼”而已。
朴素的表示就是:输入配置参数 ——> 更改对象成员变量/全局变量。如下图
思想很朴素,目的很简单。但ffmpeg的实现很复杂。
先说一个小复杂:
ffmpeg把参数统一抽象成键值对,且键和值都用字符串表示,具体内部的生效再转换成对应格式,然后配置到具体的业务对象成员变量里。
再看下它复杂实现的对象流程图——这属于总—分—分的描述写法了,先结论,再原因。
0.参数配置对象流程图
为了实现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放到第一个成员了,但是要改源码了。
上图左边虚框里,是第一步,保存参数配置到字典容器里(下面会有详解)——相当于寄存器(或者寄存地)。
上图右边虚框里,是第二步,将参数配置落地——把字典容器里的参数设置到可配参数业务类对象里的对应的参数成员里——最终落脚地,参数去的目的地。
ffmpeg的实现,直接将参数抽象成了AVDictionary字典容器类,然后把保存这些参数的全局变量或者对象的成员变量依然未变,参数最终映射到的是参数配置的业务类,但是变的是给增加了参数支持配置表AVOption类,而AVOption类由被AVClass类管理——AVClass类是个啥东西?我觉得称之为装饰器类,因为这用到了设计模式的装饰器设计模式——谁想增加参数可配置的功能,谁都带上AVClass类就行了。装饰器就是谁想有什么能力就去戴上那个能力就行了。AVClass类是可配置参数能力的装饰器。
1.AVDictionary字典容器类
AVDictionary粗暴且低效地实现了python中的字典概念,或者cpp中的map容器概念——是个键值对容器。
它和AVDictionaryEntry字典实体类是什么关系?聚合关系(根据面向对象的思想)——具体见下面对象图。
粗暴低效实现,在于它在内存中搞了个指针数组,存放一个个的键值对,每次新增都会扩展这个指针数组,每次查找都是循环遍历指针数组来匹配。
又粗暴又低效,不过能用。
1.1 类定义及类图
libavutil/dict.c中
struct AVDictionary {int count;AVDictionaryEntry *elems;
};
libavutil/dict.h中
typedef struct AVDictionaryEntry {char *key;char *value;
} AVDictionaryEntry;typedef struct AVDictionary AVDictionary;
从如上类图/数据结构中,就可以推测出,这个键值对保存,确实是很粗暴。——搞了个指针数组,指向各个键值对内存地址。如果想加入一个键值对,不是链表形式,而是调用realloc扩展指针数组的内存即elems成员指向的那块连续内存。
1.2 构造函数
oopc的构造也是类似c++的,但c++的类对象的内存开辟编程人员看不到由编译器编译时添加。
ffmpeg的实现是这个AVDictionary对象直接调用av_dict_set方法来构造。
里面包含了内存开辟。所以,直接使用即可。比如:
AVDictionary *opts = NULL;av_dict_set(&opts, "stimeout", "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.3 析构函数
av_dict_free(&opts);
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.5 参数配置实例
AVDictionary *opts = NULL;av_dict_set(&opts, "stimeout", "10000000", 0); //设置超时断开连接时间 usav_dict_set(&opts, "buffer_size", "102400", 0); //设置缓存大小 byteav_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); //接收包间隔最大延迟 usav_dict_set(&opts, "analyzeduration", "1000000", 0); //设置分析输入流所需时间 usav_dict_set(&opts, "max_analyze_duration", "1000", 0); //设置分析输入流最大时长 us
这样呢,就把这些参数变成了键值对存放到了opts所指向的字典管理类对象中。那么接下来,ffmpeg就可以拿着opts去配置下去了。
到此,第0章的参数配置对象流程图中,参数配置传递完毕,接下来所讲的就是参数配置到业务对象的成员变量中的“弯弯绕绕”“繁杂”的过程。首先碰到的就是AVOption类。
2.AVOption类
AVDictionary 负责保存用户传递进来的参数(统一抽象为键值对),那么传递进来后,先不说配置到哪里,先说配置到目的地的时候是不是得过滤下?不然你瞎写参数,ffmpeg都没有支持也能配置?AVOption类应运而生——是ffmpeg能支持的参数配置表或者叫参数过滤(识别)表。
ffmpeg把那些庞杂的参数在运行的使用的内部都抽象为了一个AVOption类的表格——每个业务功能的配置都有个AVOption类的默认配置表格——以表明这个业务只能支持哪些参数配置——这样很具有扩展性,什么样的业务定义什么样的配置表——是提前定义好的,不是随便写一个配置动态现编的,程序没有那么智能——除非是那种未来高级AI程序可以自我编程动态改变的那种。
2.1 类定义
typedef struct AVOption {const char *name;/*** short English help text* @todo What about other languages?*/const char *help;/*** The offset relative to the context structure where the option* value is stored. It should be 0 for named constants.*/int offset;enum AVOptionType type;/*** the default value for scalar options*/union {int64_t i64;double dbl;const char *str;/* TODO those are unused now */AVRational q;} default_val;double min; ///< minimum valid value for the optiondouble max; ///< maximum valid value for the optionint flags;
#define AV_OPT_FLAG_ENCODING_PARAM 1 ///< a generic parameter which can be set by the user for muxing or encoding
#define AV_OPT_FLAG_DECODING_PARAM 2 ///< a generic parameter which can be set by the user for demuxing or decoding
#define AV_OPT_FLAG_AUDIO_PARAM 8
#define AV_OPT_FLAG_VIDEO_PARAM 16
#define AV_OPT_FLAG_SUBTITLE_PARAM 32
/*** The option is intended for exporting values to the caller.*/
#define AV_OPT_FLAG_EXPORT 64
/*** The option may not be set through the AVOptions API, only read.* This flag only makes sense when AV_OPT_FLAG_EXPORT is also set.*/
#define AV_OPT_FLAG_READONLY 128
#define AV_OPT_FLAG_BSF_PARAM (1<<8) ///< a generic parameter which can be set by the user for bit stream filtering
#define AV_OPT_FLAG_RUNTIME_PARAM (1<<15) ///< a generic parameter which can be set by the user at runtime
#define AV_OPT_FLAG_FILTERING_PARAM (1<<16) ///< a generic parameter which can be set by the user for filtering
#define AV_OPT_FLAG_DEPRECATED (1<<17) ///< set if option is deprecated, users should refer to AVOption.help text for more information
#define AV_OPT_FLAG_CHILD_CONSTS (1<<18) ///< set if option constants can also reside in child objects
//FIXME think about enc-audio, ... style flags/*** The logical unit to which the option belongs. Non-constant* options and corresponding named constants share the same* unit. May be NULL.*/const char *unit;
} AVOption;
这个是内部所用的参数表抽象出来的类,里面包含了各种信息,其中offset偏移是比较重要的,是参数配置最终的落脚点。
针对具体业务,ffmpeg支持那些参数配置?看完本节,就不用网上搜了。 通过源码查找AVOption类的参数支持表,就知道了,也知道怎么配置了。
比如想配置rtsp的参数,那么可以找到rtsp的AVOption类的配置表格,如下,看看它支持的配置项:
static const AVClass rtsp_demuxer_class = {.class_name = "RTSP demuxer",.item_name = av_default_item_name,.option = ff_rtsp_options,.version = LIBAVUTIL_VERSION_INT,
};
可以看到rtsp的AVOption类的配置表格是ff_rtsp_options,如下
const AVOption ff_rtsp_options[] = {{ "initial_pause", "do not start playing the stream immediately", OFFSET(initial_pause), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DEC },FF_RTP_FLAG_OPTS(RTSPState, rtp_muxer_flags),{ "rtsp_transport", "set RTSP transport protocols", OFFSET(lower_transport_mask), AV_OPT_TYPE_FLAGS, {.i64 = 0}, INT_MIN, INT_MAX, DEC|ENC, "rtsp_transport" }, \{ "udp", "UDP", 0, AV_OPT_TYPE_CONST, {.i64 = 1 << RTSP_LOWER_TRANSPORT_UDP}, 0, 0, DEC|ENC, "rtsp_transport" }, \{ "tcp", "TCP", 0, AV_OPT_TYPE_CONST, {.i64 = 1 << RTSP_LOWER_TRANSPORT_TCP}, 0, 0, DEC|ENC, "rtsp_transport" }, \{ "udp_multicast", "UDP multicast", 0, AV_OPT_TYPE_CONST, {.i64 = 1 << RTSP_LOWER_TRANSPORT_UDP_MULTICAST}, 0, 0, DEC, "rtsp_transport" },{ "http", "HTTP tunneling", 0, AV_OPT_TYPE_CONST, {.i64 = (1 << RTSP_LOWER_TRANSPORT_HTTP)}, 0, 0, DEC, "rtsp_transport" },{ "https", "HTTPS tunneling", 0, AV_OPT_TYPE_CONST, {.i64 = (1 << RTSP_LOWER_TRANSPORT_HTTPS )}, 0, 0, DEC, "rtsp_transport" },RTSP_FLAG_OPTS("rtsp_flags", "set RTSP flags"),{ "listen", "wait for incoming connections", 0, AV_OPT_TYPE_CONST, {.i64 = RTSP_FLAG_LISTEN}, 0, 0, DEC, "rtsp_flags" },{ "prefer_tcp", "try RTP via TCP first, if available", 0, AV_OPT_TYPE_CONST, {.i64 = RTSP_FLAG_PREFER_TCP}, 0, 0, DEC|ENC, "rtsp_flags" },{ "satip_raw", "export raw MPEG-TS stream instead of demuxing", 0, AV_OPT_TYPE_CONST, {.i64 = RTSP_FLAG_SATIP_RAW}, 0, 0, DEC, "rtsp_flags" },RTSP_MEDIATYPE_OPTS("allowed_media_types", "set media types to accept from the server"),{ "min_port", "set minimum local UDP port", OFFSET(rtp_port_min), AV_OPT_TYPE_INT, {.i64 = RTSP_RTP_PORT_MIN}, 0, 65535, DEC|ENC },{ "max_port", "set maximum local UDP port", OFFSET(rtp_port_max), AV_OPT_TYPE_INT, {.i64 = RTSP_RTP_PORT_MAX}, 0, 65535, DEC|ENC },{ "listen_timeout", "set maximum timeout (in seconds) to wait for incoming connections (-1 is infinite, imply flag listen)", OFFSET(initial_timeout), AV_OPT_TYPE_INT, {.i64 = -1}, INT_MIN, INT_MAX, DEC },{ "timeout", "set timeout (in microseconds) of socket I/O operations", OFFSET(stimeout), AV_OPT_TYPE_INT64, {.i64 = 0}, INT_MIN, INT64_MAX, DEC },COMMON_OPTS(),{ "user_agent", "override User-Agent header", OFFSET(user_agent), AV_OPT_TYPE_STRING, {.str = LIBAVFORMAT_IDENT}, 0, 0, DEC },{ NULL },
};
这个AVOption表格记录了rtsp支持的参数配置,其中offset的偏移量是相对RTSPState(除常量外)。
int avformat_open_input(AVFormatContext **ps, const char *filename,const AVInputFormat *fmt, AVDictionary **options)
{AVFormatContext *s = *ps;……/* Allocate private data. */if (s->iformat->priv_data_size > 0) {if (!(s->priv_data = av_mallocz(s->iformat->priv_data_size))) {ret = AVERROR(ENOMEM);goto fail;}if (s->iformat->priv_class) {*(const AVClass **) s->priv_data = s->iformat->priv_class;av_opt_set_defaults(s->priv_data);if ((ret = av_opt_set_dict(s->priv_data, &tmp)) < 0)goto fail;}}……
}
avformat_open_input中,所有的私有数据都是继承自AVClass *成员的类,它的所在类就是offset值的参考系对象。rtsp是RTSPState类,所以rtsp的AVOption类的配置表格是ff_rtsp_options中的offset偏移量就是相对RTSPState类的起始地址的偏移量,如果理解不了,可以直接看表格上面的宏定义。
2.2 相关操作函数
//获取AVOption表格中的一个个AVOption类成员的迭代器。
const AVO获取ption *av_opt_next(const void *obj, const AVOption *last)
3.AVClass类
AVClass类是可配置参数能力的装饰器。有些业务类想要拥有参数可配置的能力,它的第一个成员就得是AVClass *类型的指针成员,如果不想有这个能力,把这个成员置为NULL即可。
4.可配参数业务类
自己起的名字,当经过参数配置过滤表格后,ffmpeg支持的参数要配置到哪里呢?总得有个落脚点吧?我运行的时候怎么使用它?于是可配参数业务类应运而生。
比如AVCodecContext/RTSPState/AVFormatContext等;
这类的形式,是如下的:
如果想要拥有可配参数能力,那么就定义个这个业务的参数支持装饰器AVClass,否则就把成员class置为NULL。
比如想要给rtsp拉流添加可配置参数功能,则需要定义一个rtsp参数业务配置类,第1个成员必须是AVClass *类的指针类型,再定义AVClass对象,将其指针赋给rtsp参数业务配置类的第1个成员。再定义AVOption支持可配参数的表格等,如下:
rtsp的可配参数装饰器是rtsp_demuxer_class,rtsp的支持可配置参数的表格是ff_rtsp_options。
2.1 相关操作函数
//把配置的参数设置到可配参数业务对象的成员变量里,
//obj就是可配置参数业务对象的地址,比如AVCodecContext/RTSPState等的地址
int av_opt_set_dict(void *obj, AVDictionary **options)
有意思的是obj其实可以看着是this指针,只不过是可配置参数业务对象的地址,类图如下:
另外有几点:
(1)av_opt_set_dict这个接口会过滤参数支持表AVOption 中的常量值,不允许配置常量值。
(2)av_opt_set_dict这个接口第2个参数,字典容器类对象,配置完成后,它会返回新的字典容器类对象,且新的字典容器值就是保存了AVOption不支持的剩余参数,且把常量配置过滤掉了。
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 && o->type == AV_OPT_TYPE_CONST && o->unit && !strcmp(o->unit, unit)))) {if (target_obj) {if (!(search_flags & AV_OPT_SEARCH_FAKE_OBJ))*target_obj = obj;else*target_obj = NULL;}return o;}}return NULL;
}
这个配置函数,就是第0章对象流程图里绘制的——代码的流程映射图——有几个关键点:
(1)av_opt_find2中,有个过滤条件,就是把常量过滤掉了,只会返回非常量的其他支持参数配置表的配置项。
(2)offset是从AVOption参数支持配置表中获取到的,它的偏移是相对于可配参数业务类对象的,比如比如AVCodecContext/RTSPState/AVFormatContext等的地址。
(3)在av_opt_set_dict2中会把不支持的非常量的参数配置放到新的字典容器中,配置完成后,把传进来的字典容器给改写了。