ffmpeg的avformat_open_input()分析过程--以mp4为例(十)

概要 

avformat_open_input(),该函数用于打开多媒体数据并且获取一些信息,它的声明位于libavformat/avformat.h。主要工作

1)通过init_input打开流媒体数据,根据probe探测流媒体最合适的协议类型AVInputFormat,通过open2设置read/write/seek相关回调

2)read_header即根据对应的协议,读取媒体头信息并创建AVStream,并对流媒体进行解析(会对mp4文件进行解析,本文以MP4为例)

框架图

ffmpeg的avformat_open_input()分析过程--以mp4为例(十)_第1张图片

 函数和文件对应关系

ffmpeg的avformat_open_input()分析过程--以mp4为例(十)_第2张图片

代码分析

avformat_open_input(http.xxx.m3u8)
    init_input(s, filename, &tmp))
        1.是否设置了pb,如果设置了就直接av_probe_input_buffer2.一般情况下pb均为空,有特殊情况,下文有做说明
        2.是否设置了iformat,如果设置了直接返回;
          如果没指定iformat,但是可以从文件名中猜出iformat,也成功.  
        3.hls一般会走到这里io_open,如果从文件名中也猜不出媒体格式,则只能打开这个文件进行探测了,先打开文件  
        io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options)) < 0)
            io_open_default
                ffio_open_whitelist
                    ffurl_alloc
                        //探测是HTTP协议 URLProtocol ff_http_protocol
                        url_find_protocol(filename);  
                    ffurl_connect    //发送HTTP报文头,下载http.xxx.m3u8文件
        //读m3u8文件探测解复用是AVInputFormat *iformat ="hls,applehttp"
        *fmt = av_probe_input_buffer2(s->pb, &s->iformat, filename, s, 0, s->format_probesize);
        s->iformat->read_header(s);  //iformat iformat  hls.c-->hls_read_header
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;

    if (!s && !(s = avformat_alloc_context())) //如果s没有初始化,先初始化s
        return AVERROR(ENOMEM);
    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);

    if (s->pb) // must be before any goto fail
        s->flags |= AVFMT_FLAG_CUSTOM_IO;

    if ((ret = av_opt_set_dict(s, &tmp)) < 0)
        goto fail;

    if (!(s->url = av_strdup(filename ? filename : ""))) {
        ret = AVERROR(ENOMEM);
        goto fail;
    }

#if FF_API_FORMAT_FILENAME
FF_DISABLE_DEPRECATION_WARNINGS
    av_strlcpy(s->filename, filename ? filename : "", sizeof(s->filename));
