写在后面

本文次要内容包含,什么是MP4、MP4文件的根本构造、Box的根本构造、常见且重要的box介绍、一般MP4与fMP4的区别、如何通过代码解析MP4文件 等。

写作背景:最近常常答复团队小伙伴对于直播 & 短视频的问题,比方 “flv.js的实现原理”、“为什么设计同学给的mp4文件浏览器里播放不了、但本地能够失常播放”、“MP4兼容性很好,可不可以用来做直播” 等。

在解答的过程中,发现常常波及 MP4 协定的介绍。之前这块有简略理解过并做了笔记,这里略微整顿一下,顺便作为团队参考文档,如有错漏,敬请指出。

什么是MP4

首先,介绍下封装格局。多媒体封装格局(也叫容器格局),是指依照肯定的规定,将视频数据、音频数据等,放到一个文件中。常见的 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文件格式概览

MP4文件由多个box组成,每个box存储不同的信息,且box之间是树状构造,如下图所示。

box类型有很多,上面是3个比拟重要的顶层box:

  • ftyp:File Type Box,形容文件听从的MP4标准与版本;
  • moov:Movie Box,媒体的metadata信息,有且仅有一个。
  • mdat:Media Data Box,寄存理论的媒体数据,个别有多个;

尽管box类型有很多,但根本构造都是一样的。下一节会先介绍box的构造,而后再对常见的box进行进一步解说。

下表是常见的box,略微看下有个大抵的印象就好,而后间接跳到下一节。

MP4 Box简介

1个box由两局部组成:box header、box body。

  1. box header:box的元数据,比方box type、box size。
  2. box body:box的数据局部,理论存储的内容跟box类型无关,比方mdat中body局部存储的媒体数据。

box header中,只有type、size是必选字段。当size==0时,存在largesize字段。在局部box中,还存在version、flags字段,这样的box叫做Full Box。当box body中嵌套其余box时,这样的box叫做container box。

Box Header

字段定义如下:

  • type:box类型,包含 “预约义类型”、“自定义扩大类型”,占4个字节;

    • 预约义类型:比方ftyp、moov、mdat等预约义好的类型;
    • 自定义扩大类型:如果type==uuid,则示意是自定义扩大类型。size(或largesize)随后的16字节,为自定义类型的值(extended_type)
  • size:蕴含box header在内的整个box的大小,单位是字节。当size为0或1时,须要非凡解决:

    • size等于0:box的大小由后续的largesize确定(个别只有装载媒体数据的mdat box会用到largesize);
    • size等于1:以后box为文件的最初一个box,通常蕴含在mdat box中;
  • largesize:box的大小,占8个字节;
  • extended_type:自定义扩大类型,占16个字节;

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 Body

box数据体,不同box蕴含的内容不同,须要参考具体box的定义。有的 box body 很简略,比方 ftyp。有的 box 比较复杂,可能嵌套了其余box,比方moov。

Box vs FullBox

在Box的根底上,扩大出了FullBox类型。相比Box,FullBox 多了 version、flags 字段。

  • version:以后box的版本,为扩大做筹备,占1个字节;
  • flags:标记位,占24位,含意由具体的box本人定义;

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(File Type Box)

ftyp用来指出以后文件遵循的标准,在介绍ftyp的细节前,先科普下isom。

什么是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定义

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 的几个字段的含意:

  • major_brand:比方常见的 isom、mp41、mp42、avc1、qt等。它示意“最好”基于哪种格局来解析以后的文件。举例,major_brand 是 A,compatible_brands 是 A1,当解码器同时反对 A、A1 标准时,最好应用A标准来解码以后媒体文件,如果不反对A标准,但反对A1标准,那么,能够应用A1标准来解码;
  • minor_version:提供 major_brand 的阐明信息,比方版本号,不得用来判断媒体文件是否合乎某个规范/标准;
  • compatible_brands:文件兼容的brand列表。比方 mp41 的兼容 brand 为 isom。通过兼容列表里的 brand 标准,能够将文件 局部(或全副)解码进去;
在理论应用中,不能把 isom 做为 major_brand,而是须要应用具体的brand(比方mp41),因而,对于 isom,没有定义具体的文件扩展名、mime type。

