MP4 实际表明的含义是 MPEG-4 Part 14
。它只是 MPEG
标准中的 14 部分。它主要参考 ISO/IEC
标准来制定的。MP4 主要做用是能够实现快进快放,边下载边播放的效果。他是基于 MOV
,而后发展成本身相关的格式内容。而后和 MP4 相关的文件还有:3GP
,M4V
这两种格式。javascript
MP4 的格式稍微比 FLV 复杂一些,它是经过嵌的方式来实现整个数据的携带。换句话说,它的每一段内容,均可以变成一个对象,若是须要播放的话,只要获得相应的对象便可。java
MP4 中最基本的单元就是 Box,它内部是经过一个一个独立的 box 拼接而成的。因此,这里,咱们先从 Box 的讲解开始。app
PS:mp4协议自己没有多复查,没啥特别难理解的地方,或许其惟一的“复杂”点就在于其“大”,嵌套的各类各样的子box,简直就是mux/remuxer的噩梦(gstreamer里面光解析box的代码,就1W多行,还不包含其余的element 逻辑代码)ide
MP4 box 能够分为 basic box 和 full box。优化
这里,再次强调一下,MP4 box 是 MP4的核心。在 decode/encode 过程当中,最好把它的基本格式背下来,这样,你写起来会开心不少(经验之谈)。ui
OK,咱们来看一下,Box 的具体结构。编码
首先来看一下 basic box 的结构:url
若是用代码来表示就是:spa
aligned(8) class Box (unsigned int(32) boxtype, optional unsigned int(8)[16] extended_type) { unsigned int(32) size; unsigned int(32) type = boxtype; if (size==1) { unsigned int(64) largesize; } else if (size==0) { // box extends to end of file } // 这里针对的是 MP4 extension 的盒子类型。通常不会发生 if (boxtype==‘uuid’) { unsigned int(8)[16] usertype = extended_type; } }
上面代码其实已经说的很清楚了。这里,我在简单的阐述一下。设计
size===1
时,会出现一个 8B 的 largesize
字段来存放大小。当 size===0
时,表示文件的结束。
实际整个盒子的结构能够用下图来表示:
这里须要强调的一点就是,在 MP4 中,默认写入字节序都是 Big-Endian 。因此,在上面,涉及到 4B 8B 等字段内容时,都是以 BE 来写入的。
上面不是说了,box 有两种基本格式吗?
还有一种为 fullBox
full box 和 box 的主要区别是增长了 version 和 flag 字段。它的应用场景不高,主要是在 trak
box 中使用。它的基本格式为:
aligned(8) class FullBox(unsigned int(32) boxtype, unsigned int(8) v, bit(24) f) extends Box(boxtype) { unsigned int(8) version = v; bit(24) flags = f; }
在实操中,若是你的没有针对 version 和 flags 的业务场景,那么基本上就能够直接设为默认值,好比 0x00。它的基本结构图为:
接下来,咱们就要正式的来看一下,MP4 中真正用到的一些 Box 了。
这里,咱们按照 MP4 box 的划分来进行相关的阐述。先看一张 MP4 给出的结构图:
说明一下,咱们只讲带星号的 box。其余的由于不是必须 box,咱们就选择性的忽略了。不过,里面带星号的 Box 仍是挺多的。由于,咱们的主要目的是为了生成一个 MP4 文件。一个正常的 MP4 文件的结构并非全部带星号的 Box 都必须有。
正常播放的 MP4 文件其实还能够分为 unfragmented MP4(简写为 MP4) 和 fragmented MP4(简写为 FMP4)。那这二者具体有什么区别呢?
能够说,彻底不一样。由于他们自己肯定 media stream 播放的方式都是彻底不一样的模式。
MP4 格式
基本 box 为:
上面这是最基本的 MP4 Box 内容。 较完整的为:
MP4 box 根据 trak 中的 stbl 下的 stts stsc 等基本 box 来完成在 mdat box 中的索引。那 FMP4 是啥呢?
看起来非标还多一个 box。但在具体编解码的时候,标准解码须要更多关注在如何编码 stbl 下的几个子 box–stts,stco,ctts 等盒子。而非标不须要关注 stbl,只须要将原本处于 stbl 的数据直接抽到 moof 中。而且在转换过程当中,moof 里面的格式相比 stbl 来讲,是很是简单的。因此,这里,咱们主要围绕上面两种的标准,来说解对应的 Box。
ftyp 盒子至关于就是该 mp4 的纲领性说明。即,告诉demuxer它的基本解码版本,兼容格式。简而言之,就是用来告诉客户端,该 MP4 的使用的解码标准。一般,ftyp 都是放在 MP4 的开头。
它的格式为:
aligned(8) class FileTypeBox extends Box(‘ftyp’) { unsigned int(32) major_brand; unsigned int(32) minor_version; unsigned int(32) compatible_brands[]; }
上面的字段一概都是放在 data
字段中(参考,box 的描述)。
isom
这个万金油便可。若是是须要特定的格式,能够自行定义。说这么多概念,还不如给代码实在。这里,咱们能够来看一下,对于通用 ftyp box 的建立。
FTYP: new Uint8Array([ 0x69, 0x73, 0x6F, 0x6D, // major_brand: isom 0x0, 0x0, 0x0, 0x1, // minor_version: 0x01 0x69, 0x73, 0x6F, 0x6D, // isom 0x61, 0x76, 0x63, 0x31 // avc1 ])
moov box 主要是做为一个很重要的容器盒子存在的,它自己的实际内容并不重要。moov 主要是存放相关的 trak 。其基本格式为:
aligned(8) class MovieExtendsBox extends Box(‘mvex’){ }
mvhd 是 moov 下的第一个 box,用来描述 media 的相关信息。其基本内容为:
aligned(8) class MovieHeaderBox extends FullBox(‘mvhd’, version, 0) { if (version==1) { unsigned int(64) creation_time; unsigned int(64) modification_time; unsigned int(32) timescale; unsigned int(64) duration; } else { // version==0 unsigned int(32) creation_time; unsigned int(32) modification_time; unsigned int(32) timescale; unsigned int(32) duration; } template int(32) rate = 0x00010000; // typically 1.0 template int(16) volume = 0x0100; // typically, full volume const bit(16) reserved = 0; const unsigned int(32)[2] reserved = 0; template int(32)[9] matrix = { 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 }; // Unity matrix bit(32)[6] pre_defined = 0; unsigned int(32) next_track_ID; }
实际上,mvhd 大部分的值,均可以设为固定值:
new Uint8Array([ 0x00, 0x00, 0x00, 0x00, // version(0) + flags 0x00, 0x00, 0x00, 0x00, // creation_time 0x00, 0x00, 0x00, 0x00, // modification_time (timescale >>> 24) & 0xFF, // timescale: 4 bytes (timescale >>> 16) & 0xFF, (timescale >>> 8) & 0xFF, (timescale) & 0xFF, (duration >>> 24) & 0xFF, // duration: 4 bytes (duration >>> 16) & 0xFF, (duration >>> 8) & 0xFF, (duration) & 0xFF, 0x00, 0x01, 0x00, 0x00, // Preferred rate: 1.0 0x01, 0x00, 0x00, 0x00, // PreferredVolume(1.0, 2bytes) + reserved(2bytes) 0x00, 0x00, 0x00, 0x00, // reserved: 4 + 4 bytes 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // ----begin composition matrix---- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, // ----end composition matrix---- 0x00, 0x00, 0x00, 0x00, // ----begin pre_defined 6 * 4 bytes---- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ----end pre_defined 6 * 4 bytes---- 0xFF, 0xFF, 0xFF, 0xFF // next_track_ID ]);
trak box 就是主要存放相关 media stream 的内容。其基本格式很简单就是简单的 box:
aligned(8) class TrackBox extends Box(‘trak’) { }
不过,有时候里面也能够带上该 media stream 的相关描述:
tkhd 是 trak box 的子一级 box 的内容。主要是用来描述该特定 trak 的相关内容信息。其主要内容为:
aligned(8) class TrackHeaderBox extends FullBox(‘tkhd’, version, flags){ if (version==1) { unsigned int(64) creation_time; unsigned int(64) modification_time; unsigned int(32) track_ID; const unsigned int(32) reserved = 0; unsigned int(64) duration; } else { // version==0 unsigned int(32) creation_time; unsigned int(32) modification_time; unsigned int(32) track_ID; const unsigned int(32) reserved = 0; unsigned int(32) duration; } const unsigned int(32)[2] reserved = 0; template int(16) layer = 0; template int(16) alternate_group = 0; template int(16) volume = {if track_is_audio 0x0100 else 0}; const unsigned int(16) reserved = 0; template int(32)[9] matrix= { 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 }; // unity matrix unsigned int(32) width; unsigned int(32) height; }
上面内容确实挺多的,可是,有些并非必定须要填一些合法值。这里简单说明一下:
mdia 主要用来包裹相关的 media 信息。自己没啥说的,格式为:
aligned(8) class MediaBox extends Box(‘mdia’) { }
mdhd 和 tkhd 来讲,内容大体都是同样的。不过,tkhd 一般是对指定的 track 设定相关属性和内容。而 mdhd 是针对于独立的 media 来设置的。不过事实上,二者通常都是同样的。
具体格式为:
aligned(8) class MediaHeaderBox extends FullBox(‘mdhd’, version, 0) { if (version==1) { unsigned int(64) creation_time; unsigned int(64) modification_time; unsigned int(32) timescale; unsigned int(64) duration; } else { // version==0 unsigned int(32) creation_time; unsigned int(32) modification_time; unsigned int(32) timescale; unsigned int(32) duration; } bit(1) pad = 0; unsigned int(5)[3] language; // ISO-639-2/T language code unsigned int(16) pre_defined = 0; }
里面就有 3 个额外的字段:pad,language,pre_defined。
根据字面意思很好理解:
实际代码的计算方式为:
new Uint8Array([ 0x00, 0x00, 0x00, 0x00, // version(0) + flags 0x00, 0x00, 0x00, 0x00, // creation_time 0x00, 0x00, 0x00, 0x00, // modification_time (timescale >>> 24) & 0xFF, // timescale: 4 bytes (timescale >>> 16) & 0xFF, (timescale >>> 8) & 0xFF, (timescale) & 0xFF, (duration >>> 24) & 0xFF, // duration: 4 bytes (duration >>> 16) & 0xFF, (duration >>> 8) & 0xFF, (duration) & 0xFF, 0x55, 0xC4, // language: und (undetermined) 0x00, 0x00 // pre_defined = 0 ])
hdlr 是用来设置不一样 trak 的处理方式的。经常使用处理方式以下:
这个,其实就和咱们在获得和接收到资源时,设置的 Content-Type
类型字段是一致的,例如 application/javascript
。
其基本格式为:
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; }
其中有两字段须要额外说明一下:
handler_type 填的值其实就是 string 转换为 hex 以后获得的值。好比:
0x76, 0x69, 0x64, 0x65
0x73, 0x6F, 0x75, 0x6E
minf 是子属内容中,重要的容器 box,用来存放当前 track 的基本描述信息。自己没啥说的,基本格式为:
aligned(8) class MediaInformationBox extends Box(‘minf’) { }
v/smhd 是对当前 trak 的描述 box。vmhd 针对的是 video,smhd 针对的是 audio。这两个盒子在解码中,非不可或缺的(有时候得看播放器),缺了的话,有可能会被认为格式不正确。
咱们先来看一下 vmhd 的基本格式:
aligned(8) class VideoMediaHeaderBox extends FullBox(‘vmhd’, version = 0, 1) { template unsigned int(16) graphicsmode = 0; // copy, see below template unsigned int(16)[3] opcolor = {0, 0, 0}; }
这很简单都是一些默认值,我这里就很少说了。
smhd 的格式一样也很简单:
aligned(8) class SoundMediaHeaderBox extends FullBox(‘smhd’, version = 0, 0) { template int(16) balance = 0; const unsigned int(16) reserved = 0; }
其中,balance 这个字段至关于和咱们一般设置的左声道,右声道有关。
dinf 是用来讲明在 trak 中,media 描述信息的位置。其实自己就是一个容器,没啥内容:
aligned(8) class DataInformationBox extends Box(‘dinf’) { }
dref 是用来设置当前 Box 描述信息的 data_entry。基本格式为:
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; } }
其中的 DataEntryBox 就是 DataEntryUrlBox/DataEntryUrnBox 中的一个。简单来讲,就是 dref 下的子 box – url
或者 urn
这两个 box。其中,entry_version 和 entry_flags 须要额外说明一下。
不过,就一般来讲,我真的没有用到过有实际数据的 dref 。因此,这里就不衍生来说了。
url box 是由 dref 包裹的子一级 box,里面是对不一样的 sample 的描述信息。不过,通常都是附带在其它 box 里。其基本格式为:
aligned(8) class DataEntryUrlBox (bit(24) flags) extends FullBox(‘url ’, version = 0, flags) { string location; }
实际并无用到过 location
这个字段,因此,通常也就不须要了。
stts 主要是用来存储 refSampleDelta。即,相邻两帧间隔的时间。它基本格式为:
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; } }
看代码其实看不出什么,咱们结合实际抓包结果,来说解。现有以下的帧:
能够看到,上面的 Decode delta 值都是 10。这就对应着 sample_delta 的值。而 sample_count 就对应出现几回的 sample_delta。好比,上面 10 的 delta 出现了 14 次,那么 sample_count 就是 14。
若是对应于 RTMP 中的 Video Msg,那么 sample_delta 就是当前 RTMP Header 中,后面一个的 timeStamp delta。
stco 是 stbl 包里面一个很是关键的 Box。它用来定义每个 sample 在 mdat 具体的位置。基本格式为:
aligned(8) class ChunkOffsetBox extends FullBox(‘stco’, version = 0, 0) { unsigned int(32) entry_count; for (i=1; i u entry_count; i++) { unsigned int(32) chunk_offset; } }
具体能够参考:
stco 有两种形式,若是你的视频过大的话,就有可能形成 chunkoffset 超过 32bit 的限制。因此,这里针对大 Video 额外建立了一个 co64 的 Box。它的功效等价于 stco,也是用来表示 sample 在 mdat
box 中的位置。只是,里面 chunk_offset 是 64bit 的。
aligned(8) class ChunkLargeOffsetBox extends FullBox(‘co64’, version = 0, 0) { unsigned int(32) entry_count; for (i=1; i u entry_count; i++) { unsigned int(64) chunk_offset; } }
stsc 这个 Box 有点绕,并非它的字段多,而是它的字段意思有点奇怪。其基本格式为:
aligned(8) class SampleToChunkBox extends FullBox(‘stsc’, version = 0, 0) { unsigned int(32) entry_count; for (i=1; i u entry_count; i++) { unsigned int(32) first_chunk; unsigned int(32) samples_per_chunk; unsigned int(32) sample_description_index; } }
关键点在于他们里面的三个字段: first_chunk
,samples_per_chunk
,sample_description_index
。
这 3 个字段实际上决定了一个 MP4 中有多少个 chunks,每一个 chunks 有多少个 samples。这里顺便普及一下 chunk 和 sample 的相关概念。在 MP4 文件中,最小的基本单位是 Chunk
而不是 Sample
。
看了上面字段就懂得,感受你要么是大牛,要么就是在装逼。官方文档和上面同样的描述,可是,看了一遍后,懵逼,再看一遍后,懵逼。因此,这里为了你们更好的理解,这里额外再补充一下。
前面说了,在 MP4 中最小的单位是 chunks,那么经过 stco 中定义的 chunk_offsets 字段,它描述的就是 chunks 在 mdat 中的位置。每个 stco chunk_offset 就对应于 某一个 index 的 chunks。那么,first_chunk
就是用来定义该 chunk entry 开始的位置。
那这样的话,stsc 须要对每个 chunk 进行定义吗?
不须要,由于 stsc 是定义一整个 entry,即,若是他们的 samples_per_chunk
,sample_description_index
不变的话,那么后续的 chunks 都是用同样的模式。
即,若是你的 stsc 只有:
也就是说,从第一个 chunk 开始,每经过切分 4 个 sample 划分为一个 chunk,而且每一个 sample 的表述信息都是 1。它会按照这样划分方法一直持续到最后。固然,若是你的 sample 最后不能被 4 整除,最后的几段 sample 就会当作特例进行处理。
一般状况下,stsc 的值是不同的:
按照上面的状况就是,第 1 个 chunk 包含 2 个 samples。第 2-4 个 chunk 包含 1 个 sample,第 5 个 chunk 包含两个 chunk,第 6 个到最后一个 chunk 包含一个 sample。
ctts 主要针对 Video 中的 B 帧来肯定的。也就是说,若是你视频里面没有 B 帧,那么,ctts 的结构就很简单了。它主要的做用,是用来记录每个 sample 里面的 cts。格式为:
aligned(8) class CompositionOffsetBox extends FullBox(‘ctts’, 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_offset; } }
仍是看实例吧,假如你视频中帧的排列以下:
其中,sample_offset 就是 Composition offset。经过合并一致的 Composition offset,能够获得对应的 sample_count。最终 ctts 的结果为:
看实例抓包的结果为:
若是,你是针对 RTMP 的 video,因为,其没有 B 帧,那么 ctts 的整个结果,就只有一个 sample_count 和 sample_offset。好比:
sample_count: 100 sample_offset: 0
一般只有 video track 才须要 ctts。
stsz 是用来存放每个 sample 的 size 信息的。基本格式为:
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; } } }
这个没啥说的,就是全部 sample 的 size 大小,以及相应的描述信息。
前面部分是标准 box 的全部内容。固然,fMP4 里面大部份内容和 MP4 标准格式有不少重复的地方,剩下的就不过多赘述,只把不一样的单独挑出来说解。
mvex 是 fMP4 的标准盒子。它的做用是告诉解码器这是一个 fMP4 的文件,具体的 samples 信息内容再也不放到 trak 里面,而是在每个 moof 中。基本格式为:
aligned(8) class MovieExtendsBox extends Box(‘mvex’){ }
trex 是 mvex 的子一级 box 用来给 fMP4 的 sample 设置默认值。基本内容为:
aligned(8) class TrackExtendsBox extends FullBox(‘trex’, 0, 0){ unsigned int(32) track_ID; unsigned int(32) default_sample_description_index; unsigned int(32) default_sample_duration; unsigned int(32) default_sample_size; unsigned int(32) default_sample_flags }
具体设哪个值,这得看你业务里面具体的要求才行。 若是实在不知道,那就能够直接设置为 0:
new Uint8Array([ 0x00, 0x00, 0x00, 0x00, // version(0) + flags (trackId >>> 24) & 0xFF, // track_ID (trackId >>> 16) & 0xFF, (trackId >>> 8) & 0xFF, (trackId) & 0xFF, 0x00, 0x00, 0x00, 0x01, // default_sample_description_index 0x00, 0x00, 0x00, 0x00, // default_sample_duration 0x00, 0x00, 0x00, 0x00, // default_sample_size 0x00, 0x01, 0x00, 0x01 // default_sample_flags ])
moof 主要是用来存放 FMP4 的相关内容。它自己没啥太多的内容:
aligned(8) class TrackFragmentBox extends Box(‘traf’){ }
tfhd 主要是对指定的 trak 进行相关的默认设置。例如:sample 的时长,大小,偏移量等。不过,这些均可以忽略不设,只要你在其它 box 里面设置完整便可:
aligned(8) class TrackFragmentHeaderBox extends FullBox(‘tfhd’, 0, tf_flags){ unsigned int(32) track_ID; // all the following are optional fields unsigned int(64) base_data_offset; unsigned int(32) sample_description_index; unsigned int(32) default_sample_duration; unsigned int(32) default_sample_size; unsigned int(32) default_sample_flags }
base_data_offset 是用来计算后面数据偏移量用到的。若是存在则会用上,不然直接是相关开头的偏移。
tfdt 主要是用来存放相关 sample 编码的绝对时间的。由于 FMP4 是流式的格式,因此,不像 MP4 同样能够直接根据 sample 直接 seek 到具体位置。这里就须要一个标准时间参考,来快速定位都某个具体的 fragment。
它的基本格式为:
aligned(8) class TrackFragmentBaseMediaDecodeTimeBox extends FullBox(‘tfdt’, version, 0) { if (version==1) { unsigned int(64) baseMediaDecodeTime; } else { // version==0 unsigned int(32) baseMediaDecodeTime; } }
baseMediaDecodeTime 基本值是前面全部指定 trak_id 中 samples 持续时长的总和,至关于就是当前 traf 里面第一个 sample 的 dts 值。
trun 存储该 moof 里面相关的 sample 内容。例如,每一个 sample 的 size,duration,offset 等。基本内容为:
aligned(8) class TrackRunBox extends FullBox(‘trun’, version, tr_flags) { unsigned int(32) sample_count; // the following are optional fields signed int(32) data_offset; unsigned int(32) first_sample_flags; // all fields in the following array are optional { unsigned int(32) sample_duration; unsigned int(32) sample_size; unsigned int(32) sample_flags if (version == 0) { unsigned int(32) sample_composition_time_offset else { signed int(32) sample_composition_time_offset }[ sample_count ] }
能够说,trun 上面的字段是 traf 里面最重要的标识字段:
tr_flags 是用来表示下列 sample 相关的标识符是否应用到每一个字段中:
后面字段,咱们这简单介绍一下。
后面的几个字段,我就不赘述了,对了,里面的 sample_flags 是一个很是重要的东西,经常用它来表示,到底哪个 sampel 是对应的 keyFrame。基本计算方法为:
(flags.isLeading << 2) | flags.dependsOn, // sample_flags (flags.isDepended << 6) | (flags.hasRedundancy << 4) | flags.isNonSync
sdtp 主要是用来描述具体某个 sample 是不是 I 帧,是不是 leading frame 等相关属性值,主要用来做为当进行点播回放时的同步参考信息。其内容一共有 4 个:
整个基本格式为:
aligned(8) class SampleDependencyTypeBox extends FullBox(‘sdtp’, version = 0, 0) { for (i=0; i < sample_count; i++){ unsigned int(2) is_leading; unsigned int(2) sample_depends_on; unsigned int(2) sample_is_depended_on; unsigned int(2) sample_has_redundancy; } }
sdtp 对于 video 来讲很重要,由于,其内容字段主要就是给 video 相关的帧设计的。而 audio,通常直接采用默认值:
isLeading: 0, dependsOn: 1, isDepended: 0, hasRedundancy: 0
到这里,整个 MP4 和 fMP4 的内容就已经介绍完了。更详细的内容能够参考 MP4 & FMP4 doc。
固然,这里只是很是皮毛的一部分,仅仅知道 box 的内容,并不足够来作一些音视频处理。更多的是关于音视频的基础知识,好比,dts/pts、音视频同步、视频盒子的封装等等。