ijkplayer的MP4解析---读取多媒体数据文件头(七)

总概
ff_ffplay.c
read_thread

    ---.>>>avformat_open_input

utils.c

avformat_open_input

    --->>>s->iformat->read_header(s)

mov.c

.read_header    = mov_read_header,

mov_read_header

    -->>>>mov_read_default

1、分析

    if (ffp->iformat_name)
        is->iformat = av_find_input_format(ffp->iformat_name);
    err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);

 当ffp->iformat_name没有指定时,即ffp->iformat_name,会直接走

avformat_open_input

然后is->iformat什么时候解析呢?同时s->iformat->read_header(s)为什么就会走到mov下的read_header?带着这些问题,分析了avformat_open_input流程

avformat_open_input

    --->>init_input
        1.是否设置了pb,如果设置了就直接av_probe_input_buffer2.一般情况下pb均为空,有特殊情况,下文有做说明
        2.是否设置了iformat,如果设置了直接返回;
          如果没指定iformat,但是可以从文件名中猜出iformat,也成功.  
        3.hls一般会走到这里io_open,如果从文件名中也猜不出媒体格式,则只能打开这个文件进行探测了,先打开文件

           --->>s->io_open

               --->>>io_open_default

                    --->>>ffio_open_whitelist

                        --->>>ffurl_open_whitelist

                             -->>>ffurl_alloc
                                 //探测是HTTP协议 URLProtocol ff_http_protocol
                                 url_find_protocol(filename);  
                    ffurl_connect    //发送HTTP报文头,下载http.xxx.m3u8文件

                            --->>>ffurl_connect

                                --->>>url_open2或者url_open

            ---->>>av_probe_input_buffer2

                 --->>>av_probe_input_format2/* Guess file format. */会通过根据格式read_probe来选择最高分的解码器

     --->>s->iformat->read_header(s)

其中io_open定义在avformat.h

    int (*io_open)(struct AVFormatContext *s, AVIOContext **pb, const char *url,

                   int flags, AVDictionary **options);

然而io_open调用哪个文件的io_open呢?跟踪发现avformat_alloc_context

opotions.c

AVFormatContext *avformat_alloc_context(void)
{
    AVFormatContext *ic;
    ic = av_malloc(sizeof(AVFormatContext));
    if (!ic) return ic;
    avformat_get_context_defaults(ic);

    return ic;
}

这个时候会调用avformat_get_context_defaults

static void avformat_get_context_defaults(AVFormatContext *s)
{

    s->io_open  = io_open_default;
    s->io_close = io_close_default;
    av_opt_set_defaults(s);
}

定义了io_open和io_close的函数指针,io_open_default和io_close_default

在 io_open_default又做了哪些操作?主要调用了ffio_open_whitelist

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

    err = ffio_fdopen(s, h);

}
int ffurl_open_whitelist(URLContext **puc, const char *filename, int flags,
                         const AVIOInterruptCB *int_cb, AVDictionary **options,
                         const char *whitelist, const char* blacklist,
                         URLContext *parent)
{
    AVDictionary *tmp_opts = NULL;
    AVDictionaryEntry *e;
    int ret = ffurl_alloc(puc, filename, flags, int_cb);


    ret = ffurl_connect(*puc, options);

}

ffurl_open_whitelist主要执行ffurl_alloc和ffurl_connect

url_open根据协议调用配对的open2,比如http协议,即调用http.c的http_open
const URLProtocol ff_https_protocol = {
    .name                = "https",
    .url_open2           = http_open,
    .url_read            = http_read,
    .url_write           = http_write,
    .url_seek            = http_seek,
    .url_close           = http_close,
    .url_get_file_handle = http_get_file_handle,
    .url_get_short_seek  = http_get_short_seek,
    .url_shutdown        = http_shutdown,
    .priv_data_size      = sizeof(HTTPContext),
    .priv_data_class     = &https_context_class,
    .flags               = URL_PROTOCOL_FLAG_NETWORK,
    .default_whitelist   = "http,https,tls,rtp,tcp,udp,crypto,httpproxy"
};