上面是常见的几种brand,以及对应的文件扩展名、mime type,更多brand能够参考 这里 。

上面是理论例子的截图,不赘述。

对于AVC/AVC1

在探讨 MP4 标准时,提到AVC,有的时候指的是“AVC文件格式”,有的时候指的是"AVC压缩规范(H.264)",这里简略做下辨别。

  • AVC文件格式:基于 ISO根底文件格式 衍生的,应用的是AVC压缩规范,能够认为是MP4的扩大格局,对应的brand 通常是 avc1,在MPEG-4 PART 15 中定义。
  • AVC压缩规范(H.264):在MPEG-4 Part 10中定义。
  • ISO根底文件格式(Base Media File Format) 在 MPEG-4 Part 12 中定义。

moov(Movie Box)

Movie Box,存储 mp4 的 metadata,个别位于mp4文件的结尾。

aligned(8) class MovieBox extends Box(‘moov’){ }

moov中,最重要的两个box是 mvhd 和 trak:

  • mvhd:Movie Header Box,mp4文件的整体信息,比方创立工夫、文件时长等;
  • trak:Track Box,一个mp4能够蕴含一个或多个轨道(比方视频轨道、音频轨道),轨道相干的信息就在trak里。trak是container box,至多蕴含两个box,tkhd、mdia;
mvhd针对整个影片,tkhd针对单个track,mdhd针对媒体,vmhd针对视频,smhd针对音频,能够认为是从 宽泛 > 具体,前者个别是从后者推导进去的。

mvhd(Movie Header Box)

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.0template 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;}

字段含意如下:

  • creation_time:文件创建工夫;
  • modification_time:文件批改工夫;
  • timescale:一秒蕴含的工夫单位(整数)。举个例子,如果timescale等于1000,那么,一秒蕴含1000个工夫单位(前面track等的工夫,都要用这个来换算,比方track的duration为10,000,那么,track的理论时长为10,000/1000=10s);
  • duration:影片时长(整数),依据文件中的track的信息推导进去,等于工夫最长的track的duration;
  • rate:举荐的播放速率,32位整数,高16位、低16位别离代表整数局部、小数局部([16.16]),举例 0x0001 0000 代表1.0,失常播放速度;
  • volume:播放音量,16位整数,高8位、低8位别离代表整数局部、小数局部([8.8]),举例 0x01 00 示意 1.0,即最大音量;
  • matrix:视频的转换矩阵,个别能够忽略不计;
  • next_track_ID:32位整数,非0,个别能够忽略不计。当要增加一个新的track到这个影片时,能够应用的track id,必须比以后曾经应用的track id要大。也就是说,增加新的track时,须要遍历所有track,确认可用的track id;

tkhd(Track Box)

单个 track 的 metadata,蕴含如下字段:

  • version:tkhd box的版本;
  • flags:按位或操作取得,默认值是7(0x000001 | 0x000002 | 0x000004),示意这个track是启用的、用于播放的 且 用于预览的。

    • Track_enabled:值为0x000001,示意这个track是启用的,当值为0x000000,示意这个track没有启用;
    • Track_in_movie:值为0x000002,示意以后track在播放时会用到;
    • Track_in_preview:值为0x000004,示意以后track用于预览模式;
  • creation_time:以后track的创立工夫;
  • modification_time:以后track的最近批改工夫;
  • track_ID:以后track的惟一标识,不能为0,不能反复;
  • duration:以后track的残缺时长(须要除以timescale失去具体秒数);
  • layer:视频轨道的叠加程序,数字越小越凑近观看者,比方1比2靠上,0比1靠上;
  • alternate_group:以后track的分组ID,alternate_group值雷同的track在同一个分组外面。同个分组里的track,同一时间只能有一个track处于播放状态。当alternate_group为0时,示意以后track没有跟其余track处于同个分组。一个分组外面,也能够只有一个track;
  • volume:audio track的音量,介于0.0~1.0之间;
  • matrix:视频的变换矩阵;
  • width、height:视频的宽高;

定义如下:

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;}

例子如下:

hdlr(Handler Reference Box)

申明以后track的类型,以及对应的处理器(handler)。