FF_ENABLE_DEPRECATION_WARNINGS
#endif
	//	1. 重点在看这里:
    if ((ret = init_input(s, filename, &tmp)) < 0)
        goto fail;
    s->probe_score = ret;

    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;
        }
    }

    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;
    }

    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;
        }
    }

    s->duration = s->start_time = AV_NOPTS_VALUE;

    /* Allocate private data. */
    if (s->iformat->priv_data_size > 0) {
        if (!(s->priv_data = av_mallocz(s->iformat->priv_data_size))) {
            ret = AVERROR(ENOMEM);
            goto fail;
        }
        if (s->iformat->priv_class) {
            *(const AVClass **) s->priv_data = s->iformat->priv_class;
            av_opt_set_defaults(s->priv_data);
            if ((ret = av_opt_set_dict(s->priv_data, &tmp)) < 0)
                goto fail;
        }
    }

    /* 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);


    if (!(s->flags&AVFMT_FLAG_PRIV_OPT) && s->iformat->read_header)
    //重点2
        if ((ret = s->iformat->read_header(s)) < 0)
            goto fail;

    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;
    }

    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;

    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;
}

avformat_open_input主要执行了三个步骤:

1)avformat_alloc_context---上一篇文章已经有单独介绍

2)init_input

a)提供的文件名信息不能探测格式

b)探测是HTTP协议 URLProtocol ff_http_protocol

c)读m3u8文件探测解复用是AVInputFormat *iformat ="hls,applehttp"

d)s->iformat->read_header2(s, &tmp2) iformat iformat  hls.c-->hls_read_header

1、init_input:

int init_input(AVFormatContext *s, const char *filename,
                      AVDictionary **options)
{
    AVProbeData pd = { filename, NULL, 0 };
    int score = AVPROBE_SCORE_RETRY;
    //1、如果自定义了AVIOContext 则使用自定义的
    if (s->pb) {
        s->flags |= AVFMT_FLAG_CUSTOM_IO;
        //1.2 如果没有定义AVInputFormat 则由ffmpeg去推测
        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;
    }
    //2、如果指定了AVInputFormat 则直接返回
    //或者如果没有指定AVInputFormat 就av_probe_input_format2(扩展名/文件类型字符串)去推测
    if ((s->iformat && s->iformat->flags & AVFMT_NOFILE) ||
        (!s->iformat && (s->iformat = av_probe_input_format2(&pd, 0, &score))))
        return score;
    //3、如果通过文件名或类型没有匹配到 就打开文件 然后读取内容再推测
    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);
}

init_input 用于打开媒体数据 并根据匹配规则 找到最合适的协议类型(AVInputFormat)

分三个步骤进行:

1)如果自定义了AVIOContext 则使用自定义的pb,比如

        unsigned char *aviobuffer=(unsigned char *)av_malloc(VIDEO_AVIO_BUFFER_SIZE);
        avio =avio_alloc_context(aviobuffer, VIDEO_AVIO_BUFFER_SIZE,0,NULL,&ff_read_buffer_func_cb,NULL,NULL);
        ic->pb = avio;
        ic->flags |= AVFMT_FLAG_CUSTOM_IO;

这个就是自定义pb = avio

如果指定了AVInputFormat 则直接返回,或者如果没有指定AVInputFormat 就av_probe_input_format2(扩展名/文件类型字符串)去推测

if (!s->iformat)
            return av_probe_input_buffer2(s->pb, &s->iformat, filename,
                                         s, 0, s->format_probesize);

2)如果指定了AVInputFormat 则直接返回,或者如果没有指定AVInputFormat 就av_probe_input_format2(扩展名/文件类型字符串)去推测

3)如果通过文件名或类型没有匹配到 就打开文件 然后读取内容再推测

s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options))

-------------------------------------------------------------------------------------------------------------------------------

问题
    avformat_open_input函数探测ES流开销是150毫秒,探测PS流开销是500毫秒。avformat_open_input函数里面已经实现了av_probe_input_buffer函数的调用,去探测AVInputFormat结构体的相关变量。所以在avformat_open_input函数之前,调用av_probe_input_buffer函数之后,就不会去探测AVInputFormat结构体

优化方向
    尝试屏蔽avformat_open_input函数,直接指定码流的输入格式pInputFormat,代码如下:
        pInputFormat = av_find_input_format("h264");
        pFormatCtx->iformat = pInputFormat;
如果这个时候屏蔽掉avformat_open_input,图像是条带状的,按照参考文献的说法,该avformat_open_input
函数就是为了探测AVInputFormat结构体的相关变量

最终优化方案
    没有屏蔽avformat_open_input函数,而是在调用之前指定AVInputFormat的码流输入格式
代码
    AVInputFormat* pInputFormat = NULL;
    AVFormatContext* pFormatContext = NULL;
    pFormatContext = avformat_alloc_context();
    pInputFormat = av_find_input_format("h264");

从这里可以看到,我们可以通过前端设置“iformat”属性来确定流的输入格式,以减小流的探测时间

--------------------------------------------------------------------------------------------------------------------------------

AVInputFormat *av_probe_input_format3(ff_const59 AVProbeData *pd
, int is_opened, int *score_ret) {
    AVProbeData lpd = *pd;
    const AVInputFormat *fmt1 = NULL;
    ff_const59 AVInputFormat *fmt = NULL;
    int score, score_max = 0;

    //遍历所有的复用解复用器
    //旧版本demuxer_list位于demuxer_list.c
//新版本在allformats.c
    while ((fmt1 = av_demuxer_iterate(&i))) {//ftm1=新版本av_format_next(ftm1)
        score = 0;
        //如果有探测函数 则调用
        if (fmt1->read_probe) {
            score = fmt1->read_probe(&lpd);
        //否则匹配扩展名(.mp4/.flv)
        } else if (fmt1->extensions) {
            if (av_match_ext(lpd.filename, fmt1->extensions))
                score = AVPROBE_SCORE_EXTENSION;//50
        }
        //匹配媒体类型(video/x-flv video/mp4)
        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;//75
            }
        }
        //记录得分最高的
        if (score > score_max) {
            score_max = score;
            fmt       = (AVInputFormat*)fmt1;
        } else if (score == score_max)
            fmt = NULL;
    }
    *score_ret = score_max;

    return fmt;
}

av_iformat_next(fmt1),fmt1->name的值为“flac”,“flv”,“hevc” ,“hls”等,这部分是否可以优化?

以mp4为例,read_probe为调用moov.c的mov_probe,mov_probe,其实就是判断第一个box是不是ftyp,若是则就是mov格式的了

mov_probe(AVProbeData *p)
{
    int64_t offset;
    uint32_t tag;
    int score = 0;
    int moov_offset = -1;

    /* check file header */
    offset = 0;
    for (;;) {
        /* ignore invalid offset */
        if ((offset + 8) > (unsigned int)p->buf_size)
            break;
        tag = AV_RL32(p->buf + offset + 4);
        switch(tag) {
        /* check for obvious tags */
        case MKTAG('m','o','o','v'):
            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'):å
            if (AV_RB32(p->buf+offset) < 8 &&
                (AV_RB32(p->buf+offset) != 1 ||
                 offset + 12 > (unsigned int)p->buf_size ||
                 AV_RB64(p->buf+offset + 8) == 0)) {
                score = FFMAX(score, AVPROBE_SCORE_EXTENSION);
            } else 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',' ')
                    )) {
                score = FFMAX(score, 5);
            } else {
                score = AVPROBE_SCORE_MAX;
            }
            offset = FFMAX(4, AV_RB32(p->buf+offset)) + offset;
            break;
        /* those are more common words, so rate then a bit less */
        case MKTAG('e','d','i','w'): /* xdcam files have reverted first tags */
        case MKTAG('w','i','d','e'):
        case MKTAG('f','r','e','e'):
        case MKTAG('j','u','n','k'):
        case MKTAG('p','i','c','t'):
            score  = FFMAX(score, AVPROBE_SCORE_MAX - 5);
            offset = FFMAX(4, AV_RB32(p->buf+offset)) + offset;
            break;
        case MKTAG(0x82,0x82,0x7f,0x7d):
        case MKTAG('s','k','i','p'):
        case MKTAG('u','u','i','d'):
        case MKTAG('p','r','f','l'):
            /* if we only find those cause probedata is too small at least rate them */
            score  = FFMAX(score, AVPROBE_SCORE_EXTENSION);
            offset = FFMAX(4, AV_RB32(p->buf+offset)) + offset;
            break;
        default:
            offset = FFMAX(4, AV_RB32(p->buf+offset)) + offset;
        }
    }
    if(score > AVPROBE_SCORE_MAX - 50 && moov_offset != -1) {
        /* moov atom in the header - we should make sure that this is not a
         * MOV-packed MPEG-PS */
        offset = moov_offset;

        while(offset < (p->buf_size - 16)){ /* Sufficient space */
               /* We found an actual hdlr atom */
            if(AV_RL32(p->buf + offset     ) == MKTAG('h','d','l','r') &&
               AV_RL32(p->buf + offset +  8) == MKTAG('m','h','l','r') &&
               AV_RL32(p->buf + offset + 12) == MKTAG('M','P','E','G')){
                av_log(NULL, AV_LOG_WARNING, "Found media data tag MPEGå indicating this is a MOV-packed MPEG-PS.\n");
                /* We found a media handler reference atom describing an
                 * MPEG-PS-in-MOV, return a
                 * low score to force expanding the probe window until
                 * mpegps_probe finds what it needs */
                return 5;
            }else
                /* Keep looking */
                offset+=2;
        }
    }

    return score;
}

