FFmpeg接口-AVDictionary的使用介绍和源码分析

目录

  1. AVDictionary的使用介绍
  2. FFmpeg中AVDictionary的使用
  3. AVDictionary的源码学习
  4. 总结

参考

  • [1] FMPEG Tips (5) 如何利用 AVDictionary 配置参数
  • [2] FFmpeg/libavutil/dict.h

1. AVDictionary的使用介绍

AVDictionary是一个健值对存储工具,类似于c++中的map,ffmpeg中有很多 API 通过它来传递参数。

AVDictionary 的定义如下:

//libavutil/dict.h
typedef struct AVDictionaryEntry {  
    char *key;  
    char *value;  
} AVDictionaryEntry;  

struct AVDictionary {  
    int count;  
    AVDictionaryEntry *elems;  
};

1.1. 基本用法

  1. 创建AVDictionary:将值为NULL的AVDictionary 指针变量的地址传递给av_dict_set()。
  • AVDictionary结构体的具体实现没有在接口中暴露,不知道AVDictionary结构体占用的内存大小,所以使用者无法直接定义一个AVDictionary的变量,使用时需要定义一个AVDictionary指针的变量。
  • int av_dict_set(AVDictionary **pm, const char *key, const char *value, int flags),如果*pm == NULL,会创建一个AVDictionary结构体,把地址赋值给*pm。av_dict_set函数的完整签名:
AVDictionary *d = NULL;
av_dict_set(&d, "version", "1.0", AV_DICT_MATCH_CASE);
  • flags可以设为不同的选项的组合,包含AV_DICT_MATCH_CASE时表示key的匹配是要区分大小写的,默认是不区分的,这一点要特别注意。
  1. 添加key-value对。
av_dict_set(&d, "version", "1.0", AV_DICT_MATCH_CASE);
av_dict_set_int(&d, "count", 3, AV_DICT_MATCH_CASE);
  1. 使用AVDictionaryEntry *av_dict_get(const AVDictionary *m, const char *key, const AVDictionaryEntry *prev, int flags)检索条目(AVDictionaryEntry)或遍历字典。
  • AVDictionaryEntry中包含(char *)类型的key和value。
  • 返回的AVDictionaryEntry不应该被修改否则会发生未定义的行为。

检索条目

AVDictionaryEntry *t = NULL;

t = av_dict_get(d, "version", NULL, AV_DICT_MATCH_CASE);
av_log(NULL, AV_LOG_DEBUG, "version: %s", t->value);

t = av_dict_get(d, "count", NULL, AV_DICT_MATCH_CASE);
av_log(NULL, AV_LOG_DEBUG, "count: %d", (int) (*t->value));

遍历字典

AVDictionaryEntry *t = NULL;
while ((t = av_dict_get(d, "", t, AV_DICT_IGNORE_SUFFIX))) {
    av_log(NULL, AV_LOG_DEBUG, "%s: %s", t->key, t->value);
}
  1. 最后使用av_dict_free()释放AVDictionary及其所有内容。
av_dict_free(&d);

1.2 flags的用法

av_dict_get()和av_dict_set()函数都有一个flags参数,下面对它的作用进行介绍,flags包含下面7个选项,可以进行组合使用:

#define AV_DICT_MATCH_CASE      1   /**< Only get an entry with exact-case key match. Only relevant in av_dict_get(). */
#define AV_DICT_IGNORE_SUFFIX   2   /**< Return first entry in a dictionary whose first part corresponds to the search key,
                                         ignoring the suffix of the found key string. Only relevant in av_dict_get(). */
#define AV_DICT_DONT_STRDUP_KEY 4   /**< Take ownership of a key that's been
                                         allocated with av_malloc() or another memory allocation function. */
#define AV_DICT_DONT_STRDUP_VAL 8   /**< Take ownership of a value that's been
                                         allocated with av_malloc() or another memory allocation function. */
#define AV_DICT_DONT_OVERWRITE 16   ///< Don't overwrite existing entries.
#define AV_DICT_APPEND         32   /**< If the entry already exists, append to it.  Note that no
                                      delimiter is added, the strings are simply concatenated. */