handler_type的取值包含:

  • vide(0x76 69 64 65),video track;
  • soun(0x73 6f 75 6e),audio track;
  • hint(0x68 69 6e 74),hint track;

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;}

stbl(Sample Table Box)

MP4文件的媒体数据局部在mdat box里,而stbl则蕴含了这些媒体数据的索引以及工夫信息,理解stbl对解码、渲染MP4文件很要害。

在MP4文件中,媒体数据被分成多个chunk,每个chunk可蕴含多个sample,而sample则由帧组成(通常1个sample对应1个帧),关系如下:

stbl中比拟要害的box蕴含stsd、stco、stsc、stsz、stts、stss、ctts。上面先来个概要的介绍,而后再一一解说细节。

stco / stsc / stsz / stts / stss / ctts / stsd 概述

上面是这几个box概要的介绍:

  • stsd:给出视频、音频的编码、宽高、音量等信息,以及每个sample中蕴含多少个frame;
  • stco:thunk在文件中的偏移;
  • stsc:每个thunk中蕴含几个sample;
  • stsz:每个sample的size(单位是字节);
  • stts:每个sample的时长;
  • stss:哪些sample是关键帧;
  • ctts:帧解码到渲染的工夫差值,通常用在B帧的场景;

stsd(Sample Description 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 Sequencesclass 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蕴含如下字段:

  • data_reference_index:当MP4文件的数据局部,能够被宰割成多个片段,每一段对应一个索引,并别离通过URL地址来获取,此时,data_reference_index 指向对应的片段(比拟少用到);
  • width、height:视频的宽高,单位是像素;
  • horizresolution、vertresolution:程度、垂直方向的分辨率(像素/英寸),16.16定点数,默认是0x00480000(72dpi);
  • frame_count:一个sample中蕴含多少个frame,对video track来说,默认是1;
  • compressorname:仅供参考的名字,通常用于展现,占32个字节,比方 AVC Coding。第一个字节,示意这个名字理论要占用N个字节的长度。第2到第N+1个字节,存储这个名字。第N+2到32个字节为填充字节。compressorname 能够设置为0;
  • depth:位图的深度信息,比方 0x0018(24),示意不带alpha通道的图片;
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.

例子如下:

stco(Chunk Offset Box)

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 presentaligned(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,其余相似。

stsc(Sample To Chunk Box)

sample 以 chunk 为单位分成多个组。chunk的size能够是不同的,chunk外面的sample的size也能够是不同的。

  • entry_count:有多少个表项(每个表项,蕴含first_chunk、samples_per_chunk、sample_description_index信息);
  • first_chunk:以后表项中,对应的第一个chunk的序号;
  • samples_per_chunk:每个chunk蕴含的sample数;
  • sample_description_index:指向 stsd 中 sample description 的索引值(参考stsd大节);
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;    }}

后面形容比拟形象,这里看个例子,这里示意的是:

  • 序号1~15的chunk,每个chunk蕴含15个sample;
  • 序号16的chunk,蕴含30个sample;
  • 序号17以及之后的chunk,每个chunk蕴含28个sample;
  • 以上所有chunk中的sample,对应的sample description的索引都是1;
first_chunksamples_per_chunksample_description_index
1151
16301
17281

stsz(Sample Size Boxes)

每个sample的大小(字节),依据 sample_size 字段,能够晓得以后track蕴含了多少个sample(或帧)。

有两种不同的box类型,stsz、stz2。

stsz:

  • sample_size:默认的sample大小(单位是byte),通常为0。如果sample_size不为0,那么,所有的sample都是同样的大小。如果sample_size为0,那么,sample的大小可能不一样。
  • sample_count:以后track外面的sample数目。如果 sample_size==0,那么,sample_count 等于上面entry的条目;
  • entry_size:单个sample的大小(如果sample_size==0的话);
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:

  • field_size:entry表中,每个entry_size占据的位数(bit),可选的值为4、8、16。4比拟非凡,当field_size等于4时,一个字节上蕴含两个entry,高4位为entry[i],低4位为entry[i+1];
  • sample_count:等于上面entry的条目;
  • entry_size:sample的大小。
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(Decoding Time to Sample Box)

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:stts 中蕴含的entry条目数;
  • sample_count:单个entry中,具备雷同时长(duration 或 sample_delta)的间断sample的个数。
  • sample_delta:sample的时长(以timescale为计量)

