FFmpeg中mp4的demuxer(mov.c)代码阅读

目录

  1. 参考
  2. 概述
  3. mov_read_header
  4. mov_read_packet
  5. mov_read_seek
  6. mov_read_close

1. 参考

2. 概述

mp4文件格式解析 文章对mp4文件格式进行了了解,本文主要从代码角度,学习FFmpeg中对mp4的解析、读取数据和seek等操作的实现。

以下是mp4主要box的示意图,解析部分主要分析这些box。


mp4主要box说明

3. mov_read_header

mov_read_header的调用时机如下图所示。


AVInputFormat.read_header.png
  • 在调用avformat_open_input过程会对mp4文件进行解析。

mov_read_header的工作

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

3.1 mov_read_default

  • 如果box中有剩余没有读取的部分,skip剩余未读取的部分。
  • 如果box读取多于了size指定的大小(这种情况理应不该发生),把读取的位置往回调。
  • mov_default_parse_table是一个数组,保存了box的type对应解析使用的函数的指针。
  • 在父box还没有读取完毕且还没有到文件结尾的时候,循环地去解析父box下的子box。
static int mov_read_default(MOVContext *c, AVIOContext *pb, MOVAtom atom)
{
    int64_t total_size = 0;
    MOVAtom a;
    int i;
    c->atom_depth ++;
...
    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);
            a.type = avio_rl32(pb);
...
            total_size += 8;
...
        }
...
        a.size -= 8;
...
        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;
            }
...
        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);
            if (err < 0) {
                c->atom_depth --;
                return err;
            }
...
        }

        total_size += a.size;
    }
...

    c->atom_depth --;
    return 0;
}

3.2 mov_read_ftyp

  • 读取ftyp box的"major_brand"、"minor_version"、"compatible_brands"信息,保存到AVFormatContext.metadata(AVDictionary结构体)中。
    • "major_brand"保存为char *类型。
    • "minor_version"保存为int类型。
    • "compatible_brands"保存为char *类型。

3.3 mov_read_moov

  • 如果MOVContext.found_moov为1,即发现了重复的moov box(理应不该发生),直接返回。
  • 如果MOVContext.found_moov为0, 调用mov_read_default()来读取子box,把MOVContext.found_moov置为1。

3.4 mov_read_mvhd

  • 读取create_time,通过strftime转换为格式化的字符串的时间表示,保存在AVFormatContext.metadata中。
  • 读取time_scale,保存在MOVContext.time_scale。
    • 校验有效性,如果小于等于0则置为1。
  • 读取duration,保存在MOVContext.duration。
  • 根据获取到的time_scale和duration计算为以FFmpeg中AV_TIME_BASE为timescale的时长保存在AVFormatContext.duratinon(int64_t)。
    • FFmpeg中AV_TIME_BASE定义为1000000,示例julin_5s.mp4文件的mvhd中duration为5022,timescale为1000,所以计算得到c->fc->duration=5022 * AV_TIME_BASE / 1000 = 5022000(us)
c->fc->duration = av_rescale(c->duration, AV_TIME_BASE, c->time_scale);
  • 读取matrix信息保存到MOVContext.movie_display_matrix
  • box中其他的字段信息未保存。

3.5 mov_read_trak

  • 使用avformat_new_stream函数在AVFormatContext中新建一个Stream。
  • 创建MOVStreamContext结构体,保存在AVStream.priv_data。
    • 用于保存trak box中读取到的一些信息,
  • 读取子box。
  • 一些完整性检查,针对MOVStreamContext中的一些变量。
    • sample_count大于0时,chunk_count不应该为0。
    • chunk_count大于0时,stts_count、stsc_count不应该为0,sample_size和sample_count不应该同时为0。
    • stsc_data的最后一个元素的first_chunk值不应该大于chunk_count。
  • mov_build_index
  1. 建立包含每个sample信息的表,保存在AVStream.index_entries(AVIndexEntry数组)。
