FFmpeg源码剖析: avformat_open_input()函数

533 阅读9分钟

函数原型

int avformat_open_input(
    AVForamtContext** ps, // 跟容器相关,比如文件格式、元数据. 通用信息存放在这里
    const char* filename, // 文件名
    const AVInputFormat* fmt, // 输入格式结构体,特定信息存放在这里
    AVDictionary** options) // 选项

ffplay中就会用到这个函数

image.png 参数如下:

image.png

在源码分析之前先看一下AVInputFormatAVFormatContext这俩有啥区别

AVInputFormat image.png

AVFormatContext image.png

AVFormatContext主要用于描述和管理媒体文件的总体信息,甚至还包含了AVInputFormat.

AVInputFormat标识输入格式的一些信息,如何解析某种格式的文件,提供了相关的回调函数

源码剖析

1. 设置变量

image.png

上面代码分配了AVFormatContext结构体,设置文件名url、将选项设置到AVFormatContext里等等.

2. 调用init_input函数

传入三个参数: AVFormatContext、文件名、参数选项 image.png

这个函数代码较短,主要做的工作就是猜测文件格式

static int init_input(AVFormatContext* s, const char* filename,
    AVDictionary** options)
{
    int ret;
    AVProbeData pd = {filename, NULL, 0};
    int score = AVPROBE_SCORE_RETRY;   // 等于25,用来计算探测后的分数
    
    if (s->pb) // 如果开始就设置了AVIOContext
    {
        s->flags |= AVFMT_FLAG_CUSTOM_IO; // 那么就设置为自定义IO
        if (!s->iformat) // 进行猜测格式
            return av_probe_input_buffer2(s->pb, &s->iformat, filename, s, 0,
                s->format_probesize);
        else if (s->iformat->flags & AVFMT_NOFILE)
            av_log(...); // 日志
        return 0;
    }
    
    // 由ffmpeg来创建AVInputFormat,然后猜测格式
    if ((s->iformat && s->iformat->flags & AVFMT_NOFILE) ||
        (!s->iformat && (s->iformat == av_probe_input_format2(&pd, 0, &score)))
        return score; // 返回分数
    
    if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)
        return ret;
        
    if (s->iformat)
        return 0;
    
    return av_probe_input_buffer2(s->pb, &s->iformat, filename, s, 0, s->format_probesize);
}

这里面用到了个结构体AVProbeData,这个主要是包含探测文件的格式所需的数据

struct AVProbeData {
    const char* filename;  // 文件名
    unsigned char* buf; // 缓冲区
    int buf_size;       // 缓冲区大小
    const char* mime_type; 
}

这个函数用来探测格式,然后定义了分数然后返回.最主要的核心函数就是av_probe_input_buffer2.所以整个函数看起来非常短小.
一般ifromat默认都是为NULL,所以我们会进入到第二个if条件里,然后调用av_probe_input_format2函数

3. av_probe_input_format2函数

同样,这个函数代码量也很小.

const AVInputFormat *av_probe_input_format2(ff_const59 AVProbeData *pd, 
    int is_opened,
    int *score_max) // 默认传入分数25
{
    int score_ret; // 存储探测格式后的分数
    // 从文件读取一些数据用于探测格式,并返回AVInputFormat对象
    const AVInputFormat *fmt = av_probe_input_format3(pd, is_opened, &score_ret);
    if (score_ret > *score_max) 
    {
        *score_max = score_ret; // 设置探测后的分数
        return fmt;
    } 
    else
        return NULL;
}

这个函数并没有做什么操作,主要调用av_probe_input_format3函数用于探测.最后将AVInputFormat对象返回并设置探测后的分数

4. av_probe_input_format3函数

const AVInputFormat* av_probe_input_format3(const AVProbeData* pd, int is_opned,
    int* socre_ret)
{
    AVProbeData lpd = *pd;
    const AVInputFormat* fmt1 = NULL;
    const AVInputFormat* fmt = NULL;
    int score, score_max = 0;
    void* i = 0; // 下标,后续遍历会使用到
    const static uint8_t zerobuffer[AVPROBE_PADDING_SIZE]; // 数组32大小
    enum nodat {
        NO_ID3,
        ID3_ALMOST_GREATER_PROBE,
        ID3_GREATER_PROBE,
        ID3_GREATER_MAX_PROBE,
    } nodat = NO_ID3;  // 默认设置为NO_ID3
    if (!lpd.buf) // 将AVProbeData的buf缓冲区指向zerobuffer
        lpd.buf = (unsigned char*)zerobuffer;
    
    ... 暂时省略
}

上面代码设置一些变量,这里定义了一个数组,然后让AVProbeData的buf指向这个数组. 然后设置了一个枚举变量设置为NO_ID3
然后继续后面的代码

    if (lpd.buf_size > 10 && ff_id3v2_match(lpd.buf, ID3v2_DEFAULT_MAGIC)) 
    {
        int id3len = ff_id3v2_tag_len(lpd.buf);
        if (lpd.buf_size > id3len + 16)
        {
            if (lpd.buf_size < 2LL * id3len + 16)
                nodat = ID3_ALMOST_GREATER_PROBE;
            lpd.buf += id3len;
            lpd.buf_size -= id3len;
        }
        else if (id3len >= PROBE_BUF_MAX)
            nodat = ID3_GREATER_MAX_PROBE;
        else
            nodat = ID3_GREATER_PROBE;
    }

