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
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(音视频帧)保存位置的计算
每个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;
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;i<s->stsc_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)来共同决定一帧视频数据何时解码,然后何时显示了。
示例
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
MP4 解析工具
MP4 Inspector;
https://sourceforge.net/projects/mp4-inspector/files/
https://jaist.dl.sourceforge.net/project/mp4-inspector/MP4%20Inspector%20-%200.1.2.0.rar
在线解析
https://www.onlinemp4parser.com/