ffmpeg面向对象——参数配置机制及其设计模式探索

目录概览

  • 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;

2
从如上类图中,它粗暴低效的实现在于它在内存中搞了个指针数组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  && 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;
}

有几点要关注:
(1)av_opt_set_dict这个接口会过滤参数支持表AVOption 中的常量值,不允许配置常量值。
因为av_opt_find2函数中,有个过滤条件,就是把常量过滤掉了,只会返回非常量的参数配置表的配置项,如下。
在这里插入图片描述

(2)在av_opt_set_dict2中会把不支持的非常量的参数配置放到新的字典容器中,配置完成后,把传进来的字典容器给改写了——也就是返回的字典类对象已经不是原来的了,是新的了。注意。

2.2 参数过滤模块

根据它们的主要作用来看,我把AVClass类和AVOption类放到这一个模块。AVClass主要是管理AVOption支持参数配置表的。ffmpeg通过AVClass类的AVOption表格来过滤参数是否支持或者可配置。
参数过滤模块是可配参数业务类的核心,因为它们可配参数业务类才叫“可配参数的类”。

2.2.1 AVClass装饰器类

AVClass类是可配置参数能力的装饰器
AVOption类是AVClass类的重要组成部分,它们2个构成了过滤模块。
AVClass类作为可配参数业务类对象的头部成员指针,是其参数配置生效的必要部分。

2.2.1.1 类定义及类图
typedef struct AVClass {
    /**
     * The name of the class; usually it is the same name as the
     * context structure type to which the AVClass is associated.
     */
    const char* class_name;

    /**
     * A pointer to a function which returns the name of a context
     * instance ctx associated with the class.
     */
    const char* (*item_name)(void* ctx);

    /**
     * a pointer to the first option specified in the class if any or NULL
     *
     * @see av_set_default_options()
     */
    const struct AVOption *option;

    /**
     * LIBAVUTIL_VERSION with which this structure was created.
     * This is used to allow fields to be added without requiring major
     * version bumps everywhere.
     */

    int version;

    /**
     * Offset in the structure where log_level_offset is stored.
     * 0 means there is no such variable
     */
    int log_level_offset_offset;

    /**
     * Offset in the structure where a pointer to the parent context for
     * logging is stored. For example a decoder could pass its AVCodecContext
     * to eval as such a parent context, which an av_log() implementation
     * could then leverage to display the parent context.
     * The offset can be NULL.
     */
    int parent_log_context_offset;

    /**
     * Category used for visualization (like color)
     * This is only set if the category is equal for all objects using this class.
     * available since version (51 << 16 | 56 << 8 | 100)
     */
    AVClassCategory category;

    /**
     * Callback to return the category.
     * available since version (51 << 16 | 59 << 8 | 100)
     */
    AVClassCategory (*get_category)(void* ctx);

    /**
     * Callback to return the supported/allowed ranges.
     * available since version (52.12)
     */
    int (*query_ranges)(struct AVOptionRanges **, void *obj, const char *key, int flags);

    /**
     * Return next AVOptions-enabled child or NULL
     */
    void* (*child_next)(void *obj, void *prev);

    /**
     * Iterate over the AVClasses corresponding to potential AVOptions-enabled
     * children.
     *
     * @param iter pointer to opaque iteration state. The caller must initialize
     *             *iter to NULL before the first call.
     * @return AVClass for the next AVOptions-enabled child or NULL if there are
     *         no more such children.
     *
     * @note The difference between child_next and this is that child_next
     *       iterates over _already existing_ objects, while child_class_iterate
     *       iterates over _all possible_ children.
     */
    const struct AVClass* (*child_class_iterate)(void **iter);
} AVClass;

在这里插入图片描述

它主要是管理AVOption类的,且AVOption类的offset是相对于AVClass所在宿主类的地址偏移。

有些业务类想要拥有参数可配置的能力,它的第一个成员就得是AVClass 类型的指针成员,如果不想有这个能力,把这个成员置为NULL即可——装饰器模式