还是看例子,如下图,entry_count为3,前250个sample的时长为1000,第251个sample时长为999,第252~283个sample的时长为1000。

假如timescale为1000,则理论时长须要除以1000。

stss(Sync Sample Box)

mp4文件中,关键帧所在的sample序号。如果没有stss的话,所有的sample中都是关键帧。

  • entry_count:entry的条目数,能够认为是关键帧的数目;
  • sample_number:关键帧对应的sample的序号;(从1开始计算)
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是关键帧。

ctts(Composition Time to Sample Box)

从解码(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(Fragmented mp4)

fMP4 跟一般 mp4 根本文件构造是一样的。一般mp4用于点播场景,fmp4通常用于直播场景。

它们有以下差异:

  • 一般mp4的时长、内容通常是固定的。fMP4 时长、内容通常不固定,能够边生成边播放;
  • 一般mp4残缺的metadata都在moov里,须要加载完moov box后,能力对mdat中的媒体数据进行解码渲染;
  • fMP4中,媒体数据的metadata在moof box中,moof 跟 mdat (通常)结对呈现。moof 中蕴含了sample duration、sample size等信息,因而,fMP4能够边生成边播放;

举例来说,一般mp4、fMP4顶层box构造可能如下。以下是通过笔者编写的MP4解析小工具打印进去,代码在文末给出。

// 一般mp4ftyp size=32(8+24) curTotalSize=32moov size=4238(8+4230) curTotalSize=4270mdat size=1124105(8+1124097) curTotalSize=1128375// fmp4ftyp size=36(8+28) curTotalSize=36moov size=1227(8+1219) curTotalSize=1263moof size=1252(8+1244) curTotalSize=2515mdat size=65895(8+65887) curTotalSize=68410moof size=612(8+604) curTotalSize=69022mdat size=100386(8+100378) curTotalSize=169408

怎么判断mp4文件是一般mp4,还是fMP4呢?个别能够看下是否存在存在mvex(Movie Extends Box)。

mvex(Movie Extends Box)

当存在mvex时,示意以后文件是fmp4(非谨严)。此时,sample相干的metadata不在moov里,须要通过解析moof box来取得。

伪代码如下:

aligned(8) class MovieExtendsBox extends Box(‘mvex’){ }

mehd(Movie Extends Header Box)

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;    }}

trex(Track Extends 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}

字段含意如下:

  • track_id:对应的 track 的 ID,比方video track、audio track 的ID;
  • default_sample_description_index:sample description 的默认 index(指向stsd);
  • default_sample_duration:sample 默认时长,个别为0;
  • default_sample_size:sample 默认大小,个别为0;
  • default_sample_flags:sample 的默认flag,个别为0;

default_sample_flags 占4个字节,比较复杂,构造如下:

老版本标准里,前6位都是保留位,新版标准里,只有前4位是保留位。is_leading 含意不是很直观,下一大节会专门解说下。
  • reserved:4 bits,保留位;
  • is_leading:2 bits,是否 leading sample,可能的取值包含:

    • 0:以后 sample 不确定是否 leading sample;(个别设为这个值)
    • 1:以后 sample 是 leading sample,并依赖于 referenced I frame 后面的 sample,因而无奈被解码;
    • 2:以后 sample 不是 leading sample;
    • 3:以后 sample 是 leading sample,不依赖于 referenced I frame 后面的 sample,因而能够被解码;
  • sample_depends_on:2 bits,是否依赖其余sample,可能的取值包含:

    • 0:不分明是否依赖其余sample;
    • 1:依赖其余sample(不是I帧);
    • 2:不依赖其余sample(I帧);
    • 3:保留值;
  • sample_is_depended_on:2 bits,是否被其余sample依赖,可能的取值包含:

    • 0:不分明是否有其余sample依赖以后sample;
    • 1:其余sample可能依赖以后sample;
    • 2:其余sample不依赖以后sample;
    • 3:保留值;
  • sample_has_redundancy:2 bits,是否有冗余编码,可能的取值包含:

    • 0:不分明是否存在冗余编码;
    • 1:存在冗余编码;
    • 2:不存在冗余编码;
    • 3:保留值;
  • sample_padding_value:3 bits,填充值;
  • sample_is_non_sync_sample:1 bits,不是关键帧;
  • sample_degradation_priority:16 bits,降级解决的优先级(个别针对如流传过程中呈现的问题);

