FFMPEG4.1源码分析之 媒体打开函数avformat_open_input()

目录

0 avformat_open_input() 功能详细描述

1 avformat_open_input()源码分析

1.1 init_input()

1.1.1 av_probe_input_buffer2()

           //1.1.1.2 ffio_rewind_with_probe_data()

1.1.2 av_probe_input_format2()

             1.1.2.1 av_probe_input_format3()

1.2 flv_read_header()

1.3 mov_read_header()

1.4 update_stream_avctx()

1.4.1 avcodec_parameters_to_context()


0 avformat_open_input() 功能详细描述


该函数的详细描述在FFMPEG4.1源码解析之 libavformat库解读的1.2节中进行了详细的阐述,此处就不再多做赘述,后续还会简单的介绍下该函数的功能。建议看本文分析之前先看下FFMPEG4.1源码解析之 最重要的结构体及其关系,了解下FFMPEG分层体系,就像网络分层一样,需要了解各层级都负责什么样的功能。

具体上,从源码来说,该函数有这么几项功能:
     1. 在用户没有提供AVFormatContext的情况下,创建一个格式上下文对象AVFormatContext;
     2. 在用户没有提供IO层上下文对象AVIOContext的情况下,打开文件并创建IO层上下文对象AVIOContext;
     3. 在用户没有指定输入文件格式AVInputFormat的情况下,探测文件格式,得到输入文件格式信息AVInputFormat;
     4. 读取文件头,在文件头描述信息足够的情况下创建流AVStream以及获取并设置编解码参数相关信息;
     5. 填充AVFormatContext其他字段信息。
总之,该函数的作用就是打开文件,尽可能的收集各方面的信息并填充AVFormatContext结构体,基本上是做了除过解码之外的所有工作。

1 avformat_open_input()源码分析


avformat_open_input 声明:

  • 所属库:libavformat(lavf
  • 头文件:libavformat/avformat.h
  • 声明:
    功能:打开音视频资源(不翻译成流是为了与AVStream概念区分),并读取文件头,获取必要的信息。注意解码器并未打开,并未对音视频包进行解码。
    参数ps:该参数可空,为空时,本函数内部将创建AVFormatContext。在需要使用自定义的I/O层回调函数方法时,那就必须要在该函数外部创建AVFormatContext,并创建AVIOContext并赋值给AVFormatContext.pb成员。
    参数url:音视频资源的url
    参数fmt:如果不为空,则表示强制指定了输入文件的文件格式,不再进行文件格式的探测,如果为空,则本函数内部将对输入文件参数进行自动的探测。ffmpeg工具命令行中的-f选项用以指定该输入格式。
    参数options该选项信息将被应用到AVFormatContext的成员字段,以及解封装器(AVInputFormat)的私有字段上去。
    /**
     * Open an input stream and read the header. The codecs are not opened.
     * The stream must be closed with avformat_close_input().
     *
     * @param ps Pointer to user-supplied AVFormatContext (allocated by avformat_alloc_context).
     *           May be a pointer to NULL, in which case an AVFormatContext is allocated by this
     *           function and written into ps.
     *           Note that a user-supplied AVFormatContext will be freed on failure.
     * @param url URL of the stream to open.
     * @param fmt If non-NULL, this parameter forces a specific input format.
     *            Otherwise the format is autodetected.
     * @param options  A dictionary filled with AVFormatContext and demuxer-private options.
     *                 On return this parameter will be destroyed and replaced with a dict containing
     *                 options that were not found. May be NULL.
     *
     * @return 0 on success, a negative AVERROR on failure.
     *
     * @note If you want to use custom IO, preallocate the format context and set its pb field.
     */
    int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);