我在想,为啥各个需要参数可配置的业务类的第一个成员要放AVClass *class这样指针形式的呢?为何不直接包含呢——AVClass class这样子?后来想想,如果包含了,你又不想拥有这样的能力了咋办?占空间不说,是不是还得加上一个标志标识是否具备这个能力。但是一旦是指针,哎呀,耦合就不深了——俗称解耦。——说到这,发现其实这也包含了软件五大设计原则中的依赖倒置原则。建议采用组合模式,而非继承模式来设计类,降低类之间的耦合性。

话说回来,采用指针,占用空间小,且就算改变这个AVClass类的结构,也不影响其宿主的内存结构——非常之好——同时也符合软件设计模式里的装饰器设计模式
非NULL就具备能力,NULL就不具备能力。比较灵活。

AVClass包含了AVOption的形式也是采用指针——同样,AVClass类由AVOtion类组合而成,这2类耦合性也降低了。

我曾想,为何不选择AVOtion类作为装饰器呢,直接去掉AVClass类不好么?后来想AVOption类太单薄了——单纯的对参数信息的抽象不具有别的功能——其实遵循了软件设计五大原则中的单一职责原则——可扩展性太小,于是,换谁都是再抽象一层——软件上遇事不决就抽象出一个中间层——AVClass可以再包含AVClass子类,灵活性大大提升。

2.2.2 AVOption类

AVDictionary 负责保存用户传递进来的参数(统一抽象为键值对的字典容器类对象),那么传递进来后,先不说配置到哪里,先说配置到目的地的时候是不是得过滤下?不然你瞎写参数,ffmpeg都没有支持也能配置?AVOption类应运而生——是ffmpeg能支持的参数配置表或者叫参数过滤(识别)表
AVOption类是AVClass类的重要组成部分,它们2个构成了过滤模块。

ffmpeg中,每个支持参数配置的业务类对象都有自己的AVOption类配置支持项表格——以表明这个业务只能支持哪些参数配置——这样很具有扩展性,什么样的业务定义什么样的配置表——是提前定义好的,不是随便写一个配置动态现编的,程序没有那么智能——除非是那种未来高级AI程序可以自我编程动态随时随地修改自己运行的代码的那种

2.2.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 option
    double max;                 ///< maximum valid value for the option

    int 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偏移是比较重要的,是参数配置最终的落脚点

2.2.2.2 查看支持的参数配置

针对具体业务,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的参数支持配置表格是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.3 实例

结合前面字典类暂存参数配置,然后到这部分的参数生效,来写个实例,只挑重点分析下。如下:

    AVDictionary *opts = NULL;
    av_dict_set(&opts, "probesize", "2097152", 0);          //设置探测输入流数据包大小
    av_dict_set(&opts, "max_delay", "1000", 0);             //接收包间隔最大延迟 us
    av_dict_set(&opts, "fastseek", "1", 0);                 //设置常量,不会生效,过滤掉了
    
    av_dict_set(&opts, "timeout", "10000000", 0);           //设置超时断开连接时间 us
    av_dict_set(&opts, "rtsp_transport", "tcp", 0);         //设置rtsp以tcp/udp方式打开
    av_dict_set(&opts, "udp", "0", 0);                      //设置常量,不会生效,过滤掉了

    AVFormatContext     *fmtCtx = NULL;
	avformat_open_input(&fmtCtx, "rtsp://192.168.1.46/0", NULL, &opts);

看到"rtsp://192.168.1.46/0", 就知道这是rtsp拉流了。然后我们关注opt字典参数是如何配置下去的。

提前剧透下,这些参数最终配置到的目标对象是FFFormatContext类对象(用户看到的是其父类AVFormatContext)和RTSPState类对象,最终分别映射到对应对象的对应参数成员变量里。

2.3.1 参数传递

实例第一部分是参数传递:
av_dict_set设置完,这些字符串就变成了键值对保存在了opts指向的字典容器对象里了。图可以参考第1章。

2.3.2 拷贝到新字典类对象

然后就调用avformat_open_input把参数配置下去,那么第一次配置参数在哪里?并且陪到到那个业务对象里呢?如下
在这里插入图片描述
在第240行时拷贝字典容器类的键值对到新键值对容器对象tmp中,原因是av_opt_set_dict会返回新的字典容器对象也就是会改变传入进去的字典对象,所以先拷贝到tmp中。