#define AV_DICT_MULTIKEY       64   /**< Allow to store several equal keys in the dictionary */
  1. AV_DICT_MATCH_CASE:在字典中检索key是需要区分大小写,默认是不区分大小写的,这点需要特别注意。看一下如下的示例。
    AVDictionary *dict = NULL;
    av_dict_set(&dict, "version", "1.0", 0); 
    av_dict_set(&dict, "VERsion", "2.0", 0); 
    AVDictionaryEntry *t = NULL;
    while ((t = av_dict_get(dict, "", t, AV_DICT_IGNORE_SUFFIX))) {
        fprintf(stdout, "%s: %s\n", t->key, t->value);
    }  
   av_dict_free(&d); 

程序的输出是

VERsion: 2.0
  • av_dict_set在不设置AV_DICT_MATCH_CASE的情况下,如果key值与字典中的key值在忽略大小写的情况下是匹配的,会覆盖字典中匹配条目的key和value。
  1. AV_DICT_IGNORE_SUFFIX:如果key值是字典中某个条目key值的前缀,则认为是匹配的。
    AVDictionary *dict = NULL;
    av_dict_set(&dict, "language", "ch", 0); 
    av_dict_set(&dict, "version", "1.0", 0); 
    AVDictionaryEntry *t1 = av_dict_get(dict, "ver", NULL, AV_DICT_IGNORE_SUFFIX);
    AVDictionaryEntry *t2 = av_dict_get(dict, "", NULL, AV_DICT_IGNORE_SUFFIX);
    fprintf(stdout, "t1, %s: %s\n", t1->key, t1->value);
    fprintf(stdout, "t2, %s: %s\n", t2->key, t2->value);
    av_dict_free(&d); 

上面示例的输出

t1, version: 1.0
t2, language: ch
  • 由于flags包含了AV_DICT_IGNORE_SUFFIX的选项,所以key的匹配是按忽略后缀的方式(key是字典中条目的key的前缀)进行的,"ver"字符串是"version"字符串的前缀,所以获取到字典中key为"version"的条目。而“”空字符串是所有字符串的前缀,所以获取到字典中的第一个条目。
  1. AV_DICT_DONT_STRDUP_KEY或AV_DICT_DONT_STRDUP_VAL:接管使用key或value指针指向的内存的管理权,不对key或value的值进行复制。如下面示例所示。
    AVDictionary *d = NULL;
    AVDictionaryEntry *t = NULL;
    
    char *k = av_strdup("key");       // if your strings are already allocated,
    char *v = av_strdup("value");     // you can avoid copying them like this
    av_dict_set(&d, k, v, AV_DICT_DONT_STRDUP_KEY | AV_DICT_DONT_STRDUP_VAL);
    
    while (t = av_dict_get(d, "", t, AV_DICT_IGNORE_SUFFIX)) {
        fprintf(stdout, "%s: %s\n", t->key, t->value);  
    }   
    av_dict_free(&d);

  1. AV_DICT_DONT_OVERWRITE:不覆盖现有条目。如下面示例所示。
    AVDictionary *d = NULL;
    AVDictionaryEntry *t = NULL;

    av_dict_set(&d, "version", "1.0", 0);
    av_dict_set(&d, "version", "2.0", AV_DICT_DONT_OVERWRITE);

    while (t = av_dict_get(d, "", t, AV_DICT_IGNORE_SUFFIX)) {
        fprintf(stdout, "%s: %s\n", t->key, t->value);
    }
    av_dict_free(&d);

输出结果为:version: 1.0

  1. AV_DICT_APPEND:如果目已经存在,则value值直接拼接到之前的值的后面。注意,没有添加分隔符,字符串只是连接在一起。如下面示例所示。
    AVDictionary *d = NULL;
    AVDictionaryEntry *t = NULL;
    
    av_dict_set(&d, "version", "1.0", 0); 
    av_dict_set(&d, "version", "2.0", AV_DICT_APPEND);
    
    while (t = av_dict_get(d, "", t, AV_DICT_IGNORE_SUFFIX)) {
        fprintf(stdout, "%s: %s\n", t->key, t->value);  
    }   
    av_dict_free(&d);