typedef struct AVIndexEntry {
    int64_t pos;
    int64_t timestamp; //Timestamp in AVStream.time_base units   
#define AVINDEX_KEYFRAME 0x0001
#define AVINDEX_DISCARD_FRAME  0x0002   
    int flags:2;
    int size:30; 
    int min_distance;   //Minimum distance between this and the previous keyframe, used to avoid unneeded searching. */
} AVIndexEntry;
  • pos,当前sample相对文件的偏移。
  • timestamp:当前sample的dts。以AVStream.time_base为单位。
  • flags:标识,是否为关键帧,或者要舍弃的帧。
  • size:当前sample的字节大小。
  • min_distance:距离前一个关键帧的距离,序号的偏差。
  1. 建立表的方式是通过遍历stsc表,结合stco、stsz、stss、stts表的信息来完成的。
  • AVIndexEntry.pos:使用stco(chunk在文件中的偏移)、stsz(每个sample的字节大小)来计算得到。
  • AVIndexEntry.min_distance和AVIndexEntry.flags:依据stss来获得。
  • AVIndexEntry.size:通过stsz获得。
  • 代码如下所示(原始代码删去了部分逻辑)
static void mov_build_index(MOVContext *mov, AVStream *st) {
    MOVStreamContext *sc = st->priv_data;
    int64_t current_offset;
    int64_t current_dts = 0;
    unsigned int stts_index = 0;
    unsigned int stsc_index = 0;
    unsigned int stss_index = 0;
    unsigned int stps_index = 0;
    if (!(st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO &&
          sc->stts_count == 1 && sc->stts_data[0].duration == 1)) {
        unsigned int current_sample = 0;
        unsigned int stts_sample = 0;
        unsigned int sample_size;
        unsigned int distance = 0;
        unsigned int rap_group_index = 0;
        unsigned int rap_group_sample = 0;
        int64_t last_dts = 0;
        int64_t dts_correction = 0;
        int rap_group_present = sc->rap_group_count && sc->rap_group;
        int key_off = (sc->keyframe_count && sc->keyframes[0] > 0) || (sc->stps_count && sc->stps_data[0] > 0);
        current_dts -= sc->dts_shift;
        last_dts     = current_dts;
...
        for (i = 0; i < sc->chunk_count; i++) {//遍历每个chunk
            int64_t next_offset = i+1 < sc->chunk_count ? sc->chunk_offsets[i+1] : INT64_MAX;
            current_offset = sc->chunk_offsets[i];//当前chunk的文件位置偏移。
            while (mov_stsc_index_valid(stsc_index, sc->stsc_count) &&
                i + 1 == sc->stsc_data[stsc_index + 1].first)//(i+1)等于下一组chunk的first_chunk的时候,stsc_index后移指向下一组chunk。
                stsc_index++;
...
            for (j = 0; j < sc->stsc_data[stsc_index].count; j++) {//遍历chunk中的每个sample。
...
                int keyframe = 0;
                if (!sc->keyframe_absent && (!sc->keyframe_count || current_sample+key_off == sc->keyframes[stss_index])) {//判断是否是关键帧的条件。
                    keyframe = 1;
                    if (stss_index + 1 < sc->keyframe_count)
                        stss_index++;//stss表指向一下个关键帧。
                } 
...
                if (sc->keyframe_absent
                    && !sc->stps_count
                    && !rap_group_present
                    && (st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO || (i==0 && j==0)))
                     keyframe = 1;
                if (keyframe)
                    distance = 0;
...
                    e = &st->index_entries[st->nb_index_entries++];
                    e->pos = current_offset;
                    e->timestamp = current_dts;
                    e->size = sample_size;
                    e->min_distance = distance;
                    e->flags = keyframe ? AVINDEX_KEYFRAME : 0;
...
                    if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && st->nb_index_entries < 100)
                        ff_rfps_add_frame(mov->fc, st, current_dts);
                }

                current_offset += sample_size;//sample的文件位置偏移向后移动当前sample的大小
...
//更新current_dts和last_dts
                current_dts += sc->stts_data[stts_index].duration;
                if (!dts_correction || current_dts + dts_correction > last_dts) {
                    current_dts += dts_correction;
                    dts_correction = 0;
                } else {
                    /* Avoid creating non-monotonous DTS */
                    dts_correction += current_dts - last_dts - 1;
                    current_dts = last_dts + 1;
                }
                last_dts = current_dts;
                distance++;//与前一个关键帧的距离更新
                stts_sample++;//stts_sample是针对stts,sample在每个entry中的序号,不是实际的序号。
                current_sample++;//更新当前sample的序号。
                if (stts_index + 1 < sc->stts_count && stts_sample == sc->stts_data[stts_index].count) {//stts_data[stts_index]中的sample都遍历完了。
                    stts_sample = 0;
                    stts_index++;
                }
            }
        }
        if (st->duration > 0)
            st->codecpar->bit_rate = stream_size*8*sc->time_scale/st->duration;
  }