2.3.3 第1个可配参数业务类对象

1-5标记可以看到,第一次参数配置是到哪个对象哇?是FFFormatContext对象,但是FFFormatContext对象包含AVFormatContext(父类,oopc的继承),所以说是AVFormatContext也对,反正首地址都是一样,只需强转就能改变访问权限(范围)。而ffmpeg实际上把参数配置放到了AVFormatContext里(因为5.x才分出来FFFormatContext,应该历史缘故)。其对应的参数装饰器类实例化的对象是av_format_context_class这个全局变量,然后就找到了其支持的参数配置表格avformat_options,具体如下:

#define OFFSET(x) offsetof(AVFormatContext,x)

static const AVOption avformat_options[] = {
{"avioflags", NULL, OFFSET(avio_flags), AV_OPT_TYPE_FLAGS, {.i64 = DEFAULT }, INT_MIN, INT_MAX, D|E, "avioflags"},
{"direct", "reduce buffering", 0, AV_OPT_TYPE_CONST, {.i64 = AVIO_FLAG_DIRECT }, INT_MIN, INT_MAX, D|E, "avioflags"},
{"probesize", "set probing size", OFFSET(probesize), AV_OPT_TYPE_INT64, {.i64 = 5000000 }, 32, INT64_MAX, D},
{"formatprobesize", "number of bytes to probe file format", OFFSET(format_probesize), AV_OPT_TYPE_INT, {.i64 = PROBE_BUF_MAX}, 0, INT_MAX-1, D},
{"packetsize", "set packet size", OFFSET(packet_size), AV_OPT_TYPE_INT, {.i64 = DEFAULT }, 0, INT_MAX, E},
{"fflags", NULL, OFFSET(flags), AV_OPT_TYPE_FLAGS, {.i64 = AVFMT_FLAG_AUTO_BSF }, INT_MIN, INT_MAX, D|E, "fflags"},
{"flush_packets", "reduce the latency by flushing out packets immediately", 0, AV_OPT_TYPE_CONST, {.i64 = AVFMT_FLAG_FLUSH_PACKETS }, INT_MIN, INT_MAX, E, "fflags"},
{"ignidx", "ignore index", 0, AV_OPT_TYPE_CONST, {.i64 = AVFMT_FLAG_IGNIDX }, INT_MIN, INT_MAX, D, "fflags"},
{"genpts", "generate pts", 0, AV_OPT_TYPE_CONST, {.i64 = AVFMT_FLAG_GENPTS }, INT_MIN, INT_MAX, D, "fflags"},
{"nofillin", "do not fill in missing values that can be exactly calculated", 0, AV_OPT_TYPE_CONST, {.i64 = AVFMT_FLAG_NOFILLIN }, INT_MIN, INT_MAX, D, "fflags"},
{"noparse", "disable AVParsers, this needs nofillin too", 0, AV_OPT_TYPE_CONST, {.i64 = AVFMT_FLAG_NOPARSE }, INT_MIN, INT_MAX, D, "fflags"},
{"igndts", "ignore dts", 0, AV_OPT_TYPE_CONST, {.i64 = AVFMT_FLAG_IGNDTS }, INT_MIN, INT_MAX, D, "fflags"},
{"discardcorrupt", "discard corrupted frames", 0, AV_OPT_TYPE_CONST, {.i64 = AVFMT_FLAG_DISCARD_CORRUPT }, INT_MIN, INT_MAX, D, "fflags"},
{"sortdts", "try to interleave outputted packets by dts", 0, AV_OPT_TYPE_CONST, {.i64 = AVFMT_FLAG_SORT_DTS }, INT_MIN, INT_MAX, D, "fflags"},
{"fastseek", "fast but inaccurate seeks", 0, AV_OPT_TYPE_CONST, {.i64 = AVFMT_FLAG_FAST_SEEK }, INT_MIN, INT_MAX, D, "fflags"},
{"nobuffer", "reduce the latency introduced by optional buffering", 0, AV_OPT_TYPE_CONST, {.i64 = AVFMT_FLAG_NOBUFFER }, 0, INT_MAX, D, "fflags"},
{"bitexact", "do not write random/volatile data", 0, AV_OPT_TYPE_CONST, { .i64 = AVFMT_FLAG_BITEXACT }, 0, 0, E, "fflags" },
{"shortest", "stop muxing with the shortest stream", 0, AV_OPT_TYPE_CONST, { .i64 = AVFMT_FLAG_SHORTEST }, 0, 0, E, "fflags" },
{"autobsf", "add needed bsfs automatically", 0, AV_OPT_TYPE_CONST, { .i64 = AVFMT_FLAG_AUTO_BSF }, 0, 0, E, "fflags" },
{"seek2any", "allow seeking to non-keyframes on demuxer level when supported", OFFSET(seek2any), AV_OPT_TYPE_BOOL, {.i64 = 0 }, 0, 1, D},
{"analyzeduration", "specify how many microseconds are analyzed to probe the input", OFFSET(max_analyze_duration), AV_OPT_TYPE_INT64, {.i64 = 0 }, 0, INT64_MAX, D},
{"cryptokey", "decryption key", OFFSET(key), AV_OPT_TYPE_BINARY, {.dbl = 0}, 0, 0, D},
{"indexmem", "max memory used for timestamp index (per stream)", OFFSET(max_index_size), AV_OPT_TYPE_INT, {.i64 = 1<<20 }, 0, INT_MAX, D},
{"rtbufsize", "max memory used for buffering real-time frames", OFFSET(max_picture_buffer), AV_OPT_TYPE_INT, {.i64 = 3041280 }, 0, INT_MAX, D}, /* defaults to 1s of 15fps 352x288 YUYV422 video */
{"fdebug", "print specific debug info", OFFSET(debug), AV_OPT_TYPE_FLAGS, {.i64 = DEFAULT }, 0, INT_MAX, E|D, "fdebug"},
{"ts", NULL, 0, AV_OPT_TYPE_CONST, {.i64 = FF_FDEBUG_TS }, INT_MIN, INT_MAX, E|D, "fdebug"},
{"max_delay", "maximum muxing or demuxing delay in microseconds", OFFSET(max_delay), AV_OPT_TYPE_INT, {.i64 = -1 }, -1, INT_MAX, E|D},
{"start_time_realtime", "wall-clock time when stream begins (PTS==0)", OFFSET(start_time_realtime), AV_OPT_TYPE_INT64, {.i64 = AV_NOPTS_VALUE}, INT64_MIN, INT64_MAX, E},
{"fpsprobesize", "number of frames used to probe fps", OFFSET(fps_probe_size), AV_OPT_TYPE_INT, {.i64 = -1}, -1, INT_MAX-1, D},
{"audio_preload", "microseconds by which audio packets should be interleaved earlier", OFFSET(audio_preload), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX-1, E},
{"chunk_duration", "microseconds for each chunk", OFFSET(max_chunk_duration), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX-1, E},
{"chunk_size", "size in bytes for each chunk", OFFSET(max_chunk_size), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX-1, E},
/* this is a crutch for avconv, since it cannot deal with identically named options in different contexts.
 * to be removed when avconv is fixed */
{"f_err_detect", "set error detection flags (deprecated; use err_detect, save via avconv)", OFFSET(error_recognition), AV_OPT_TYPE_FLAGS, {.i64 = AV_EF_CRCCHECK }, INT_MIN, INT_MAX, D, "err_detect"},
{"err_detect", "set error detection flags", OFFSET(error_recognition), AV_OPT_TYPE_FLAGS, {.i64 = AV_EF_CRCCHECK }, INT_MIN, INT_MAX, D, "err_detect"},
{"crccheck", "verify embedded CRCs", 0, AV_OPT_TYPE_CONST, {.i64 = AV_EF_CRCCHECK }, INT_MIN, INT_MAX, D, "err_detect"},
{"bitstream", "detect bitstream specification deviations", 0, AV_OPT_TYPE_CONST, {.i64 = AV_EF_BITSTREAM }, INT_MIN, INT_MAX, D, "err_detect"},
{"buffer", "detect improper bitstream length", 0, AV_OPT_TYPE_CONST, {.i64 = AV_EF_BUFFER }, INT_MIN, INT_MAX, D, "err_detect"},
{"explode", "abort decoding on minor error detection", 0, AV_OPT_TYPE_CONST, {.i64 = AV_EF_EXPLODE }, INT_MIN, INT_MAX, D, "err_detect"},
{"ignore_err", "ignore errors", 0, AV_OPT_TYPE_CONST, {.i64 = AV_EF_IGNORE_ERR }, INT_MIN, INT_MAX, D, "err_detect"},
{"careful",    "consider things that violate the spec, are fast to check and have not been seen in the wild as errors", 0, AV_OPT_TYPE_CONST, {.i64 = AV_EF_CAREFUL }, INT_MIN, INT_MAX, D, "err_detect"},
{"compliant",  "consider all spec non compliancies as errors", 0, AV_OPT_TYPE_CONST, {.i64 = AV_EF_COMPLIANT | AV_EF_CAREFUL }, INT_MIN, INT_MAX, D, "err_detect"},
{"aggressive", "consider things that a sane encoder shouldn't do as an error", 0, AV_OPT_TYPE_CONST, {.i64 = AV_EF_AGGRESSIVE | AV_EF_COMPLIANT | AV_EF_CAREFUL}, INT_MIN, INT_MAX, D, "err_detect"},
{"use_wallclock_as_timestamps", "use wallclock as timestamps", OFFSET(use_wallclock_as_timestamps), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, D},
{"skip_initial_bytes", "set number of bytes to skip before reading header and frames", OFFSET(skip_initial_bytes), AV_OPT_TYPE_INT64, {.i64 = 0}, 0, INT64_MAX-1, D},
{"correct_ts_overflow", "correct single timestamp overflows", OFFSET(correct_ts_overflow), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, D},
{"flush_packets", "enable flushing of the I/O context after each packet", OFFSET(flush_packets), AV_OPT_TYPE_INT, {.i64 = -1}, -1, 1, E},
{"metadata_header_padding", "set number of bytes to be written as padding in a metadata header", OFFSET(metadata_header_padding), AV_OPT_TYPE_INT, {.i64 = -1}, -1, INT_MAX, E},
{"output_ts_offset", "set output timestamp offset", OFFSET(output_ts_offset), AV_OPT_TYPE_DURATION, {.i64 = 0}, -INT64_MAX, INT64_MAX, E},
{"max_interleave_delta", "maximum buffering duration for interleaving", OFFSET(max_interleave_delta), AV_OPT_TYPE_INT64, { .i64 = 10000000 }, 0, INT64_MAX, E },
{"f_strict", "how strictly to follow the standards (deprecated; use strict, save via avconv)", OFFSET(strict_std_compliance), AV_OPT_TYPE_INT, {.i64 = DEFAULT }, INT_MIN, INT_MAX, D|E, "strict"},
{"strict", "how strictly to follow the standards", OFFSET(strict_std_compliance), AV_OPT_TYPE_INT, {.i64 = DEFAULT }, INT_MIN, INT_MAX, D|E, "strict"},
{"very", "strictly conform to a older more strict version of the spec or reference software", 0, AV_OPT_TYPE_CONST, {.i64 = FF_COMPLIANCE_VERY_STRICT }, INT_MIN, INT_MAX, D|E, "strict"},
{"strict", "strictly conform to all the things in the spec no matter what the consequences", 0, AV_OPT_TYPE_CONST, {.i64 = FF_COMPLIANCE_STRICT }, INT_MIN, INT_MAX, D|E, "strict"},
{"normal", NULL, 0, AV_OPT_TYPE_CONST, {.i64 = FF_COMPLIANCE_NORMAL }, INT_MIN, INT_MAX, D|E, "strict"},
{"unofficial", "allow unofficial extensions", 0, AV_OPT_TYPE_CONST, {.i64 = FF_COMPLIANCE_UNOFFICIAL }, INT_MIN, INT_MAX, D|E, "strict"},
{"experimental", "allow non-standardized experimental variants", 0, AV_OPT_TYPE_CONST, {.i64 = FF_COMPLIANCE_EXPERIMENTAL }, INT_MIN, INT_MAX, D|E, "strict"},
{"max_ts_probe", "maximum number of packets to read while waiting for the first timestamp", OFFSET(max_ts_probe), AV_OPT_TYPE_INT, { .i64 = 50 }, 0, INT_MAX, D },
{"avoid_negative_ts", "shift timestamps so they start at 0", OFFSET(avoid_negative_ts), AV_OPT_TYPE_INT, {.i64 = -1}, -1, 2, E, "avoid_negative_ts"},
{"auto",              "enabled when required by target format",    0, AV_OPT_TYPE_CONST, {.i64 = AVFMT_AVOID_NEG_TS_AUTO },              INT_MIN, INT_MAX, E, "avoid_negative_ts"},
{"disabled",          "do not change timestamps",                  0, AV_OPT_TYPE_CONST, {.i64 = AVFMT_AVOID_NEG_TS_DISABLED },          INT_MIN, INT_MAX, E, "avoid_negative_ts"},
{"make_non_negative", "shift timestamps so they are non negative", 0, AV_OPT_TYPE_CONST, {.i64 = AVFMT_AVOID_NEG_TS_MAKE_NON_NEGATIVE }, INT_MIN, INT_MAX, E, "avoid_negative_ts"},
{"make_zero",         "shift timestamps so they start at 0",       0, AV_OPT_TYPE_CONST, {.i64 = AVFMT_AVOID_NEG_TS_MAKE_ZERO },         INT_MIN, INT_MAX, E, "avoid_negative_ts"},
{"dump_separator", "set information dump field separator", OFFSET(dump_separator), AV_OPT_TYPE_STRING, {.str = ", "}, 0, 0, D|E},
{"codec_whitelist", "List of decoders that are allowed to be used", OFFSET(codec_whitelist), AV_OPT_TYPE_STRING, { .str = NULL },  0, 0, D },
{"format_whitelist", "List of demuxers that are allowed to be used", OFFSET(format_whitelist), AV_OPT_TYPE_STRING, { .str = NULL },  0, 0, D },
{"protocol_whitelist", "List of protocols that are allowed to be used", OFFSET(protocol_whitelist), AV_OPT_TYPE_STRING, { .str = NULL },  0, 0, D },
{"protocol_blacklist", "List of protocols that are not allowed to be used", OFFSET(protocol_blacklist), AV_OPT_TYPE_STRING, { .str = NULL },  0, 0, D },
{"max_streams", "maximum number of streams", OFFSET(max_streams), AV_OPT_TYPE_INT, { .i64 = 1000 }, 0, INT_MAX, D },
{"skip_estimate_duration_from_pts", "skip duration calculation in estimate_timings_from_pts", OFFSET(skip_estimate_duration_from_pts), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, D},
{"max_probe_packets", "Maximum number of packets to probe a codec", OFFSET(max_probe_packets), AV_OPT_TYPE_INT, { .i64 = 2500 }, 0, INT_MAX, D },
{NULL},
};

