首先,介绍下封装格式。多媒体封装格式(也叫容器格式),是指按照一定的规则,将视频数据、音频数据等,放到一个文件中。常见的 MKV、AVI 以及本文介绍的 MP4 等,都是封装格式。
MP4是最常见的封装格式之一,因为其跨平台的特性而得到广泛应用。MP4文件的后缀为.mp4,基本上主流的播放器、浏览器都支持MP4格式。
MP4文件的格式主要由 MPEG-4 Part 12、MPEG-4 Part 14 两部分进行定义。其中,MPEG-4 Part 12 定义了ISO基础媒体文件格式,用来存储基于时间的媒体内容。MPEG-4 Part 14 实际定义了MP4文件格式,在MPEG-4 Part 12的基础上进行扩展。
对从事直播、音视频相关工作的同学,很有必要了解MP4格式,下面简单介绍下。
MP4文件由多个box组成,每个box存储不同的信息,且box之间是树状结构,如下图所示。
box类型有很多,下面是3个比较重要的顶层box:
虽然box类型有很多,但基本结构都是一样的。下一节会先介绍box的结构,然后再对常见的box进行进一步讲解。
下表是常见的box,稍微看下有个大致的印象就好,然后直接跳到下一节。
1个box由两部分组成:box header、box body。
box header中,只有type、size是必选字段。当size==0时,存在largesize字段。在部分box中,还存在version、flags字段,这样的box叫做Full Box。当box body中嵌套其他box时,这样的box叫做container box。
字段定义如下:
Box的伪代码如下:
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
}
if (boxtype==‘uuid’) {
unsigned int(8)[16] usertype = extended_type;
}
}复制代码
box数据体,不同box包含的内容不同,需要参考具体box的定义。有的 box body 很简单,比如 ftyp。有的 box 比较复杂,可能嵌套了其他box,比如moov。
在Box的基础上,扩展出了FullBox类型。相比Box,FullBox 多了 version、flags 字段。
FullBox 伪代码如下:
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;
}复制代码
FullBox主要在moov中的box用到,比如 moov.mvhd
,后面会介绍到。
aligned(8) class MovieHeaderBox extends FullBox(‘mvhd’, version, 0) {
// 字段略...
}复制代码
ftyp用来指出当前文件遵循的规范,在介绍ftyp的细节前,先科普下isom。
isom(ISO Base Media file)是在 MPEG-4 Part 12 中定义的一种基础文件格式,MP4、3gp、QT 等常见的封装格式,都是基于这种基础文件格式衍生的。
MP4 文件可能遵循的规范有mp41、mp42,而mp41、mp42又是基于isom衍生出来的。
3gp(3GPP):一种容器格式,主要用于3G手机上; QT:QuickTime的缩写,.qt 文件代表苹果QuickTime媒体文件;
ftyp 定义如下:
aligned(8) class FileTypeBox extends Box(‘ftyp’) {
unsigned int(32) major_brand;
unsigned int(32) minor_version;
unsigned int(32) compatible_brands[]; // to end of the box
}
复制代码
下面是是 brand 的描述,其实就是具体封装格式对应的代码,用4个字节的编码来表示,比如 mp41。
A brand is a four-letter code representing a format or subformat. Each file has a major brand (or primary brand), and also a compatibility list of brands.
ftyp 的几个字段的含义:
在实际使用中,不能把 isom 做为 major_brand,而是需要使用具体的brand(比如mp41),因此,对于 isom,没有定义具体的文件扩展名、mime type。
下面是常见的几种brand,以及对应的文件扩展名、mime type。
下面是实际例子的截图,不赘述。
在讨论 MP4 规范时,提到AVC,有的时候指的是“AVC文件格式”,有的时候指的是"AVC压缩标准(H.264)",这里简单做下区分。
Movie Box,存储 mp4 的 metadata,一般位于mp4文件的开头。
aligned(8) class MovieBox extends Box(‘moov’){ }复制代码
moov中,最重要的两个box是 mvhd 和 trak:
mvhd针对整个影片,tkhd针对单个track,mdhd针对媒体,vmhd针对视频,smhd针对音频,可以认为是从 宽泛 > 具体,前者一般是从后者推导出来的。
MP4文件的整体信息,跟具体的视频流、音频流无关,比如创建时间、文件时长等。
定义如下:
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;
}复制代码
字段含义如下:
单个 track 的 metadata,包含如下字段:
定义如下:
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;
}复制代码
例子如下:
声明当前track的类型,以及对应的处理器(handler)。
handler_type的取值包括:
name为utf8字符串,对handler进行描述,比如 L-SMASH Video Handler(参考 这里)。
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;
}复制代码
MP4文件的媒体数据部分在mdat box里,而stbl则包含了这些媒体数据的索引以及时间信息,了解stbl对解码、渲染MP4文件很关键。
在MP4文件中,媒体数据被分成多个chunk,每个chunk可包含多个sample,而sample则由帧组成(通常1个sample对应1个帧),关系如下:
stbl中比较关键的box包含stsd、stco、stsc、stsz、stts、stss、ctts。下面先来个概要的介绍,然后再逐个讲解细节。
下面是这几个box概要的介绍:
stsd给出sample的描述信息,这里面包含了在解码阶段需要用到的任意初始化信息,比如 编码 等。对于视频、音频来说,所需要的初始化信息不同,这里以视频为例。
伪代码如下:
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;
}
// Visual Sequences
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;
}
// AudioSampleEntry、HintSampleEntry 定义略过
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 u entry_count ; i++) {
switch (handler_type){
case ‘soun’: // for audio tracks
AudioSampleEntry();
break;
case ‘vide’: // for video tracks
VisualSampleEntry();
break;
case ‘hint’: // Hint track
HintSampleEntry();
break;
}
}
}复制代码
在SampleDescriptionBox 中,handler_type 参数 为 track 的类型(soun、vide、hint),entry_count 变量代表当前box中 smaple description 的条目数。
stsc 中,sample_description_index 就是指向这些smaple description的索引。
针对不同的handler_type,SampleDescriptionBox 后续应用不同的 SampleEntry 类型,比如video track为VisualSampleEntry。
VisualSampleEntry包含如下字段:
In video tracks, the frame_count field must be 1 unless the specification for the media format explicitly documents this template field and permits larger values. That specification must document both how the individual frames of video are found (their size information) and their timing established. That timing might be as simple as dividing the sample duration by the frame count to establish the frame duration.
例子如下:
chunk在文件中的偏移量。针对小文件、大文件,有两种不同的box类型,分别是stco、co64,它们的结构是一样的,只是字段长度不同。
chunk_offset 指的是在文件本身中的 offset,而不是某个box内部的偏移。
在构建mp4文件的时候,需要特别注意 moov 所处的位置,它对于chunk_offset 的值是有影响的。有一些MP4文件的 moov 在文件末尾,为了优化首帧速度,需要将 moov 移到文件前面,此时,需要对 chunk_offset 进行改写。
stco 定义如下:
# Box Type: ‘stco’, ‘co64’
# Container: Sample Table Box (‘stbl’) Mandatory: Yes
# Quantity: Exactly one variant must be present
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;
}
}
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;
}
}复制代码
如下例子所示,第一个chunk的offset是47564,第二个chunk的偏移是120579,其他类似。
sample 以 chunk 为单位分成多个组。chunk的size可以是不同的,chunk里面的sample的size也可以是不同的。
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 |
---|---|---|
1 | 15 | 1 |
16 | 30 | 1 |
17 | 28 | 1 |
每个sample的大小(字节),根据 sample_size 字段,可以知道当前track包含了多少个sample(或帧)。
有两种不同的box类型,stsz、stz2。
stsz:
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 u sample_count; i++) {
unsigned int(32) entry_size;
}
}
}复制代码
stz2:
aligned(8) class CompactSampleSizeBox extends FullBox(‘stz2’, version = 0, 0) {
unsigned int(24) reserved = 0;
unisgned int(8) field_size;
unsigned int(32) sample_count;
for (i=1; i u sample_count; i++) {
unsigned int(field_size) entry_size;
}
}复制代码
例子如下:
stts包含了DTS到sample number的映射表,主要用来推导每个帧的时长。
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;
}
}复制代码
还是看例子,如下图,entry_count为3,前250个sample的时长为1000,第251个sample时长为999,第252~283个sample的时长为1000。
假设timescale为1000,则实际时长需要除以1000。
mp4文件中,关键帧所在的sample序号。如果没有stss的话,所有的sample中都是关键帧。
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;
}
}复制代码
例子如下,第1、31、61、91、121...271个sample是关键帧。
从解码(dts)到渲染(pts)之间的差值。
对于只有I帧、P帧的视频来说,解码顺序、渲染顺序是一致的,此时,ctts没必要存在。
对于存在B帧的视频来说,ctts就需要存在了。当PTS、DTS不相等时,就需要ctts了,公式为 CT(n) = DT(n) + CTTS(n) 。
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;
}
}复制代码
例子如下,不赘述:
fMP4 跟普通 mp4 基本文件结构是一样的。普通mp4用于点播场景,fmp4通常用于直播场景。
它们有以下差别:
举例来说,普通mp4、fMP4顶层box结构可能如下。以下是通过笔者编写的MP4解析小工具打印出来,代码在文末给出。
// 普通mp4
ftyp size=32(8+24) curTotalSize=32
moov size=4238(8+4230) curTotalSize=4270
mdat size=1124105(8+1124097) curTotalSize=1128375
// fmp4
ftyp size=36(8+28) curTotalSize=36
moov size=1227(8+1219) curTotalSize=1263
moof size=1252(8+1244) curTotalSize=2515
mdat size=65895(8+65887) curTotalSize=68410
moof size=612(8+604) curTotalSize=69022
mdat size=100386(8+100378) curTotalSize=169408复制代码
怎么判断mp4文件是普通mp4,还是fMP4呢?一般可以看下是否存在存在mvex(Movie Extends Box)。
当存在mvex时,表示当前文件是fmp4(非严谨)。此时,sample相关的metadata不在moov里,需要通过解析moof box来获得。
伪代码如下:
aligned(8) class MovieExtendsBox extends Box(‘mvex’){ }复制代码
mehd是可选的,用来声明影片的完整时长(fragment_duration)。如果不存在,则需要遍历所有的fragment,来获得完整的时长。对于fmp4的场景,fragment_duration一般没办法提前预知。
aligned(8) class MovieExtendsHeaderBox extends FullBox(‘mehd’, version, 0) {
if (version==1) {
unsigned int(64) fragment_duration;
} else { // version==0
unsigned int(32) fragment_duration;
}
}复制代码
用来给 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
}复制代码
字段含义如下:
default_sample_flags 占4个字节,比较复杂,结构如下:
老版本规范里,前6位都是保留位,新版规范里,只有前4位是保留位。is_leading 含义不是很直观,下一小节会专门讲解下。
例子如下:
is_leading 不是特别好解释,这里贴上原文,方便大家理解。
A leading sample (usually a picture in video) is defined relative to a reference sample, which is the immediately prior sample that is marked as “sample_depends_on” having no dependency (an I picture). A leading sample has both a composition time before the reference sample, and possibly also a decoding dependency on a sample before the reference sample. Therefore if, for example, playback and decoding were to start at the reference sample, those samples marked as leading would not be needed and might not be decodable. A leading sample itself must therefore not be marked as having no dependency.
为方便讲解,下面的 leading frame 对应 leading sample,referenced frame 对应 referenced samle。
以 H264编码 为例,H264 中存在 I帧、P帧、B帧。由于 B帧 的存在,视频帧的 解码顺序、渲染顺序 可能不一致。
mp4文件的特点之一,就是支持随机位置播放。比如,在视频网站上,可以拖动进度条快进。
很多时候,进度条定位的那个时刻,对应的不一定是 I帧。为了能够顺利播放,需要往前查找最近的一个 I帧,如果可能的话,从最近的 I帧 开始解码播放(也就是说,不一定能从前面最近的I帧播放)。
将上面描述的此刻定位到的帧,称作 leading frame。leading frame 前面最近的一个 I 帧,叫做 referenced frame。
回顾下 is_leading 为 1 或 3 的情况,同样都是 leading frame,什么时候可以解码(decodable),什么时候不能解码(not decodable)?
1: this sample is a leading sample that has a dependency before the referenced I‐picture (and is therefore not decodable); 3: this sample is a leading sample that has no dependency before the referenced I‐picture (and is therefore decodable);
1、is_leading 为 1 的例子: 如下所示,帧2(leading frame) 解码依赖 帧1、帧3(referenced frame)。在视频流里,从 帧2 往前查找,最近的 I帧 是 帧3。哪怕已经解码了 帧3,帧2 也解不出来。
2、is_leading 为 3 的例子: 如下所示,此时,帧2(leading frame)可以解码出来。
moof是个container box,相关 metadata 在内嵌box里,比如 mfhd、 tfhd、trun 等。
伪代码如下:
aligned(8) class MovieFragmentBox extends Box(‘moof’){ }复制代码
结构比较简单,sequence_number 为 movie fragment 的序列号。根据 movie fragment 产生的顺序,从1开始递增。
aligned(8) class MovieFragmentHeaderBox extends FullBox(‘mfhd’, 0, 0){
unsigned int(32) sequence_number;
}复制代码
aligned(8) class TrackFragmentBox extends Box(‘traf’){ }复制代码
对 fmp4 来说,数据被氛围多个 movie fragment。一个 movie fragment 可包含多个track fragment(每个 track 包含0或多个 track fragment)。每个 track fragment 中,可以包含多个该 track 的 sample。
每个 track fragment 中,包含多个 track run,每个 track run 代表一组连续的 sample。
tfhd 用来设置 track fragment 中 的 sample 的 metadata 的默认值。
伪代码如下,除了 track_ID,其他都是 可选字段。
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
}复制代码
sample_description_index、default_sample_duration、default_sample_size 没什么好讲的,这里只讲解下 tf_flags、base_data_offset。
首先是 tf_flags,不同 flag 的值如下(同样是求按位求或) :
sample 位置计算公式为 base_data_offset + data_offset,其中,data_offset 每个 sample 单独定义。如果未显式提供 base_data_offset,则 sample 的位置的通常是基于 moof 的相对位置。
举个例子,比如 tf_flags 等于 57,表示 存在 base_data_offset、default_sample_duration、default_sample_flags。
base_data_offset 为 1263 (ftyp、moov 的size 之和为 1263)。
trun 伪代码如下:
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 ]
}复制代码
前面听过,track run 表示一组连续的 sample,其中:
tr_flags 如下,大同小异:
举例如下,tr_flags 为 2565。此时,存在 data_offset 、first_sample_flags、sample_size、sample_composition_time_offset。
纸上得来终觉浅,绝知此事要coding。根据 mp4 文件规范,可以写个简易的 mp4 文件解析工具,比如前文对比 普通mp4、fMP4 的 box 结构,就是笔者自己写的分析脚本。
核心代码如下,完整代码有点长
class Box {
constructor(boxType, extendedType, buffer) {
this.type = boxType; // 必选,字符串,4个字节,box类型
this.size = 0; // 必选,整数,4个字节,box的大小,单位是字节
this.headerSize = 8; //
this.boxes = [];
// this.largeSize = 0; // 可选,8个字节
// this.extendedType = extendedType || boxType; // 可选,16个字节
this._initialize(buffer);
}
_initialize(buffer) {
this.size = buffer.readUInt32BE(0); // 4个字节
this.type = buffer.slice(4, 8).toString(); // 4个字节
let offset = 8;
if (this.size === 1) {
this.size = buffer.readUIntBE(8, 8); // 8个字节,largeSize
this.headerSize += 8;
offset = 16;
} else if (this.size === 1) {
// last box
}
if (this.type === 'uuid') {
this.type = buffer.slice(offset, 16); // 16个字节
this.headerSize += 16;
}
}
setInnerBoxes(buffer, offset = 0) {
const innerBoxes = getInnerBoxes(buffer.slice(this.headerSize + offset, this.size));
innerBoxes.forEach(item => {
let { type, buffer } = item;
type = type.trim(); // 备注,有些box类型不一定四个字母,比如 url、urn
if (this[type]) {
const box = this[type](buffer);
this.boxes.push(box);
} else {
this.boxes.push('TODO 待实现');
// console.log(`unknowed type: ${type}`);
}
});
}
}
class FullBox extends Box {
constructor(boxType, buffer) {
super(boxType, '', buffer);
const headerSize = this.headerSize;
this.version = buffer.readUInt8(headerSize); // 必选,1个字节
this.flags = buffer.readUIntBE(headerSize + 1, 3); // 必选,3个字节
this.headerSize = headerSize + 4;
}
}
// FileTypeBox、MovieBox、MediaDataBox、MovieFragmentBox 代码有点长这里就不贴了
class Movie {
constructor(buffer) {
this.boxes = [];
this.bytesConsumed = 0;
const innerBoxes = getInnerBoxes(buffer);
innerBoxes.forEach(item => {
const { type, buffer, size } = item;
if (this[type]) {
const box = this[type](buffer);
this.boxes.push(box);
} else {
// 自定义 box 类型
}
this.bytesConsumed += size;
});
}
ftyp(buffer) {
return new FileTypeBox(buffer);
}
moov(buffer) {
return new MovieBox(buffer);
}
mdat(buffer) {
return new MediaDataBox(buffer);
}
moof(buffer) {
return new MovieFragmentBox(buffer);
}
}
function getInnerBoxes(buffer) {
let boxes = [];
let offset = 0;
let totalByteLen = buffer.byteLength;
do {
let box = getBox(buffer, offset);
boxes.push(box);
offset += box.size;
} while(offset < totalByteLen);
return boxes;
}
function getBox(buffer, offset = 0) {
let size = buffer.readUInt32BE(offset); // 4个字节
let type = buffer.slice(offset + 4, offset + 8).toString(); // 4个字节
if (size === 1) {
size = buffer.readUIntBE(offset + 8, 8); // 8个字节,largeSize
} else if (size === 0) {
// last box
}
let boxBuffer = buffer.slice(offset, offset + size);
return {
size,
type,
buffer: boxBuffer
};
}
复制代码
作者:程序猿小卡
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。