AVFormatContext、AVIOContext与AVStream(一)

一、简介

  • 在音视频文件编解码中有三个重要的阶段,对于解码:先将文件读取到内存中,再解封装,再解码;对于编码过程也是一样的,这里边涉及三个结构体:
    • AVIOContext主要是定义如何读取文件(从本地文件中,还是网络流)
    • AVFormatContext主要是解封装(AVInputFormat)或者封装(AVOutputFormat)协议
    • AVCodecContext则是解码、编码协议,包含了编解码器实体对象
  • 本文对AVFormatContext、AVIOContext、AVStream三个结构体的做一介绍,主要涉及解封装过程,不涉及解码流程,因此暂时不涉及AVCodecContext
  • AVFormatContext 是⼀个全局数据结构,几乎储存了所有的音视频处理所需信息。包括 :
    • AVInputFormat或者AVOutputFormat:⼆者只能同时存在⼀个(解封装、封装)
    • AVIOContext * pb:是的AVIOContext包含在AVFormatContext结构体中,随着后者一起初始化,用于读取待解码的文件到内存中,判断该文件的格式以确定demuxer类型等
    • AVStream:代表该音频文件中的一个轨道,可能是视频、音频、字幕等,其中主要包括:
      • AVCodecParameters *codecpar:储存了本轨道mov遍历box信息后的metedata信息
      • AVCodecContext * codec:是的AVCodecContext包含在AVStream结构体中,代表了本轨道的编解码器上下文,但是他的AVCodec *成员(编解码器实体)需要使用单独的方法查找并分配
      • AVIndexEntry *index_entries:遍历完box信息后,当前轨道每一帧的dts等信息
    • 此外AVFormatContext还包含了本音视频文件的duration、bit_rate、probesize等其他信息

二、确定解封装器

1.ffmpeg解码流程简介

  • 对于一个普通的ffmpeg解码流程,可用下图来概括:

ffmpeg解码流程.jpg

  • 可以看到,除了av_register_all()、avcodec_register_all()两个注册函数外(分别注册解封装器、解码器),avformat_open_input是所有ffmpeg工具使用过程中执行的第一个重要的功能函数

iShot_2023-05-31_01.02.57.png

2.avformat_open_input确定demuxer流程

  • avformat_open_input是所有ffmpeg工具使用过程中执行的第一个重要的函数,他的作用是打开一个输入流(视频流/音频流),并解析头文件。需要注意的是,这一步不涉及codec打开
  • 首先要做的事是,是确定输入流的demuxer类型:

avformat_open_input确定demuxer.jpg

  • 由上图可知,avformat_open_input()函数执行过程主要做了三件事:

    • 调用libavforamt/options.c#avformat_alloc_context()函数,创建了AVFormatContext结构体。 这里就是简单的malloc对象,没什么可说的:
    AVFormatContext *avformat_alloc_context(void){
        AVFormatContext *ic;
        ic = av_malloc(sizeof(AVFormatContext));
        if (!ic) return ic;
        avformat_get_context_defaults(ic);
    
        ic->internal = av_mallocz(sizeof(*ic->internal));
        if (!ic->internal) {
            avformat_free_context(ic);
            return NULL;
        }
        ic->internal->offset = AV_NOPTS_VALUE;
        ic->internal->raw_packet_buffer_remaining_size = RAW_PACKET_BUFFER_SIZE;
        ic->internal->shortest_end = AV_NOPTS_VALUE;
    
        return ic;
    }
    
    
    • 调用libavforamt/utils.c#init_input()函数,根据各种策略找到合适的demuxer解封装。这个函数执行过程涉及AVIOContext和AVInputFormat对象的创建和初始化:
      16858748091953.jpg

      • 第一步是执行av_probe_input_format2()/av_probe_input_format3()函数,根据路径判断需要的demuxer类型。但是由于此时AVInputFormat *av_probe_input_format2(AVProbeData *pd, int is_opened, int *score_max)的第二个参数传的是0,也就是文件并没有打开,导致在av_probe_input_format3()中执行while ((fmtl = av demuxer iteratel&il)遍历已经注册的demuxer时,绝大多数情况会直接continue,因此除了极少数比较特殊的类型外,绝大多数常用的文件/输入流类型在这里都找不到合适的demuxer
      • 第二步是执行AVFormatContext#io_open()函数,该函数主要的工作就是,根据路径名,在已经注册的所有URLProtocol中,找到合适的URLProtocol。以本地文件file.c为例,这个结构体中注册了文件的open、write、read、seek等方法,是后续读取本地文件等操作的实际执行函数。之后会创建URLContext对象,将上面找到的URLProtocol对象赋值给URLContext对象(前者是后者的成员变量)。之后会调用avio_alloc_context()函数创建AVIOContext对象,并将上面初始化好的URLContext对象赋值给AVIOContext对象(前者是后者的成员变量)。
      • 第三步是执行av_probe_input_buffer2()函数,使用上述URLProtocol的.url_read()函数,读取文件流。同时调用av_probe_input_format3()函数,依次遍历已经注册的demuxer/AVInputFormat中的.read_probe()函数,解析读取到的文件流,创建正确的demuxer/AVInputFormat对象
    • 调用AVInputFormat#read_header()函数,读取并解析文件头信息,创建AVStream对象。 对于mov文件,这一步就是遍历解析box信息。我们在下一篇文章中介绍这个过程。

三、AVFormatContext、AVInputFormat与AVIOContext

  • 上述分析avformat_open_input确定demuxer流程中,可以看到AVFormatContext、AVInputFormat与AVIOContext三个结构体都已经悉数出现,并各自承担了不同的职责。本节将我们逐个分析每个结构体的功能及用法

1.AVIOContext解析

  • 先置顶雷神的博客:FFMPEG结构体分析:AVIOContext

  • 根据AVIOContext的构造方法,可以得出比较重要的成员变量、函数:

    AVIOContext *avio_alloc_context(
              unsigned char *buffer,
              int buffer_size,
              int write_flag,
              void *opaque,
              int (*read_packet)(void *opaque, uint8_t *buf, int buf_size),
              int (*write_packet)(void *opaque, uint8_t *buf, int buf_size),
              int64_t (*seek)(void *opaque, int64_t offset, int whence))
    
    {
        AVIOContext *s = av_malloc(sizeof(AVIOContext));
        if (!s)
            return NULL;
        ffio_init_context(s, buffer, buffer_size, write_flag, opaque,
                      read_packet, write_packet, seek);
        return s;
    }
    
    
    • unsigned char *buffer:缓存开始位置
    • int buffer_size:缓存大小(默认32768)
    • void *opaque:不透明指针,是 read_packet / write_packet 的第⼀个参数,用于传递给读写操作的回调函数:
      • 在ffmpeg默认的aviobuf.c源码中串的是URLContext结构体
      • 如果是自定义AVIOContext对象,就传用户自定义的数据,用的时候强转一下就行
      • 甚至也可以直接传null,如果后面用不到的话
    • int (*read_packet)(void *opaque, uint8_t *buf, int buf_size):AVIOContext真正实现读取输入流的地方,这个函数指针可以自己注册。比如我们可以自定义从网络流读取文件,则可以自定义.read_packet()函数,实现网络文件读取的逻辑
    • int (*write_packet)(void *opaque, uint8_t *buf, int buf_size):用法同上
    • int64_t (*seek)(void *opaque, int64_t offset, int whence)):用法同上
  • URLContext与URLProtocol解析

    • URLContext是AVIOContext结构体的成员,而URLProtocol又是URLContext结构体的成员
    • URLProtocol是预先注册好的输入流读写处理结构体,每一个预先注册的类型都要实现该协议中的方法。以本地文件读取为例子,其URLProtocol实现在file.c文件中,其中定义了文件的open、read、write、seek、close等必要功能的实现:
    const URLProtocol ff_file_protocol = {
        .name                = "file",
        .url_open            = file_open,
        .url_read            = file_read,
        .url_write           = file_write,
        .url_seek            = file_seek,
        .url_close           = file_close,
        .url_get_file_handle = file_get_handle,
        .url_check           = file_check,
        .url_delete          = file_delete,
        .url_move            = file_move,
        .priv_data_size      = sizeof(FileContext),
        .priv_data_class     = &file_class,
        .url_open_dir        = file_open_dir,
        .url_read_dir        = file_read_dir,
        .url_close_dir       = file_close_dir,
        .default_whitelist   = "file,crypto"
    };
    
  • URLProtocol查找过程比较简单粗暴,主要是根据文件路径名,遍历已经注册的所有URLProtocol去匹配:

    static const struct URLProtocol *url_find_protocol(const char *filename) {
        const URLProtocol **protocols;
        char proto_str[128], proto_nested[128], *ptr;
        size_t proto_len = strspn(filename, URL_SCHEME_CHARS);
        int i;
        if (filename[proto_len] != ':' &&
            (strncmp(filename, "subfile,", 8) || !strchr(filename + proto_len + 1, ':')) ||
            is_dos_path(filename))
            strcpy(proto_str, "file");
        else
            av_strlcpy(proto_str, filename,
                       FFMIN(proto_len + 1, sizeof(proto_str)));
    
        av_strlcpy(proto_nested, proto_str, sizeof(proto_nested));
        if ((ptr = strchr(proto_nested, '+')))
            *ptr = '\0';
    
        protocols = ffurl_get_protocols(NULL, NULL);
        if (!protocols)
            return NULL;
        for (i = 0; protocols[i]; i++) {
                const URLProtocol *up = protocols[i];
            if (!strcmp(proto_str, up->name)) {
                av_freep(&protocols);
                return up;
            }
            if (up->flags & URL_PROTOCOL_FLAG_NESTED_SCHEME &&
                !strcmp(proto_nested, up->name)) {
                av_freep(&protocols);
                return up;
            }
        }
        av_freep(&protocols);
    
        return NULL;
    }
    
  • AVIOContext中的.read_packet()函数,是输入流的读取入口函数,其中可能会用到URLProtocol中定义的.url_open()、.url_read等函数,主要看.read_packet()的具体实现。如果我们自己重新定义了.read_packet()函数,并在该函数中直接实现了读取功能,那么也不一定非要用到URLProtocol中的读取函数了(参考这篇文章:FFmpeg 自定义 IO 操作之 AVIO 解析

2.AVFormatContext、AVInputFormat解析

  • FFMPEG结构体分析:AVFormatContext

  • 在使用FFMPEG进行开发的时候,AVFormatContext是一个贯穿始终的数据结构,很多函数都要用到它作为参数。它是FFMPEG解封装(flv,mp4,rmvb,avi)功能的结构体:

    • AVIOContext *pb:输入数据的缓存
    • AVInputFormat *iformat:解封装器/demuxer
    • AVOutputFormat *oformat:封装器/muxer
    • unsigned int nb_streams:视音频流的个数
    • AVStream **streams:视音频流
    • char filename[1024]:文件名
    • int64_t duration:时长(单位:微秒us,转换为秒需要除以1000000)
    • int bit_rate:比特率(单位bps,转换为kbps需要除以1000)
    • AVDictionary *metadata:元数据
  • 可以看到AVFormatContext中包含了AVIOContext、AVInputFormat、AVStream三个重要的结构体,其中AVInputFormat就是解封装器,在avformat_open_input()函数中主流程中,第二步init_input()函数的主要目的就是确定AVInputFormat对象的:

    static int init_input(AVFormatContext *s, const char *filename, AVDictionary **options)
    {
        int ret;
        AVProbeData pd = { filename, NULL, 0 };
        int score = AVPROBE_SCORE_RETRY;
        if (s->pb) {
            s->flags |= AVFMT_FLAG_CUSTOM_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(s, AV_LOG_WARNING, "Custom AVIOContext makes no sense and "
                                          "will be ignored with AVFMT_NOFILE format.\n");
            return 0;
        }
    
        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);
    }
    
    • 根据第二小节“avformat_open_input确定demuxer流程”中的分析可知,对于绝大多数文件类型,都是要在av_probe_input_buffer2函数中,遍历已经注册的所有demuxer,挨个调用各个demuxer的.read_probe()函数来判断,当前解封装器是否合适
    • mov的解封装器类mov.c文件中,定义了一系列mov文件在解析过程中的关键函数,如mov_read_header、mov_read_packet、mov_read_seek等,我们将在下一章中详细解读mov文件的解析流程
    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,
    };
    

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYXk59QQ' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片