...
    // Update start time of the stream.
    if (st->start_time == AV_NOPTS_VALUE && st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && st->nb_index_entries > 0) {
        st->start_time = st->index_entries[0].timestamp + sc->dts_shift;
        if (sc->ctts_data) {
            st->start_time += sc->ctts_data[0].duration;
        }
    }

    mov_estimate_video_delay(mov, st);

3.6 mov_read_tkhd

  • 读取flags,如果flags开启了MOV_TKHD_FLAG_ENABLED则,AVStreamst.disposition置为AV_DISPOSITION_DEFAULT,否则置为0。 具体的作用,尚未了解
  • 读取track id,保存到AVStream.id。
  • 读取matrix,根据matrix和读mvhd得到的MOVContext.movie_display_matrix计算的到res_display_matrix,保存到MOVStreamContext.display_matrix。具体的作用,尚未了解
    • 根据MOVStreamContext.display_matrix一通计算得到AVStream.sample_aspect_ratio。具体的作用,尚未了解

3.7 mdia(mov_read_default)

3.8 mdhd(mov_read_mdhd)

  • 一些异常检查。
    • AVFormatContex中没有AVStream的数量小于1,直接返回。
    • 检测到重复的mdhd box,返回错误。
    • version 大于1,返回错误。
  • 读取creation_time,通过strftime转换为格式化的字符串的时间表示,保存在AVStream.metadata(AVDictionary结构体)中。
  • 读取time_scale,保存在MOVStreamContext.time_scale。
    • 校验有效性,如果小于等于0则置为1。
  • 读取duration,保存在AVStream.duration。
  • 读取language,保存在AVStream.metadata中,char*的形式,key值为"language"。

3.9 hdlr(mov_read_hdlr)

  • 读取handler_type。
    • 如果handler_type为"vide"、"soun"或"subp",设置AVStream->codecpar->codec_type为AVMEDIA_TYPE_VIDEO、AVMEDIA_TYPE_AUDIO或AVMEDIA_TYPE_SUBTITLE。
    • 如果handler_type为"m1a",设置AVStream->codecpar->id为AV_CODEC_ID_MP2。
    • 读取name,保存到AVStream.metadata,key为"handler_name",类型为char *。

3.10 minf(mov_read_default)

3.11 stbl(mov_read_default)

3.12 stsd(mov_read_stsd)

  • 一些异常检查。
    • AVFormatContex中没有AVStream的数量小于1,直接返回。
    • 读取到的entry数量<=0或者超过最大可能的数,则返回错误。
    • 检测到重复的mdhd box,返回错误。
  • 读取version,保存到MOVStreamContext.stsd_version。
  • 读取format,保存到MOVStreamContext.format。
    • 根据format,找到相应的codec_id,赋值给AVStream->codecpar->codec_id。
  • 对于视频(即handler_type为"vide")
    • 读取width和height保存到AVStream.codecpar的width和height。
    • 读取compressorname,即编码器的名称到AVStream.metadata,key为"encoder",类型为char *。
    • 读取depth,即位深,保存到AVStream.codecpar->bits_per_coded_sample。
  • 对于音频(即handler_type为"soun")
    • 读取channelcount保存到AVStream.codecpar->channels。
    • 读取samplesize,即采样比特,保存到AVStream->codecpar->bits_per_coded_sample
    • 读取audio channel id,保存到MOVStreamContext.audio_cid。
    • 读取samplerate,保存到AVStream->codecpar->sample_rate
  • 读取额外的box,比如wave, alac, damr, avcC, hvcC, SMI等。

