1. 程式人生 > >js對flv提取h264、aac音視訊流

js對flv提取h264、aac音視訊流

# FLV提取裡面的h264視訊流 ### FLV和MP4支援的編碼 ![](//blog.suyuanli.ink/1607829706000-033.png) ### 流媒體和媒體檔案的區別 流媒體是指將一連串的多媒體資料壓縮後,經過網際網路分段傳送資料,在網際網路上即時傳輸影音以供觀賞的一種技術與過程,此技術使得資料資料包得以像流水一樣傳送,如果不使用此技術,就必須在使用前下載整個媒體檔案。flv屬於流媒體格式,所以很適合做低延時的直播 ### 對比hls和mp4 相對於mp4,flv更加靈活體積更小,mp4不是流媒體需要索引表才可以正常播放 相對於hls,flv可以做到延時更低,因為hls需要發起多次http短連線請求播放,而flv可以通過http長連線結合ReadableStream做到更小切片的播放。 ** ps:下面的圖片很多是採用別人的,我也忘記備註來源了 ** ## 1.flv的協議結構 FLV檔案由FLV header和FLV body組成,FLV body由一系列的FLV tags組成,如下圖所示: ![flv協議(圖2)](//blog.suyuanli.ink/1607161603000-721.png "flv協議(圖2)") tag又可以分成三類:audio,video,script,分別代表音訊流,視訊流,指令碼流,而每個tag又由tag header和tag data組成。每個Tag前面還包含了Previous Tag Size欄位,表示前面一個Tag的大小。整個FLV檔案的詳細的組成如下圖所示: ![flv協議(圖1)](//blog.suyuanli.ink/1609039147000-659.png "flv協議(圖1)") ### 下面是一個flv視訊的hex編碼: #### flv header ![flv協議(圖3)](//blog.suyuanli.ink/1607162096000-390.png "flv協議(圖3)") 這裡前面9個位元組為flv的header 0x46:ASCII編碼裡的"F" 0x4c: ASCII編碼裡的"L" 0x56: ASCII編碼裡的"V" 0x01: FLV的版本號 0x05: 對應二進位制為0000 0101,意思為包含視訊和音訊 0x 00 00 00 09: 表示flv body的起始位元組位置 #### flv的body: ![flv協議(圖4)](//blog.suyuanli.ink/1607163498000-492.png "flv協議(圖4)") ****這裡flv body的前4個位元組總是0 #### tag的結構 ##### tag Header ![](//blog.suyuanli.ink/1607831978000-526.png) type:0x12 (18為元資料tag,9為視訊tag,8為音訊tag,佔1個位元組) dataSize:0x0001AC = 428 (tagbody的長度,佔3個位元組) timeStamp: 0x00000000 = 0 (tag對應的時間戳,其中最後一個位元組代表高位,一共4個位元組) streamId:0x000000 = 0 (一直為0) ##### tag Data ###### video data構成 ![](//blog.suyuanli.ink/1607834030000-803.png) 視訊Tag也用開始的第1個位元組包含視訊資料的引數資訊,從第2個位元組為視訊流資料。結構如下圖所示 ![](//blog.suyuanli.ink/1607833878000-496.png) 第1個位元組的前4位表示幀型別,各個取值的含義如下: ![](//blog.suyuanli.ink/1607833823000-815.png) 後4位表示視訊編碼型別,各個取值的含義如下: ![](//blog.suyuanli.ink/1607833958000-965.png) | 位元組位置 | 描述 | | ---- | ---- | | 1 | 視訊引數資訊,幀型別和編碼型別(如上圖) | | 2 | 該video tag data的型別,0為AVC packet type, 1為NALU,這裡AVC packet type包含了該視訊下面的一下公共資訊,NALU則是h264的基本構成。2為結束標誌| |3~5|composition time,AVC時,全0,無意義| 看下截圖的資料: 0x17:1-keyframe 7-avc 0x00:AVC sequence header -- AVC packet type 0x000000: composition time,AVC時,全0,無意義 - AVC sequence header資料結構(video data第6個位元組開始) | 位元組位置 | 描述 | 截圖資料 | | ---- | ---- | ---- | | 6 | configurationVersion 配置版本 | 0x01 | | 7 | AVCProfileIndication AVC配置檔案指示| 0x64 | |8 |profileCompatibility 配置檔案相容性|0x00| |9|AVCLevelIndication AVC級別|0x1e| |10|lengthSizeMinusOne FLV中NALU包長資料所使用的位元組數,(lengthSizeMinusOne & 3)+1,實際測試時發現總為ff,計算結果為4|(0xff & 3) + 1 = 4| |11|numOfSequenceParameterSets (E1 -- SPS 的個數,numOfSequenceParameterSets & 0x1F)|0xe1 & 0x1f = 1| |12~13|sequenceParameterSetLength SPS 的長度,2個位元組|0x001a=26| |14~14+sequenceParameterSetLength|SPS 資料|0x27 ... 0x92| |14+sequenceParameterSetLength+1|PPS 的個數,實際測試時發現總為01|0x01| |14+sequenceParameterSetLength+2|pictureParameterSetLength PPS 的長度|0x0004=4| |14+sequenceParameterSetLength+3 ~ dataEnd|PPS 資料|0x28ee3cb0| 分析截圖資料中,比較重要的只有lengthSizeMinusOne = 4位元組,這裡需要存起來因為下面的NALU解析時需要用到。 - NALU資料結構 NALU的小知識 ![](//blog.suyuanli.ink/1607837501000-980.png) |型別|描述| |----|----| |SPS|序列引數集,SPS中儲存了⼀組編碼視訊序列(Coded video sequence)的全域性引數| |PPS|影象引數集,對應的是⼀個序列中某⼀幅影象或者某⼏幅影象的引數| |I幀|幀內編碼幀,可獨⽴解碼⽣成完整的圖⽚| |P幀|前向預測編碼幀,需要參考其前⾯的⼀個I 或者B 來⽣成⼀張完整的圖⽚| |B幀|雙向預測內插編碼幀,則要參考其前⼀個I或者P幀及其後⾯的⼀個P幀來⽣成⼀張完整的圖⽚| 下面是第二個video tag的截圖: ![](//blog.suyuanli.ink/1607837735000-202.png) 第二個位元組為0x01,說明下面是NALU包,一個tag可以包含多個NALU(h264的NALU之間需要用0X000000或0x00000000作為間隔,不過flv內是不包含的) 第3~5位元組為composition time,可以忽略不記 所以由第6個位元組開始,從第一個video tag的AVC sequence header可以得知每個NALU的資料長度由起始的4個位元組描述。 所以第一個NALU的資料長度為:0x0000001A = 26byte 資料為:0x276400 ... 92 這裡其中第一個位元組的前5位為該NAL包的型別 ```c 0x27 & 0x1f = 7 ``` NAl的型別對照表: ```c #define NALU_TYPE_SLICE 1 #define NALU_TYPE_DPA 2 #define NALU_TYPE_DPB 3 #define NALU_TYPE_DPC 4 #define NALU_TYPE_IDR 5 #define NALU_TYPE_SEI 6 #define NALU_TYPE_SPS 7 #define NALU_TYPE_PPS 8 #define NALU_TYPE_AUD 9 #define NALU_TYPE_EOSEQ 10 #define NALU_TYPE_EOSTREAM 11 #define NALU_TYPE_FILL 12 ``` 一個NALU結束後的4個位元組為下個NALU的長度,以此下去。 程式碼實現抽取NALU: ```javascript uint8Array // 以獲得的flv資料,下面只是針對video tag的解析,不是完整程式碼 let idx dataLeng = (uint8Array[idx++] << 0x10) + (uint8Array[idx++] << 0x08) + uint8Array[idx++]; timeStamp = (uint8Array[idx + 3] << 24) + (uint8Array[idx++] << 16) + (uint8Array[idx++] << 8) + uint8Array[idx++] idx+= (1 + 3) const dataStartIdx = idx // data起始idx videoTotalTime += timeStamp const isIKeyframe = (uint8Array[idx] & 0xf0) === 16 // 是否為關鍵幀 const codeId = (uint8Array[idx++] & 0x0f) // 視訊編碼型別(7為avc) const isAVCSequenceHeader = uint8Array[idx++] === 0 // 是否為avc頭部,只有一個 if (isAVCSequenceHeader) { const compositionTime = 0 // AVC時,全0,無意義(直接跳過3個位元組) idx+=3 const configurationVersion = uint8Array[idx++] // 配置版本 const AVCProfileIndication = uint8Array[idx++] // AVC配置檔案指示 const profileCompatibility = uint8Array[idx++] // 配置檔案相容性 const AVCLevelIndication = uint8Array[idx++] // AVC等級指示 const lengthSizeMinusOne = (uint8Array[idx++] & 3) + 1 //FLV中NALU包長資料所使用的位元組數,(lengthSizeMinusOne & 3)+1,實際測試時發現總為ff,計算結果為4 const numOfSequenceParameterSets = uint8Array[idx++] & 0x1f // 01 -- SPS 的個數,numOfSequenceParameterSets & 0x1F const sequenceParameterSetLength = (uint8Array[idx++] << 8) + uint8Array[idx++] // SPS 的長度,2個位元組 videoArr.push(this.concatenate(Uint8Array, [new Uint8Array([0,0,0,1]), uint8Array.slice(idx, idx + sequenceParameterSetLength)])) idx += sequenceParameterSetLength const numOfPictureParameterSets = uint8Array[idx++] // PPS 的個數,實際測試時發現總為E1 const pictureParameterSetLength = (uint8Array[idx++] << 8) + uint8Array[idx++] // PPS 的長度 videoArr.push(this.concatenate(Uint8Array, [new Uint8Array([0,0,0,1]), uint8Array.slice(idx, idx + pictureParameterSetLength)])) idx += pictureParameterSetLength videoConfig = { compositionTime, configurationVersion, AVCProfileIndication, profileCompatibility, AVCLevelIndication, lengthSizeMinusOne, } } else { // 非頭部tag const compositionTime = (uint8Array[idx++] << 16) + (uint8Array[idx++] << 8) + uint8Array[idx++] // header得到的lengthSizeMinusOne while(dataLeng + dataStartIdx > idx) { let i = 1 let naluLength = 0 while(i <= videoConfig.lengthSizeMinusOne) { naluLength += (uint8Array[idx++] << ((videoConfig.lengthSizeMinusOne - i) * 8)) i++ } videoArr.push(this.concatenate(Uint8Array, [new Uint8Array([0,0,0,1]), uint8Array.slice(idx, idx + naluLength)])) idx += naluLength } } idx += 4 // preTagSize ``` ###### audio data構成 ![](//blog.suyuanli.ink/1607840366000-472.png) ![](//blog.suyuanli.ink/1607841177000-183.png) 前兩個位元組為公共頭部 |位元組位置|描述| |----|----| |1|音訊引數| |2|AACPacketType 0為AudioSpecificConfig, 1為AACframeData| 音訊引數資料結構 |位|描述|截圖資料分析| |----|----|----| |1~4|format編碼型別|0xAF&0xF0=10| |5~6|rate取樣率|(0xAF&0x0c)>>2=3| |7|sampleSize取樣精度|(0xAF & 0x02) >> 1=1| |8|audiotype音訊型別|0xAF&0x01=1| ![](//blog.suyuanli.ink/1607841736000-540.png) ![](//blog.suyuanli.ink/1607841760000-155.png) ![](//blog.suyuanli.ink/1607841782000-312.png) ![](//blog.suyuanli.ink/1607841807000-912.png) 第二個位元組為0x00,所以下面為AudioSpecificConfig資料,因為AudioSpecificConfig只出現一次,所以需要記錄起來。 AudioSpecificConfig的資料可以由第3、4個位元組獲取。 具體資料結構如下: |位|欄位|描述| |----|----|----| |1~5|audioObjectType|編碼結構型別| |6~9|samplingFrequencyIndex|音訊取樣率索引值,44100對應值4| |10~13|channelConfiguration|音訊輸出聲道| |14|frameLengthFlag|標誌位,用於表明IMDCT視窗長度,0| |15|dependsOnCoreCoder|標誌位,表明是否依賴於corecoder,0| |16|extensionFlag|延時標誌位| - flv儲存的AAC資料為AAC為es資料流,不能直接播放,如果想要播放需要在每個es流前面加上ADTS頭部,所以一個完整可播放的AAC為: ![](//blog.suyuanli.ink/1607842827000-707.png) 這裡ADTS由adts_fixed_header和adts_variable_header組成 其一為固定頭資訊,緊接著是可變頭資訊。固定頭資訊中的資料每一幀都相同,而可變頭資訊則在幀與幀之間可變 adts_fixed_header: |欄位|描述|長度(bits)| |----|----|----| |syncword|同步頭 總是0xFFF, all bits must be 1,代表著一個ADTS幀的開始|12| |ID|MPEG識別符號,0標識MPEG-4,1標識MPEG-2|1| |Layer|always: '00'|2| |protection_absent|表示是否誤碼校驗。Warning, set to 1 if there is no CRC and 0 if there is CRC|1| |profile|表示使用哪個級別的AAC,如01 Low Complexity(LC)--- AAC LC。有些晶片只支援AAC LC,值等於 Audio Object Type的值減1|2| |sampling_frequency_index|表示使用的取樣率下標|4| |private bit|0|1| |channel_configuration|表示聲道數,比如2表示立體聲雙聲道|3| |original|0|1| |home|0|1| adts_variable_header: |欄位|描述|長度(bits)| |----|----|----| |copyright_id_bit|0|1| |copyright_id_start|0|1| |aac_frame_length|一個ADTS幀的長度包括ADTS頭和AAC原始流|13| |adts_buffer_fullness|0x7FF 說明是位元速率可變的碼流|11| |number_of_raw_data_blocks_in_frame|00|2| 第二個audio data裡的AACPacketType都會為1,所以只要便利所有的audio tag,給每個es流前面加上ADTS頭部就可以了 實現程式碼: ```javascript uint8Array // 以獲得的flv資料,下面只是針對audio tag的解析,不是完整程式碼 let idx dataLeng = (uint8Array[idx++] << 0x10) + (uint8Array[idx++] << 0x08) + uint8Array[idx++]; timeStamp = (uint8Array[idx + 3] << 24) + (uint8Array[idx++] << 16) + (uint8Array[idx++] << 8) + uint8Array[idx++] idx += (1 + 3) const audioDataEndIdx = idx + dataLeng const info = uint8Array[idx++] const format = info & 0xF0 // 編碼型別 const rate = (info & 0x0c) >
> 2 // 取樣率 const sampleSize = (info & 0x02) >> 1 // 取樣精度 const audiotype = (info & 0x01) // 音訊型別 const isAudioSpecificConfig = !uint8Array[idx++] if (isAudioSpecificConfig) { audioSpecificConfig = this.getAudioSpecificConfig(uint8Array[idx++], uint8Array[idx++]) idx = audioDataEndIdx } else { const adtsLen = dataLeng - 2 + 7 let ADTS = new Uint8Array(7) ADTS[0] = 0xff // syncword:0xfff 高8bits ADTS[1] = 0xf0 // syncword:0xfff 低4bits ADTS[1] |= (0 << 3) // MPEG Version:0 for MPEG-4,1 for MPEG-2 1bit ADTS[1] |= (0 << 1) // Layer:0 2bits ADTS[1] |= 1 // protection absent:1 1bit ADTS[2] = (audioSpecificConfig.audioObjectType - 1) << 6 // profile:audio_object_type - 1 2bits ADTS[2] |= (audioSpecificConfig.samplingFrequencyIndex & 0x0f) << 2 // sampling frequency index:sampling_frequency_index 4bits ADTS[2] |= (0 << 1) // private bit:0 1bit ADTS[2] |= (audioSpecificConfig.channelConfiguration & 0x04) >
> 2 // channel configuration:channel_config 高1bit ADTS[3] = (audioSpecificConfig.channelConfiguration & 0x03) << 6 // channel configuration:channel_config 低2bits ADTS[3] |= (0 << 5) // original:0 1bit ADTS[3] |= (0 << 4) // home:0 1bit ADTS[3] |= (0 << 3) // copyright id bit:0 1bit ADTS[3] |= (0 << 2) // copyright id start:0 1bit ADTS[3] |= (adtsLen & 0x1800) >> 11 // frame length:value 高2bits ADTS[4] = (adtsLen & 0x7f8) >> 3 // frame length:value 中間8bits ADTS[5] = (adtsLen & 0x7) << 5 // frame length:value 低3bits ADTS[5] |= 0x1f // buffer fullness:0x7ff 高5bits ADTS[6] = 0xfc audioArr.push(this.concatenate(Uint8Array, [ADTS, uint8Array.slice(idx, audioDataEndIdx)])) idx = audioDataEndIdx } idx += 4 ``` ##### Metadata Tag 主要是描述該flv的資訊,例如寬高,時長等等。所處位置為第一個tag ### 播放h264和aac ![](//blog.suyuanli.ink/1607848933000-932.png) ### Fragmented MP4檔案格式 在Fragmented MP4檔案中都有三個非常關鍵的boxes:‘moov’、‘moof’和‘mdat’。 (1)‘moov’(movie metadata box) 和普通MP4檔案的‘moov’一樣,包含了file-level的metadata資訊,用來描述file。 (2)‘mdat’(media data box) 和普通MP4檔案的‘mdat’一樣,用於存放媒體資料,不同的是普通MP4檔案只有一個‘mdat’box,而Fragmented MP4檔案中,每個fragment都會有一個‘mdat’型別的box。 (3)‘moof’(movie fragment box) 該型別的box存放的是fragment-level的metadata資訊,用於描述所在的fragment。該型別的box在普通的MP4檔案中是不存在的,而在Fragmented MP4檔案中,每個fragment都會有一個‘moof’型別的box。 一個‘moof’和一個‘mdat’組成Fragmented MP4檔案的一個fragment,這個fragment包含一個video track或audio track,並且包含足夠的metadata以保證這部分資料可以單