作者: haodongyuan
文章介紹了M4A檔案的大概結構,詳細解讀了其中的Sample Table Box,并結合圖例,詳細講解了如何使用它來完成M4A檔案的随機通路。
本文屬原創作品,轉載請保留出處!
在講解M4A的随機通路之前,我們先來大概了解一下MP4檔案結構,以及MP4和M4A的關系。
整個MP4檔案由若幹個box組成,box可以嵌套。每個box包含自己的大小和類型等資訊,之後就是包含的内容,box也可以作為其内容,形成嵌套,如下圖所示:

圖檔來源
類似面向對象程式設計語言,box也有“繼承”的概念,所有box都繼承于Box類,其結構如下:
其中,size就是這個box的大小,包含所有字段(包括size自己)和它包含的box。type就是此box的類型,必須由四個英文字母表示。
有了這兩個值,就可以快速定位到某個box了。
另外一個常見的box是FullBox,stbl裡面的box都繼承于此類,其結構如下:
MP4規範中描述了非常多的box,不過最常用的到的其實隻有這些:
圖檔來源:"MP4檔案格式的解析,以及MP4檔案的分割算法"
M4A可以了解為隻包含音頻的MP4,最初由Apple提出。
具體到Sample Table Box裡面差別,由于所有音頻幀都是同步幀,是以M4A沒有stss。至于有沒有elst,還沒有找到任何規範說明M4A是否存在elst,但是從faad解碼庫的源碼裡面找不到任何elst相關邏輯,是以本文将不讨論elst。
現在進入主題:在MP4中,如何進行随機通路。
在MP4中,一個軌道一定并且隻會存在一個Sample Table Box,簡寫為stbl。它的官方定義如下:它包含一個軌道中所有媒體采樣的時間-資料索引。說人話,它的主要功能就是:将時間轉換成對應采樣在檔案中的位置。
這對流媒體播放是至關重要的。比如說,在流媒體播放中,如果使用者seek(既拖動進度條)到了1:50處,如果1:50的資料還沒有被緩沖,就需要我們馬上從這裡開始緩沖。
那麼問題來了:如何知道1:50對應的資料在檔案中哪個位置呢?
一個簡單的方法就是用平均碼率來計算:
如果歌曲是恒定碼率(CBR),并且頭不大的話,用這個方法計算offset,再加上一些補償,也是可行的。
如果想更精确地計算offset,就必須使用Sample Table Box,既stbl。
stbl裡面包含很多box,有必需,也有可選的。這裡對必需的進行詳細講解,可選的隻做簡單介紹。
首先來看一下如何找到stbl,以及它包含哪些子box:
圖檔來源:"MP4檔案elst研究"
然後,我用僞代碼描述一下完整的流程:
其中,必需的box有:stts、stsc、stco或co64其中一個、stsz,一共四個。可選的box有:elst、stss。
接下來,我們來看下上面僞代碼中各個操作的意義。
MP4内部的使用的時間機關不是秒、毫秒等實體意義上的時間機關,要經過以下轉換:
其中,timeScale的含義是:一秒内流過多少個時間機關,對于音頻,就是每秒采樣率,對于視訊,就是每秒幀率。
如果trak中存在elst,事情就有些複雜了,它的出現,說明MP4中的某條軌道的時間戳有偏移,比如視訊比音頻慢10s,或者某一幀畫面停留一段時間等等。
這裡不做詳解,有興趣的話,可以參考:link和link,使用方法可以參考ffmpeg的代碼,見mov.c的mov_build_index方法。
這個box儲存了sample序号和對應的播放時間資訊。其中,播放時間通過內插補點的方式進行儲存,以減少box的大小。
它的結構如下:
一個entry就像這樣:
Sample Count
Sample-delta
14
10
說明這個entry包含14個sample。每個sample的時間相差10個時間機關。
如果整個stts隻有這一個entry,那麼就很容易計算出:
當time < 14 * 10 時,sample = time / 10
當time >= 14 * 10 時,sample = 14(因為總共隻有14個sample)
以此類推即可得出任意entry時的算法。
這是一個可選的box,如果stbl中不存在此box,說明每一個sample都是同步的。否則就要通過此box查找同步sample。
每一個音頻sample都是同步sample,是以M4A不會存在stss。
查找方法很簡單,用二分法查找即可。
在繼續之前,有必要先來介紹一下,在MP4中,媒體資料是如何儲存的。
所有chunk位于mdat中,每個chunk大小可以不一樣,其中包含的每個sample也可以有不同大小。
一個chunk中包含一個軌道的若幹個連續sample。不同軌道的chunk交錯存放。
如下圖所示,chunk1包含軌道1的4個連續sample,chunk2包含軌道2的4個連續采樣。
stsc用于查詢sample所在的chunk。它的結構是這樣的:
舉個例子,比如stsc包含兩個entry:
first_chunk
samples_per_chunk
sample_description_index
1
不關心
50
20
說明第1個到第49個chunk,每個chunk都包含10個sample,第50個以及之後的chunk,每個chunk包含20個sample。如下圖所示:
這樣就可以計算出sample所在的chunk了,比如sample 490位于chunk 49第一個sample,sample 499位于chunk 49最末,sample 500剛好位于chunk 50的第一個sample。如下圖:
得到chunk序号之後,用stco或者co64,可以計算出該chunk在檔案中的位置。co64其實就是stco的64位版本,使用方式是一樣的,兩者隻能同時存在一個。
這個box包含每個chunk相對檔案開頭的偏移,結構如下:
使用方法很簡單,用chunk的序号去chunk_offset數組裡面取就行。如下圖:
注意,上面得到chunk的序号是從1開始的,去數組裡面取的時候注意減一。
這個box包含sample的大小資訊。它的結構如下:
sample大小不一定是固定的,如果是固定的,sample_size就不為0;否則,每個sample的大小儲存在entry_size數組裡面。
比如有這樣一個stsz box,它的sample_size為0,entry_size中記錄每個sample在所屬chunk内部的偏移:
在第5步(stsc的使用)中,我們擷取到了sample所屬chunk的序号,以及chunk第一個sample的序号,通過stsz,就可以獲得該sample在所處chunk内部的偏移。
比如要計算sample 497的内部偏移,需要從497所屬chunk的第一個sample(在這裡是490)開始,将偏移累加起來:
看到這裡,你是否會想到:既然stsz包含了所有sample的大小,僅通過sample大小就可以計算出對應的偏移,不再需要計算chunk偏移了。 但不要忘了:chunk是按照不同軌道交錯排列的,而且即便隻有一個軌道,每個chunk自身頭部的大小也不能忽略。
chunk的偏移加上sample在目前chunk内的偏移,就是sample的完整偏移了。如下圖:
box的解析比較簡單,讀取前8個位元組,其中前4個位元組為box大小,後4個為類型,知道類型後,按照類型定義的字段按序讀取即可。
其中有兩點需要注意:
将byte[]轉換成int時,使用大端序
解析多個數組時,要“交錯”地解析,比如stts,應該這樣解析:for (uint8 i = 0; i < entry_count; i++) { sample_count[i] = readInt(); sample_delta[i] = readInt(); }
相對于Flac珍惜每一個bit的辦事風格,MP4還是比較慷慨的,是以解析起來比較友善。
而且,經過觀察,MP4的關鍵sample間隔僅在0.02s ~ 0.04s,作為比較,flac的seektable則是平均10s一個關鍵sample。
至于STBL所占大小,我觀察了幾個檔案,所占空間很小:
檔案大小(KB)
時長(S)
STBL大小(KB)
1319
209
19
1887
193
6.6
3550
333
61
由于文章隻關注M4A的随機通路,MP4中可見的elst、stss,ctts等等box就沒有解析了,如果對這方面有興趣,可以參考MP4的規範以及網上資料。
ISO/IEC 14496-12 (内容很多,其實隻看Appendix A就好了,對MP4檔案做了一個大緻的介紹,此外,第11、12頁是其中最常用的)
MP4檔案格式的解析,以及MP4檔案的分割算法
MP4檔案elst研究