3.13 stts(mov_read_stts)

  • 一些异常检查。
  • 读取time to sample表到MOVContext.stts_data(MOVStts数组)。
typedef struct MOVStts {
    unsigned int count;
    int duration;
} MOVStts;
  • 根据stts表,统计sample总数,保存到AVStream.nb_frames。
  • 根据stts表,统计时长duration。
    • 校正AVStream.duration,注意mdhd中有读取duration信息到AVStream.duration。
    • 保存到MOVContext.track_end。
if (duration)
        st->duration= FFMIN(st->duration, duration);

3.14 ctts(mov_read_ctts)

  • 读取composition time to sample表到MOVContext.ctts_data(MOVStts数组)。
  • 更新MOVStreamContext.dts_shift。

3.15 stss(mov_read_stss)

  • 如果读取到的entries值为0且此媒体类型为video,则AVStream.need_parsing置为AVSTREAM_PARSE_HEADERS。
  • 读取关键帧序号数组,保存到MOVStreamContext.keyframes(int 数组)。
  • 统计到的关键帧数组元素的数量保存到MOVStreamContext.keyframe_count。

3.16 stsz/stz2(mov_read_stsz)

  • 读取sample_size,保存到MOVStreamContext.stsz_sample_size。
  • 如果是stz2 box,读取field_size。stsz box时field_size设置为32。
  • 读取entries,保存到MOVStreamContext.sample_count。
  • 如果sample_size不为0,返回。
  • 保存sample_size的数组到MOVStreamContext.sample_size(int 数组)
  • 总计的sample的size总和保存到MOVStreamContext.data_size。
  • 统计到的sample_size数组的元素的个数保存到MOVStreamContext.sample_count。

3.17 stsc(mov_read_stsc)

  • 读取sample to chunk表,保存到MOVStreamContext.stsc_data(MOVStsc数组)
typedef struct MOVStsc {
    int first;
    int count;
    int id;
} MOVStsc;
  • 统计到的MOVStsc元素的个数保存到MOVStreamContext.stsc_count。
  • 对数组做一些有效性检查并校正。

3.18 stco/co64(mov_read_stco)

  • 读取chunk offset 表保存到MOVStreamContext.chunk_offsets(int64_t数组)
  • 数组元素的个数保存到MOVStreamContext.chunk_count。

3.19 mdat(mov_read_mdat)

  • 如果body的size为0,则返回0。
  • 否则,MOVContext.found_mdat置为1。

4. mov_read_packet

mov_read_packet的调用时机如下图所示:


av_read_frame.png

mov_read_packet中的主要工作:

mov_find_next_sample
avio_seek
av_get_packet
pkt->dts = ...; pkt->pts = ..; ...

  1. mov_find_next_sample找到要读取的sample和对应的AVStream。
  2. avio_seek(sc->pb, sample->pos, SEEK_SET); 将文件的指针定位到文件的偏移处
  3. av_get_packet(sc->pb, pkt, sample->size); 从文件的偏移读取size的数据到AVPacket中。
  4. 计算AVPacket的dts和pts。

4.1 mov_find_next_sample