avformat_open_input 源码:

  • 所属库:libavformat(lavf
  • 源文件:libavformat/utils.c
  • 源码:
    int avformat_open_input(AVFormatContext **ps, const char *filename,
                            AVInputFormat *fmt, AVDictionary **options)
    {
        AVFormatContext *s = *ps;
        int i, ret = 0;
        AVDictionary *tmp = NULL;
        ID3v2ExtraMeta *id3v2_extra_meta = NULL;
    
        // 判断传入的AVFormatContext对象是否为空
        // 为空则创建该对象
        if (!s && !(s = avformat_alloc_context()))
            return AVERROR(ENOMEM);
    
        // 判断AVFormatContext对象是否被合法的创建
        // 由于AVFormatContext对象只能使用avformat_alloc_context()来创建
        // 若外部传入了一个由av_malloc()创建的该对象,此处将检测这种情况的发生
        if (!s->av_class) {
            av_log(NULL, AV_LOG_ERROR, "Input context has not been properly allocated by avformat_alloc_context() and is not NULL either\n");
            return AVERROR(EINVAL);
        }
    
        // 判断外部是否强制指定了输入文件格式
        if (fmt)
            s->iformat = fmt;
    
        // 拷贝选项
        if (options)
            av_dict_copy(&tmp, *options, 0);
    
        // 如果外部指定了I/O层,那么设置标志位AVFMT_FLAG_CUSTOM_IO
        if (s->pb) // must be before any goto fail
            s->flags |= AVFMT_FLAG_CUSTOM_IO;
    
        // 应用选项信息到AVFormatContext
        if ((ret = av_opt_set_dict(s, &tmp)) < 0)
            goto fail;
    
        // 拷贝filename到AVFormatContext.url字段
        if (!(s->url = av_strdup(filename ? filename : ""))) {
            ret = AVERROR(ENOMEM);
            goto fail;
        }
    
        // 拷贝filename到AVFormatContext.filename字段
        // 注意该字段已经被声明为deprecated
    #if FF_API_FORMAT_FILENAME
    FF_DISABLE_DEPRECATION_WARNINGS
        av_strlcpy(s->filename, filename ? filename : "", sizeof(s->filename));
    FF_ENABLE_DEPRECATION_WARNINGS
    #endif
    
        // 探测文件格式,将探测得分存储于AVFormatContext.probe_score字段 
        if ((ret = init_input(s, filename, &tmp)) < 0)
            goto fail;
        s->probe_score = ret;
    
        // 拷贝协议白名单到AVFormatContext.protocol_whitelist
        if (!s->protocol_whitelist && s->pb && s->pb->protocol_whitelist) {
            s->protocol_whitelist = av_strdup(s->pb->protocol_whitelist);
            if (!s->protocol_whitelist) {
                ret = AVERROR(ENOMEM);
                goto fail;
            }
        }
    
        // 拷贝协议黑名单到AVFormatContext.protocol_blacklist
        if (!s->protocol_blacklist && s->pb && s->pb->protocol_blacklist) {
            s->protocol_blacklist = av_strdup(s->pb->protocol_blacklist);
            if (!s->protocol_blacklist) {
                ret = AVERROR(ENOMEM);
                goto fail;
            }
        }
    
        // 如果格式白名单存在,那么探测到的格式必须要属于白名单
        if (s->format_whitelist && av_match_list(s->iformat->name, s->format_whitelist, ',') <= 0) {
            av_log(s, AV_LOG_ERROR, "Format not on whitelist \'%s\'\n", s->format_whitelist);
            ret = AVERROR(EINVAL);
            goto fail;
        }
    
        // 设置AVIOContext内部缓冲区跳过初始字节  
        avio_skip(s->pb, s->skip_initial_bytes);
    
        // 检查文件名是否图片序列中的一个,名称中必须包含图片序号数字
        /* Check filename in case an image number is expected. */
        if (s->iformat->flags & AVFMT_NEEDNUMBER) {
            if (!av_filename_number_test(filename)) {
                ret = AVERROR(EINVAL);
                goto fail;
            }
        }
    
        // 初始化文件的持续时间和起始时间为AV_NOPTS_VALUE
        s->duration = s->start_time = AV_NOPTS_VALUE;
    
        // 设置AVFormatContext的私有数据,套路与设置AVIOContext的私有数据一样
        // 注意该私有数据也是一个上下文对象,后续以FLV格式来分析该私有数据
        /* Allocate private data. */
        if (s->iformat->priv_data_size > 0) {
            // 分配AVFormatContext私有数据空间
            if (!(s->priv_data = av_mallocz(s->iformat->priv_data_size))) {
                ret = AVERROR(ENOMEM);
                goto fail;
            }
            
            if (s->iformat->priv_class) {
                // 设置AVFormatContext私有数据的第一个成员字段AVClass*
                *(const AVClass **) s->priv_data = s->iformat->priv_class;
                // 初始化AVFormatContext私有数据的其他成员    
                av_opt_set_defaults(s->priv_data);
                // 应用用户提供的选项信息到AVFormatContext私有数据
                if ((ret = av_opt_set_dict(s->priv_data, &tmp)) < 0)
                    goto fail;
            }
        }
    
        // 如果AVIOContext存在,读取ID3信息
        /* e.g. AVFMT_NOFILE formats will not have a AVIOContext */
        if (s->pb)
            ff_id3v2_read_dict(s->pb, &s->internal->id3v2_meta, ID3v2_DEFAULT_MAGIC, &id3v2_extra_meta);
    
        // 读取文件头,可能获取到metadata信息,流信息
        if (!(s->flags&AVFMT_FLAG_PRIV_OPT) && s->iformat->read_header)
            if ((ret = s->iformat->read_header(s)) < 0)
                goto fail;
    
        // 处理metadata信息与id3v2信息
        if (!s->metadata) {
            s->metadata = s->internal->id3v2_meta;
            s->internal->id3v2_meta = NULL;
        } else if (s->internal->id3v2_meta) {
            int level = AV_LOG_WARNING;
            if (s->error_recognition & AV_EF_COMPLIANT)
                level = AV_LOG_ERROR;
            av_log(s, level, "Discarding ID3 tags because more suitable tags were found.\n");
            av_dict_free(&s->internal->id3v2_meta);
            if (s->error_recognition & AV_EF_EXPLODE)
                return AVERROR_INVALIDDATA;
        }
    
        // 处理id3v2额外信息
        if (id3v2_extra_meta) {
            if (!strcmp(s->iformat->name, "mp3") || !strcmp(s->iformat->name, "aac") ||
                !strcmp(s->iformat->name, "tta")) {
                if ((ret = ff_id3v2_parse_apic(s, &id3v2_extra_meta)) < 0)
                    goto fail;
                if ((ret = ff_id3v2_parse_chapters(s, &id3v2_extra_meta)) < 0)
                    goto fail;
                if ((ret = ff_id3v2_parse_priv(s, &id3v2_extra_meta)) < 0)
                    goto fail;
            } else
                av_log(s, AV_LOG_DEBUG, "demuxer does not support additional id3 data, skipping\n");
        }
        ff_id3v2_free_extra_meta(&id3v2_extra_meta);
    
        // 封面图片处理
        if ((ret = avformat_queue_attached_pictures(s)) < 0)
            goto fail;
    
        // 更新AVFormatContext.AVFormatInternal的成员 
        if (!(s->flags&AVFMT_FLAG_PRIV_OPT) && s->pb && !s->internal->data_offset)
            s->internal->data_offset = avio_tell(s->pb);
    
        s->internal->raw_packet_buffer_remaining_size = RAW_PACKET_BUFFER_SIZE;
    
        update_stream_avctx(s);
    
        for (i = 0; i < s->nb_streams; i++)
            s->streams[i]->internal->orig_codec_id = s->streams[i]->codecpar->codec_id;
    
        if (options) {
            av_dict_free(options);
            *options = tmp;
        }
        *ps = s;
        return 0;
    
    fail:
        ff_id3v2_free_extra_meta(&id3v2_extra_meta);
        av_dict_free(&tmp);
        if (s->pb && !(s->flags & AVFMT_FLAG_CUSTOM_IO))
            avio_closep(&s->pb);
        avformat_free_context(s);
        *ps = NULL;
        return ret;
    }
    

    该函数内容比较长,按如下分段理解会很容易
    1. 入参检查:检查所有的入参,并做相应的处理。
           1.1) 处理AVFormatContext **ps:确保后续操作有一个非空的并且是合法创建的AVFormatContext对象可以使用。具体做法见源码注释
           1.2)处理AVInputFormat *fmt:判断是否强制指定了输入文件格式,若指定了,那么将该文件格式挂载于AVFormatContext.iformat成员上,后续将不再自动探测音视频文件格式。
           1.3)处理AVDictionary **options:先将选项信息拷贝到内部临时变量,然后调用av_opt_set_dict()方法来应用选项信息到AVFormatContext。av_opt_set_dict() 方法的具体分析见 FFMPEG源码分析之 av_opt_set_dict()
           1.4)处理AVFormatContext.pb:如果用户希望用自己代码处理IO层,也即AVIOContext在外部已经被创建并赋值给AVFormatContext.pb,那么确保AVFormatContext.flags的AVFMT_FLAG_CUSTOM_IO置位。
           1.5)处理const char *filename:拷贝filename到AVFormatContext.url和AVFormatContext.filename字段,后者已经被标注为deprecated,为了兼容以前的版本,此处也会赋值。
    2. 调用init_input()打开文件,并进行文件格式探测:过程复杂,后文详述
            2.1)init_input()进行文件格式探测,创建合适的AVIOContext上下文对象,并找到最合适的AVInputFormat。
            2.2)将探测的文件格式得分赋值给AVFormatContext.probe_score。
    3.  获取信息并填充AVFormatContext的其他字段
           
    3.1)AVFormatContext.protocol_whitelist&& .protocol_blacklist:复制AVIOContext持有的协议黑白名单到对应字段
            3.2)AVFormatContext.format_whitelist:判断推断的输入文件格式是否在格式白名单中。以mov格式为例,其AVInputFormat对象为以下源码所示,那么名称为一个列表,可以看到 "mov, mp4, m4a, 3gp, 3g2, mj2" 这些文件格式共享一个文件格式对象,因为这几个格式都是基于ISO base media file format衍生出来的格式,基本上遵循同样的规范。文件格式白名单format_whitelist要么不存在,要么也是以","分隔的文件格式,只要文件格式名称中的一个格式与白名单中的一个匹配则认为文件格式在白名单中。

    AVInputFormat ff_mov_demuxer = {
        .name           = "mov,mp4,m4a,3gp,3g2,mj2",
        .long_name      = NULL_IF_CONFIG_SMALL("QuickTime / MOV"),
        .priv_class     = &mov_class,
        .priv_data_size = sizeof(MOVContext),
        .extensions     = "mov,mp4,m4a,3gp,3g2,mj2",
        .read_probe     = mov_probe,
        .read_header    = mov_read_header,
        .read_packet    = mov_read_packet,
        .read_close     = mov_read_close,
        .read_seek      = mov_read_seek,
        .flags          = AVFMT_NO_BYTE_SEEK,
    };

            3.3)AVFormatContext. skip_initial_bytes:该参数释义为“set number of bytes to skip before reading header and frames”,通过avio_skip()函数跳过skip_initial_bytes,该值在创建初始化AVFormatContext时被设置为0。avio_skip()分析见FFRMPEG4.1源码分析之 IO层结构体AVIOContext && APIs
            3.4)检查AVInputFormat.flags标志AVFMT_NEEDNUMBER 与 输入文件名中是否包含数字序列所匹配。如何理解这点呢? 首先要知道,ffmpeg支持视频文件与图片文件互转,比如支持一系列的图片组合成一个视频,那么对应的ffmpeg命令类似于"ffmpeg -i image-%3d.jpeg out.mp4",那么输入文件名为"image-%3d.jpeg"。av_filename_number_test()方法可以检测文件名中是否存在“%3d”这个字符串。
            3.5)AVFormatContext.duration && .start_time:设置媒体持续时长以及起始时间为AV_NOPTS_VALUE,该值为(int64_t)UINT64_C(0x8000000000000000),表示没有提供时间信息。
            3.6)AVFormatContext.priv_data:AVFormatContext的私有数据,类型为void*,根据输入文件格式不同,该字段为不同类型的,以mov格式的文件为例,见上述ff_mov_demuxer源码:该私有字段将是MOVContext的结构,源码中先分配MOVContext空间->将AVClass对象mov_class赋值给MOVContext的第一个成员->调用av_opt_set_defaults()来给MOVContext设置默认值->调用av_opt_set_dict()将用户传入的信息应用到MOVContext成员。
            3.7)AVFormatContext.AVFormatInternal.id3v2_meta读取文件开头的ID3V2信息,保存到该字段。额外的id3v2信息存在id3v2_extra_meta的临时变量中
            3.8)通过AVInputFormat.read_header()来获取metadata信息,创建流等等
                    以flv格式和mp4格式为例来说明,flv_read_header() 和 mov_read_header()方法在后文中详述,二者基本代表了两种类型的数据:
                    flv格式是适合流传输的格式,除了最初始有个简单的header外,就是一个个TAG的结构体,每个TAG都可以自描述,包含了自描述信息和音视频数据,这种格式的好处是不论从流的哪儿开始播放都很方便。相应的flv_read_header()函数比较简单,因为flv的头比较单,包含信息量很少,因此并没有获取到metadata信息,也就是s->metadata不会被赋值,并且,也没有找到足够多的关于流的信息,只简单的知道是否存在视频流,是否存在音频流,不会调用avformat_new_stream()来创建AVStream对象,所以s->nb_streams,s->streams都不会被赋值。
                   mp4格式是一种交换格式,所有的音视频信息集中在一起,并建立索引信息来查找正真的音视频数据,而音视频数据在一起。相应的mov_read_header()函数(为啥不是mp4_read_header?请参阅上面的文章描述)就特别复杂,由于mp4是由一个个box组成,mov_read_header()会读取所有的box以获取mp4文件的完整信息,最重要的就是moov这个box,因此,s->metadata会被赋值,并且流的个数,流本身的信息也都非常清楚,因此avformat_new_stream() 会被调用来创建AVStream对象,s->nb_streams,s->streams都会被正确的赋值。
             3.9)AVFormatContext.metadata:在metadata存在的条件下,其更可靠,因此3.7) 中的id3v2_meta可以丢弃,若不存在那么AVFormatContext.metadata将取值AVFormatContext.AVFormatInternal.id3v2_meta。
             3.10)mp3,aac,tta的音频格式,id3v2_extra_meta数据中会存储封面(apic),章节(chapters),私有帧(priv)信息。
             3.11)将封面图片放到队列中
             3.12)更新AVFormatContext.AVFormatInternal的成员 
             3.13)通过调用update_stream_avctx(s)更新流的上下文信息,见后文分析