上面代码主要检测ID3v2标签(通常用于MP3文件元数据).如果缓冲区大小大于10,并且缓冲区的开头与ID3v2的魔数匹配,则检测是否存在ID3标签. 根据ID3标签的大小调用lpd.buflpd.buf_size,使得后续的格式探测从ID3标签之后的数据开始

接下来遍历格式列表,进行格式探测,然后调整分数

    while ((fmt1 = av_demuxer_iterate(&i)) // 从demuxer_list列表中获取AVInputFormat
    {
        // 如果is_opened == 0 且格式要求不能打开文件(AVFMT_NOFILE),则跳过
        // 如果格式为image2则跳过,因为image2不能进行格式探测
        if (!is_opened == !(fmt1->flags & AVFMT_NOFILE) && strcmp(fmt1->name, "image2"))
            continue;
        score = 0;
        if (fmt1->read_probe) // 如果设置了read_probe回调函数
        {
            socre = fmt1->read_probe(&lpd); // 则调用read_probe进行探测并返回得分
            if (score)
                av_log(...); // 日志输出
                
            // 如果支持文件扩展名,并且文件名与扩展名匹配,则根据探测到的ID3标签调整分数
            if (fmt1->extensions && av_match_ext(lpd.filename, fmt1->extensions))
            {
                switch (nodat) 
                {
                    case NO_ID3:
                        score = FFMAX(score, 1);
                        break;
                    case ID3_GREATER_PROBE:
                    case ID3_ALMOST_GREATER_PROBE:
                        score = FFMAX(score, AVPROBE_SCORE_EXTENSION / 2 - 1);
                        break;
                    case ID3_GREATER_MAX_PROBE:
                        score = FFMAX(score, AVPROBE_SCORE_EXTENSION);
                        break;
                }
            }
        }
        else if (fmt1->extensions)
        {
            if (av_match_ext(lpd.filename, fmt1->extensions))
                score = AVPROBE_SCORE_EXTENSION; // 扩展格式分数为 50
        }
        if (av_match_name(lpd.mime_type, fmt1->mime_type))
        {
        // 如果输入数据的MIME类型与当前格式的MIME类型匹配,则分数设置为75(较高分数)
            if (AVPROBE_SCORE_MIME > score)
            {
                av_log(..); // 日志输出
                score = AVPROBE_SCORE_MIME; // MIME分数为 75
            }
        }
        if (score > score_max) // 更新最高分数和匹配格式
        {
            score_max = score;
            fmt = (AVInputFormat*)fmt1;
        }
        else if (score == score_max)
            fmt = NULL;
    }
    
    if (nodat == ID3_GREATER_PROBE)
        score_max = FFMIN(AVPROBE_SCORE_EXTENSION / 2 - 1, score_max);
    *score_ret = score_max;
    
    return fmt;

上面代码都加了注释,在这里总结一下这个函数所做的事情.

默认有个demuxer_list列表,这个大小是325,类型是AVInputFormat. 这个函数遍历完整个这个列表,然后调用read_probe回调函数探测格式设置分数,这个是每个格式都有自己的格式解析函数. 然后设置最高分数.
如果设置了最高分数还会继续遍历,直到把这个列表遍历完,最终返回分数最高demuxer_list中的某个AVInputFormat

所以时间复杂度还是挺高的,O(N)

上面代码av_demuxer_iterate是怎么做的呢?下面来分析一下:

const AVInputFormat* av_demuxer_iterate(void** opaque)
{
    // 计算这个数组的大小
    static const uintptr_t size = sizeof(demuxer_list) / sizeof(demuxer_list[0]) - 1;
    uintptr_t i = (uintptr_t)*opaque; // 设置数组下标,默认是0
    const AVInputFormat* f = NULL;
    
    if (i < size)
        f = demuxer_list[i]; // 读取列表的AVInputFormat对象
    else if (indev_list)
        f = indev_list[i - size];
        
    if (f)
        *opaque = (void*)(i + 1); // 下标加1
    return f;
}

参数opaque存储的是0,在这里就代表数组下标,从0开始获取.然后遍历demuxer_list列表,从中获取AVInputFormat对象
demuxer_list这个列表定义在libavformat/demuxer_list.c
image.png 然后每个解复用器都会自己定义上图的结构,比如ff_aa_demuxer
image.png

至此av_probe_input_format2函数解析完成,让我们把视角再次回到init_input函数中

image.png 前面我们分析的是这个函数,那么计算完分数后,该执行下面的s->io_open函数

5. s->io_open函数

static int io_open_default(AVFormatContext* s, AVIOContext** pb,
    const char* url, int flags, AVDictionary** options)
{
    int loglevel;
    if (!strcmp(url, s->url) ||
        s->iformat && !strcmp(s->iformat->name, "image2") ||
        s->oformat && !strcmp(s->oformat->name, "image2"))
    {
        loglevel = AV_LOG_DEBUG;
    }
    else
        loglevel = AV_LOG_INFO;
    if (s->open_cb)
        return s->open_cb(s, pb, url, flags, &s->interrupt_callback, options);
    return ffio_open_whitelist(pb, url, flags, &s->interrupt_callback, options,
        s->protocol_whitelist, s->protocol_blacklist);
}

这个函数最主要的就是最后一行代码,ffio_open_whitelist,用来设置AVIOContext的读、写、seek回调函数.还有就是设置AVFormatContextAVIOContext成员,用于文件IO

然后再回到init_input函数,接下来剩下最后一个函数分析了.

image.png

6. av_probe_input_buffer2函数

int av_probe_input_buffer2(AVIOContext* pb, const AVInputFormat** fmt,
    const char* filename, void* logctx, unsigned int offset, 
    unsigned int max_probe_size)
{
    AVProbeData pd = {filename ? filename : ""};
    uint8_t buf = NULL;
    int ret = 0, probe_size, buf_offset = 0;
    int score = 0;
    int ret2;
    
    if (!max_probe_size)
        max_probe_size = PROBE_BUF_MAX;
    else if (max_probe_size < PROBE_BUF_MIN)
    {
        av_log(...); // 日志输出
        return AVERROR(EINVAL);
    }
    
    if (offset >= max_probe_size)
        return AVERROR(EINVAL);
        
    if (pb->av_class)
    {
        uint8_t mime_type_opt = NULL;
        char* semi;
        av_opt_get(pb, "mime_type", AV_OPT_SEARCH_CHILDREN, &mime_type_opt);
        pd.mime_type = (const char*)mime_type_opt;
        semi = pd.mime_type ? strchr(pd.mime_type, ';') : NULL;
        if (semi)
            *semi = '\0';
    }
    
    
    .. 暂时省略代码,后续继续分析
}

上面代码就是做一些简单的设置工作,没什么好解释的.下面讲解后面的for循环读取数据然后探测格式

    // 探测数据大小默认2048,每次循环后增加2倍
    for (probe_size = PROBE_BUF_MIN; probe_size <= max_probe_size && !*fmt;
        probe_size = FFMIN(probe_size << 1, FFMAX(max_probe_size, probe_size + 1)))
    {
        score = probe_size < max_probe_size ? AVPROBE_SCORE_RETRY : 0;
        
        // 分配缓冲区
        if ((ret = av_reallocp(&buf, probe_size + AVPROBE_PADDING_SIZE)) < 0)
            goto fail;
        // 读取数据到缓冲区里
        if ((ret = avio_read(pb, buf + buf_offset, probe_size - buf_offset)) < 0)
        {
            if (ret != AVERROR_EOF) // 读取失败则跳转到fail标号
                goto fail;
                
            score = 0;
            ret = 0;
        }
        buf_offset += ret;  // 记录已读取的字节数
        if (buf_offset < offset) 
            continue;
        pd.buf_size = buf_offset - offset;
        pd.buf = &buf[offset];
        
        memset(pd.buf + pd.buf_size, 0, AVPROBE_PADDING_SIZE);
        // 探测数据格式
        *fmt = av_probe_input_format2(&pd, 1, &score);
        if (*fmt)
        {
            if (score <= AVPROBE_SCORE_RETRY) // 分数较低发出警告可能存在误判
            {
                av_log(...); // 日志输出
            }
            else 
                // 同样也是日志 <--- 这里是分数较高
        }
    }
    
    if (!*fmt) // 未找到匹配的格式
        ret = AVERROR_INVALIDDATA;
fail:
    // 重置缓冲区
    ret2 = ffio_rwind_with_probe_data(pb, &buf, buf_offset);
    if (ret >= 0)
        ret = ret2;
        
    av_freep(&pd.mime_type); // 释放MIME内存,返回探测分数或错误码
    return ret < 0 ? ret : score;

上面的代码总结就是分配缓冲区,默认大小是2048,每次增加2倍直到max_probe_size大小(1048576). 然后调用avio_read读取数据到缓冲区,调用av_probe_input_format2探测格式.最终返回分数和AVInputFormat对象

至此init_input函数到此解析完成.让我们把视角重新回到avformat_open_input调用init_input函数后

7. avformat_open_input函数后续代码

image.png 此处的ret返回值应该是100,表示成功探测出了格式.

然后剩余后续代码工作:

  1. 设置duration和start_time
  2. 分配私有数据priv_data内存
  3. 调用s->iformat->read_header函数确定流的信息,有几路流
  4. 处理元数据
  5. 设置编解码器id

最后总结

  • 初始化AVFormatContext
  • 打开输入源
  • 探测输入格式
  • 读取并解析文件头,确定输入流信息.比如有几路流,编解码器id是什么