Android-mp4文件详解

                          MP4文件简介

一、MP4文件格式:

                  Android-mp4文件详解_第1张图片

          Android-mp4文件详解_第2张图片

  mp4是由很多box组成的,每个box包含header和data,其中data可以是数据,也可以是别的box。

  其中主要的box有:ftypbox、moovbox、mdatbox等。

  • ftypbox,有且只有一个,在文件的开始位置,描述的文件的版本、兼容协议等;
  • moovbox,有且只有一个,这个box中不包含具体媒体数据,但包含本文件中所有媒体数据的宏观描述信息,moov box下有mvhd和trak box。

   >>mvhd中记录了创建时间、修改时间、时间度量标尺、可播放时长等信息。

   >>一般mp4中都包含两个track,一个audio track,一个video track。track里面包含了mp4中各种信息表,如: stts/ctts/stss/等。

  • mdatbox,可以有多个,也可以没有,实际媒体数据。我们最终解码播放的数据都在这里面。

 

二、 stts/ctts/stsc/stsz等表格的解析

1. stts: Time-To-Sample Atoms

  Time-To-Sample Atoms,存储了媒体sample的时长信息,提供了时间和相关sample之间的映射关系。该atom包含了一个表,关于time和sample号之间的索引关系。表的每个entry给出了具有相同时间间隔的连续的sample的个数和这些sample的时间间隔值。将这些时间间隔相加在一起,就可以得到一个完整的time与sample之间的映射。将所有的时间间隔相加在一起,就可以得到该track的时间总长。

                Android-mp4文件详解_第3张图片

  例如上表表示,有4个sample的duration为3,2个sample的duration为1, 3个sample的duration为2……注意,这里的duration并不是真实时间,是由时间戳和timescale计算得到的值。

具体stts的写入过程如下:

void MPEG4Writer::Track::addOneSttsTableEntry(
        size_t sampleCount, int32_t duration) {

    if (duration == 0) {
        ALOGW("0-duration samples found: %zu", sampleCount);
    }
    mSttsTableEntries->add(htonl(sampleCount));
    mSttsTableEntries->add(htonl(duration));
}

void MPEG4Writer::Track::writeSttsBox() {
    mOwner->beginBox("stts");
    mOwner->writeInt32(0);  // version=0, flags=0
    if (mMinCttsOffsetTicks == mMaxCttsOffsetTicks) {
        // For non-vdeio tracks or video tracks without ctts table,
        // adjust duration of first sample for tracks to account for
        // first sample not starting at the media start time.
        // TODO: consider signaling this using some offset
        // as this is not quite correct.
        uint32_t duration;
        CHECK(mSttsTableEntries->get(duration, 1));
        duration = htonl(duration);  // Back to host byte order
        mSttsTableEntries->set(htonl(duration + getStartTimeOffsetScaledTime()), 1);
    }
    mSttsTableEntries->write(mOwner);
    mOwner->endBox();  // stts
}

  所以,当mMinCttsOffsetTicks == mMaxCttsOffsetTicks (无B帧)的情况下,第一帧的duration需要加上starttimeoffsetScaledTime。其中starttimeoffsetScaledTime是当前track的startime减去所有track中最小的startime。

 

2. ctts: Composition Offset Atom

ctts:Composition Offset Atom。每一个视频sample都有一个解码时间和一个显示时间。在有B帧的情况下解码时间和显示时间不一致,这时候就需要通过ctts去做调整。

 Android-mp4文件详解_第4张图片

  如上表,其中DT为解码时间,CT为显示时间,composition offset为ctts表中保存的每帧的偏移量。ctts[n] = CT[n] - DT[n]。

ctts中数据的保存方法如下:

void MPEG4Writer::Track::addOneCttsTableEntry(
        size_t sampleCount, int32_t duration) {

    if (!mIsVideo) {
        return;
    }
    mCttsTableEntries->add(htonl(sampleCount));
    mCttsTableEntries->add(htonl(duration));
}

