当前位置: 首页 > news >正文

有个印度做网站的天天找我清远今日头条最新消息

有个印度做网站的天天找我,清远今日头条最新消息,京创影视app,广州seo诊断目录概览 0.参数配置对象流程图0.1 用到的设计模式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 AVOption类2.1.1.1 类…

目录概览

  • 0.参数配置对象流程图
    • 0.1 用到的设计模式
    • 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 AVOption类
        • 2.1.1.1 类定义
        • 2.1.1.2 相关操作函数
        • 2.1.1.3 查看支持的参数配置
      • 2.1.2 AVClass类
    • 2.2 可配参数业务类
      • 2.2.1 类定义及类图
      • 2.2.2 相关操作函数
    • 2.3 实例
      • 2.3.1 参数传递
      • 2.3.2 拷贝传递字典到新字典
      • 2.3.3 第1个参数配置业务类对象
      • 2.3.3 第2个参数配置业务类对象
  • 3.小结

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放到第一个成员了,但是要改源码了。

0.1 用到的设计模式

ffmpeg将AVDictionary字典容器类对象里的参数映射到的是参数配置的业务类这一过程中增加了参数支持配置表AVOption类,而AVOption类是被AVClass类管理的——AVClass类是个啥东西?我觉得称之为装饰器类,因为这用到了设计模式的装饰器设计模式——谁想增加参数可配置的功能,谁就戴上AVClass类就行了。装饰器就是谁想有什么能力就去戴上那个能力就行了。

因此,AVClass类是可配置参数能力的装饰器

0.2 与朴素思想的对比

下图是ffmpeg与朴素思想进行对比,它的实现只是朴素思想实现的演化或者复杂化——但万变不离其宗。

在这里插入图片描述

上图左边虚框里,是第一步,保存参数配置到字典容器里(下面会有详解)——相当于寄存器(或者寄存地)。
上图右边虚框里,是第二步,将参数配置落地——把字典容器里的参数设置到可配参数业务类对象里对应的参数成员里——最终落脚地,参数去的目的地。

从这看出,ffmpeg也是万变不离其中,和我们最初的梦想一样,都是把参数保存到全局变量或者对象的成员变量里面去,等运行的时候直接拿来使用。
初心不改,只是过程复杂。——或者说本质原理是极其简单的,一点也不复杂——复杂的是实现手段(一堆弯弯绕绕)。

对比朴素、简单的思想来说,为啥变成了这么多类呢?输入参数搞成字典类,保存参数的变量搞成了可配参数业务类——由参数配置装饰器类AVClass组合而成,参数配置器类AVClass主要管理参数支持表类AVOption。搞的这么弯弯绕绕,这样耦合性降低,同时增加了一个参数过滤的过程,不支持的参数不会配置。

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
从如上类图/数据结构中,它粗暴低效的实现,在于它在内存中搞了个指针数组,存放一个个的键值对,每次新增都会扩展这个指针数组,每次查找都是循环遍历指针数组来匹配。

如果想加入一个键值对,不是链表形式,而是调用realloc扩展指针数组的内存即elems成员指向的那块连续内存。

又粗暴又低效,不过能用。

1.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.1.3 析构函数

av_dict_free(&opts);

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, "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章的参数配置对象流程图中,参数配置传递完毕,接下来所讲的就是参数配置到业务对象的成员变量中的“弯弯绕绕”“繁杂”的过程。

2.参数配置生效部分

该部分是参数配置最终到达的目的地,首先是在生效过程增加了参数过滤,匹配到支持的参数,才会去设置,还有记录了参数要映射到地址偏移,最后才能找到设置的内存地址(最终参数去的地方)。
ffmpeg把这一过程弄的有点复杂,原因在于去耦合,模块化,采用了装饰器模式,谁需要参数配置功能谁就得按照这个规则包含装饰器类AVClass类指针——且作为第一个成员。同时,也加入了过滤参数的功能。

其实配置生效部分,主要是2大块,一块是参数配置最终去的地方——参数配置业务类对象中各个参数成员。另一块是参数过滤模块。这块就是AVClass参数配置装饰器类。

因为AVClass参数配置装饰器类的出现,就和最终放参数的业务配置类对象解耦了。

AVClass和最终参数去的对象的关系是,AVClass中AVOption表格里的offset要和最终对象的参数成员偏移保持一直,相匹配,否则,配置会出错。

还是分成配置过滤模块和配置业务类对象模块吧。

2.1参数过滤模块

我把AVClass类和AVOption类放到这一个模块吧。AVClass主要是管理AVOption支持参数配置表的。ffmpeg通过AVClass类的AVOption表格来过滤参数是否支持或者可配置。

2.1.1 AVOption类