以hls为例,当Name为hls时,相对应的函数指针均会指向hls.c的函数指针

ffmpeg的avformat_open_input()分析过程--以mp4为例(十)_第3张图片ffmpeg的avformat_open_input()分析过程--以mp4为例(十)_第4张图片

ffmpeg的avformat_open_input()分析过程--以mp4为例(十)_第5张图片

av_probe_input_format3总结:

遍历所有的解复用器 然后记录最匹配的解复用器
1、如果有探测函数 则调用 并记录分数 否则匹配扩展名(50分)
2、然后匹配媒体类型 如果匹配且比以前分高 就为75分

由于调试的例程是私有协议,所以会走第3个步骤

3)如果通过文件名或类型没有匹配到 就打开文件 然后读取内容再推测

s->io_open函数指针指向io_open_default

static int io_open_default(AVFormatContext *s, AVIOContext **pb,
                           const char *url, int flags, AVDictionary **options)
{
    int loglevel;

    if (!strcmp(url, s->filename) ||
        s->iformat && !strcmp(s->iformat->name, "image2") ||
        s->oformat && !strcmp(s->oformat->name, "image2")
    ) {
        loglevel = AV_LOG_DEBUG;
    } else
        loglevel = AV_LOG_INFO;

    av_log(s, loglevel, "Opening \'%s\' for %s\n", url, flags & AVIO_FLAG_WRITE ? "writing" : "reading");

#if FF_API_OLD_OPEN_CALLBACKS
FF_DISABLE_DEPRECATION_WARNINGS
    if (s->open_cb)
        return s->open_cb(s, pb, url, flags, &s->interrupt_callback, options);
FF_ENABLE_DEPRECATION_WARNINGS
#endif

    return ffio_open_whitelist(pb, url, flags, &s->interrupt_callback, options, s->protocol_whitelist, s->protocol_blacklist);
}