1.1 init_input()


init_input() 源码:

  • 所属库:libavformat(lavf
  • 源文件:libavformat/utils.c   静态函数,无头文件
  • 源码: 
       功能:该函数有两个目标:一个是需要找到合适的I/O层对象,也即AVIOContext,其用于资源的访问;一个是探测文件格式,得到合适的AVInputFormat结构体对象。后续的代码分析过程中务必带着这个思维去看。
    /* Open input file and probe the format if necessary. */
    static int init_input(AVFormatContext *s, const char *filename,
                          AVDictionary **options)
    {
        int ret;
        AVProbeData pd = { filename, NULL, 0 };
        int score = AVPROBE_SCORE_RETRY;
    
        // 用户指定了文件访问I/O层上下文对象AVIOContext
        if (s->pb) {
            // 修改下标志,表示IO层是用户提供的
            s->flags |= AVFMT_FLAG_CUSTOM_IO;
            // 如果用户没有指定文件格式
            if (!s->iformat)
                // 使用用户提供的I/O层接口来读取文件,并使用av_probe_input_buffer2
                // 进行文件格式探测,并返回探测得分
                return av_probe_input_buffer2(s->pb, &s->iformat, filename,
                                             s, 0, s->format_probesize);
            // 用户指定了文件访问IO,指定了输入文件格式,但是文件格式的标志位AVFMT_NOFILE又告知
            // 根本不需要访问文件,所以用户提供IO层的AVIOContext岂不是毫无作用,因此,打印下面这
            // 条警告日志
            else if (s->iformat->flags & AVFMT_NOFILE)
                av_log(s, AV_LOG_WARNING, "Custom AVIOContext makes no sense and "
                                          "will be ignored with AVFMT_NOFILE format.\n");
            // 直接返回0,也就是说用户指定了输入格式了
            // 不需要进行文件格式探测了。
            return 0;
        }
    
        // 如果指定了输入文件格式,该文件格式的标志AVFMT_NOFILE被设置,表示不需要IO层接口了,
        // 因为没有文件需要打开与读取,后续的操作肯定也不需要I/O层,此时,直接返回即可;
        // 若没有指定文件格式,那么通过av_probe_input_format2函数以及AVProbeData中提供的信息
        // 进行文件格式探测。
        if ((s->iformat && s->iformat->flags & AVFMT_NOFILE) ||
            (!s->iformat && (s->iformat = av_probe_input_format2(&pd, 0, &score))))
            return score;
    
        // 函数走到此处还未返回有两种情况:
        // 一种是用户指定了输入文件格式,但是AVFMT_NOFILE标志未被设置,即后续的操作是需要读取文件的
        // 那么必不可少需要I/O层的对象,因此,调用io_open()来打开文件,初始化I/O层的一切吧。
        // 另一种情况是用户没有指定输入文件格式,但是之前根据文件扩展名也猜不出输入文件格式,那么
        // 只能打开文件,读取数据来分析文件格式了,既然要读取文件,必然也需要I/O层的对象了,因此,也
        // 调用io_open()来打开文件,初始化I/O层的一切吧
        if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)
            return ret;
    
        // 函数走到这,若输入文件格式已确定,这种情形只可能是用户指定了
        // 输入文件格式,并且文件也是需要读取的,即s->iformat->flags的AVFMT_NOFILE标志位未被设置
        // I/O层也初始化,那么该做的都做了,返回吧~~
        if (s->iformat)
            return 0;
    
        // 函数走到此处还未返回只可能是一种情况:
        // 用户没有指定输入文件格式,根据文件扩展名也无法确定文件格式;
        // 此时需要av_probe_input_buffer2来读取文件内容进行文件格式确认
        return av_probe_input_buffer2(s->pb, &s->iformat, filename,
                                     s, 0, s->format_probesize);
    }
    

    1.  初始化探测参数AVProbeData以及探测得分score:
          1.1)AVProbeData:{ filename, NULL, 0 };  只提供了资源URL

    /**
     * This structure contains the data a format has to probe a file.
     */
    typedef struct AVProbeData {
        const char *filename;
        unsigned char *buf; /**< Buffer must have AVPROBE_PADDING_SIZE of extra allocated bytes filled with zero. */
        int buf_size;       /**< Size of buf except extra allocated bytes */
        const char *mime_type; /**< mime_type, when known. */
    } AVProbeData;

          1.2)score初始值设置为AVPROBE_SCORE_RETRY,该值为25。
                 1.2.1) score变量是一个判决AVInputFormat的分数的门限值,如果最后得到的AVInputFormat的分数低于该门限值, 
           就认为没有找到合适的AVInputFormat。FFmpeg内部判断封装格式的原理实际上是对每种AVInputFormat给出一个分
           数,满分是100分,越有可能正确的AVInputFormat给出的分数就越高。最后选择分数最高的AVInputFormat作为推测结
           果。如果推测后得到的最佳AVInputFormat的分值低于25,就认为没有找到合适的AVInputFormat
                 1.2.2) 与文件探测得分相关的几个宏定义如下:根据不同条件推导出AVInputFormat得分是不一样的,如下所示

    #define AVPROBE_SCORE_RETRY (AVPROBE_SCORE_MAX/4) 
    #define AVPROBE_SCORE_STREAM_RETRY (AVPROBE_SCORE_MAX/4-1)
    
    #define AVPROBE_SCORE_EXTENSION  50 ///< score for file extension
    #define AVPROBE_SCORE_MIME       75 ///< score for file mime type
    #define AVPROBE_SCORE_MAX       100 ///< maximum score

    2.   用户提供了I/O层,即AVIOContext上下文对象情形下的处理:
         
    出现用户提供I/O层的情形很少,可能有这么两个:一个是从内存中读取数据,而非解析某个协议的情形,此时需要初始化自定义的AVIOContext;一个是ffmpeg无法识别的新协议,这时,需要用户来提供字定义的协议解析相关接口来初始化AVIOContext。
         2.1)用户指定了输入文件格式AVInputFormat,此时,输入文件格式有了,I/O层也有了,该有的都有了,直接返回;
         2.2)用户未指定输入文件格式AVInputFormat,那么此时使用av_probe_input_buffer2()来读取文件内容,进行文件格式探测,非常重要的一点是,此时的I/O层是用户提供的,该函数内部使用用户提供的I/O层接口访问文件,读取数据。探测到格式后直接返回。
    3.  用户没有提供I/O层的情形:
         
    3.1)用户指定了输入文件格式AVInputFormat,并且AVInputFormat.flags的标志AVFMT_NOFILE被设置,表明根本没有文件需要访问,后续操作那也不需要也没有文件可以访问,既然这样,那就不需要IO层接口了,直接返回即可;
         3.2)用户未指定输入文件格式AVInputFormat,那么优先通过av_probe_input_format2()函数进行文件格式探测,该函数通过AVProbeData结构中提供的三类信息进行格式识别:文件后缀(由资源的URL提供),文件的MIME类型(此处为空),缓冲数据(探测数据,后续会分析该缓冲数据如何得到,此处空)。非常需要认识到的一点是该函数不会进行I/O层的操作,当av_probe_input_format2()函数根据以上三类信息识别到格式后,本函数就退出了,此时,并没有创建一个合适的I/O层对象AVIOContext。
                 重要!重要!重要!我在这个地方困扰了比较长的时间,撸完av_probe_input_format2()代码才真正知道,其实该函数有个入参is_opened,表征着文件是否被打开,在文件未被打开的情况下,该函数绝大多数情况下会返回空,除非对应的文件格式是"image2"并且文件格式明确不需要读文件,即AVFMT_NOFILE被置位。这样,3.2)的情形下绝大多数是不会返回的。
         3.3)在3.1)3.2)情形下函数未返回,有两种情况:
                 3.3.1)一种情况是用户指定了输入文件格式,并且存在文件后续需要被访问,那么使用s->io_open()打开文件,创建I/O层的对象AVIOContext,此时,输入文件格式有了,I/O层也有了,该有的都有了,直接返回;
                 3.3.2)另外一种情况是用户未指定输入文件格式,并且3.2)探测文件格式的方式失败,那么使用s->io_open()打开文件,创建I/O层的对象AVIOContext,并调用av_probe_input_buffer2()函数进行文件格式探测。注意在用户提供I/O层的情形下也使用了av_probe_input_buffer2()函数进行文件格式探测,差别在于av_probe_input_buffer2()使用的I/O层对象AVIOContext对象,一个是用户外部提供的,一个是s->io_open()自主分析资源URL的所属访问协议来产生的。
    4. 分析完毕
        到3该函数就已经分析完毕,分析过程中有3个重要的函数未进行具体解析:
        1)根据文件后缀等探测文件格式的函数av_probe_input_format2(),后文详述;
        2)打开文件读取文件内容来推测文件格式的函数av_probe_input_buffer2(),后文详述;
        3)另,s->io_open是一个函数指针,该函数到底是啥?详细的源码分析见FFMPEG4.1源码分析之 avformat_alloc_context() 以及 FFRMPEG4.1源码分析之 io_open_default() && io_close_default()