AVDictionary 负责保存用户传递进来的参数(统一抽象为键值对),那么传递进来后,先不说配置到哪里,先说配置到目的地的时候是不是得过滤下?不然你瞎写参数,ffmpeg都没有支持也能配置?AVOption类应运而生——是ffmpeg能支持的参数配置表或者叫参数过滤(识别)表

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

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

2.1.1.2 相关操作函数
//循环遍历获取AVOption表格中的一个个AVOption类成员的迭代器。 
const AVOtion *av_opt_next(const void *obj, const AVOption *last)

它类似一个迭代器,把表格里的所有配置项编辑出来,使用例程如下:

AVOtion *opt = NULL;
while(opt = av_opt_next(obj, opt))
{
//循环遍历出一个个配置项,和c++ python等高级语言的迭代器是一模一样的,模拟了它们高级语言的特性
}
2.1.1.3 查看支持的参数配置

针对具体业务,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.1.2 AVClass类

AVClass类是可配置参数能力的装饰器
它主要是管理上面AVOption类的,且AVOption类的offset是相对于AVClass所在宿主类的偏移。

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

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

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

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

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

2.2 可配参数业务类

自己起的名字,比如AVFormatContext/AVCodecContext/RTSPState等类为典型代表。

当经过参数配置过滤模块后,ffmpeg支持的参数到底要配置到哪里呢?总得有个落脚点吧?于是可配参数业务类应运而生。(我运行的时候怎么使用它?暂时不讨论)

2.2.1 类定义及类图

这类的形式,是如下的:

在这里插入图片描述

如果想要拥有可配参数能力,那么就定义个这个业务的参数支持装饰器AVClass,否则就把成员class置为NULL。

比如想要给rtsp拉流添加可配置参数功能,则需要定义一个rtsp参数业务配置类,第1个成员必须是AVClass类的指针类型,再实例化AVClass类对象和AVOption类对象——支持可配参数的表格等,然后绑定一起,如下:
在这里插入图片描述
rtsp的可配参数装饰器AVClass类实例化是rtsp_demuxer_class对象(全局变量),AVOption类实例化是ff_rtsp_options表格(全局变量)。

2.2.2 相关操作函数

//把配置的参数设置到可配参数业务对象的成员变量里,
//obj就是可配置参数业务对象的地址(即this指针),比如AVCodecContext/RTSPState等的地址
int av_opt_set_dict(void *obj, AVDictionary **options)

有意思的是obj其实是this指针——可配置参数业务对象的地址,类图如下:
在这里插入图片描述
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.3 实例

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

    AVDictionary *opts = NULL;av_dict_set(&opts, "probesize", "2097152", 0);          //设置探测输入流数据包大小av_dict_set(&opts, "max_delay", "1000", 0);             //接收包间隔最大延迟 usav_dict_set(&opts, "fastseek", "1", 0);                 //设置常量,不会生效,过滤掉了av_dict_set(&opts, "timeout", "10000000", 0);           //设置超时断开连接时间 usav_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);             //接收包间隔最大延迟 usav_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);           //设置超时断开连接时间 usav_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调用,返回后,你可以把它输入的字典容器类对象里的键值对打印出来,看看还剩余哪些参数没有配置成功!!!!

3.小结

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

http://www.tj-hxxt.cn/news/15031.html

相关文章:

  • 那个b2b网站可以做外贸宣传软文是什么
  • 怎样做移动端网站郑州seo实战培训
  • 怎么在百度做网站百度网
  • 挂马网站教程关键词排名查询工具有什么作用?
  • 宾川网站建设百度官方网站网址
  • 贺州网站制作湘潭seo优化
  • 美食网站开发目的与意义百度关键词价格查询
  • 黄江镇网站建设西安网站排名优化培训
  • 莱州哪有做网站的短期培训学什么好
  • 有哪些专门做校企合作的网站高清网站推广免费下载
  • 武汉网站设计招聘站长工具关键词查询
  • 合肥建站网站百度竞价seo排名
  • 珠海响应式网站建设费用今日头条搜索优化怎么做
  • wordpress的模板文件刷关键词排名seo软件
  • 长春电商网站建设报价搜索引擎入口官网
  • 网站建设公司宣传武汉seo网站推广
  • 做淘宝客网站需要什么平台推广方案
  • 网站要做手机版怎么做的google官网注册账号入口
  • 怎么样增加网站权重网络营销的方式有十种
  • 做网站 网络映射做推广哪个平台好
  • 购物网站开发文档mvc网络营销推广方法十种
  • 广州购物网站设计珠海关键词优化软件
  • 苏州市建设局安监站网站google seo
  • 深圳网站制作西安网络营销技能大赛优秀作品
  • 网站空间数据放单平台大全app
  • 平台网站建设需要什么技术卡一卡二卡三入口2021
  • 安徽省网站肥建设网站武汉网络推广seo
  • 广州有几个区图片爱站网seo
  • 龙华做网站yihe kj怎么投放广告
  • 做网站哪里学怎么开一个网站平台