Sample: 採樣,對於音視頻來說就是一個編碼幀;Sample_count即總幀數,Sample_index即幀下標。
在一個Mp4文件裏面,所有Box處理的Samples都是嚴格按照幀序號排列的。
刪除或者修改一幀,很多個Box裏面的內容需要從新計算。
在一個Chunk裏面Sample順序緊湊排列。
Tarck: 軌,是音頻或者視頻流信息定義的集合。一個軌包括一個或者多個Chunk。各軌之間有同步的問題。
Box(也叫atom): 盒,MP4文件各種信息定義的結構。一個MP4文件就是有很多個各種類型的Box組成的。
Box有固定的格式
4Bytes 4Bytes 8Bytes (Box_length-8)Bytes
Box_length | Box_type | Box_long_length | Box_data
b. box_length 包含了自己和4字節的type。因此一般情況下Box的負載是Box_length-8字節。
Box有兩大類,一種是Container Box,這種Box裏可以嵌套別的Box。它的負載就是其他的Box。
下圖加^的Box就是Container Box。
另外一種就是單獨的Box,裏面定義某一些信息數據。
time_scale: 很多個Box裏面都有time_scale,它定義了該媒體在1秒中內的採樣刻度, 可以理解爲一秒鐘有多少個單元。
duration: 相對於time_scale的時間長度。真實的時長 = duration/time_scale。
例如我手頭這個MP4文件,視頻track的time_scale爲25000. duration=9670000.
這樣time = 9670000/25000 = 386.8s = 6m27s
不同Box定義的time_scale和duration可能是不同的,原因在於:
a. 文件時長和track時長本身就不一定是相同的,Mp4格式容許雜合多個不同長短不同起始時間的Tracks在同一個文件裏面;
b. 一般mvhd,vmhd, smhd裏面的time_scale,duration都不相同。所以一定注意要用同一組來計算。
2.一個基本的MP4文件Box結構如下:
Root (虛擬的沒有這個box類型)
|----ftyp
|----moov^
| |----mvhd
| |----trak^
| | |----tkhd
| | |----mdia^
| | |----mdhd
| | |----hdlr
| | |----minf^
| | |----vmhd/smhd
| | |----dinf^
| | | |----dref^
| | | |----url
| | |----stbl^
| | |----stsd^
| | | |----mp4v/mp4a^
| | |----stts
| | |----stss
| | |----stsc
| | |----stsz
| | |----stco
| |----trak^
| | |---- ....
| | ....
| |
| |----udta^
| |---- meta^
| |----hdlr
| |----ilist^
|----mdat
|----free
3. 幾個重要的Box,先總結如下,再各個敘述。
stbl Box是Mp4文件裏面最複雜的Box. 有多種stxxBox, 其中st是Sample Table.
stsd: Sample Description, 具體音視頻解碼器的定義
stts: Time to Sample, 定義每個Sample的duration, 這個duration也是相對time_scale的單位
stss: Sync Sample, 定義同步Sample Index, 即I幀Sample的Index.
stsc: Sample to Chunk,定義Sample在多個Chunk裏面的劃分情況。
stsz: Sample siZe,定義每個Sample的字節長度
stco: Chunk Offset,定義各個Chunk在文件中的起始地址。
stts: Sample duration. 定義的方法類似於遊程編碼。
(Sample_number1, duration1), (Sample_number2, duration2), ... (Sample_numberN, durationN).
一個遊程對定義了相同duration的連續Sample個數。N定義了有多少個這種遊程對。
最常見的一種情況是所有的Sample具有相同的duration, 這樣stts:
stts.entry_count = 1,
stts.sample_couint[0] = Sample_Count;
stts.sample_delta[0] = duration.
當給定一個時間,找對應的sample_index時需要用到stts.
stss: I幀Sample Index.
由於視頻編碼幀間依賴性,不是從任意Samplea開始都可以連續解碼的,只有從I幀的Sample處纔可以。
stss定義了隨機訪問點的Index值。在做Seek的時候需要用到stss
stsc: Sample在Chunk裏面的分佈表。有4個主要參數。
a. uint32_t entry_count, 定義一個Track裏面包含多少個Chunk組,注意這裏不是Chunk count,而是 chunk組 count.
所謂chunk組是指那些包含相同個數Samples,而且這些Samples的Sample Description相同的Chunk集合,類似遊程編碼。
b. uint32_t *first_chunk, 定義每個Chunk組起始Chunk index
c. uint32_t *samples_per_chunk, 定義每個Chunk組內各個Chunk包含Sample個數
d. uint32_t *sample_description_index,定義每個Chunk組內各個Chunk所包含的Sample的解碼描述。
for example
inx first_chunk samples_per_chunk sample_description_index
0 1 13 1
1 2 12 1
2 806 9 1
意義:有3個chunk組.
組0 有 ( 2-1)個chunk (下標爲 1), 每個chunk含13個samples。 組0有(2-1)*13幀
組1 有 (806-2)個chunk (下標爲2..805), 每個chunk含12個Sample。組1有(806-2)*12幀
組2 有 1 個chunk* (下標爲806), 這個chunk含9個samples。組2有1*9幀.
一個Chunk組內chunk數目chunk_count = first_chunk[n+1] - first_chunk[n]。
一般最後一個chunk組就只有一個chunk. 在stco裏面有chunk count的定義。
如果不放心可以用那個數字來計算最後一個chunk組的chunk數目。
根據這個表,可以展開得到每一個Sample在Chunk中的分佈。
stsz: 定義每個Sample長度,這個Box很大。
在計算某一個Sample的偏移地址時需要stsz。
stco/co64:定義每個Chunk相對於文件頭的偏移地址。stco裏面的地址是32位,co64裏面的是64位,根據實際情況選用。
4.計算
輸入i_chunk, 輸出inx是Chunk組下標
uint32_t sample_to_chunk_group_index(MP4_Box_data_trak_t *p_track, uint32_t i_chunk) {
uint32_t i;
for(i=0; i<p_track->i_stsc_count-1; i++)
if(i_chunk >= p_track->i_stsc_first_chunk[i] && i_chunk < p_track->i_stsc_first_chunk[i+1]) break;
return i;
}
// 展開stsc,計算得到每個Chunk的i_sample_first, i_sample_count,i_sample_description_index 和 i_offset
unsigned int i_chunk;
uint32_t i_first = 0;
uint32_t i_chunk_group_index = 0;
for( i_chunk = 0; i_chunk < p_track->i_chunk_count; i_chunk++ ) {
mp4_chunk_t *ck = &p_track->chunk[i_chunk];
ck->i_offset = p_track->i_co64_sample_offset[i_chunk];
i_chunk_group_index = chunk_group_index(p_track, i_chunk); // 輸出Chunk組下標
ck->i_sample_description_index = p_track->i_stsc_sample_description_index[i_chunk_group_index];
ck->i_sample_count = p_track->i_stsc_samples_per_chunk[i_chunk_group_index];
ck->i_sample_first = i_first;
i_first += p_track->i_stsc_samples_per_chunk[i_chunk_group_index];
}
// 由Sample_index計算chunk_index
uint32_t sample_to_chunk(MP4_Box_data_trak_t *p_track, uint64_t sample) {
uint32_t i;
for(i=0; i<p_track->i_chunk_count-1; i++)
if(sample >= p_track->chunk[i].i_sample_first && sample < p_track->chunk[i+1].i_sample_first) break;
return i;
}
// 由Sample_index計算該Sample數據起始地址
// sample起始地址 = 該sample所在Chunk偏移地址(stco) + 該sample在Chunk裏面的偏移地址 (stsz)
// sample在chunk裏面的偏移地址 = 該chunk的第一幀到該幀之前的Sample長度和
uint64_t smaple_to_offset(MP4_Box_data_trak_t *p_track, uint64_t sample) {
uint32_t chunk_inx = sample_to_chunk(p_track, sample);
uint64_t start_address = p_track->i_co64_sample_offset[chunk_inx];
if(p_track->i_stsz_sample_size == 0) {
uint32_t i;
for(i=p_track->chunk[chunk_inx].i_sample_first; i<sample; i++)
start_address += p_track->i_stsz_entry_size[i];
} else
start_address += (sample - p_track->chunk[chunk_inx].i_sample_first) * p_track->i_stsz_sample_size;
return start_address;
}
// time的單位爲毫秒
uint64_t time_to_duration(MP4_Box_data_trak_t *p_track, uint64_t time) {
return time * p_track->i_timescale/1000;
}
// 由time(單位是毫秒)計算sample_index.
// 根據stts計算,應該把每個Chunk第一個sample對應的duration一次性計算出來,這樣查找起來會更快一些。
uint64_t time_to_sample(MP4_Box_data_trak_t *p_track, uint64_t time) {
uint64_t i_start = time_to_duration(p_track, time);
if(i_start > p_track->i_duration) return 0;
uint32_t i;
uint64_t i_sample = 0;
if(p_track->i_stts_count == 1) { // all the sample has same duration
i = 0;
} else {
for(i=0; i<p_track->i_stts_count; i++) {
uint64_t range = p_track->i_stts_sample_count[i] * p_track->i_stts_sample_delta[i];
if(range > i_start) break;
i_start -= range;
i_sample += p_track->i_stts_sample_count[i];
}
}
i_sample += i_start / p_track->i_stts_sample_delta[i];
if(i_sample > p_track->i_stsz_sample_count) i_sample = 0;
return i_sample;
}