1.1.1 av_probe_input_buffer2()


av_probe_input_buffer2声明:

  • 所属库:libavformat(lavf
  • 头文件:libavformat/avformat.h
  • 声明:
    功能:读取开的文件中的比特流,探测输入文件格式。每次探测返回一个得分,若得分太低,则增加探测缓冲的长度,读取更多的数据,再进行尝试。当达到探测缓冲区的上限时,返回具有最大探测得分的输入格式。
    参数logctx: 日志上下文对象,影响到日志输出,详细情况需要分析ffmpeg中的日志系统,可以参见雷神博客 FFmpeg源代码简单分析:日志输出系统(av_log()等
    参数offset:从读取比特流的哪个位置(offset)开始进行探测,一般为0,从头开始
    参数max_probe_size:探测缓冲区的最大值,该值一般为AVFormatContext.format_probesize字段提供,该字段在AVFormatContext创建过程中被设置为默认值5000000,具体被赋值为该默认值的过程,请参阅 FFMPEG4.1源码分析之 avformat_alloc_context()
    /**
     * Probe a bytestream to determine the input format. Each time a probe returns
     * with a score that is too low, the probe buffer size is increased and another
     * attempt is made. When the maximum probe size is reached, the input format
     * with the highest score is returned.
     *
     * @param pb the bytestream to probe
     * @param fmt the input format is put here
     * @param url the url of the stream
     * @param logctx the log context
     * @param offset the offset within the bytestream to probe from
     * @param max_probe_size the maximum probe buffer size (zero for default)
     * @return the score in case of success, a negative value corresponding to an
     *         the maximal score is AVPROBE_SCORE_MAX
     * AVERROR code otherwise
     */
    int av_probe_input_buffer2(AVIOContext *pb, AVInputFormat **fmt,
                               const char *url, void *logctx,
                               unsigned int offset, unsigned int max_probe_size);

av_probe_input_buffer2声明:

  • 所属库:libavformat(lavf
  • 源文件:libavformat/format.c
  • 源码:
    int av_probe_input_buffer2(AVIOContext *pb, 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(logctx, AV_LOG_ERROR,
                   "Specified probe size value %u cannot be < %u\n", max_probe_size, PROBE_BUF_MIN);
            return AVERROR(EINVAL);
        }
    
        // 检查探测的初始偏移量是否合法
        if (offset >= max_probe_size)
            return AVERROR(EINVAL);
    
        // 找寻音视频资源的mime_type
        if (pb->av_class) {
            uint8_t *mime_type_opt = NULL;
            char *semi;
            // 搜索AVIOContext以及子对象的mime_type选项信息
            av_opt_get(pb, "mime_type", AV_OPT_SEARCH_CHILDREN, &mime_type_opt);
            pd.mime_type = (const char *)mime_type_opt;
            // 选项信息中存在多个,以分号分隔,截断pd_mime_type取第一个
            semi = pd.mime_type ? strchr(pd.mime_type, ';') : NULL;
            if (semi) {
                *semi = '\0';
            }
        }
    
        // probe_size初始化为最小buffer大小2048
        // 退出条件是probe_size超过最大探测缓冲大小max_probe_size或者是找到了AVInputFormat
        // probe_size没循环一次,缓冲区大小乘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;
    
            // 分配缓冲区大小为probe_size+AVPROBE_PADDING_SIZE
            // 为什么缓冲区的长度要加AVPROBE_PADDING_SIZE,将会在后文分析。
            // 注意此处使用的是av_realloc而非av_malloc,为了新分配的缓冲区中还保留上次读取的数据
            /* Read probe data. */
            if ((ret = av_reallocp(&buf, probe_size + AVPROBE_PADDING_SIZE)) < 0)
                goto fail;
    
            // 调用avio_read从上次读取数据之处(合理的偏移量)处读取缓冲扩展大小的字节数
            // 填充到扩展的空间上
            if ((ret = avio_read(pb, buf + buf_offset,
                                 probe_size - buf_offset)) < 0) {
                // 读文件失败
                /* Fail if error was not end of file, otherwise, lower score. */
                if (ret != AVERROR_EOF)
                    goto fail;
    
                // 读到文件尾了
                score = 0;
                ret   = 0;          /* error was end of file, nothing read */
            }
    
            // 更新读取数据bytes
            buf_offset += ret;
    
            // 读取数据bytes不够传入的探测数据offset
            // 不进行文件格式探测,此时数据是不够的,因此,进入下一轮继续读取数据
            if (buf_offset < offset)
                continue;
    
            // 填充AVProbeData对象,数据大小是已读取数据字节数-探测偏移offset
            // 设置相应的指针从offset开始,注意一点AVProbeData的缓冲是复用了
            // 本函数内部临时变量buf分配的内存,并没有再申请新的内存。
            pd.buf_size = buf_offset - offset;
            pd.buf = &buf[offset];
    
            // 缓冲尾部多余的AVPROBE_PADDING_SIZE清零
            // 为什么要这么做?见av_probe_input_format2函数分析
            memset(pd.buf + pd.buf_size, 0, AVPROBE_PADDING_SIZE);
    
            // av_probe_input_format2根据AVProbeData提供的文件名
            // mime类型,读取的文件数据来猜测文件格式
            /* Guess file format. */
            *fmt = av_probe_input_format2(&pd, 1, &score);
            if (*fmt) {
                /* This can only be true in the last iteration. */
                if (score <= AVPROBE_SCORE_RETRY) {
                    av_log(logctx, AV_LOG_WARNING,
                           "Format %s detected only with low score of %d, "
                           "misdetection possible!\n", (*fmt)->name, score);
                } else
                    av_log(logctx, AV_LOG_DEBUG,
                           "Format %s probed with size=%d and score=%d\n",
                           (*fmt)->name, probe_size, score);
    #if 0
                FILE *f = fopen("probestat.tmp", "ab");
                fprintf(f, "probe_size:%d format:%s score:%d filename:%s\n", probe_size, (*fmt)->name, score, filename);
                fclose(f);
    #endif
            }
        }
    
        // 如果上述过程还未探测到输入文件格式,那么返回错误
        if (!*fmt)
            ret = AVERROR_INVALIDDATA;
    
    fail:
        // 注意,探测完毕需要将AVIOContext中的buffer归位
        /* Rewind. Reuse probe buffer to avoid seeking. */
        ret2 = ffio_rewind_with_probe_data(pb, &buf, buf_offset);
        if (ret >= 0)
            ret = ret2;
    
        av_freep(&pd.mime_type);
        return ret < 0 ? ret : score;
    }

    该函数内容比较长,但是按如下分段理解就会变得很容易:
    1)函数内部参数声明以及入参检查
         
    1.1)初始化AVProbeData,注意,此时AVProbeData中只有filename是有效的
          1.2)检查max_probe_size探测缓冲区最大长度,确保其取合适的值:
                   当传入max_probe_size为0时,max_probe_size取值PROBE_BUF_MAX如下所示,十进制为524288
                   当传入max_probe_size不为0时,那么max_probe_size必须大于PROBE_BUF_MIN,为2048

    /** size of probe buffer, for guessing file type from file contents */
    #define PROBE_BUF_MIN 2048
    #define PROBE_BUF_MAX (1 << 20)

           1.3)检查offset探测的初始偏移量是否合法,不能超过max_probe_size
    2)搜索mime_type,填充AVProbeData
          通过av_opt_get()设置选项AV_OPT_SEARCH_CHILDREN来层层搜索AVIOContext及其子对象,由FFMPEG4.1源码解析之 最重要的结构体及其关系 可知AVIOContext 持有 URLContext,URLContext持有URLProtocol以及XXXContext,XXX为协议名,目前只有http协议的HttpContext上下文对象中才会有mime_type成员。在http协议交互的过程中,http协议头 key为"Content-Type"会传输mime_type,如果可能存在多个mime_type时,其value会以";"分隔。分析代码可知:此处,取第一个mime_type。av_opt_get()详细分析见 FFRMPEG4.1源码分析之 av_opt_get()
    3)读取文件数据,进行文件格式探测。
          这个是本函数的主体内容,详细分析见源码注释
         
    3.1)缓冲区大小初始化PROBE_BUF_MIN,每循环一次缓冲区大小乘2,当缓冲区超过max_probe_size或者是找到文件格式了则退出循环。
         3.2)使用avio_read()读取文件字节填充临时缓冲区,然后AVProbeData中的缓冲区复用这个临时缓冲区,该函数是FFMPEG中IO层api函数,详细分析见 FFRMPEG4.1源码分析之 IO层结构体AVIOContext && APIs
         3.2)调用av_probe_input_format2()以及AVProbeData进行文件格式探测,后文将分析该函数
         3.3)重复3.2)3.3)以便找到满足条件的文件格式。
    4)收尾工作
         
    4.1)若3)中未找到合适的输入文件格式,说明无法识别输入文件格式,直接返回错误就行了,一般应用程序运行到此也就结束了
          4.2)若3)中找到了合适的输入文件格式,那么调用ffio_rewind_with_probe_data()将I/O层的AVIOContext内部缓冲区归位,此时内部还保留着文件探测过程中读取的数据。