例子如下:

对于 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(Movie Fragment Box)

moof是个container box,相干 metadata 在内嵌box里,比方 mfhd、 tfhd、trun 等。

伪代码如下:

aligned(8) class MovieFragmentBox extends Box(‘moof’){ }

mfhd(Movie Fragment Header Box)

构造比较简单,sequence_number 为 movie fragment 的序列号。依据 movie fragment 产生的程序,从1开始递增。

aligned(8) class MovieFragmentHeaderBox extends FullBox(‘mfhd’, 0, 0){    unsigned int(32)  sequence_number;}

traf(Track Fragment Box)

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 Header Box)

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 的值如下(同样是求按位求或) :

  • 0x000001 base‐data‐offset‐present:存在 base_data_offset 字段,示意 数据地位 绝对于整个文件的 根底偏移量。
  • 0x000002 sample‐description‐index‐present:存在 sample_description_index 字段;
  • 0x000008 default‐sample‐duration‐present:存在 default_sample_duration 字段;
  • 0x000010 default‐sample‐size‐present:存在 default_sample_size 字段;
  • 0x000020 default‐sample‐flags‐present:存在 default_sample_flags 字段;
  • 0x010000 duration‐is‐empty:示意以后时间段不存在sample,default_sample_duration 如果存在则为0 ,;
  • 0x020000 default‐base‐is‐moof:如果 base‐data‐offset‐present 为1,则疏忽这个flag。如果 base‐data‐offset‐present 为0,则以后 track fragment 的 base_data_offset 是从 moof 的第一个字节开始计算;

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(Track Fragment Run Box)

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,其中:

  • sample_count:sample 的数目;
  • data_offset:数据局部的偏移量;
  • first_sample_flags:可选,针对以后 track run中 第一个 sample 的设置;

tr_flags 如下,大同小异:

  • 0x000001 data‐offset‐present:存在 data_offset 字段;
  • 0x000004 first‐sample‐flags‐present:存在 first_sample_flags 字段,这个字段的值,只会笼罩第一个 sample 的flag设置;当 first_sample_flags 存在时,sample_flags 则不存在;
  • 0x000100 sample‐duration‐present:每个 sample 都有本人的 sample_duration,否则应用默认值;
  • 0x000200 sample‐size‐present:每个 sample 都有本人的 sample_size,否则应用默认值;
  • 0x000400 sample‐flags‐present:每个 sample 都有本人的 sample_flags,否则应用默认值;
  • 0x000800 sample‐composition‐time‐offsets‐present:每个 sample 都有本人的 sample_composition_time_offset;
  • 0x000004 first‐sample‐flags‐present,笼罩第一个sample的设置,这样就能够把一组sample中的第一个帧设置为关键帧,其余的设置为非关键帧;

举例如下,tr_flags 为 2565。此时,存在 data_offset 、first_sample_flags、sample_size、sample_composition_time_offset。

编程实际:解析MP4文件构造

纸上得来终觉浅,绝知此事要coding。依据 mp4 文件标准,能够写个繁难的 mp4 文件解析工具,比方前文比照 一般mp4、fMP4 的 box 构造,就是笔者本人写的剖析脚本。

外围代码如下,残缺代码有点长,能够在 笔者的github 上找到。

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    };}

写在前面

受限于工夫,同时为了不便解说,局部内容可能不是很谨严,如有错漏,敬请指出。如有问题,也欢送随时交换。

相干链接

ISO/IEC 14496-12:2015 Information technology — Coding of audio-visual objects — Part 12: ISO base media file format
https://www.iso.org/standard/...

Introduction to QuickTime File Format Specification
https://developer.apple.com/l...

AVC_(file_format)
http://fileformats.archivetea...

AV1 Codec ISO Media File Format Binding
https://aomediacodec.github.i...