哎呦,我写的实例代码前3项的配置,刚好在这个表格里,我真会举例。

回顾下前3项配置

    av_dict_set(&opts, "probesize", "2097152", 0);          //设置探测输入流数据包大小
    av_dict_set(&opts, "max_delay", "1000", 0);             //接收包间隔最大延迟 us
    av_dict_set(&opts, "fastseek", "1", 0);                 //设置常量,不会生效,过滤掉了

这个时候呢,前2项分别映射到AVFormatContext中的probesize和max_delay成员里了,那么第3项是常量,不允许设置,过滤掉了。

这是配置对象流程图:
在这里插入图片描述

2.3.3 第2个可配参数业务类对象

然后还剩下3项,注意,这个时候av_opt_set_dict(s, &tmp)) 返回的tmp指向的地址已经是新的地址了,它指向的字典容器类对象已经是个全新的对象了,里面就剩下3项了,因为前3项被领走了(匹配到就各回各家各找各妈)。

然后继续看剩余这3项配置到哪里去了。

在这里插入图片描述

原来配置到私有数据里面了,这篇说过,如果是rtsp的话,它匹配到的参数配置对象是RTSPState对象,参数装饰器类对象是rtsp_demuxer_class这个全局变量,然后就找到了其支持的参数配置表格ff_rtsp_options,具体如下:

#define OFFSET(x) offsetof(RTSPState, x)

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 },
};

现在剩余3项了,回顾下

    av_dict_set(&opts, "timeout", "10000000", 0);           //设置超时断开连接时间 us
    av_dict_set(&opts, "rtsp_transport", "tcp", 0);         //设置rtsp以tcp/udp方式打开
    av_dict_set(&opts, "udp", "0", 0);                      //设置常量,不会生效,过滤掉了

这剩余的3项,刚刚好在这里面,而且前2项还刚好是非常量,其分别映射到了RTSPState类对象的stimeout成员和lower_transport_mask成员。第3项也是最后一项是常量,不让设置,过滤掉了。

同样,其参数配置对象流程图如下:
在这里插入图片描述

所以当 av_opt_set_dict(s->priv_data, &tmp)执行完毕,tmp指向为NULL。

在avformat_open_input最后,看怎么处理
在这里插入图片描述

此时tmp是NULL,这样就把avformat_open_input传入的字典容器对象释放了。
如果瞎写的参数,那么此时tmp就会保存你这瞎写的参数,不为NULL,这个时候avformat_open_input调用返回后,你可以把它输入的字典容器类对象里的键值对打印出来,看看还剩余哪些参数没有配置成功!!!!

2.3.4 一个内存泄漏的点