//1.1.1.2 ffio_rewind_with_probe_data()


1.1.2 av_probe_input_format2()


av_probe_input_format2 声明:

  • 所属库:libavformat(lavf
  • 头文件:libavformat/avformat.h
  • 声明:
    功能:猜测文件格式
    参数pd:需要探测的数据,AVProbeData 结构体对象
    参数is_opened:文件是否已被打开,相当重要的一个参数,决定了过滤掉哪些文件格式!!!!
    参数 score_max:
    传入初始化的文件格式探测得分门限,传出整个探测过程中得到最适文件格式的值。
    
    /**
     * Guess the file format.
     *
     * @param pd        data to be probed
     * @param is_opened Whether the file is already opened; determines whether
     *                  demuxers with or without AVFMT_NOFILE are probed.
     * @param score_max A probe score larger that this is required to accept a
     *                  detection, the variable is set to the actual detection
     *                  score afterwards.
     *                  If the score is <= AVPROBE_SCORE_MAX / 4 it is recommended
     *                  to retry with a larger probe buffer.
     */
    AVInputFormat *av_probe_input_format2(AVProbeData *pd, int is_opened, int *score_max);

av_probe_input_format2 源码:

  • 所属库:libavformat(lavf
  • 源文件:libavformat/format.c
  • 源码:
    AVInputFormat *av_probe_input_format2(AVProbeData *pd, int is_opened, int *score_max)
    {
        int score_ret;
        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()去完成,找到最适合的输入文件格式后,判断这个最适文件格式得分是否比门限值(25)大,如果确实大于门限25,那么这次探测是成功的,返回这个最适文件格式以及得分;若是探测得分不高于(小于等于)门限值25,那么表示本次探测失败,返回空的文件格式。

1.1.2.1 av_probe_input_format3()


av_probe_input_format3 声明:

  • 所属库:libavformat(lavf
  • 头文件:libavformat/avformat.h
  • 声明:
    功能:猜测文件格式
    参数pd:需要探测的数据,AVProbeData 结构体对象
    参数is_opened:文件是否已被打开,相当重要的一个参数,决定了过滤掉哪些文件格式!!!!
    参数 score_ret:
    传出整个探测过程中得到最适文件格式的值。
    /**
     * Guess the file format.
     *
     * @param is_opened Whether the file is already opened; determines whether
     *                  demuxers with or without AVFMT_NOFILE are probed.
     * @param score_ret The score of the best detection.
     */
    AVInputFormat *av_probe_input_format3(AVProbeData *pd, int is_opened, int *score_ret);

av_probe_input_format3 源码:

  • 所属库:libavformat(lavf
  • 源文件:libavformat/format.c
  • 源码:
    AVInputFormat *av_probe_input_format3(AVProbeData *pd, int is_opened,
                                          int *score_ret)
    {
        AVProbeData lpd = *pd;
        const AVInputFormat *fmt1 = NULL;
        AVInputFormat *fmt = NULL;
        int score, score_max = 0;
        void *i = 0;
        const static uint8_t zerobuffer[AVPROBE_PADDING_SIZE];
        enum nodat {
            NO_ID3,
            ID3_ALMOST_GREATER_PROBE,
            ID3_GREATER_PROBE,
            ID3_GREATER_MAX_PROBE,
        } nodat = NO_ID3;
    
        // 检查参数的有效性,必须保证buf的最尾部有32字节的0
        // 当buf为空时,提供一个栈上的32个字节0的数组
        if (!lpd.buf)
            lpd.buf = (unsigned char *) zerobuffer;
    
        // 对文件头部是否存在ID3进行处理,得出当前探测缓冲区长度与ID3长度之间的关系
        // 处理ID3,ID3的头部10个字节长,分析文件前10个字节分析是否存在ID3
        if (lpd.buf_size > 10 && ff_id3v2_match(lpd.buf, ID3v2_DEFAULT_MAGIC)) {
            // 分析id3头获取id3所占长度
            int id3len = ff_id3v2_tag_len(lpd.buf);
    
            // 将id3长度与缓冲区长度比较
            if (lpd.buf_size > id3len + 16) {      // id3长度小于探测缓冲区大小
                if (lpd.buf_size < 2LL*id3len + 16)// id3长度小于探测缓冲区大小,但差不多要大于了
                    nodat = ID3_ALMOST_GREATER_PROBE;
                // 将探测buffer起始位置进行偏移,从id3数据往后进行探测
                lpd.buf      += id3len;
                lpd.buf_size -= id3len;
            } else if (id3len >= PROBE_BUF_MAX) {  // id3长度大于探测缓冲区的最大值了
                nodat = ID3_GREATER_MAX_PROBE;
            } else                                 // id3长度大于探测缓冲区
                nodat = ID3_GREATER_PROBE;
        }
    
        // 文件格式探测
        // 迭代获取一个FFMPEG支持的文件格式AVInputFormat
        while ((fmt1 = av_demuxer_iterate(&i))) {
            // 过滤掉一些格式
            // IO层已打开,那么,不需要读取数据的文件格式都会被过滤掉
            // IO层未打开,那么,需要读取数据的文件格式都会被过滤掉
            if (!is_opened == !(fmt1->flags & AVFMT_NOFILE) && strcmp(fmt1->name, "image2"))
                continue;
    
            // 开始计算得分
            score = 0;
            // 优先使用read_probe()进行打分
            if (fmt1->read_probe) {
                // 打分
                score = fmt1->read_probe(&lpd);
                if (score)
                    av_log(NULL, AV_LOG_TRACE, "Probing %s score:%d size:%d\n", fmt1->name, score, lpd.buf_size);
                // 综合考虑计算得分与文件扩展名匹配得分
                if (fmt1->extensions && av_match_ext(lpd.filename, fmt1->extensions)) {
                    switch (nodat) {
                    case NO_ID3:     // 该情况,取计算得分
                        score = FFMAX(score, 1);
                        break;
                    case ID3_GREATER_PROBE://该情况计算得分一般小于24,需读取更多数据再次进行判断
                    case ID3_ALMOST_GREATER_PROBE: //该情况,取计算得分与24的较大值
                        score = FFMAX(score, AVPROBE_SCORE_EXTENSION / 2 - 1);
                        break;
                    case ID3_GREATER_MAX_PROBE: //该情况取计算得分与50的最大值,读再多数据也无用
                        score = FFMAX(score, AVPROBE_SCORE_EXTENSION);
                        break;
                    }
                }
    
            // 不存在read_probe(),而存在扩展名,则根据扩展名判断
            } else if (fmt1->extensions) {
                if (av_match_ext(lpd.filename, fmt1->extensions))
                    score = AVPROBE_SCORE_EXTENSION;
            }
    
            // 再次根据MIME类型进行判断
            if (av_match_name(lpd.mime_type, fmt1->mime_type)) {
                if (AVPROBE_SCORE_MIME > score) {
                    av_log(NULL, AV_LOG_DEBUG, "Probing %s score:%d increased to %d due to MIME type\n", fmt1->name, score, AVPROBE_SCORE_MIME);
                    score = AVPROBE_SCORE_MIME;
                }
            }
    
            // 保留最高得分以及对应的文件格式
            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;
    }

    先画个重点:文件格式探测有3类数据可以参考:一个是文件扩展名,一个是MIME类型,还一个是读取文件数据进行分析。本函数倾向于读取文件数据,然后通过各个AVInputFormat.read_probe来分析确定是否是该格式,这样找到的文件格式是最可信的,而通过文件扩展名判断文件格式是下下策,因为文件扩展名嘛,谁都能修改,当然,通过MIME类型来判断文件格式比扩展名可信度上升一个台阶,但小于读取数据进行判定。也即可信度排序:
                         读取数据进行判定  >  根据MIME类型判定 > 根据文件扩展名判定
    但是!有个问题是当读取的文件数据不够时,也即探测缓冲区的里面的数据不太多时,根据某个格式的AVInputFormat.read_probe()方法计算得分会比较低,这样,这个得分无法真实判定该文件是否就是该格式,这样,我们一般会期待读取更多的数据到探测缓冲区进一步的判断,FFMPEG中就是这样一个逻辑。

    基于以上的分析,我们来对函数逻辑进行梳理:
    1. 检查参数有效性
     保证AVProbeData.buf的最尾部有AVPROBE_PADDING_SIZE(32字节)的0,当buf为空时,提供一个栈上的32个字节0的数组空间。为什么要这么做?为什么是32字节的0?
    2. 对文件头部是否存在ID3进行处理,得出当前探测缓冲区长度与ID3长度之间的关系:
         要理解这块代码那么就需要了解什么是ID3,此处简单的引用下wikipedia对ID3描述:ID3是一种metadata容器,多应用于MP3格式的音频文件中。它可以将相关的曲名、演唱者、专辑、音轨数等信息存储在MP3文件中,又称作“ID3Tags”。
         可以参考如下几个链接对ID3进行详细的了解:
                            https://zh.wikipedia.org/wiki/ID3
                            https://blog.csdn.net/u014294166/article/details/53153507
                            https://blog.csdn.net/thomasyuan8/article/details/81571362
         函数内部声明了一个枚举变量,表征了ID3长度与当前探测缓冲区长度之间的关系,如下:

        enum nodat {
            NO_ID3,                    //不存在ID3
            ID3_ALMOST_GREATER_PROBE,  //ID3长度小于探测缓冲区长度,但差不多要大于了
            ID3_GREATER_PROBE,         //ID3长度大于探测缓冲区长度
            ID3_GREATER_MAX_PROBE,     //ID3长度大于探测缓冲区长度,并且大于探测缓冲区最大长度
        } nodat = NO_ID3;
    该参数的值会影响到后续格式探测函数AVInputFormat.read_probe得分的有效性,列举如下:
            1)nodat为NO_ID3:参与read_probe()探测的数据是刨除掉ID3数据后真实有效数据,因此,通过该函数计算得分可信程度高。
            2)nodat为ID3_ALMOST_GREATER_PROBE:参与read_probe()探测的数据是刨除掉ID3数据后真实有效数据,但比NO_ID3可信度要低,因为其意思是"ID3长度小于探测缓冲区长度,但差不多要大于了",意味着探测缓冲区内可用于read_probe()探测的有效数据可能不会太长,因此,得分可能不太准确,如果判断得分很低,会期待读取更多的数据再进一步的判断。但也保不准数据已经够了,得分高。
            3)nodat为ID3_GREATER_PROBE:参与read_probe()探测的数据全都是ID3数据,通过该函数计算得分是不可靠的。我们需要也可以读取更多的数据再进一步的判断。
            4)nodat为ID3_GREATER_MAX_PROBE:参与read_probe()探测的数据全都是ID3数据,通过该函数计算得分是不可靠的。 但是由于ID3长度实在太大,已经超过了用于探测的最大缓冲区长度,读再多数据也是无用,因此,该情况还是使用文件扩展名和MIME类型进行文件类型断定吧

     3. 文件格式探测
         
    3.1)av_demuxer_iterate()迭代获取一个AVInputFormat对象
          3.2)过滤掉一些文件格式,如源码上注释所说。这个地方非常重要,因为在前文分析init_input()函数中,在没有打开文件的情况下,就会调用本函数进行文件格式探测,此时,一般常规的文件格式,比如mp4,flv等等都会被过滤掉,从而获取不到对应的文件格式。
          3.3)文件格式探测
                 3.3.1)AVInputFormat.read_probe()存在
                           3.3.1.1)使用其进行文件格式探测,并计算得分。
                           3.3.1.2)判断文件扩展名是否匹配,若匹配,则根据nodat值进行分类处理,综合考虑计算得分与文件扩展名匹配的得分AVPROBE_SCORE_EXTENSION给出最终得分
                 3.3.2)AVInputFormat.read_probe()不存在,则根据文件扩展名。
                 3.3.3)根据MIME是否匹配,来更新得分
                 3.3.4)保留最大得分以及对应的文件格式

       4.  如果nodat==ID3_GREATER_PROBE,那么期待读取更多的有效数据到缓冲区,然后进行read_probe()来计算得分,因此将最终得分更新为24与当前最大得分中的较小值。