回到前面

avformat_open_input

    --->>init_input

           --->>s->io_open

   --->>s->iformat->read_header(s)
//p是流的指针,其中buf就是原始数据
static int 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);//将p->buf+4即‘f’ ‘t’ 'y' 'p'组成一个int
        switch(tag) {//判断这个int是不是ftyp
        /* 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') &&//如果是的话score=100,说明就是mov格式的文件
                       (   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;
//p->buf_size = 2048
        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");
//判断是否是MPEG-PS-in-MOV,如果是就返回scroe = 5
                /* 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;
}

mov_probe会得到score,其调用流程

init_input会得到iformat_name为mp4,从而read_header会调用mov.c文件里的mov_read_header

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

下面开始分析mov_read_header

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

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);//该box大小(size+type+body大小)
            a.type = avio_rl32(pb);//该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个字节
            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);

       //通过比对将对应的解析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;
            }

        // 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) { /* skip leaf atoms data */
            avio_skip(pb, a.size);//起播的时候,会先seek到文件moov分区起始地址
        } else {
            int64_t start_pos = avio_tell(pb);//当前文件指针位置和文件首地址的偏移,为了验证该box或atom是否已经读完
            int64_t left;
            int err = parse(c, pb, a);
            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是否已经读完
            if (left > 0) /* skip garbage at atom end */
                avio_skip(pb, left);//起播的时候,会先seek到文件结尾
            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 += 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_default解析了如下分区 

顺便说一下,mp4起播过程会seek两次,第一次是离文件结尾73结束的位置的free分区,第二次是seek到moov分区的起始地址

重要发现

起播过程,左图会seek两次,右图不会seek

同一个视频源,一个是带free分区,一个是没有free分区,带free分区的,起播会先seek到free分区,然后再seek到moov分区,也就是说起播会有两次seek,而不带free分区的视频源,起播不会seek

结论:

把free分区去掉或者mp4文件只流ftyp/moov/mdat三个分区,可以提高起播速度

经测试发现

如果文件长度设为free分区的起始地址

带有free分区的文件也不会有seek操作,如果带有free分区的视频,长度是本身文件的长度,就会有两次seek

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Casia-OLHWDB是一个手写字数据集,其中包含来自不同作者的手写数字、字母和汉字。这个数据集的文件解析可以通过以下步骤进行。 1. 下载Casia-OLHWDB数据集文件。你可以在官方网站上找到该数据集并下载。解压缩下载的文件。 2. 了解数据集文件结构。Casia-OLHWDB数据集使用一种简单的文件格式来存储手写字样本。数据集文件通常以“.txt”或“.pkl”为扩展名。你可以查看数据集文档以了解具体的文件结构和格式。 3. 打开数据集文件。使用Python的文件读取函数打开数据集文件。如果文件是文本文件,可以使用“open”函数读取。如果文件是二进制文件,则可以使用适当的库(如NumPy或Pandas)来读取。 4. 解析数据集文件内容。根据数据集文件的结构和格式,你需要编写代码来解析文件内容。这通常涉及读取文件中的每一行,并将其分解为适当的数据结构(如数组、字典或数据帧)。 5. 提取手写字样本。在解析文件内容后,你可以提取手写字样本。这些样本可以是图像、矢量图、像素数组或其他形式的数据。你可以根据需要对这些样本进行处理和转换。 6. 进一步处理和分析。一旦你解析并提取了手写字样本,你可以对数据进行进一步的处理和分析。这可能包括图像增强、特征提取、机器学习模型训练等。 总之,Casia-OLHWDB数据集文件解析涉及下载数据集文件,了解文件结构和格式,打开文件,解析文件内容,并提取手写字样本。通过这些步骤,你可以开始使用这个数据集进行手写字识别、字形分析或其他相关任务。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值