美文网首页
FFmpeg开发——AVFormatContext API透视

FFmpeg开发——AVFormatContext API透视

作者: 拉丁吴 | 来源:发表于2024-05-03 02:32 被阅读0次

    前言

    上上篇文章我们实现了一个小demo:通过ffmpeg读取视频,然后截取一帧或几帧视频帧,转换色彩空间YUV为RGB,然后保存为图片。展示了ffmpeg开发的基本方法,当然也只简单展示了解封装,解码,色彩转换这些基础能力,这只是ffmpeg功能的一部分。

    接着在上一篇文章中,我们简单分析了MP4文件的文件结构,以便于能够帮助我们更好的理解ffmpeg的一些操作逻辑。

    现在我们需要针对ffmpeg中的一些比较常见的API,分析它的基本实现原理,以便于更好的理解ffmpeg开发过程中的API调用逻辑。

    ffmpeg中申请与释放内存的API

    在ffmpeg中,对于申请内存空间和释放内存空间的操作进行了封装,而不是让开发者直接使用c/c++的原生接口。之所以这么做是为了在不同的操作系统中都能实现内存对齐(至于内存对齐的好处就是比较基础的知识了)。

    这里针对初学者或者对c/c++不算太熟悉的同学做一些解释,在c/cpp这种需要手动管理内存的语言来说,申请内存是需要通过一个函数调用来完成的。但是申请完内存之后,这块内存空间还是一片荒芜(或者可能赋了初始值0),总之不是有效的内容,在完成了内存空间的申请之后,再对这块空间中的各种成员进行赋值完成初始化才算完成。

    比如ffmpeg中的各种结构体,我们首先通过av_malloc对该结构体进行内存空间的申请,然后再对结构体内的成员一一赋值,之后然后才算得到一个有效的可用的结构体对象。

    这一点上和Java这样的高级语言差别很大,Java中内存申请是被隐藏起来的,我们只需要关注对象的创建。

    这些API具体如下:

    // 申请一块大小为size的内存
    void *av_malloc(size_t size)
    
    // 申请一块大小为size的内存,并把所有字节设置为0
    void *av_mallocz(size_t size)
    
    //申请一块nmemb*size大小的内存空间(数组空间)
    void *av_calloc(size_t nmemb, size_t size)
    
    //重新调整ptr指向的内存区域,调整为size大小
    // 根据ptr是否为空,size是否大于0可能是释放,申请,扩张,缩减内存空间
    void *av_realloc(void *ptr, size_t size)
    
    // 释放ptr指向的空间,
    void av_free(void *ptr);
    // 释放ptr指向的空间并把ptr指向为NUL (recommend)
    void av_freep(void *ptr);
    

    我们以其中基础的av_malloc为例看一下它的实现

    
    #define ALIGN (HAVE_AVX512 ? 64 : (HAVE_AVX ? 32 : 16))
    
    // 申请一个字节数为size的内存空间,并返回指向该内存空间的指针给调用者 
    void *av_malloc(size_t size)
    {
        void *ptr = NULL;
        // 申请的size不能大于最大值max_alloc_size,(原子操作)
        if (size > atomic_load_explicit(&max_alloc_size, memory_order_relaxed))
            return NULL;
    
    #if HAVE_POSIX_MEMALIGN
        // ALIGN = 16 表示按照16字节 进行内存对齐
        if (size) //OS X on SDK 10.6 has a broken posix_memalign implementation
        if (posix_memalign(&ptr, ALIGN, size)) // 按照16字节对齐来申请size大小的空间,假如size=24,最终申请到的内存大小应该是32
            ptr = NULL;
    ...
    ...
    #endif
        if(!ptr && !size) { // 假如ptr为空并且size=0,做一下兜底逻辑
            size = 1;
            ptr= av_malloc(1);
        }
    ...
    ...
        return ptr;
    }
    
    

    然后我们再看看内存释放av_free和av_freep

    void av_free(void *ptr)
    {
    #if HAVE_ALIGNED_MALLOC // windows下条件为真,linux下为假
        _aligned_free(ptr);
    #else
        free(ptr); // 直接调用cpp的释放内存的接口
    #endif
    }
    
    //@param arg Pointer to the pointer to the memory block which should be freed
    void av_freep(void *arg) // 根据API文档,arg应该是一个二级指针,指向一个准备释放内存块的指针
    {
        void *val;
        memcpy(&val, arg, sizeof(val));// 把等待释放的内存的地址保存到val中
        memcpy(arg, &(void *){ NULL }, sizeof(val)); // 把arg指向的指针设置为nullptr,
        av_free(val); // 释放val指针
    }
    

    av_freep的实现理解起来确实有点费脑子,大家可以就简单理解为av_freep的功能就是释放指针指向的内存,并且把指针置空,不过传参需要按照二级指针来传。

    了解了这些基础的内容之后,我们再看ffmpeg中主要的几个结构体的内存申请和释放。

    AVFormatContext

    avformat_alloc_context (申请内存)

    
    AVFormatContext *avformat_alloc_context(void)
    {
        // FFFormatContext是算是ForamtContext内部实现类
        // 申请FFFormatContext结构体的内存空间
        // 它内部有一个成员AVFormatContext pub;也会同样开辟内存空间
        FFFormatContext *const si = av_mallocz(sizeof(*si));
        AVFormatContext *s;
        if (!si)
            return NULL;
        s = &si->pub; // 直接把FFFormatContext->pub这个AVFormatContext结构体对象返回
        s->av_class = &av_format_context_class;
        s->io_open  = io_open_default;
    
        s->io_close2= io_close2_default;
        av_opt_set_defaults(s); // 设置一些默认值
        si->pkt = av_packet_alloc(); 
        si->parse_pkt = av_packet_alloc(); 
        if (!si->pkt || !si->parse_pkt) {
            avformat_free_context(s);
            return NULL;
        }
        ...
        return s;
    
    }
    

    主要使用了av_mallocz函数手动申请AVFormatContext结构体的内存空间,此时它内部的成员变量都会获得空间的分配(指针类型只会获得指针所占用的空间:4byte或者8byte)。

    至于AVFormatContext中的重要成员的真正被创建和可用,则是在avformat_open_input函数中。

    avformat_open_input

    avformat_open_input函数的主要功能:完成AVFormatContext的初始化(关键成员的创建和初始化),对文件解复用(解封装),读取有效信息。

    static av_always_inline FFFormatContext *ffformatcontext(AVFormatContext *s)
    {
        return (FFFormatContext*)s;
    }
    
    int avformat_open_input(AVFormatContext **ps, const char *filename,
                            const AVInputFormat *fmt, AVDictionary **options)
    {
        AVFormatContext *s = *ps;
        FFFormatContext *si;
    
        int ret = 0;
        // 如果AVFormatContext结构体是空,则创建它
        if (!s && !(s = avformat_alloc_context()))
            return AVERROR(ENOMEM);
        // 直接把AVFormatContext转换成FFFormatContext 是有点夸张,
        // 应该是因为AVFormatContext是FFFormatContext结构体的第一个成员,
        // FFFormatContext又和我们持有同一个AVFormatContext,
        // 所以此时AVFormatContext的地址就是FFFormatContext的地址
        si = ffformatcontext(s);
        
        ...
        // 重点: 初始化input,创建并初始化AVIOContext AVInputFormat等结构体
        if ((ret = init_input(s, filename, &tmp)) < 0)
            goto fail;
    
        ...
        ...
        // 读取文件头数据
       if (s->iformat->read_header)
            if ((ret = s->iformat->read_header(s)) < 0) {
                if (s->iformat->flags_internal & FF_FMT_INIT_CLEANUP)
                    goto close;
                goto fail;
            }
    
    }
    

    init_input函数不仅创建了一些关键的结构体比如AVInputFormat,还通过AVInputFormat读取了文件头的数据。

    AVInputFormat的创建

    这其中相对最重要的是AVInputFormat结构体,它是对不同的媒体文件进行解复用的关键结构体,我们跟进看看AVInputFormat是如何被初始化的。

    // init_input ==> av_probe_input_buffer2 ==> av_probe_input_format3
    static int init_input(AVFormatContext *s, const char *filename,
                          AVDictionary **options)
    {
        int ret;
        AVProbeData pd = { filename, NULL, 0 };
        int score = AVPROBE_SCORE_RETRY;
        ...
      
        return av_probe_input_buffer2(s->pb, &s->iformat, filename,
                                      s, 0, s->format_probesize);
    }
    
    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;
        int eof = 0;
    
    
        for (probe_size = PROBE_BUF_MIN; probe_size <= max_probe_size && !*fmt && !eof;
             probe_size = FFMIN(probe_size << 1,
                                FFMAX(max_probe_size, probe_size + 1))) {
            
            // 读取一些探针数据,用来确定文件的真实格式
            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) {
                ...
            }
            ...
    
            // 获取AVInputFormat
            *fmt = av_probe_input_format2(&pd, 1, &score);
    
    
        av_freep(&pd.mime_type);
        return ret < 0 ? ret : score;
    }
    
    
    
    
    
    const AVInputFormat *av_probe_input_format2(const AVProbeData *pd,
                                                int is_opened, int *score_max)
    {
        int score_ret;
        const AVInputFormat *fmt = av_probe_input_format3(pd, is_opened, &score_ret);
        ...
    }
    
    
    
    const AVInputFormat *av_probe_input_format3(const AVProbeData *pd,
                                                int is_opened, int *score_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];
        enum nodat {
            NO_ID3,
            ID3_ALMOST_GREATER_PROBE,
            ID3_GREATER_PROBE,
            ID3_GREATER_MAX_PROBE,
        } nodat = NO_ID3;
        ...
        // 1,迭代循环获取demuxer
        while ((fmt1 = av_demuxer_iterate(&i))) {
            ...
            score = 0;
            // read_probe是文件读取探针函数,作用是读取文件部分数据进行测试匹配程度
            if (fmt1->read_probe) {
                // 2,对文件进行部分读取,返回一个匹配的分数值
                score = fmt1->read_probe(&lpd);
                ...
            }
            ...
            // 保留匹配最大的得分的AVInputFormat
            if (score > score_max) {
                score_max = score;
                fmt       = fmt1;
            } else if (score == score_max)
                fmt = NULL;
        }
        ...
        return fmt;
    }
    
    
    

    我们看一下av_demuxer_iterate是一个怎样的遍历函数:

    // allformats.c
    const AVInputFormat *av_demuxer_iterate(void **opaque)
    {
    // demuxer_list就是一列demuxer的列表
        static const uintptr_t size = sizeof(demuxer_list)/sizeof(demuxer_list[0]) - 1;
        uintptr_t i = (uintptr_t)*opaque;
        const AVInputFormat *f = NULL;
        uintptr_t tmp;
    
        if (i < size) { // 判断下标没有越界
            f = demuxer_list[i]; // 获取一个demuxer
        }
        ...
    
        return f;
    }
    
    
    // muxer_list.c
    static const AVInputFormat * const demuxer_list[] = {
        &ff_aa_demuxer,
        &ff_aac_demuxer,
        &ff_aax_demuxer,
        &ff_ac3_demuxer,
        ...
        ...
        &ff_mov_demuxer,
        &ff_mp3_demuxer,
        &ff_mpc_demuxer,
        ...
        ...
    }
    

    我们先来看AVInputFormat的结构体的定义:

    typedef struct AVInputFormat {
    
        const char *name;
    
        const char *long_name;
        ...
    
        int (*read_header)(struct AVFormatContext *);
    
        int (*read_packet)(struct AVFormatContext *, AVPacket *pkt);
    
        int (*read_close)(struct AVFormatContext *);
        ...
    
    } AVInputFormat;
    

    它内部除了一些成员属性,还定义了一些关键函数,用来读取媒体文件的函数接口。我们知道不同的封装格式的文件,它的文件头,文件体的数据格式都是不同的,那么在解复用(解封装)的时候就需要根据不同的封装格式来适配不同的读取方法,所以demuxer_list中就是定义了不同文件类型的对应的demuxer(AVInputFormat的实现对象)。

    我们的测试MP4视频文件最终找到的是ff_mov_demuxer这个结构体,我们就以ff_mov_demuxer为例看看它的实现。

    // mov.c
    
    // 创建了一个AVInputFormat结构体的对象
    const 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,psp,m4b,ism,ismv,isma,f4v,avif",
        .flags_internal = FF_FMT_INIT_CLEANUP,
        .read_probe     = mov_probe, // 探针函数
        .read_header    = mov_read_header, // 读取文件头
        .read_packet    = mov_read_packet, // 读取数据
        .read_close     = mov_read_close, // 关闭
        .read_seek      = mov_read_seek, // seek
        .flags          = AVFMT_NO_BYTE_SEEK | AVFMT_SEEK_TO_PTS | AVFMT_SHOW_IDS,
    };
    
    

    我们看到ff_mov_demuxer是根据QuickTime协议定义的一个解复用器,我们在ffmpeg开发——深入理解MP4文件格式文章中简略的提到过QuickTime,它可以正常的解码MP4文件的,某种程度上是MP4协议格式借鉴了QuickTime协议的文件构造,因此QT可以兼容MP4。

    读取探针数据

    在获取匹配的demuxer过程中,都需要读取文件的部分数据来做一个对比,接下来我们看ff_mov_demuxer是如何读取quicktime协议定义的文件头数据。

    static int mov_probe(const AVProbeData *p)
    {
        int64_t offset;
        uint32_t tag;
        int score = 0;
        int moov_offset = -1;
    
        /* check file header */
        offset = 0;
        for (;;) {
            int64_t size;
            int minsize = 8;
            ...
            // 读取一个tag?
            tag = AV_RL32(p->buf + offset + 4);
            switch(tag) { 判断tag是否是MP4文件结构中的box类型
            /* check for obvious tags */
            case MKTAG('m','o','o','v'): //一般moov box不会直接放在开头,至少跟在ftyp box之后
                moov_offset = offset + 4;
            case MKTAG('m','d','a','t'):
            case MKTAG('p','n','o','t'): /* detect movs with preview pics like ew.mov and april.mov */
            case MKTAG('u','d','t','a'): /* Packet Video PVAuthor adds this and a lot of more junk */
            case MKTAG('f','t','y','p'): // ftyp box属于MP4的文件头的box
                if (tag == MKTAG('f','t','y','p') &&
                           (   AV_RL32(p->buf + offset + 8) == MKTAG('j','p','2',' ')
                            || AV_RL32(p->buf + offset + 8) == MKTAG('j','p','x',' ')
                            || AV_RL32(p->buf + offset + 8) == MKTAG('j','x','l',' ')
                        )) { // 某些特殊类型的图片类型,最多5分
                    score = FFMAX(score, 5);
                } else { // 否则可以得到最高分 100分
                    score = AVPROBE_SCORE_MAX;
                }
                break;
            ...
            }
            if (size > INT64_MAX - offset)
                break;
            offset += size;
        }
        ...
        ...
        return score;
    }
    

    根据我输入的MP4测试视频,这个ff_mov_demuxer能够理解MP4文件结构,正确的解析出MP4文件的开头部分ftyp box的数据,从而得到一个高分。(关于MP4文件结构可以看ffmpeg开发——深入理解MP4文件格式)。

    读取文件头

    获取到AVInputFormat之后,会调用iformat->read_header(s)函数,读取文件头数据,获取一些有效信息,比如duration,time_scale,width,height,bit_rate等。

    那么ff_mov_demuxer是如何读取MP4文件的文件头的呢?当然我们看看ff_mov_demuxer中的read_header的实现:

    
    static int mov_read_header(AVFormatContext *s)
    {
        MOVContext *mov = s->priv_data;
        AVIOContext *pb = s->pb;
        int j, err;
        // 定义一个Atom,在MP4协议中叫box
        MOVAtom atom = { AV_RL32("root") };
        int i;
        ...
        mov->fc = s; // 持有AVFormatContext
        if (pb->seekable & AVIO_SEEKABLE_NORMAL)
            atom.size = avio_size(pb); //64kb大小
        else
            atom.size = INT64_MAX;
    
        
        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");
                return err;
            }
        } while ((pb->seekable & AVIO_SEEKABLE_NORMAL) && !mov->found_moov && !mov->moov_retry++);
        
        // 到这里AVFormatContext中的对应的结构(AVStream)差不多有了有效的信息
        ...
        ...
        return 0;
    }
    
    

    读取文件头的逻辑主要在mov_read_default函数中,接着看mov_read_default函数的实现

    static int mov_read_default(MOVContext *c, AVIOContext *pb, MOVAtom atom)
    {
        int64_t total_size = 0;
        MOVAtom a;
        int i;
        ...
        ...
        if (atom.size < 0)
            atom.size = INT64_MAX;
        while (total_size <= atom.size - 8) { // 大概要读取64kb的数据(为什么-8?)
            int (*parse)(MOVContext*, AVIOContext*, MOVAtom) = NULL;
            //根据之前的关于MP4文件结构分析,box结构的开始是box header ,头两个字段是size和type,各占4个字节
            a.size = avio_rb32(pb); // 读取4个byte 获取该box size
            a.type = avio_rl32(pb); // 读取4个byte  获取该box type
            ...
            ...
            total_size += 8;
            // 也是之前文章提到的,假如box header中size=1,表示要用拓展字段largesize,占8个字节
            if (a.size == 1 && total_size + 8 <= atom.size) { /* 64 bit extended size */
                a.size = avio_rb64(pb) - 8;
                total_size += 8;
            }
    
            if (a.size == 0) {
                a.size = atom.size - total_size + 8;
            }
            if (a.size < 0)
                break;
            a.size -= 8; // 应该是减去box header的两个字段(或者加上拓展字段)占用的空间?剩余只有box data
            if (a.size < 0)
                break;
            a.size = FFMIN(a.size, atom.size - total_size);
            // 遍历quicktime的MP4文件解析器列表
            for (i = 0; mov_default_parse_table[i].type; i++)
                // 找到type匹配的解析器
                if (mov_default_parse_table[i].type == a.type) {
                    parse = mov_default_parse_table[i].parse;
                    break;
                }
    
            // prase为空,且type是udta box(用户信息的box) 或者 ilst box的话,
            // 可以指定mov_read_udta_string解析方法
            if (!parse && (atom.type == MKTAG('u','d','t','a') ||
                           atom.type == MKTAG('i','l','s','t')))
                parse = mov_read_udta_string;
    
    
    
            if (!parse) { /* skip leaf atoms data */
                avio_skip(pb, a.size);
            } else {
                int64_t start_pos = avio_tell(pb);
                int64_t left;
                int err = parse(c, pb, a); // 调用对应type的解析函数
                ...
                ...
            }
    
            total_size += a.size;
        }
        ...
        c->atom_depth --;
        return 0;
    }
    
    // quicktime解析MP4文件的解析器列表{type,prase}
    static const MOVParseTableEntry mov_default_parse_table[] = {
        ...
        { MKTAG('m','d','h','d'), mov_read_mdhd }, // mdhd box
        ...
        { MKTAG('s','t','b','l'), mov_read_default },
        { MKTAG('s','t','c','o'), mov_read_stco },
        { MKTAG('s','t','p','s'), mov_read_stps },
        { MKTAG('s','t','r','f'), mov_read_strf },
        { MKTAG('s','t','s','c'), mov_read_stsc },
        { MKTAG('s','t','s','d'), mov_read_stsd }, /* sample description */
        { MKTAG('s','t','s','s'), mov_read_stss }, /* sync sample */
        { MKTAG('s','t','s','z'), mov_read_stsz }, /* sample size */
        { MKTAG('s','t','t','s'), mov_read_stts },
        { MKTAG('s','t','z','2'), mov_read_stsz }, /* compact sample size */
        { MKTAG('s','d','t','p'), mov_read_sdtp }, /* independent and disposable samples */
        { MKTAG('t','k','h','d'), mov_read_tkhd }, /* track header */
        { MKTAG('t','f','d','t'), mov_read_tfdt },
        { MKTAG('t','f','h','d'), mov_read_tfhd }, /* track fragment header */
        { MKTAG('t','r','a','k'), mov_read_trak },
        ...
    }
    

    接下来,我们选择一个box对应的解析函数来看看解析逻辑是怎样的,就选择mdhd box为例,在上一篇文章ffmpeg开发——深入理解MP4文件格式中,我们对mdhd box有一定的了解,它的结构如下

    image.png

    这里还要说一下,FullBox这个拓展类型的Box,在Box Header中还拓展了两个字段version,flags,分别占用1个字节和3个字节(具体也可从上一篇文章全面了解)。

    image.png
    
    static int mov_read_mdhd(MOVContext *c, AVIOContext *pb, MOVAtom atom)
    {
        AVStream *st;
        MOVStreamContext *sc;
        int version;
        char language[4] = {0};
        unsigned lang;
    
        ..
        // AVFormatContext->streams
        st = c->fc->streams[c->fc->nb_streams-1];
        sc = st->priv_data;
        ...
        // 读取version字段
        version = avio_r8(pb);
        if (version > 1) {
            avpriv_request_sample(c->fc, "Version %d", version);
            return AVERROR_PATCHWELCOME;
        }
        // flags,应该暂时无用
        avio_rb24(pb); /* flags */
        // 读取create_time和modification time
        mov_metadata_creation_time(c, pb, &st->metadata, version);
        // 读取time_scale字段,不管version的取值,timescale都只占32个位
        sc->time_scale = avio_rb32(pb);
        if (sc->time_scale <= 0) {
            av_log(c->fc, AV_LOG_ERROR, "Invalid mdhd time scale %d, defaulting to 1\n", sc->time_scale);
            sc->time_scale = 1;
        }
       // 根据version的取值,决定读取64位还是32位来获取该轨道的数据的时长
        st->duration = (version == 1) ? avio_rb64(pb) : avio_rb32(pb); /* duration */
    
        if ((version == 1 && st->duration == UINT64_MAX) ||
            (version != 1 && st->duration == UINT32_MAX)) {
            st->duration = 0;
        }
        // 其他字段
        lang = avio_rb16(pb); /* language */
        if (ff_mov_lang_to_iso639(lang, language))
            av_dict_set(&st->metadata, "language", language, 0);
        avio_rb16(pb); /* quality */
    
        return 0;
    }
    

    基本上是按照MP4标准文档定义的结构来读取数据,不仅mdhd box是这样,对其他类型的box的读取方式也是一样的。

    avformat_close_input

    void avformat_close_input(AVFormatContext **ps)
    {
        AVFormatContext *s;
        AVIOContext *pb;
        ...
    
        s  = *ps;
        pb = s->pb;
     
        if ((s->iformat && strcmp(s->iformat->name, "image2") && s->iformat->flags & AVFMT_NOFILE) ||
            (s->flags & AVFMT_FLAG_CUSTOM_IO))
            pb = NULL;
    
        if (s->iformat)
            if (s->iformat->read_close)
                s->iformat->read_close(s);  // 关闭输入流
    
        avformat_free_context(s); // 释放AVFormatContext结构体
    
        *ps = NULL; // AVFormatContext指针设置为NULL
    
        avio_close(pb); // 关闭输入缓冲等
    }
    
    

    关闭输入操作主要是释放输入相关的资源,然后释放AVFormatContext结构体。

    avformat_free_context

    avformat_free_context 是释放AVFormatContext结构体的函数,avformat_close_input函数内也会调用这个函数。

    void avformat_free_context(AVFormatContext *s)
    {
        FFFormatContext *si;
    
        if (!s)
            return;
        si = ffformatcontext(s);
    
        if (s->oformat && ffofmt(s->oformat)->deinit && si->initialized)
            ffofmt(s->oformat)->deinit(s);
    
        av_opt_free(s);
        if (s->iformat && s->iformat->priv_class && s->priv_data)
            av_opt_free(s->priv_data);
        if (s->oformat && s->oformat->priv_class && s->priv_data)
            av_opt_free(s->priv_data);
    
        for (unsigned i = 0; i < s->nb_streams; i++)
            ff_free_stream(&s->streams[i]);
        s->nb_streams = 0;
    
        for (unsigned i = 0; i < s->nb_programs; i++) {
            av_dict_free(&s->programs[i]->metadata);
            av_freep(&s->programs[i]->stream_index);
            av_freep(&s->programs[i]);
        }
        s->nb_programs = 0;
    
        av_freep(&s->programs);
        av_freep(&s->priv_data);
        while (s->nb_chapters--) {
            av_dict_free(&s->chapters[s->nb_chapters]->metadata);
            av_freep(&s->chapters[s->nb_chapters]);
        }
        av_freep(&s->chapters);
        av_dict_free(&s->metadata);
        av_dict_free(&si->id3v2_meta);
        av_packet_free(&si->pkt);
        av_packet_free(&si->parse_pkt);
        av_freep(&s->streams);
        ff_flush_packet_queue(s);
        av_freep(&s->url);
        av_free(s);
    }
    

    就是逐个释放AVForamtContext中各种成员所占用的内存空间,然后释放AVForamtContext自身。

    相关文章

      网友评论

          本文标题:FFmpeg开发——AVFormatContext API透视

          本文链接:https://www.haomeiwen.com/subject/sfhcfjtx.html