本文通过对一个真实mp4文件进行层层剖析,来了解一下mp4文件的具体组成。
我们借助一个在线的mp4解析工具mp4parse来进行分析和验证。
名词定义
在解析mp4封装格式之前,我们需要先了解mp4涉及的名词定义,如下所示:
- box:也叫Atom,是mp4文件的基本组成单位,由唯一类型标识符(type)和长度(size)定义的面向对象的构建块,包括header和data两部分
- Fullbox:基于box的扩展,在box的基础上增加了versino(1字节)和flags(3字节)
- Containerbox:当一个box的data中包括其他box时,该box被称为一个container box
- header:box头文件,包括size和type两部分,如果是fullbox,则还包括versino(1字节)和flags(3字节)
- data:box的真实数据,可以是具体数据也可以是嵌套的box
- sample:可以看成媒体数据的基本组成单位。video sample表示一帧数据或者一组连续视频帧。audio sample 是一段连续的压缩音频,包含一定数量的音频采样
- chunk:同一个媒体类型连续几个sample组成的集合。每个chunk的sample数量不固定,可以一样,可以不一样
- track:一个包含连续时间戳的完整媒体资源,一般mp4文件包括audio track和video track
box结构可以由下图来所示:
图片来自MP4文件格式详解--结构概述
mp4结构组成
mp4文件是由一个一个的box组成,box可以看成是mp4的基本组成单元。具体有哪写box见ISO_IEC_14496-12文档 table 1,也可以参考这篇文章
实例分析
我们以一个具体的mp4文件来分析下每个具体box。每个box从二进制数据、box结构、字段解析表等几个部分来进行介绍。分析所用的视频点击这里下载。
ftyp(file type box)
ftyp是mp4文件的标识box,在文件中有且只有一个,并放在文件最开始的位置。其中包含了当前视频遵循的具体规范及其版本号。
moov(movie box)
Moov box是个container box,具体数据存储在子box当中,有且只有一个。主要用来存储mp4元数据信息,比如音视频时长,timescale等。一般在点播文件中为了快速打开视频文件,放在mdat之前,紧随ftyp出现。如果是录播的数据,一般放在mdat 之后。
mvhd(Movie Header Box )
Mvhd box 是moov的子box, 定义了媒体相关的整体信息,如文件创建、修改时间、视频时长,timescale等。
trak(track box)
Trak 是个container box,是moov的子box。 moov中可以有一个或者多个trak box。每个trak中包含了该track的元数据信息。
tkhd(track header box)
tkhd是trak的子box,是个full box,该Track的媒体整体信息包括时长、图像的宽度和高度等。一般默认header中的flags字段的默认值是7,通过按位或操作获得,既track_enabled(0x000001)、track_in_movie(0x000002)、track_in_preview(0x000004)三种值的按位或结果值。falgs对应的值有以下几种:
- Track_enabled:值为0x000001,表示这个track是启用的,当值为0x000000,表示这个track没有启用;
- Track_in_movie:值为0x000002,表示当前track在播放时会用到;
- Track_in_preview:值为0x000004,表示当前track用于预览模式;
- Track_size_is_aspect_ratio: 值为0x000008,表示宽高不以像素为单位表示
mdia(media box)
Mdia box是trak的子box,是个container box,所以只有box header ,data部分是子box,需要进一步解析
mdhd(media header box)
mdhd是mdia的子box,是个full box。该Box里面主要定义了媒体无关的整体信息,其中最关心的两个字段是timescale和duration,分别表示了该Track的时间戳和时长信息
hdlr(Handler Reference Box)
hdlr是mdia的子box,是个full box。主要用来标识track的类型。
aligned(8) class HandlerBox extends FullBox(‘hdlr’, version = 0, 0) {
unsigned int(32) pre_defined = 0;
unsigned int(32) handler_type;
const unsigned int(32)[3] reserved = 0;
string name;
}
Minf(media information box)
minf是mdia的子box,是一个container box,该box中包括了track中的所有关键信息,主要包括vmhd、dinf、stbl三个box
vmhd(video media header box)
vmhd box是minf的子box,是full box。该box定义了特定的颜色和图形模式信息
aligned(8) class VideoMediaHeaderBox extends FullBox(‘vmhd’, version = 0, 1) {
template unsigned int(16) graphicsmode = 0;
template unsigned int(16)[3] opcolor = {0, 0, 0};
}
dinf(data information box)
dinf box用来确定如何定位媒体信息,是一个container box。“dinf”一般包含一个“dref”,即data reference box;“dref”下会包含若干个“url”或“urn”,这些box组成一个表,用来定位track数据。简单的说,track可以被分成若干段,每一段都可以根据“url”或“urn”指向的地址来获取数据,sample描述中会用这些片段的序号将这些片段组成一个完整的track。一般情况下,当数据被完全包含在文件中时,“url”或“urn”中的定位字符串是空的。
Dinf box是个container box,只包括size和type,主要看data中的子box dref dref(data reference box)
dref是个full box
aligned(8) class DataReferenceBox extends FullBox(‘dref’, version = 0, 0) {
unsigned int(32) entry_count;
for (i=1; i <= entry_count; i++) {
DataEntryBox(entry_version, entry_flags) data_entry;
}
}
URL/URN
URL/URN是个full box,从二进制数据看到,location字段值是空的。因此当前track是一个完成的track。
aligned(8) class DataEntryUrlBox (bit(24) flags) extends FullBox(‘url ’, version = 0, flags) {
string location;
}
aligned(8) class DataEntryUrnBox (bit(24) flags) extends FullBox(‘urn ’, version = 0, flags) {
string name;
string location;
}
stbl(Sample Table Box )
stbl包含了媒体流每一个sample在文件中的offset,pts,duration等信息,是个container box,包括sample description box(stsd)、time to sample box(stts)、composition time to sample box(ctts)、sample to chunk box(stsc)、sample size box(stsz或者stz2)、 time to sample box(stts)、chunk offset box (stco 或 co64)等box。如果要想在mdat表中找到对应的数据,需要从上面这些表中查找到sample对应的offset
主要看下header部分数据,data部分是子box,会在接下来继续分析。
stsd(Sample Description Box )
帧描述表,full box,也是个container box。提供了有关所用编码类型的详细信息,以及该编码所需的任何初始化信息。具体的编解码初始化信息放在子box当中。
aligned(8) class SampleDescriptionBox (unsigned int(32) handler_type) extends FullBox('stsd', 0, 0){
int i ;
unsigned int(32) entry_count;
for (i = 1 ; i <= entry_count ; i++){
switch (handler_type){
// audio track
case ‘soun’:
AudioSampleEntry();
break;
// video track
case ‘vide’:
VisualSampleEntry();
break;
// hint track
case ‘hint’:
HintSampleEntry();
break;
}
}
}
avc1
mp4中使用的视频数据是avc1编码的,既通过固定长度字段进行nalu的分割。box中包含了视频的宽高等信息.
aligned(8) abstract class SampleEntry (unsigned int(32) format) extends Box(format){
const unsigned int(8)[6] reserved = 0;
unsigned int(16) data_reference_index;
}
class VisualSampleEntry(codingname) extends SampleEntry (codingname){
unsigned int(16) pre_defined = 0;
const unsigned int(16) reserved = 0;
unsigned int(32)[3] pre_defined = 0;
unsigned int(16) width;
unsigned int(16) height;
template unsigned int(32) horizresolution = 0x00480000; // 72 dpi
template unsigned int(32) vertresolution = 0x00480000; // 72 dpi
const unsigned int(32) reserved = 0;
template unsigned int(16) frame_count = 1;
string[32] compressorname;
template unsigned int(16) depth = 0x0018;
int(16) pre_defined = -1;
// other boxes from derived specifications
CleanApertureBox clap; // optional
PixelAspectRatioBox pasp; // optional
}
class AVCSampleEntry() extends VisualSampleEntry (‘avc1’){
AVCConfigurationBox config;
MPEG4BitRateBox (); // optional
MPEG4ExtensionDescriptorsBox (); // optional
}
- avcc
avc1 box的子box,该Box则包含了真实的SPS PPS等信息,包含着视频编解码参数
class AVCConfigurationBox extends Box(‘avcC’) {
AVCDecoderConfigurationRecord() AVCConfig;
}
aligned(8) class AVCDecoderConfigurationRecord {
unsigned int(8) configurationVersion = 1;
unsigned int(8) AVCProfileIndication;
unsigned int(8) profile_compatibility;
unsigned int(8) AVCLevelIndication;
bit(6) reserved = ‘111111’b;
unsigned int(2) lengthSizeMinusOne;
bit(3) reserved = ‘111’b;
unsigned int(5) numOfSequenceParameterSets;
for (i=0; i< numOfSequenceParameterSets; i++) {
unsigned int(16) sequenceParameterSetLength;
bit(8*sequenceParameterSetLength) sequenceParameterSetNALUnit;
}
unsigned int(8) numOfPictureParameterSets;
for (i=0; i< numOfPictureParameterSets; i++) {
unsigned int(16) pictureParameterSetLength;
bit(8*pictureParameterSetLength) pictureParameterSetNALUnit;
}
if( profile_idc == 100 || profile_idc == 110 ||
profile_idc == 122 || profile_idc == 144 ) {
bit(6) reserved = ‘111111’b;
unsigned int(2) chroma_format;
bit(5) reserved = ‘11111’b;
unsigned int(3) bit_depth_luma_minus8;
bit(5) reserved = ‘11111’b;
unsigned int(3) bit_depth_chroma_minus8;
unsigned int(8) numOfSequenceParameterSetExt;
for (i=0; i< numOfSequenceParameterSetExt; i++) {
unsigned int(16) sequenceParameterSetExtLength;
bit(8*sequenceParameterSetExtLength) sequenceParameterSetExtNALUnit;
}
}
}
stts(Decoding Time to Sample Box )
该box是stbl的子box,是个full box。该表中存储了dts(解码时间戳)与sample的对应关系,可以计算出每个sample的dts。为了节省存储空间,不是记录每帧的dts,而是把连续的且每帧的duration相等的sample分为一组。
aligned(8) class TimeToSampleBox extends FullBox(’stts’, version = 0, 0) {
unsigned int(32) entry_count;
int i;
for (i=0; i < entry_count; i++) {
unsigned int(32) sample_count;
unsigned int(32) sample_delta;
}
}
stss(Sync Sample Box )
stss是stbl的子box,是个full box。包含了每个关键帧在所有帧中的数组下标+1,即关键帧队列从1开始算起。在音频track中不存在这个box
aligned(8) class SyncSampleBox extends FullBox(‘stss’, version = 0, 0) {
unsigned int(32) entry_count;
int i;
for (i=0; i < entry_count; i++) {
unsigned int(32) sample_number;
}
}
ctts(Composition Time to Sample Box )
Ctts box是full box,是存储pts与dts差值的box。如果没有B帧,没有该box。存储形式与stts类似,存储的值由sample duration变成了当前sample 的pts 与dts的差值。这里通过这个表和stts就可以计算出Sample的PTS。
Pts(n) = offset(n) + dts(n)
aligned(8) class CompositionOffsetBox extends FullBox(‘ctts’, version, 0) {
unsigned int(32) entry_count;
int i;
if (version==0) {
for (i=0; i < entry_count; i++) {
unsigned int(32) sample_count;
unsigned int(32) sample_offset;
}
}
else if (version == 1) {
for (i=0; i < entry_count; i++) {
unsigned int(32) sample_count;
signed int(32) sample_offset;
}
}
}
注:只截取了部分数据
stsc(Sample To Chunk Box )
该表记录了每个chunk中包含了多少个sample,如果连续的chunk包含的sample数量相同,会放到同一个集合当中。每个集合包含如下三个字段:
first_chunk:当前集合最开始chunk的索引值,从1开始
samples_per_chunk:每个chunk个有多少个sample
sample_description_index: 描述采样点的采样描述项的索引值,范围为1到样本描述表中的表项数目
举个例子:
第一个chunk包含10个sample,第二个chunk包含10个sample,第三个chunk包含12个sample
第一个chunk与第二个chunk有相同的sample,所以会放在一个集合中,且first_chunk为1。第三个chunk的sample与前两个都不相同,在另一个集合成,该集合的first_chunk为3,因为该集合当中最开始的chunk的索引值是3。
每个集合的值如下表所示:
first_chunk | samples_per_chunk | sample_description_index |
---|---|---|
1 | 10 | 1 |
3 | 12 | 1 |
aligned(8) class SampleToChunkBox
extends FullBox(‘stsc’, version = 0, 0) {
unsigned int(32) entry_count;
for (i=1; i <= entry_count; i++) {
unsigned int(32) first_chunk;
unsigned int(32) samples_per_chunk;
unsigned int(32) sample_description_index;
}
}
stsz(sample size box)
每个Sample的Size即包含的字节数.包含了媒体中全部sample的数目和一张给出每个sample大小的表。这个box相对来说体积是比较大的。
aligned(8) class SampleSizeBox extends FullBox(‘stsz’, version = 0, 0) {
unsigned int(32) sample_size;
unsigned int(32) sample_count;
if (sample_size==0) {
for (i=1; i <= sample_count; i++) {
unsigned int(32) entry_size;
}
}
}
注:包含部分数据
stco(Chunk Offset Box )
full box。该Box存储了chunk Offset,表示了每个chunk在文件中的位置,这样我们就能找到了chunk在文件的偏移量,然后根据其它表的关联关系就可以读取每个sample的大小。
stco 有两种形式,如果你的视频过大的话,就有可能造成 chunk offset 超过 32bit 的限制。所以,这里针对大 video 额外创建了一个 co64 的 Box。它的功效等价于 stco,也是用来表示 sample 在 mdat box 中的位置。只是,里面 chunk_offset 是 64bit 的。
aligned(8) class ChunkOffsetBox
extends FullBox(‘stco’, version = 0, 0) {
unsigned int(32) entry_count;
for (i=1; i <= entry_count; i++) {
unsigned int(32) chunk_offset;
}
}
mdat(media data box)
mdat box存储真实的媒体数据。从mp4parse 中看到mdat的偏移量为40.
字段 | 字节数 | 数据 | 对应值 | 含义 |
---|---|---|---|---|
Box size | 4 | 00 42 1D 4D | 4332877 | box大小 |
Box type | 4 | 6D 64 61 74 | mdat | ASCII值 |
data |
如何找到对应的sample数据呢。我们来试下。
从stco中知道,第一个sample的偏移量是48。从stsz中知道第一个sample的大小为766个字节。所以
00 00 02 7F开始的数据是视频的第一帧数据。
该视频中存储的是avc1数据,avc1数据的由如下格式组成:
avc1 data = NALU length + NALU header + Nalu Data....... NALU length + NALU header + Nalu Data
NALU header的占用字节数从avcc中的lengthSizeMinusOne字段知道为4个字节,表示当前nal的长度(nalu header length + nalu data length)。
所以第一个nal的长度为00 00 02 7F,既639个字节。但是上面提到第一个sample的大小为766。为什么不对呢,我们接下来再看下。
第一个nal长度为639,加上最开始的偏移量48和nalu length的长度4个字节。第二个nal的偏移量为48+ 4 +639 = 691。我们看下偏移量691(0x2b3)的数据为00 00 00 77,既119。(4 + 119 ) + (4+639 )= 766。现在可以得出结论,第一个sample当中包含了两个nalu。
以此类推,可以找个每个sample对应的data
总结
这篇文章主要是通过分析一个真正的mp4文件来了解和熟悉mp4的结构。对于一些刚入门的同学来说,熟悉mp4会容易一些。