就是上面说的如果瞎写的参数或者和ffmpeg版本不匹配的参数,就设置不下去,那么这个时候它内部会new新的字典类对象就是tmp返回回去,那么avformat_open_input调用返回后,如果不为NULL,你可以打印它没有设置成功的参数,同时,不要忘了释放它。这就是内存泄漏的点。千万注意!虽然是小的内存泄漏!!

3.参数支持表有没有共性的表?

有的,比如2.3节举的例子,AVFormatContext类的第一个成员AVClass类的配置表格avformat_options就是提取的共性参数配置。

4.参数在哪里使用?

对于公共的和各自私有的参数表格,最终映射到对应内存的参数成员里,之后在哪里使用呢?在公共或者各个私有方法里使用。比如:rtsp拉流业务中,在avformat_open_input中,配置完字典参数后,如下流程
在这里插入图片描述
上图的标起来的标志在什么时候配置的呢?就是avformat_open_input中前面的av_opt_set_defaults和av_opt_set_dict配置的,如果没有字典参数配置,那么就是默认配置。

5.参数解析

前面说的是参数映射流程,但是对于参数映射的解析流程没有探索。这节补上,把目前探索到的解析给写出家——注意不是详细代码而是解析的设计原理。具体解析的一步一步代码,雷神等博客是有的雷神相关博客(致敬雷神)。

