5分鐘入門MP4檔案格式

tone12345發表於2020-12-09

寫在前面
本文主要內容包括,什麼是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。

box header:box的後設資料,比如box type、box size。
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 (size1) {
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 (version1) {
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;
}
複製程式碼
欄位含義如下:

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 (version1) {
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 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包含如下欄位:

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 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,其他類似。

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_index11511630117281

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_size0,那麼,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解析小工具列印出來,程式碼在文末給出。
// 普通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(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 (version1) {
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
};

}
複製程式碼
寫在後面
受限於時間,同時為了方便講解,部分內容可能不是很嚴謹,如有錯漏,敬請指出。如有問題,也歡迎隨時交流。

作者:程式猿小卡
連結:
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

相關文章