1.2 flv_read_header()


flv_read_header源码源码的解释见注释,可见,对于flv格式来说,本函数只做了这么几件很简单的工作
                   1) 对AVFormatContext的媒体起始时间字段赋初值0:s->start_time = 0;
                   2) 对AVFormatContext的私有数据AVFormatContext.priv_data数据(此处为FLVContext)的字段进行了初始化,尤其是flv->missing_streams字段,在读取flv header的基础上进行了是否存在音频流,视频流的断定,该值不为0,表示存在流,为0表示不存在流
                   3)读取flv header之后,还读取了PreviousTagSize0,这个记录了前一个TAG的数据大小,由于这个数据之前没有TAG存在,因此恒定为0,若读到数据不为0,那么肯定不是标准的flv格式,打印下警告信息。

static int flv_read_header(AVFormatContext *s)
{
    int flags;
    // 注意在前文AVFormat_open_input()函数中,s->priv_data已经被创建正确的创建并且初始化
    FLVContext *flv = s->priv_data;   
    int offset;
    int pre_tag_size = 0;

    // 跳过前4个字节,前4个字节分别是"F" "L" "V"和版本号
    avio_skip(s->pb, 4);
    
    // 读取一个字节到flags
    flags = avio_r8(s->pb);
    
    // flags的高5个bit为0,第二个bit为0,第1个bit和第3个bit分别是否存在视频流和音频流,只要有流
    // 那么missing_streams为真
    flv->missing_streams = flags & (FLV_HEADER_FLAG_HASVIDEO | FLV_HEADER_FLAG_HASAUDIO);
    
    // 设置流信息flag标志位为AVFMTCTX_NOHEADER,告知该格式无头信息,其实就是告知流本身的具体信
    // 息需要进一步读取数据包才能获知
    s->ctx_flags |= AVFMTCTX_NOHEADER;

    // 读取4个字节到offset,该正数值表示了整个flv头的长度,一半就是0x00000009,整个头(包含本 
    // offset数据本身)共9个字节,avio_seek()直接跳过整个文件头
    offset = avio_rb32(s->pb);
    avio_seek(s->pb, offset, SEEK_SET);

    // 文件头后的4个字节是记录前一个TAG的长度,由于前面没有TAG,只有flv header,因此该值应该为0
    // 如果不为0,则输出告警日志,告知本flv文件不是标准的flv格式
    /* Annex E. The FLV File Format
     * E.3 TheFLVFileBody
     *     Field               Type    Comment
     *     PreviousTagSize0    UI32    Always 0
     * */
    pre_tag_size = avio_rb32(s->pb);
    if (pre_tag_size) {
        av_log(s, AV_LOG_WARNING, "Read FLV header error, input file is not a standard flv format, first PreviousTagSize0 always is 0\n");
    }

    // 设置其实时间戳为0,从0开始;
    // 设置当前位置总共的flv tag的大小为0,因为当前位置还未读取任一TAG
    // 设置最后关键帧的流index为-1
    s->start_time = 0;
    flv->sum_flv_tag_size = 0;
    flv->last_keyframe_stream_index = -1;

    return 0;
}