来个引子(可跳过,本来想删除的,啰啰嗦嗦的):
有没有想过,参数支持表AVOption类里的各个参数类型,其中常量类型是什么作用?在前面参数配置时,取配置参数时为何会过滤常量的参数?也就是常量参数是不可配的。那么问题来了?它的存在有啥用?和参数解析又有什么千丝万缕的关系?
上面是type成员,那么unit又有什么用?
参数支持表AVOption类这个结构体为何抽象为这样的成员?每个都有其作用。

5.1 标志类 AV_OPT_TYPE_FLAGS

前面2.3的实例中的参数配置为例:

 av_dict_set(&opts, "rtsp_transport", "tcp", 0);  

key是字符串"rtsp_transport", value是字符串"tcp",最终映射到RTSPState类对象的lower_transport_mask成员,并改变其值为:
1 << RTSP_LOWER_TRANSPORT_TCP

为啥?先上图,看rtsp的AVOption类对象ff_rtsp_options表格:
在这里插入图片描述
看出来为啥了么?没有的话,说下表格的设计原理:
对于参数类型是AV_OPT_TYPE_FLAGS的,把key和value都放到参数表格里。
这是总体思想,这样的话,用户配置的时候,key和value直接在表格里找。一个key对应多少个value也能在表格里找到,且这些value对应的实际值是多少在放到表格里了——配置的时候解析用。