ffmpeg的avformat_open_input()分析过程--以mp4为例(十)_第6张图片

int ffurl_connect(URLContext *uc, AVDictionary **options)
{
    int err;
    AVDictionary *tmp_opts = NULL;
    AVDictionaryEntry *e;

    if (!options)
        options = &tmp_opts;

    // Check that URLContext was initialized correctly and lists are matching if set
    av_assert0(!(e=av_dict_get(*options, "protocol_whitelist", NULL, 0)) ||
               (uc->protocol_whitelist && !strcmp(uc->protocol_whitelist, e->value)));
    av_assert0(!(e=av_dict_get(*options, "protocol_blacklist", NULL, 0)) ||
               (uc->protocol_blacklist && !strcmp(uc->protocol_blacklist, e->value)));

    if (uc->protocol_whitelist && av_match_list(uc->prot->name, uc->protocol_whitelist, ',') <= 0) {
        av_log(uc, AV_LOG_ERROR, "Protocol '%s' not on whitelist '%s'!\n", uc->prot->name, uc->protocol_whitelist);
        return AVERROR(EINVAL);
    }

    if (uc->protocol_blacklist && av_match_list(uc->prot->name, uc->protocol_blacklist, ',') > 0) {
        av_log(uc, AV_LOG_ERROR, "Protocol '%s' on blacklist '%s'!\n", uc->prot->name, uc->protocol_blacklist);
        return AVERROR(EINVAL);
    }

    if (!uc->protocol_whitelist && uc->prot->default_whitelist) {
        av_log(uc, AV_LOG_DEBUG, "Setting default whitelist '%s'\n", uc->prot->default_whitelist);
        uc->protocol_whitelist = av_strdup(uc->prot->default_whitelist);
        if (!uc->protocol_whitelist) {
            return AVERROR(ENOMEM);
        }
    } else if (!uc->protocol_whitelist)
        av_log(uc, AV_LOG_DEBUG, "No default whitelist set\n"); // This should be an error once all declare a default whitelist

    if ((err = av_dict_set(options, "protocol_whitelist", uc->protocol_whitelist, 0)) < 0)
        return err;
    if ((err = av_dict_set(options, "protocol_blacklist", uc->protocol_blacklist, 0)) < 0)
        return err;

    err =
        uc->prot->url_open2 ? uc->prot->url_open2(uc,
                                                  uc->filename,
                                                  uc->flags,
                                                  options) :
        uc->prot->url_open(uc, uc->filename, uc->flags);

    av_dict_set(options, "protocol_whitelist", NULL, 0);
    av_dict_set(options, "protocol_blacklist", NULL, 0);

    if (err)
        return err;
    uc->is_connected = 1;
    /* We must be careful here as ffurl_seek() could be slow,
     * for example for http */
    if ((uc->flags & AVIO_FLAG_WRITE) || !strcmp(uc->prot->name, "file"))
        if (!uc->is_streamed && ffurl_seek(uc, 0, SEEK_SET) < 0)
            uc->is_streamed = 1;
    return 0;
}

ffmpeg的avformat_open_input()分析过程--以mp4为例(十)_第7张图片ffmpeg的avformat_open_input()分析过程--以mp4为例(十)_第8张图片

uc->prot->url_open2根据协议,会调用对应协议的open2,这里协议是私有协议ijkbds,所以会调用ijkbds_open

ffmpeg的avformat_open_input()分析过程--以mp4为例(十)_第9张图片

ijkbds_open会打开流,最终uc->is_connected = 1