1.3 mov_read_header()


mov_read_header源码由于该函数非常复杂,为了不至于本文过长,将拆分到以后的ffmpeg处理mp4文件格式的一篇文章中去详细描述,当前,只指出一些比较重要的点:该函数会去找所有的mp4文件格式的box,主要是moov以及mdat这两个主要的box,会去层层解析moov box,找到文件中包含几个流,调用 avformat_new_stream()方法创建对应的流,并读取流相关信息(比如码率,帧率,宽高,时间基,采样率,采样格式等等),用于流的编解码。

static int mov_read_header(AVFormatContext *s)
{
    MOVContext *mov = s->priv_data;
    AVIOContext *pb = s->pb;
    int j, err;
    MOVAtom atom = { AV_RL32("root") };
    int i;

    if (mov->decryption_key_len != 0 && mov->decryption_key_len != AES_CTR_KEY_SIZE) {
        av_log(s, AV_LOG_ERROR, "Invalid decryption key len %d expected %d\n",
            mov->decryption_key_len, AES_CTR_KEY_SIZE);
        return AVERROR(EINVAL);
    }

    mov->fc = s;
    mov->trak_index = -1;
    /* .mov and .mp4 aren't streamable anyway (only progressive download if moov is before mdat) */
    if (pb->seekable & AVIO_SEEKABLE_NORMAL)
        atom.size = avio_size(pb);
    else
        atom.size = INT64_MAX;

    // 读取moov box
    /* check MOV header */
    do {
        if (mov->moov_retry)
            avio_seek(pb, 0, SEEK_SET);
        if ((err = mov_read_default(mov, pb, atom)) < 0) {
            av_log(s, AV_LOG_ERROR, "error reading header\n");
            mov_read_close(s);
            return err;
        }
    } while ((pb->seekable & AVIO_SEEKABLE_NORMAL) && !mov->found_moov && !mov->moov_retry++);
    if (!mov->found_moov) {
        av_log(s, AV_LOG_ERROR, "moov atom not found\n");
        mov_read_close(s);
        return AVERROR_INVALIDDATA;
    }
    av_log(mov->fc, AV_LOG_TRACE, "on_parse_exit_offset=%"PRId64"\n", avio_tell(pb));

    if (pb->seekable & AVIO_SEEKABLE_NORMAL) {
        if (mov->nb_chapter_tracks > 0 && !mov->ignore_chapters)
            mov_read_chapters(s);
        for (i = 0; i < s->nb_streams; i++)
            if (s->streams[i]->codecpar->codec_tag == AV_RL32("tmcd")) {
                mov_read_timecode_track(s, s->streams[i]);
            } else if (s->streams[i]->codecpar->codec_tag == AV_RL32("rtmd")) {
                mov_read_rtmd_track(s, s->streams[i]);
            }
    }

    /* copy timecode metadata from tmcd tracks to the related video streams */
    for (i = 0; i < s->nb_streams; i++) {
        AVStream *st = s->streams[i];
        MOVStreamContext *sc = st->priv_data;
        if (sc->timecode_track > 0) {
            AVDictionaryEntry *tcr;
            int tmcd_st_id = -1;

            for (j = 0; j < s->nb_streams; j++)
                if (s->streams[j]->id == sc->timecode_track)
                    tmcd_st_id = j;

            if (tmcd_st_id < 0 || tmcd_st_id == i)
                continue;
            tcr = av_dict_get(s->streams[tmcd_st_id]->metadata, "timecode", NULL, 0);
            if (tcr)
                av_dict_set(&st->metadata, "timecode", tcr->value, 0);
        }
    }
    export_orphan_timecode(s);

    for (i = 0; i < s->nb_streams; i++) {
        AVStream *st = s->streams[i];
        MOVStreamContext *sc = st->priv_data;
        fix_timescale(mov, sc);
        if(st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && st->codecpar->codec_id == AV_CODEC_ID_AAC) {
            st->skip_samples = sc->start_pad;
        }
        if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && sc->nb_frames_for_fps > 0 && sc->duration_for_fps > 0)
            av_reduce(&st->avg_frame_rate.num, &st->avg_frame_rate.den,
                      sc->time_scale*(int64_t)sc->nb_frames_for_fps, sc->duration_for_fps, INT_MAX);
        if (st->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
            if (st->codecpar->width <= 0 || st->codecpar->height <= 0) {
                st->codecpar->width  = sc->width;
                st->codecpar->height = sc->height;
            }
            if (st->codecpar->codec_id == AV_CODEC_ID_DVD_SUBTITLE) {
                if ((err = mov_rewrite_dvd_sub_extradata(st)) < 0)
                    return err;
            }
        }
        if (mov->handbrake_version &&
            mov->handbrake_version <= 1000000*0 + 1000*10 + 2 &&  // 0.10.2
            st->codecpar->codec_id == AV_CODEC_ID_MP3
        ) {
            av_log(s, AV_LOG_VERBOSE, "Forcing full parsing for mp3 stream\n");
            st->need_parsing = AVSTREAM_PARSE_FULL;
        }
    }

    if (mov->trex_data) {
        for (i = 0; i < s->nb_streams; i++) {
            AVStream *st = s->streams[i];
            MOVStreamContext *sc = st->priv_data;
            if (st->duration > 0) {
                if (sc->data_size > INT64_MAX / sc->time_scale / 8) {
                    av_log(s, AV_LOG_ERROR, "Overflow during bit rate calculation %"PRId64" * 8 * %d\n",
                           sc->data_size, sc->time_scale);
                    mov_read_close(s);
                    return AVERROR_INVALIDDATA;
                }
                st->codecpar->bit_rate = sc->data_size * 8 * sc->time_scale / st->duration;
            }
        }
    }

    if (mov->use_mfra_for > 0) {
        for (i = 0; i < s->nb_streams; i++) {
            AVStream *st = s->streams[i];
            MOVStreamContext *sc = st->priv_data;
            if (sc->duration_for_fps > 0) {
                if (sc->data_size > INT64_MAX / sc->time_scale / 8) {
                    av_log(s, AV_LOG_ERROR, "Overflow during bit rate calculation %"PRId64" * 8 * %d\n",
                           sc->data_size, sc->time_scale);
                    mov_read_close(s);
                    return AVERROR_INVALIDDATA;
                }
                st->codecpar->bit_rate = sc->data_size * 8 * sc->time_scale /
                    sc->duration_for_fps;
            }
        }
    }

    for (i = 0; i < mov->bitrates_count && i < s->nb_streams; i++) {
        if (mov->bitrates[i]) {
            s->streams[i]->codecpar->bit_rate = mov->bitrates[i];
        }
    }

    ff_rfps_calculate(s);

    for (i = 0; i < s->nb_streams; i++) {
        AVStream *st = s->streams[i];
        MOVStreamContext *sc = st->priv_data;

        switch (st->codecpar->codec_type) {
        case AVMEDIA_TYPE_AUDIO:
            err = ff_replaygain_export(st, s->metadata);
            if (err < 0) {
                mov_read_close(s);
                return err;
            }
            break;
        case AVMEDIA_TYPE_VIDEO:
            if (sc->display_matrix) {
                err = av_stream_add_side_data(st, AV_PKT_DATA_DISPLAYMATRIX, (uint8_t*)sc->display_matrix,
                                              sizeof(int32_t) * 9);
                if (err < 0)
                    return err;

                sc->display_matrix = NULL;
            }
            if (sc->stereo3d) {
                err = av_stream_add_side_data(st, AV_PKT_DATA_STEREO3D,
                                              (uint8_t *)sc->stereo3d,
                                              sizeof(*sc->stereo3d));
                if (err < 0)
                    return err;

                sc->stereo3d = NULL;
            }
            if (sc->spherical) {
                err = av_stream_add_side_data(st, AV_PKT_DATA_SPHERICAL,
                                              (uint8_t *)sc->spherical,
                                              sc->spherical_size);
                if (err < 0)
                    return err;

                sc->spherical = NULL;
            }
            if (sc->mastering) {
                err = av_stream_add_side_data(st, AV_PKT_DATA_MASTERING_DISPLAY_METADATA,
                                              (uint8_t *)sc->mastering,
                                              sizeof(*sc->mastering));
                if (err < 0)
                    return err;

                sc->mastering = NULL;
            }
            if (sc->coll) {
                err = av_stream_add_side_data(st, AV_PKT_DATA_CONTENT_LIGHT_LEVEL,
                                              (uint8_t *)sc->coll,
                                              sc->coll_size);
                if (err < 0)
                    return err;

                sc->coll = NULL;
            }
            break;
        }
    }
    ff_configure_buffers_for_index(s, AV_TIME_BASE);

    for (i = 0; i < mov->frag_index.nb_items; i++)
        if (mov->frag_index.item[i].moof_offset <= mov->fragment.moof_offset)
            mov->frag_index.item[i].headers_read = 1;

    return 0;
}