那么怎么做呢?还是举个例子,看上图,ffmpeg把key为“rtsp_transport”添加到表格里,设置为AV_OPT_TYPE_FLAGS类型,unit也设置为“rtsp_transport”。
然后该key可选的value字符串有哪些呢?也写到表格里,如上图表中unit设置为“rtsp_transport”,且类型设置为AV_OPT_TYPE_CONST常量类型的字符串配置项就是该key能选的value。而且这个可选的value里面保存了实际的值,比如“tcp”的常量配置项值为
1 << RTSP_LOWER_TRANSPORT_TCP。这样前面的值就知道怎么来的了吧?!
也知道常量在表格中的作用了吧?常量类型就是把value字符串和实际格式的值做了映射。

因此,根据上表格比如rtsp传输类型可以配置如下其中一个:

av_dict_set(&opts, "rtsp_transport", "tcp", 0); 
av_dict_set(&opts, "rtsp_transport", "udp", 0); 
av_dict_set(&opts, "rtsp_transport", "udp_multicast", 0); 
av_dict_set(&opts, "rtsp_transport", "http", 0); 
av_dict_set(&opts, "rtsp_transport", "https", 0); 

5.2 其他类型

除了标志类有点小麻烦外,其他类型的参数相对好说了,直接把value转换成对应的格式就行了比如字符串格式,int等等。

6.AVClass类对象绑定机制

看到很多这样同类代码,有必要总结下。
比如有如下代码

static int url_alloc_for_protocol(URLContext **puc, const URLProtocol *up,
                                  const char *filename, int flags,
                                  const AVIOInterruptCB *int_cb)
{
	……
    if (up->priv_data_size) 
    {
        uc->priv_data = av_mallocz(up->priv_data_size);
        if (up->priv_data_class) 
        {
            char *start;
            *(const AVClass **)uc->priv_data = up->priv_data_class;
            av_opt_set_defaults(uc->priv_data);
        }
        ……
     }
     ……
 }
int avformat_open_input(AVFormatContext **ps, const char *filename,
                        const AVInputFormat *fmt, AVDictionary **options)
{
	……
    /* 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;
        }
    }
    ……
}

是不是感觉很相似,其他地方肯定也有,这块就是给可配参数类对象绑定AVClass的操作,这个绑定机制,就是只需要知道可配参数业务类对象大小和AVClass对象地址,就能分配参数业务类对象并绑定好AVClass类对象。这个机制,ffmpeg把这些信息放到各种表格里,比如输入格式配置表格(AVInputFormat)等,然后就这样完全“瞎摸”,但是只要按照它的规则来就没问题——最重要的是可配参数业务类对象第一个成员是AVClass类指针成员——如果有一天不想放到一个成员,上面这些类似的代码全部都要修改,那工程量,比较大,轻易不能动,这也是AVClass类对象地址必须放到可配参数业务类对象的第一个成员的原因所在。

比如,ffmpeg在rtsp的AVInputFormat表格放进了如下信息:
在这里插入图片描述

那么ffmpeg就能根据私有数据大小priv_data_size和priv_class,来实例化私有数据RTSPState类对象并给其装配priv_class指向的AVClass类对象。对象图前面有。

7.小结

ffmpeg把参数配置生效一过程弄的有点复杂,原因在于去耦合,模块化,采用了装饰器模式,谁需要参数配置功能谁就得按照这个规则包含装饰器类AVClass类指针——且作为第一个成员。同时,也加入了过滤参数的功能。

因为AVClass参数配置装饰器类的出现,就和最终放参数的业务配置类对象解耦了。AVClass和最终参数去的对象的关系是,AVClass中管理的AVOption表格里各个参数配置项的offset要和最终对象的参数成员偏移保持一直,相匹配,否则,配置会出错。

可配参数业务类比如AVFormatContext/AVCodecContext/RTSPState等类,与AVClass、AVOption的关系:
可配参数业务类与AVClass类是组合关系,与AVClass类中的AVOption成员才是具有千丝万缕的关系——AVOption的offset偏移就是指的是该参数在可配参数业务类中的偏移量。这个也是参数最终达到的内存地址。

8.参考博客

关于ffmpeg中的AV_CLASS 和 priv_data的设计技巧

FFmpeg源码简单分析:结构体成员管理系统-AVOption

【FFmpeg笔记】05-AVDictionary使用介绍

(音视频学习笔记):FFmpeg库简介、常用函数及数据结构解析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值