static AVIndexEntry *mov_find_next_sample(AVFormatContext *s, AVStream **st)
{
    AVIndexEntry *sample = NULL;
    int64_t best_dts = INT64_MAX;
    int i;
    for (i = 0; i < s->nb_streams; i++) {
        AVStream *avst = s->streams[i];
        FFStream *const avsti = ffstream(avst);
        MOVStreamContext *msc = avst->priv_data;
        if (msc->pb && msc->current_sample < avsti->nb_index_entries) {
            AVIndexEntry *current_sample = &avsti->index_entries[msc->current_sample];
            int64_t dts = av_rescale(current_sample->timestamp, AV_TIME_BASE, msc->time_scale);
            av_log(s, AV_LOG_TRACE, "stream %d, sample %d, dts %"PRId64"\n", i, msc->current_sample, dts);
            if (!sample || (!(s->pb->seekable & AVIO_SEEKABLE_NORMAL) && current_sample->pos < sample->pos) ||
                ((s->pb->seekable & AVIO_SEEKABLE_NORMAL) &&
                 ((msc->pb != s->pb && dts < best_dts) || (msc->pb == s->pb && dts != AV_NOPTS_VALUE &&
                 ((FFABS(best_dts - dts) <= AV_TIME_BASE && current_sample->pos < sample->pos) ||
                  (FFABS(best_dts - dts) > AV_TIME_BASE && dts < best_dts)))))) {
                sample = current_sample;
                best_dts = dts;
                *st = avst;
            }
        }
    }
    return sample;
}
  1. 如果流的输入和文件的输入不是同一个文件,(msc->pb != s->pb && dts < best_dts),则选择dts更小的流。
  2. 如果流的输入和文件的输入是同一个文件且dts的差异小于AV_TIME_BASE(即1s)则选择pos更小的那个流的packet,即offset更小的那个packet,如果大于1s,则选择dts更小的那个流的packet。这里的dts是算上editlist作用后的dts。

4.2 计算AVPacket的dts和pts。

//mov_read_packet()
    pkt->dts = sample->timestamp;
    if (sample->flags & AVINDEX_DISCARD_FRAME) {
        pkt->flags |= AV_PKT_FLAG_DISCARD;
    }
    if (sc->ctts_data && sc->ctts_index < sc->ctts_count) {
        pkt->pts = pkt->dts + sc->dts_shift + sc->ctts_data[sc->ctts_index].duration;
        /* update ctts context */
        sc->ctts_sample++;
        if (sc->ctts_index < sc->ctts_count &&
            sc->ctts_data[sc->ctts_index].count == sc->ctts_sample) {
            sc->ctts_index++;
            sc->ctts_sample = 0;
        }
    } else {
        int64_t next_dts = (sc->current_sample < st->nb_index_entries) ?
            st->index_entries[sc->current_sample].timestamp : st->duration;

        if (next_dts >= pkt->dts)
            pkt->duration = next_dts - pkt->dts;
        pkt->pts = pkt->dts;
    }
    if (st->discard == AVDISCARD_ALL)
        goto retry;
    pkt->flags |= sample->flags & AVINDEX_KEYFRAME ? AV_PKT_FLAG_KEY : 0;
    pkt->pos = sample->pos;
  • dts为sample->timestamp。
  • 如果有ctts表(创建时间与编码时间的差值表),根据ctts表计算pts。pkt->pts=pkt->dts + sc->dts_shift + sc->ctts_data[sc->ctts_index].duration,不知道为什么要加上sc->dts_shift?
  • 如果没有ctts表,pkt->pts = pkt->dts,并且计算pkt->duration== next_dts - pkt->dts

5. mov_read_seek

mov_read_seek的调用时机如下所示。


AVInputFormat.read_seek

未完待续。。

6. mov_read_close

avformat_close_input
  - AVInputFormat.read_close --> mov_read_close 
  • 释放各种申请的资源和内存。比如MOVContext和MOVStreamContext中申请的各种内存。
static int mov_read_close(AVFormatContext *s)
{
    MOVContext *mov = s->priv_data;
    int i, j;

    for (i = 0; i < s->nb_streams; i++) {
        AVStream *st = s->streams[i];
        MOVStreamContext *sc = st->priv_data;

        if (!sc)
            continue;
...
        if (!sc->pb_is_copied)
            ff_format_io_close(s, &sc->pb);

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