void MPEG4Writer::Track::writeCttsBox() {
    // There is no B frame at all
    if (mMinCttsOffsetTicks == mMaxCttsOffsetTicks) {
        return;
    }

    // Do not write ctts box when there is no need to have it.
    if (mCttsTableEntries->count() == 0) {
        return;
    }

    ALOGV("ctts box has %d entries with range [%" PRId64 ", %" PRId64 "]",
            mCttsTableEntries->count(), mMinCttsOffsetTicks, mMaxCttsOffsetTicks);

    mOwner->beginBox("ctts");
    mOwner->writeInt32(0);  // version=0, flags=0
    int64_t deltaTimeUs = kMaxCttsOffsetTimeUs -
                mOwner->getStartTimeOffsetTimeUs(mStartTimestampUs);
    int64_t delta = (deltaTimeUs * mTimeScale + 500000LL) / 1000000LL;
    mCttsTableEntries->adjustEntries([delta](size_t /* ix */, uint32_t (&value)[2]) {
        // entries are  pairs; adjust only ctts
        uint32_t duration = htonl(value[1]); // back to host byte order
        // Prevent overflow and underflow
        if (delta > duration) {
            duration = 0;
        } else if (delta < 0 && UINT32_MAX + delta < duration) {
            duration = UINT32_MAX;
        } else {
            duration -= delta;
        }
        value[1] = htonl(duration);
    });
    mCttsTableEntries->write(mOwner);
    mOwner->endBox();  // ctts
}

3. stsc: Sample-To-Chunk Atom。

  为了优化数据访问,通常把sample封装到chunk中,一个chunk可能会包含一个或者多个sample。每个chunk会有不同的size,每个chunk中的sample也会有不同的size。stsc table提供了从sample到chunk的一个映射,每个table entry可能包含一个或者多个chunk。Table entry包含的内容包括第一个chunk号、每个chunk包含的sample的个数以及sample的描述ID:

          Android-mp4文件详解_第5张图片

如上表表示,第1、2个chunk包含的sample个数都是3个,sample的ID都是23,第3、4个chunk包含的sample个数都是1个,sample的ID都是23,第5个到最后一个chunk包含的sample个数都是1个,sample的ID都是24.

stsc中数据的保存方法如下:

void MPEG4Writer::Track::addOneStscTableEntry(
        size_t chunkId, size_t sampleId) {
    mStscTableEntries->add(htonl(chunkId));
    mStscTableEntries->add(htonl(sampleId));
    mStscTableEntries->add(htonl(1));
}

void MPEG4Writer::Track::writeStscBox() {
    mOwner->beginBox("stsc");
    mOwner->writeInt32(0);  // version=0, flags=0
    mStscTableEntries->write(mOwner);
    mOwner->endBox();  // stsc
}

4.stsz: Sample Size Atom

stsz表中存储了每个sample的size。

                  Android-mp4文件详解_第6张图片

如上表表示,1-5个sample的size都是"size"。

stsc中数据的保存方法如下:

在MPEG4Writer::Track::threadEntry()中会调用mStszTableEntries->add(htonl(sampleSize));把每个sample的size写入mStszTableEntries。

void MPEG4Writer::Track::writeStszBox() {
    mOwner->beginBox("stsz");
    mOwner->writeInt32(0);  // version=0, flags=0
    mOwner->writeInt32(0);
    mStszTableEntries->write(mOwner);
    mOwner->endBox();  // stsz
}

5.stco/co64: Chunk Offset Atom

stco/co64中存储了每个chunk在文件中的位置。stco表示use32BitFileOffset,co64表示use64BitFileOffset。

                  Android-mp4文件详解_第7张图片

如上表表示,1-5个chunk在文件中的位置"offset"。

stco中数据的保存方法如下:

  在MPEG4Writer::Track::threadEntry()中会调用addChunkOffset

  MPEG4Writer::Track::addChunkOffset中调用mStcoTableEntries->add(htonl(value))把每个chunk的offset写入mStcoTableEntries。

void MPEG4Writer::Track::writeStcoBox(bool use32BitOffset) {
    mOwner->beginBox(use32BitOffset? "stco": "co64");
    mOwner->writeInt32(0);  // version=0, flags=0
    if (use32BitOffset) {
        mStcoTableEntries->write(mOwner);
    } else {
        mCo64TableEntries->write(mOwner);
    }
    mOwner->endBox();  // stco or co64
}

ps: 每个chunk的大小受音视频交错时间的影响,音视频交错时间越长,chunk的size越大。音视频交错时间在MPEG4Writer.cpp中用mInterleaveDurationUs表示,初始化为1s,如果APP调用的是mediarecorder也可通过(key == "interleave-duration-us")去设置。

6. stss: Sync Sample Atom

  标记了每个关键帧的sample。如果该表不存在,就表示媒体流每个sample都是关键帧。

                   Android-mp4文件详解_第8张图片

  如上表表示第1个关键帧是第1帧,第2个关键帧是第31帧,第3个关键帧是第61帧,第4个关键帧是第91帧……

stss中数据的保存方法如下:

void MPEG4Writer::Track::addOneStssTableEntry(size_t sampleId) {
    mStssTableEntries->add(htonl(sampleId));
}