int ffio_open_whitelist(AVIOContext **s, const char *filename, int flags,
                         const AVIOInterruptCB *int_cb, AVDictionary **options,
                         const char *whitelist, const char *blacklist
                        )
{
    URLContext *h;
    int err;

    err = ffurl_open_whitelist(&h, filename, flags, int_cb, options, whitelist, blacklist, NULL);
    if (err < 0)
        return err;
    err = ffio_fdopen(s, h);
    if (err < 0) {
        ffurl_close(h);
        return err;
    }
    return 0;
}

两个步骤:

1)ffurl_connect会最终调用open2打开对应协议的open

2)ffio_fdopen会通过avio_alloc_context设置read/write/seek相关回调

int ffio_fdopen(AVIOContext **s, URLContext *h)
{
    AVIOInternal *internal = NULL;
    uint8_t *buffer = NULL;
    int buffer_size, max_packet_size;

    max_packet_size = h->max_packet_size;
    if (max_packet_size) {
        buffer_size = max_packet_size; /* no need to bufferize more than one packet */
    } else {
        buffer_size = IO_BUFFER_SIZE;
    }
    buffer = av_malloc(buffer_size);
    if (!buffer)
        return AVERROR(ENOMEM);

    internal = av_mallocz(sizeof(*internal));
    if (!internal)
        goto fail;

    internal->h = h;
//设置read/write/seek相关回调
    *s = avio_alloc_context(buffer, buffer_size, h->flags & AVIO_FLAG_WRITE,
                            internal, io_read_packet, io_write_packet, io_seek);
    if (!*s)
        goto fail;

    (*s)->protocol_whitelist = av_strdup(h->protocol_whitelist);
    if (!(*s)->protocol_whitelist && h->protocol_whitelist) {
        avio_closep(s);
        goto fail;
    }
    (*s)->protocol_blacklist = av_strdup(h->protocol_blacklist);
    if (!(*s)->protocol_blacklist && h->protocol_blacklist) {
        avio_closep(s);
        goto fail;
    }
    (*s)->direct = h->flags & AVIO_FLAG_DIRECT;

    (*s)->seekable = h->is_streamed ? 0 : AVIO_SEEKABLE_NORMAL;
    (*s)->max_packet_size = max_packet_size;
    (*s)->min_packet_size = h->min_packet_size;
    if(h->prot) {
        (*s)->read_pause = io_read_pause;
        (*s)->read_seek  = io_read_seek;

        if (h->prot->url_read_seek)
            (*s)->seekable |= AVIO_SEEKABLE_TIME;
    }
    (*s)->short_seek_get = io_short_seek;
    (*s)->av_class = &ff_avio_class;
    return 0;
fail:
    av_freep(&internal);
    av_freep(&buffer);
    return AVERROR(ENOMEM);
}

ffmpeg的avformat_open_input()分析过程--以mp4为例(十)_第10张图片

2、read_header

mp4的函数指针会指向mov.c文件的mov_read_header

因为本文以mp4为例,这里先了解mp4的文件结构

ffmpeg的avformat_open_input()分析过程--以mp4为例(十)_第11张图片

ffmpeg的avformat_open_input()分析过程--以mp4为例(十)_第12张图片

从图中可以看出来这些box是有层级关系的。每一个box它的头部的8个字节是固定的,前四个字节是这个box的大小,后四个字节是这个box的类型,也就是途中的fytp,moov之类的。这个信息在ffmpeg中使用MOVAtom来标示:

//在代码中atom,其实就是MP4协议中的box,总共8个字节
typedef struct MOVAtom {
    uint32_t type;//box类型
    int64_t size; /* box整体大小(size+type+body三部分) total size (excluding the size and type fields) */
} MOVAtom;

MP4格式是以BOX的形式存储数据的,包括嵌套box,所有的box都是由size+type+body三部分组成。在最外层的BOX中主要有ftyp、moov、mdat三种类型的BOX。ftyp主要为MP4格式的标识信息,moov为这个MP4文件sample数据的metadata,用来描述sample大小,位置,dts等,mdat为sample具体数据。每个sample数据在文件中的位置,是通过moov中的trak得到的。mp4 box的解析其实就是读取各种类型box,将里面的一些数据通过file_read读取并赋值。

