FFmpeg开发——AVFormatContext API透视

前言

上上篇文章我们实现了一个小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自身。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354

推荐阅读更多精彩内容