void MPEG4Writer::Track::writeStssBox() {
    mOwner->beginBox("stss");
    mOwner->writeInt32(0);  // version=0, flags=0
    mStssTableEntries->write(mOwner);
    mOwner->endBox();  // stss
}

三、 具体应用

step1: 具体播放时,比如用户需要seek到某一位置,播放器首先可以根据进度条得到用户seek到的时间seektime。

step2: 利用stts表,根据seektime找到其对应的seeksample

step3: 利用stss表,查看seeksample是否为关键帧,如果是则继续,如果不是则按照播放器规定的方法(查找最邻近关键帧、查找前面一个关键帧、查找后面一个关键帧)更新seeksample。

step4: 利用stsc表,根据通过seeksample找到对应的seekchunk和其在chunk中是第几个sample

step5: 利用stsz表,计算seeksample在其所在chunk中的地址偏移量sampleindex

step6: 利用stco表,查找seekchunk在文件中的位置chunkindex

step7: chunkindex + sampleindex 就是用户seek到的帧在文件中的位置。

step8: 取出数据去解码。

step9: 如果文件存在B帧需要根据ctts去调整pts,否则pts=dts。

step10: rennder

 

                       ffmpeg解析mp4文件

命令:

ffprobe -unit -show_frames -select_streams V -i input.mp4 -print_format xml > output.xml

 

部分解析结果:

Android-mp4文件详解_第9张图片

 

部分参数解析:

pkt_dts:

  pkt_dts[1] = 0;

  pkt_dts[n+1] = pkt_dts[n] + stts[n];

pkt_dts_time:

  pkt_dts_time[n] = pkt_dts[n] / timescale;

pkt_pts:

  pkt_pts[n] = pkt_dts[n]; 无B帧的情况

  pkt_pts[n] = pkt_dts[n] + ctts[n]; 有B帧的情况

pkt_pts_time:

  pkt_pts_time[n] = pkt_pts[n] / timescale;

 

                          实际问题分析

起因:

视频录制帧率不达标

 

720P@30fps

720P@60fps

1080P@30fps

1080P@60fps

4k@30fps

4k@60fps

EIS on

24.5fps

55.3fps

25.3fps

54.6fps

25.8fps

52.7fps

EIS off

29.9fps

58.6fps

29.9fps

59.3fps

29.2fps

58.4fps

 

原因分析:

  加入视频防抖功能后,视频数据需要经过好几个pipline的处理才送给encoder,所以视频数据的StartTime较大,一般为200ms。音频由于数据量少且处理简单,所以音频数据的StartTime较小,一般为30ms。

  在MPEG4Writer中会用stts表格记录每帧的duration,第一帧的duration为第二帧timestamp减去第一帧timestamp再加上mStartTimestampUs。其中mStartTimestampUs是当前track的StartTime减去所有track中最小的StartTime。因此,stts中记录的视频第一帧的duration一般为230ms。

  ffmpeg解析视频的frame rate,是根据stts中视频帧的个数之和除以stts中视频帧的duration之和。由于视频第一帧的duration为230ms,所以计算的frame rate就会变小。

 

解决办法:

基于以上分析我们研究了两个解决方案:

a.在MediaRecorder中增加startoffset

这个方案就是在录像开始的时候扔掉startoffset时长的数据,保证音视频第一帧有效数据的时间戳相差不大。

b. 让MPEG4Writer中stts第一帧的duration为实际duration(不加mStartTimestampUs),并在mpeg4中增加kWaitDuration。

这个方案首先设置第一帧的duration,从而改变ffmpeg解析出的视频总时长。这个改动可以提高计算的帧率,但是音视频的时长差会变大,视频播放结束后,音频还会继续

播放。所以需要再增加kWaitDuration,在视频录制结束时,让video继续录制kWaitDuration时间后再结束。

a方案的优点,音视频开始时间戳对齐更准。缺点,录像开始需要丢掉部分数据。

b方案的优点,开始时候不用丢掉很多数据。缺点,audio和video的时间戳无法对齐。

针对以上方案我们进行了对比测试,两个方案均可fix此问题。方案a用户体验更好,所以最终选择方案a。

 

结果: 

帧率达标。

 

720P@30fps

720P@60fps

1080P@30fps

1080P@60fps

4k@30fps

4k@60fps

EIS on

30.04fps

60.01fps

30.03fps

60.07fps

29.97fps

59.57fps

EIS off

30.06fps

60.01fps

30.03fps

60.08fps

30.03fps

60.01fps

 

 

 

 

 

 

 

 

你可能感兴趣的:(安卓基础)