static int mov_read_header(AVFormatContext *s)
{
    MOVContext *mov = s->priv_data;  //AVClass类型 memset 为0
    AVIOContext *pb = s->pb;
    int j, err;
    MOVAtom atom = { AV_RL32("root") }; //创建父box,包含ftyp、moov、mdat三种类型的box并赋初值
    int i;
    ...
    mov->fc = s; //将AVFormatContext 赋值给MOVContext 方便书写,代码具有扩展性
    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); //源MP4文件整体大小
    else
        atom.size = INT64_MAX;

    /* check MOV header */
    //当读完moov的box时跳出,但这里只读一次,因为有mov_read_default接口会不断向下读取嵌套box,只要atom.size还有足够数据
    do {
        if (mov->moov_retry)
            avio_seek(pb, 0, SEEK_SET);
            //有嵌套BOX,继续往下读
        if ((err = mov_read_default(mov, pb, atom)) < 0) {
            av_log(s, AV_LOG_ERROR, "error reading header\n");
            goto fail;
        }
    } while ((pb->seekable & AVIO_SEEKABLE_NORMAL) && !mov->found_moov && !mov->moov_retry++);
    if (!mov->found_moov) { //moov是否读完标志
        av_log(s, AV_LOG_ERROR, "moov atom not found\n");
        err = AVERROR_INVALIDDATA;
        goto fail;
    }
    av_log(mov->fc, AV_LOG_TRACE, "on_parse_exit_offset=%"PRId64"\n", avio_tell(pb));

    if (pb->seekable & AVIO_SEEKABLE_NORMAL) {
        ......
    }

    /* copy timecode metadata from tmcd tracks to the related video streams */
    for (i = 0; i < s->nb_streams; i++) {
        ...
    }
    export_orphan_timecode(s);

    //s->streams是在读取trak box开的内存并赋值的
    for (i = 0; i < s->nb_streams; i++) {
        AVStream *st = s->streams[i];
        MOVStreamContext *sc = st->priv_data;
        fix_timescale(mov, sc); //mdhd中的时间缩放比例,一般为1
        if (st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO &&
            st->codecpar->codec_id   == AV_CODEC_ID_AAC) {
            st->internal->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)
                    goto fail;
            }
        }
        ...
    }
    if (mov->trex_data) {
        ...
    }
    if (mov->use_mfra_for > 0) {
        ...
    }
    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)
                goto fail;
            break;
        case AVMEDIA_TYPE_VIDEO:
            ...
    }
    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;
fail:
    mov_read_close(s);
    return err;
}
//将所有解析atom的接口都注册到一起
typedef struct MOVParseTableEntry {
    uint32_t type;
    int (*parse)(MOVContext *ctx, AVIOContext *pb, MOVAtom atom);
} MOVParseTableEntry;

