锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

FFmpeg的MOV封装解析

时间:2022-11-08 11:30:00 stps10l40ct二三极管

目录

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

1. 参考

  • [1] github.com/FFmpeg/FFmpeg/blob/master/libavformat/mov.c
  • [2] wangcong02345/ffmpeg源码分析--11.mov的mov_read_header
  • [3] wangcong02345/ffmpeg源码分析--12.mov的mov_read_packet
  • [4] QQ音乐技术团队/关于M4A随机访问文件

2. 概述

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

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

3. mov_read_header

mov_read_header调用时间如下图所示。

  • 在调用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定义为1万个示例julin_5s.mp4文件的mvhd中duration为5022,timescale因此计算得到1000c->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 distanc 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、stsz来计算得到。
  • 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的调用时机如下图所示:

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

  1. 从多个AVStream中选择其中当前sample的pos或者dts更小的作为当前要处理的AVStream和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];
        MOVStreamContext *msc = avst->priv_data;
        if (msc->pb && msc->current_sample < avst->nb_index_entries) {
            AVIndexEntry *current_sample = &avst->index_entries[msc->current_sample];
            int64_t dts = av_rescale(current_sample->timestamp, AV_TIME_BASE, msc->time_scale);
            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 &&
                 ((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;
}

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的调用时机如下所示。



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

 

 

锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章