MP4文件的组成
MP4文件的格式遵循ISO/IEC 14496-12标准,即ISO base media file format。所有数据都封装在被称为Box的数据结构中,一个MP4文件,是由多个Box组成的。
如上图所示,该MP4文件由ftype、free、mdat和moov四个Box组成。
其中moov Box属于container box,它又可以包含有其他的Box。它里面保存的数据如下图所示
- 在这里moov box及其子box包含了该MP4文件的元数据,用于指定音视频数据的存储位置,数据类型,时间戳之类的信息。
- mdat box为长度最大的box,该文件中的音视频数据都包含在该box中,可以通过解析moov box来获取每帧音视频数据具体保存的位置。
- moov box包含有每帧音视频数据在文件中的偏移量信息,所以一般都是位于文件尾,用于方便保存文件时记录偏移量信息。但是也可以通过其他方式将其移动到文件前面的位置(MP4box和ffmpeg都可以做到),这样做的好处是播放器在播放网络上的MP4文件时,可以直接读取到文件的索引信息,使得开播更快。
Box结构的定义
- size字段表示该Box的长度,如果size值为1,则表示box的长度超过了32位的表示范围,需要由type之后的64位用于表示实际的长度。
- type字段表示该Box的类型,一般使用4个可打印的字符组合表示,也称为FOURCC,如ftyp、moov、meta、mdat等。
- 大部分box除了包含有size和type字段外,还包含有version和flag字段,用于处理在标准升级时产生的box内容定义不一致的问题。
- 除去以上数据后box剩余的数据为该box的实际数据,根据type不同,表示的含义也各不相同。
moov box
如上图所示,moov box中会包含有一个mvhd box和一个或多个trak box,每个trak box表示一个音视频的流。
-
mvhd box
定义如下: mvhd box中的duration和timescale字段用来指定该文件的播放时长,duration/timescale的值即为单位为秒的时长。如果文件中多个流的时长不一致,该位置为最大时长。如下图所示,该文件的播放时长为189167/1000=189.167秒。
-
trak box
每个trak box表示一路单独的流,可能是音频也可能是视频。
mdia box下的hdlr box用来指定该流是音频还是视频
stsd box的子box用于保存该流的编码类型
上图中avcC box指定了该流的编码类型为H264,且存储了解码所需的SPS、PPS信息。
stsc stsz stco三个box用于保存没帧视频或音频数据在文件中的保存位置。
stts stss ctts三个box用于保存媒体数据和时间戳的对应关系。
Sample(音视频帧)保存位置的计算
- stsz(SampleSizeBox)用于保存每个sample对应的大小
sample_count字段指明sample的个数;
如果每个sample大小都相等的话,则sample_size字段为sample的大小。否则sample_size设置为0,每个sample的大小由后续的一个数组来指定。 -
stsc (SampleToChunkBox)
多个sample组成一个chunk,stsc box保存了sample和chunk之间的对应关系。
每个chunk可以有一个或多个sample,如果相邻的chunk含有相同的sample数量,则first_count字段用于指明第一个chunk的索引,sample_per_chunk指明该组chunk中每个chunk中sample的数量。
如上图
index为1的chunk含有3个sample;
index为2的chunk含有1个sample;
然后下一个first_chunk值为4,则表明index为3的chunk含有和2相同数量的sample,也是1个;
继续,index为4的chunk含有2个sample;
index为5和6的chunk含有1个sample;7有2个sample;8有1个sample;9含有2个sample; -
stco(ChunkOffsetBox)
stco box指明了每个chunk在文件中的存储位置
entry_count指明了总的chunk的数量
chunk_offset指明了该chunk在文件中的偏移量
以上三个box结合起来,即可计算每个sample在文件中保存的位置和大小
void mp4Parser::GetSamplePosition(Stream* s)
{
int sample_count = s->stsz_count;
int chunk_count = s->stco_count;
if(sample_count > 0)
{
s->sample_position = new uint64_t[sample_count];
}
int remain_chunk_count = chunk_count;
int sample_index = 0;
for(int i=0;istsc_count;i++)
{
int c_count = 0;
if (i != s->stsc_count - 1)
{
c_count = s->stsc_data[i + 1].first_chunk - s->stsc_data[i].first_chunk;
remain_chunk_count -= c_count;
}
else
{
c_count = remain_chunk_count;
}
for (int j = 0; j < c_count; j++)
{
int chunk_index = s->stsc_data[i].first_chunk + j;
uint64_t offset = s->stco_data[chunk_index - 1];
for (int k = 0; k < s->stsc_data[i].samples_per_chunk; k++)
{
s->sample_position[sample_index] = offset;
offset += s->stsz_data[sample_index];
sample_index++;
if (sample_index > sample_count)
return;
}
}
}
}
PTS和DTS的计算
I P B 帧的概念
在视频压缩中,为了提高压缩率,会将每帧画面压缩为不同类型的视频帧数据。
I帧表示关键帧,包含有一帧画面的完整信息,解码时只需要本帧数据就可以解码出完整的一帧画面。
P帧表示前向参考帧,它保存了本帧与上一帧的差异信息,它不能单独解码,需要根据上一帧的画面加上本帧保存的差值来获取本帧的完整画面。
B帧为双向参考帧,它解码时需要依赖它之前和之后的帧来获取最终的画面
因为B帧需要依赖它后面的帧来进行解码,所以它的解码顺序就必然和显示顺序不能保持一致,这是就需要解码时间戳(DTS)和显示时间戳(PTS)来共同决定一帧视频数据何时解码,然后何时显示了。-
stts(TimeToSampleBox)
根据stts box可以计算出每个sample的dts,其中sample_delta为该sample的dts相对于上一个smaple的差值,比如entry_count=1,sample_count=5,sample_delta=1024
时,5个sample的dts将依次为0 1024 2048 3072 4096
。 -
ctts(CompositionOffsetBox)
cttsbox保存了每个sample的composition time和decode time之间的差值,这里CompositionTime就直接理解成PTS吧。
如果不存在ctts box,则代表该流不存在B帧,那么PTS就直接等于DTS,例如音频数据就不存在ctts box。
根据stts和ctts两个box可以计算出sample的DTS和PTS -
stss(SyncSampleBox)
stss box保存了哪些帧是关键帧(即I帧),做seek跳转时,视频需要从关键帧开始解码,否则解码会出现异常。
示例
这里我们选择一个只有5帧画面的MP4文件进行分析
stsz内容:
sample_count = 5
index = 1, size = 919
index = 2, size = 39
index = 3, size = 36
index = 4, size = 36
index = 5, size = 36
stsc内容:
entry_count = 2
first_chunk = 1, samples_per_chunk = 3, sample_description_index = 1
first_chunk = 2, samples_per_chunk = 1, sample_description_index = 1
stco内容
entry_count = 3
index = 1, chunk_offset = 48
index = 2, chunk_offset = 1051
index = 3, chunk_offset = 1096
index为1、2、3的三帧组成为chunk1
chunk1的起始地址为48,则sample1的起始地址为48,sample2的起始地址为48+919=967(919为sample1的大小),sample3的起始地址为967+39=1006(39为sample2的大小)。
chunk2和chunk3只包含有1个sample,分别为sample4和sample5
chunk2的起始地址为1051,则sample4的起始地址为1051
chunk3的起始地址为1096,则sample5的起始地址为1096
stts内容:
stts_count = 1
count:5, delte:512
ctts内容:
ctts_count = 5
count:1, offset:1024
count:1, offset:2560
count:1, offset:1024
count:1, offset:0
count:1, offset:512
根据stts可知,5个sample的DTS分别为 0、512、1024、1536、2048
与ctts内容相加,可得PTS分别为1024、3072、2048、1536、2560
即实际显示的顺序应该是按照PTS从小到大的顺序(1、4、3、5、2)
-
DTS和PTS值转换为时间
以上计算出来的DTS和PTS为一个整形的数值,但是他们如何转换为以秒为单位的实际时间呢?
参看上面第二幅图,moov/trak/mdia/mdhd这个顺序下的mdhd box
此box中有和mvhd中同样的timesacle和duration字段,两处并不一定一致,mdhd box中的timescale和duration表示当前流的时长,duration/timescale的值即为当前流的时长。
同样,PTS和DTS除以timescale即为相应的以秒为单位的时间
上面那个例子中,视频流的timescale=15360,则相应的DTS和PTS应该为(0、0.033、0.067、0.1、0.133)(0.067、0.2、0.133、0.1、0.167)。
elst(EditListBox)
moov/trak/edts/elst box同样对PTS会产生影响,它可以是实际时间戳产生偏移
- segment_duration:表示该edit段的时长,以Movie Header Box(mvhd)中的timescale为单位。
- media_time:表示该edit段的起始时间,以track中Media Header Box(mdhd)中的timescale为单位。如果值为-1,表示是空edit,一个track中最后一个edit不能为空。
- media_rate:edit段的速率为0的话,edit段相当于一个”dwell”,即画面停止。画面会在media_time点上停止segment_duration时间。否则这个值始终为1。
为使PTS从0开始,media_time字段一般设置为第一个CTTS的值,计算PTS和DTS的时候,他们分别都减去media_time字段的值就可以将PTS调整为从0开始的值
如果media_time是从一个比较大的值,则表示要求PTS值大于该值时画面才进行显示,这时应该将第一个大于或等于该值的PTS设置为0,其他的PTS和DTS也相应做调整
如果elst box中有多个设置,表示会有多段的显示,具体用法这里不再说明,可以查询elst box用法。
本文用到的MP4解析工具:https://github.com/mayudong1/MediaParser