static const MOVParseTableEntry mov_default_parse_table[] = {
{ MKTAG('f','t','y','p'), mov_read_ftyp },
{ MKTAG('t','r','a','k'), mov_read_trak },
{ MKTAG('t','r','a','f'), mov_read_default },
{ 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 */
.....
}

//这个接口是最重要的接口,主要是负责读取嵌套的box,里面有循环代码,只要atom.size数据足够多的
//比如 moov->trak->mdia->stbl->stsd、stts、stss、ctts、stco等
//所以上面mov_read_header接口中只用执行一次mov_read_default就行了,就是从创建的父box开始循环读取嵌套box
static int mov_read_default(MOVContext *c, AVIOContext *pb, MOVAtom atom)
{
    int64_t total_size = 0;//读取一个atom(无论是否是嵌套box),已经读取的字节数
    MOVAtom a;
    int i;

    if (c->atom_depth > 10) {
        av_log(c->fc, AV_LOG_ERROR, "Atoms too deeply nested\n");
        return AVERROR_INVALIDDATA;
    }
    c->atom_depth ++; //atom读取层数

    if (atom.size < 0)
        atom.size = INT64_MAX;
    while (total_size <= atom.size - 8 && !avio_feof(pb)) {
        int (*parse)(MOVContext*, AVIOContext*, MOVAtom) = NULL;
        a.size = atom.size;
        a.type=0;
        if (atom.size >= 8) {
            a.size = avio_rb32(pb);//avio_rb32读四字节函数,该box大小(size+type+body大小)
            a.type = avio_rl32(pb);//avio_rb32读四字节函数,该box类型
            //类型比对
            if (((a.type == MKTAG('f','r','e','e') && c->moov_retry) ||
                  a.type == MKTAG('h','o','o','v')) &&
                a.size >= 8 &&
                c->fc->strict_std_compliance < FF_COMPLIANCE_STRICT) {
                uint32_t type;
                avio_skip(pb, 4);
                type = avio_rl32(pb);
                avio_seek(pb, -8, SEEK_CUR);
                if (type == MKTAG('m','v','h','d') ||
                    type == MKTAG('c','m','o','v')) {
                    av_log(c->fc, AV_LOG_ERROR, "Detected moov in a free or hoov atom.\n");
                    a.type = MKTAG('m','o','o','v');
                }
            }
            if (atom.type != MKTAG('r','o','o','t') &&
                atom.type != MKTAG('m','o','o','v')) {
                if (a.type == MKTAG('t','r','a','k') ||
                    a.type == MKTAG('m','d','a','t')) {
                    av_log(c->fc, AV_LOG_ERROR, "Broken file, trak/mdat not at top-level\n");
                    avio_skip(pb, -8);
                    c->atom_depth --;
                    return 0;
                }
            }
            total_size += 8; //一次读取8个字节,其中4个字节为box长度,4个字节为box的tpye
            if (a.size == 1 && total_size + 8 <= atom.size) { /* 64 bit extended size */
                a.size = avio_rb64(pb) - 8;
                total_size += 8;
            }
        }
        av_log(c->fc, AV_LOG_TRACE, "type:'%s' parent:'%s' sz: %"PRId64" %"PRId64" %"PRId64"\n",
               av_fourcc2str(a.type), av_fourcc2str(atom.type), a.size, total_size, atom.size);
        if (a.size == 0) {
            a.size = atom.size - total_size + 8;
        }
        a.size -= 8;
        if (a.size < 0)
            break;
        a.size = FFMIN(a.size, atom.size - total_size);

       //通过mov_default_parse_table与type比较,对将对应的解析box接口进行赋值
        for (i = 0; mov_default_parse_table[i].type; i++)
            if (mov_default_parse_table[i].type == a.type) {
                parse = mov_default_parse_table[i].parse;
                break;//找到对应的box,退出比较
            }

        // container is user data
        if (!parse && (atom.type == MKTAG('u','d','t','a') ||
                       atom.type == MKTAG('i','l','s','t')))
            parse = mov_read_udta_string;

        // Supports parsing the QuickTime Metadata Keys.
        // https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/Metadata/Metadata.html
        if (!parse && c->found_hdlr_mdta &&
            atom.type == MKTAG('m','e','t','a') &&
            a.type == MKTAG('k','e','y','s') &&
            c->meta_keys_count == 0) {
            parse = mov_read_keys;
        }

        if (!parse) { /* 如果没有找到对应的type类型,就跳过这个atom数据skip leaf atoms data */
            avio_skip(pb, a.size);
        } else {
            int64_t start_pos = avio_tell(pb);//当前文件指针位置和文件首地址的偏移,为了验证该box或atom是否已经读完,比如第一次执行解析,start_pos=8
            int64_t left;
            int err = parse(c, pb, a);//parse是个函数指针,如果tpye=ftyp,则会调用mov_read_ftyp
            if (err < 0) {
                c->atom_depth --;
                return err;
            }
            if (c->found_moov && c->found_mdat &&
                ((!(pb->seekable & AVIO_SEEKABLE_NORMAL) || c->fc->flags & AVFMT_FLAG_IGNIDX || c->frag_index.complete) ||
                 start_pos + a.size == avio_size(pb))) {
                if (!(pb->seekable & AVIO_SEEKABLE_NORMAL) || c->fc->flags & AVFMT_FLAG_IGNIDX || c->frag_index.complete)
                    c->next_root_atom = start_pos + a.size;
                c->atom_depth --;
                return 0;
            }
            left = a.size - avio_tell(pb) + start_pos;//验证 该box或atom是否已经读完;avio_tell相对于文件起始位置的偏移量,第一次执行:a.size = 16,start_pos = 8,avio_tell(pb) = 24,left = 0
            if (left > 0) /* 跳过box剩余的偏移量skip garbage at atom end */
                avio_skip(pb, left);
            else if (left < 0) {
                av_log(c->fc, AV_LOG_WARNING,
                       "overread end of atom '%s' by %"PRId64" bytes\n",
                       av_fourcc2str(a.type), -left);
                avio_seek(pb, left, SEEK_CUR);
            }
        }
//total_size 为一个循环总共读取的size
        total_size += a.size;//一个atom已经读完
    }

    if (total_size < atom.size && atom.size < 0x7ffff)
        avio_skip(pb, atom.size - total_size);

    c->atom_depth --;
    return 0;
}

mov_read_header的工作

  • 检测参数的有效性。
    • 比如decryption_key_len的范围。
  • 调用mov_read_default进行root下的box解析。

其中

int (*parse)(MOVContext*, AVIOContext*, MOVAtom) = NULL

这个是个函数指针,会根据type的类型调用相关的解析函数。比如box的type为ftyp的时候,就会调用mov_read_ftyp

/* read major brand, minor version and compatible brands and store them as metadata */
static int mov_read_ftyp(MOVContext *c, AVIOContext *pb, MOVAtom atom)
{
    uint32_t minor_ver;
    int comp_brand_size;
    char* comp_brands_str;
    uint8_t type[5] = {0};
    int ret = ffio_read_size(pb, type, 4);
    if (ret < 0)
        return ret;

    if (strcmp(type, "qt  "))
        c->isom = 1;
    av_log(c->fc, AV_LOG_DEBUG, "ISO: File Type Major Brand: %.4s\n",(char *)&type);
    av_dict_set(&c->fc->metadata, "major_brand", type, 0);
    minor_ver = avio_rb32(pb); /* minor version */
    av_dict_set_int(&c->fc->metadata, "minor_version", minor_ver, 0);

    comp_brand_size = atom.size - 8;
    if (comp_brand_size < 0)
        return AVERROR_INVALIDDATA;
    comp_brands_str = av_malloc(comp_brand_size + 1); /* Add null terminator */
    if (!comp_brands_str)
        return AVERROR(ENOMEM);

    ret = ffio_read_size(pb, comp_brands_str, comp_brand_size);
    if (ret < 0) {
        av_freep(&comp_brands_str);
        return ret;
    }
    comp_brands_str[comp_brand_size] = 0;
    av_dict_set(&c->fc->metadata, "compatible_brands", comp_brands_str, 0);//comp_brands_str = "ios4hvc1"
    av_freep(&comp_brands_str);

    return 0;
}

由于ftyp没有嵌套,所以没有调用mov_read_default,而moov就会有嵌套

ftyp的长度为0x18,即对应avio_tell(pb) = 24,moov的长度为0x00110b48,即1117000,所以mdat的真实数据其实地址为ftyp+moov+8(其中4个字节为mdat长度,4个字节为mdat的tpye)=24+1117000+8=1117032

static int mov_read_moov(MOVContext *c, AVIOContext *pb, MOVAtom atom)
{
    int ret;

    if (c->found_moov) {
        av_log(c->fc, AV_LOG_WARNING, "Found duplicated MOOV Atom. Skipped it\n");
        avio_skip(pb, atom.size);
        return 0;
    }

    if ((ret = mov_read_default(c, pb, atom)) < 0)
        return ret;
    /* 我们解析了'moov'这个box,只要找到'mdat',我们就可以终止解析 */
    /* 因此,如果通过网络,我们不会解析整个文件 */
    c->found_moov=1;
    return 0; /* now go for mdat */
}

c->found_moov && c->found_mdat均为1的时候

            if (c->found_moov && c->found_mdat &&
                ((!(pb->seekable & AVIO_SEEKABLE_NORMAL) || c->fc->flags & AVFMT_FLAG_IGNIDX || c->frag_index.complete) ||
                 start_pos + a.size == avio_size(pb))) {
                if (!(pb->seekable & AVIO_SEEKABLE_NORMAL) || c->fc->flags & AVFMT_FLAG_IGNIDX || c->frag_index.complete)
                    c->next_root_atom = start_pos + a.size;
                c->atom_depth --;
                return 0;
            }

发现 start_pos + a.size == avio_size(pb)这个条件不满足要求,不会退出解析,为啥要这个条件呢?能不能把这个条件去掉?

由于这个box只是并没有真正去读取数据,只是把标志置1

static int mov_read_mdat(MOVContext *c, AVIOContext *pb, MOVAtom atom)
{
    if (atom.size == 0) /* wrong one (MP4) */
        return 0;
    c->found_mdat=1;
    return 0; /* now go for moov */
}

所以后面会走到

left = a.size - avio_tell(pb) + start_pos;//验证 该box或atom是否已经读完
            if (left > 0) /* skip garbage at atom end */
                avio_skip(pb, left);//起播的时候,会先seek到文件结尾

总结

可以看到,avformat_open_input的主要主要就是根据输入的文件,创建AVFormatContext,根据文件的头格式,后缀找到合适的AVInputFormat,并初始化AVIOContext,完成读写协议,然后读取文件头。完成AVFormatContext的初始化。

优化点

1)前端设置“iformat”属性

2)probe_input去掉部分格式

3)去掉free的box或者是修改c->found_moov && c->found_mdat均为1后的执行逻辑

你可能感兴趣的:(流媒体,ijkplayer,ffmpeg)