输出结果为:version: 1.02.0

  1. AV_DICT_MULTIKEY:允许在字典中存储几个相等的key。如下面示例所示。
    AVDictionary *dict = NULL;
    av_dict_set(&dict, "version", "1.0", 0);
    av_dict_set(&dict, "VERsion", "2.0", AV_DICT_MULTIKEY);
    av_dict_set(&dict, "version", "3.0", AV_DICT_MULTIKEY);
    AVDictionaryEntry *t = NULL;
    while ((t = av_dict_get(dict, "", t, AV_DICT_IGNORE_SUFFIX))) {
        fprintf(stdout, "%s: %s\n", t->key, t->value);
    }
    av_dict_free(&dict);

输出结果为:

version: 1.0
VERsion: 2.0
version: 3.0

2. FFmpeg中AVDictionary的使用

ffmpeg 中很多 API 都是靠 AVDictionary 来传递参数的,比如常用的:

int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);

最后一个参数就是AVDictionary,我们可以在打开码流前指定各种参数,比如:探测时间、超时时间、最大延时、支持的协议的白名单等等,例如:

AVDictionary *options = NULL;
av_dict_set(&options, “probesize”, “4096", 0);
av_dict_set(&options, “max_delay”, “5000000”, 0);

AVFormatContext *ic = avformat_alloc_context();
if (avformat_open_input(&ic, url, NULL, &options) < 0) {
    LOGE("could not open source %s", url);
    return -1;
} 

那么,我们怎么知道 ffmpeg 的这个 API 支持哪些可配置的参数呢 ?

我们可以查看 ffmpeg 源码,比如 avformat_open_input 是结构体 AVFormatContext 提供的 API,在 libavformat/options_table.h 中定义了 AVFormatContext 所有支持的 options 选项,如下所示:doxygen文档

同理,AVCodec 相关 API 支持的 options 选项则可以在 libavcodec/options_table.h 文件中找到,如下所示:doxygen文档

3. AVDictionary的源码学习

主要看一下以下几个函数的实现:

  • av_dict_get
  • av_dict_set
  • av_dict_set_int
  • av_dict_copy
  • av_dict_free

3.1 av_dict_get

av_dict_get的逻辑比较简单,代码如下所示。

//dict.c
AVDictionaryEntry *av_dict_get(const AVDictionary *m, const char *key,
                               const AVDictionaryEntry *prev, int flags)
{
    unsigned int i, j;

    if (!m)
        return NULL;

    if (prev)
        i = prev - m->elems + 1;
    else
        i = 0;

    for (; i < m->count; i++) {
        const char *s = m->elems[i].key;
        if (flags & AV_DICT_MATCH_CASE)
            for (j = 0; s[j] == key[j] && key[j]; j++)
                ;
        else
            for (j = 0; av_toupper(s[j]) == av_toupper(key[j]) && key[j]; j++)
                ;
        if (key[j])
            continue;
        if (s[j] && !(flags & AV_DICT_IGNORE_SUFFIX))
            continue;
        return &m->elems[i];
    }
    return NULL;
}
  • 如果prev不为空,则从prev之后的条目开始检索,prev - m->elems + 1为prev的下一个条目的索引。
  • 如果flags中包含AV_DICT_MATCH_CASE则直接对key的每个字符进行比较,否则都转换成大写的字符进行比较。
  • 比较过程中,if (key[j]) continue;表示传入的key有未匹配完的字符即跟当前条目的key不匹配,所以再去跟下一个条目进行匹配。
  • if (s[j] && !(flags & AV_DICT_IGNORE_SUFFIX)) continue;表示传入的key是当前条目的key的前缀,但不是按照AV_DICT_IGNORE_SUFFIX的方式进行匹配,即这个条目与检索的key是不匹配的,所以再去跟下一个条目进行匹配。

3.2 av_dict_set

av_dict_set逻辑要稍微复杂一些,源码如下所示。

//dict.c
int av_dict_set(AVDictionary **pm, const char *key, const char *value,
                int flags)
{
    AVDictionary *m = *pm;
    AVDictionaryEntry *tag = NULL;
    char *oldval = NULL, *copy_key = NULL, *copy_value = NULL;

    if (!(flags & AV_DICT_MULTIKEY)) {
        tag = av_dict_get(m, key, NULL, flags);
    }
    if (flags & AV_DICT_DONT_STRDUP_KEY)
        copy_key = (void *)key;
    else
        copy_key = av_strdup(key);
    if (flags & AV_DICT_DONT_STRDUP_VAL)
        copy_value = (void *)value;
    else if (copy_key)
        copy_value = av_strdup(value);
    if (!m)
        m = *pm = av_mallocz(sizeof(*m));
    if (!m || (key && !copy_key) || (value && !copy_value))
        goto err_out;

    if (tag) {
        if (flags & AV_DICT_DONT_OVERWRITE) {
            av_free(copy_key);
            av_free(copy_value);
            return 0;
        }
        if (flags & AV_DICT_APPEND)
            oldval = tag->value;
        else
            av_free(tag->value);
        av_free(tag->key);
        *tag = m->elems[--m->count];
    } else if (copy_value) {
        AVDictionaryEntry *tmp = av_realloc(m->elems,
                                            (m->count + 1) * sizeof(*m->elems));
        if (!tmp)
            goto err_out;
        m->elems = tmp;
    }
    if (copy_value) {
        m->elems[m->count].key = copy_key;
        m->elems[m->count].value = copy_value;
        if (oldval && flags & AV_DICT_APPEND) {
            size_t len = strlen(oldval) + strlen(copy_value) + 1;
            char *newval = av_mallocz(len);
            if (!newval)
                goto err_out;
            av_strlcat(newval, oldval, len);
            av_freep(&oldval);
            av_strlcat(newval, copy_value, len);
            m->elems[m->count].value = newval;
            av_freep(©_value);
        }
        m->count++;
    } else {
        av_freep(©_key);
    }
    if (!m->count) {
        av_freep(&m->elems);
        av_freep(pm);
    }

    return 0;

err_out:
    if (m && !m->count) {
        av_freep(&m->elems);
        av_freep(pm);
    }
    av_free(copy_key);
    av_free(copy_value);
    return AVERROR(ENOMEM);
}
  • 如果不支持AV_DICT_MULTIKEY,则在字典中检索key是否有对应的条目,保存为变量tag。
    if (!(flags & AV_DICT_MULTIKEY)) {
        tag = av_dict_get(m, key, NULL, flags);
    }
  • 如果支持AV_DICT_DONT_STRDUP_KEY,则key指向的字符串不进行拷贝。
if (flags & AV_DICT_DONT_STRDUP_KEY)
        copy_key = (void *)key;
else
        copy_key = av_strdup(key);
  • 如果传入的字典为NULL,则创建一个AVDictionary。
    if (!m)
        m = *pm = av_mallocz(sizeof(*m));
  • 如果key在字典中已经有对应的条目,并且flags中包含AV_DICT_DONT_OVERWRITE选项,即不覆盖已有的条目,则不对字典进行修改,释放key和value的内存。
    if (tag) {
        if (flags & AV_DICT_DONT_OVERWRITE) {
            av_free(copy_key);
            av_free(copy_value);
            return 0;
        }
  • 如果key在字典中已经有对应的条目,并且flags中不包含AV_DICT_DONT_OVERWRITE选项,如果flags包含AV_DICT_APPEND的选项,则把tag->value赋给oldval,后续拼接使用。寻找到的条目是elems数组的一个元素,把一项的值赋值为最后一个条目的值,即修改的这个条目出现在elems数组的最后一个元素。
    if (tag) {
...
        if (flags & AV_DICT_APPEND)
            oldval = tag->value;
        else
            av_free(tag->value);
        av_free(tag->key);
        *tag = m->elems[--m->count];
  • 如果key在字典中没有对应的条目且value不为NULL,则需要对原有的elems数组扩容,增加一项。
    } else if (copy_value) {
        AVDictionaryEntry *tmp = av_realloc(m->elems,
                                            (m->count + 1) * sizeof(*m->elems));
        if (!tmp)
            goto err_out;
        m->elems = tmp;
    }
  • 如果value不为NULL,设置的key和value放置在elems最后一个元素,如果flags包含AV_DICT_APPEND,则需要对oldval和value进行拼接。如果value为NULL,则释放掉key的内存,即删除了这个key匹配到的条目。
    if (copy_value) {
        m->elems[m->count].key = copy_key;
        m->elems[m->count].value = copy_value;
        if (oldval && flags & AV_DICT_APPEND) {
            size_t len = strlen(oldval) + strlen(copy_value) + 1;
            char *newval = av_mallocz(len);
            if (!newval)
                goto err_out;
            av_strlcat(newval, oldval, len);
            av_freep(&oldval);
            av_strlcat(newval, copy_value, len);
            m->elems[m->count].value = newval;
            av_freep(©_value);
        }
        m->count++;
    } else {
        av_freep(©_key);
    }
  • 如果条目个数为0了,则释放elems和AVDictionary的内存。
    if (!m->count) {
        av_freep(&m->elems);
        av_freep(pm);
    }

3.3 av_dict_set_int

av_dict_set_int的逻辑很简单

//dict.c
int av_dict_set_int(AVDictionary **pm, const char *key, int64_t value,
                int flags)
{
    char valuestr[22];
    snprintf(valuestr, sizeof(valuestr), "%"PRId64, value);
    flags &= ~AV_DICT_DONT_STRDUP_VAL;
    return av_dict_set(pm, key, valuestr, flags);
}
  • 把int通过snprintf转换成字符串的形式,需要把flags中的AV_DICT_DONT_STRDUP_VAL位抹掉,因为valuestr是一个局部变量,需要进行拷贝,再调用av_dict_set把key和value保存在字典中。

3.4 av_dict_copy

//dict.c
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;
}
  • 遍历src字典中的每个条目,然后设置到dst字典中。

3.5 av_dict_free

void av_dict_free(AVDictionary **pm)
{
    AVDictionary *m = *pm;

    if (m) {
        while (m->count--) {
            av_freep(&m->elems[m->count].key);
            av_freep(&m->elems[m->count].value);
        }
        av_freep(&m->elems);
    }
    av_freep(pm);
}
  • 释放每个条目的key和value的内存、释放条目数组elems的内存、释放AVDictionary 字典的内存。

4. 总结

AVDictionary是一个健值对存储工具,内部是通过数组实现的。

  • 通过av_dict_set保存一个key、value对到字典AVDictionary中,如果条目中没有对应的key,则新增一个条目;如果有则根据flags决定是否覆盖;如果value为NULL,则删除key匹配的第一个条目。
  • 通过av_dict_get获取key匹配的第一个条目。
  • av_dict_set和av_dict_get中的flags参数包含不同的匹配方式和设置选项。
    1. AV_DICT_MATCH_CASE:匹配大小写
    2. AV_DICT_IGNORE_SUFFIX:匹配时忽略后缀)
    3. AV_DICT_DONT_STRDUP_KEY:不对key进行拷贝
    4. AV_DICT_DONT_STRDUP_VAL:不对value进行拷贝
    5. AV_DICT_DONT_OVERWRITE:不覆盖现有条目
    6. AV_DICT_APPEND:如果目已经存在,则value值直接拼接到之前的值的后面。
    7. AV_DICT_MULTIKEY:允许在字典中存储相等的key。

[2]中提到AVDictionary在实现上和API上都是低效的,它不具有伸缩性,使用字典很大时速度非常慢,在允许的情况下建议新代码使用tree容器,它使用AVL树来实现O(log n)性能。

你可能感兴趣的:(FFmpeg接口-AVDictionary的使用介绍和源码分析)