1.4 update_stream_avctx()


update_stream_avctx源码AVStream.codecpar中保存着流的最新编码信息。在前文文件格式的读取文件头函数xxx_read_header()中,如果能获取流的信息,则会创建流AVStream对象,并且会对AVStream的AVCodecParameters类型的成员AVStream.codecpar进行赋值,但是AVStream.internal.avctx以及为了兼容老版本的AVStream.codec都需要进行赋值,这两个参数都是AVCodecContext的结构体,本函数的作用就是调用avcodec_parameters_to_context()函数将编码器参数从AVStream.codecpar拷贝到AVStream.internal.avctx以及AVStream.codec这两个AVCodecContext对象中。

static int update_stream_avctx(AVFormatContext *s)
{
    int i, ret;
    for (i = 0; i < s->nb_streams; i++) {
        AVStream *st = s->streams[i];

        // 该字段表征是否需要进行参数拷贝
        if (!st->internal->need_context_update)
            continue;

        // 关闭解析器,依赖于编码器
        /* close parser, because it depends on the codec */
        if (st->parser && st->internal->avctx->codec_id != st->codecpar->codec_id) {
            av_parser_close(st->parser);
            st->parser = NULL;
        }

        // 拷贝编解码参数到st->internal->avctx
        /* update internal codec context, for the parser */
        ret = avcodec_parameters_to_context(st->internal->avctx, st->codecpar);
        if (ret < 0)
            return ret;

        // 拷贝编解码参数到st->codec
#if FF_API_LAVF_AVCTX
FF_DISABLE_DEPRECATION_WARNINGS
        /* update deprecated public codec context */
        ret = avcodec_parameters_to_context(st->codec, st->codecpar);
        if (ret < 0)
            return ret;
FF_ENABLE_DEPRECATION_WARNINGS
#endif

        // 参数已更新,设置标志位
        st->internal->need_context_update = 0;
    }
    return 0;
}

1.4.1 avcodec_parameters_to_context()


avcodec_parameters_to_context声明

  • 所属库:libavcodec(lavf
  • 头文件:libavcodec/avcodec.h
  • 声明:根据编码器参数中的值来填充编码器上下文结构体。编解码器上下文中的已分配的,在编码器参数中能找到对应的字段将被释放并替换成编码器参数中的对应值,那些存在于编码器上下文中但是不在编码器参数中的字段将不变。
/**
 * Fill the codec context based on the values from the supplied codec
 * parameters. Any allocated fields in codec that have a corresponding field in
 * par are freed and replaced with duplicates of the corresponding field in par.
 * Fields in codec that do not have a counterpart in par are not touched.
 *
 * @return >= 0 on success, a negative AVERROR code on failure.
 */
int avcodec_parameters_to_context(AVCodecContext *codec,
                                  const AVCodecParameters *par);

avcodec_parameters_to_context源码

int avcodec_parameters_to_context(AVCodecContext *codec,
                                  const AVCodecParameters *par)
{
    // 编码器类型-视频?音频?字幕?
    codec->codec_type = par->codec_type;
    // 编码器id,可以找到唯一的编码器
    codec->codec_id   = par->codec_id;
    // 编码器tag,编码器的另外一种表述
    codec->codec_tag  = par->codec_tag;

    // 平均码率
    codec->bit_rate              = par->bit_rate;
    // 采样点编码后所占位数
    codec->bits_per_coded_sample = par->bits_per_coded_sample;
    // 原始采样点所占位数
    codec->bits_per_raw_sample   = par->bits_per_raw_sample;
    // 编码profile
    codec->profile               = par->profile;
    // 编码level
    codec->level                 = par->level;

    // 针对不同的编码器类型赋值
    switch (par->codec_type) {
    // 视频
    case AVMEDIA_TYPE_VIDEO:
        // 像素格式
        codec->pix_fmt                = par->format;
        // 视频宽度
        codec->width                  = par->width;
        // 视频高度
        codec->height                 = par->height;
        // 场帧顺序-逐行扫描?顶场先编码先展示,顶场先编码后展示,底场先编码先展示,底场先编码后展示
        codec->field_order            = par->field_order;
        // 颜色相关的几个参数,不太懂
        codec->color_range            = par->color_range;
        codec->color_primaries        = par->color_primaries;
        codec->color_trc              = par->color_trc;
        codec->colorspace             = par->color_space;
        codec->chroma_sample_location = par->chroma_location;
        // SAR,样本的宽高比
        codec->sample_aspect_ratio    = par->sample_aspect_ratio;
        // 是否有b帧,对应par中是否有视频延迟
        codec->has_b_frames           = par->video_delay;
        break;

    // 音频
    case AVMEDIA_TYPE_AUDIO:
        // 采样格式
        codec->sample_fmt       = par->format;
        // 通道布局
        codec->channel_layout   = par->channel_layout;
        // 通道数
        codec->channels         = par->channels;
        // 采样率
        codec->sample_rate      = par->sample_rate;
        // 音频包中的对齐块大小
        codec->block_align      = par->block_align;
        // 一帧音频中单个通道采样个数(Number of samples per channel in an audio fram)
        codec->frame_size       = par->frame_size;
        // 编码延迟,为送入若干采样点之后编码器才能产生合法的输出,此时的采样点个数
        codec->delay            =
        // 音频编码会在编码数据前加initial_padding个字节,解码后需要丢弃这么多字节,才能得到真正的音频数据
        codec->initial_padding  = par->initial_padding;
        // 音频编码会在编码数据后加trailing_padding个字节,解码后需要丢弃这么多字节,才能得到真正的音频数据
        codec->trailing_padding = par->trailing_padding;
        // 遇到不连续时需要跳过的样本数(编码时使用)
        codec->seek_preroll     = par->seek_preroll;
        break;

    // 字幕
    case AVMEDIA_TYPE_SUBTITLE:
        // 宽
        codec->width  = par->width;
        // 高
        codec->height = par->height;
        break;
    }

    // 拷贝extradata,对于h.264视频流来说,sps和pps存在此处
    if (par->extradata) {
        av_freep(&codec->extradata);
        codec->extradata = av_mallocz(par->extradata_size + AV_INPUT_BUFFER_PADDING_SIZE);
        if (!codec->extradata)
            return AVERROR(ENOMEM);
        memcpy(codec->extradata, par->extradata, par->extradata_size);
        codec->extradata_size = par->extradata_size;
    }

    return 0;
}

 

你可能感兴趣的:(FFMPEG源码分析,FFMPEG)