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

FLV提取裏面的h264視頻流

FLV和MP4支持的編碼

流媒體和媒體文件的區別

流媒體是指將一連串的多媒體資料壓縮後,經過互聯網分段發送資料,在互聯網上即時傳輸影音以供觀賞的一種技術與過程,此技術使得資料數據包得以像流水一樣發送,如果不使用此技術,就必須在使用前下載整個媒體文件。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)
tag又可以分成三類:audio,video,script,分別代表音頻流,視頻流,腳本流,而每個tag又由tag header和tag data組成。每個Tag前面還包含了Previous Tag Size字段,表示前面一個Tag的大小。整個FLV文件的詳細的組成如下圖所示:
flv協議(圖1)

下面是一個flv視頻的hex編碼:

flv header

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)
****這裏flv body的前4個字節總是0

tag的結構

tag Header


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構成


視頻Tag也用開始的第1個字節包含視頻數據的參數信息,從第2個字節爲視頻流數據。結構如下圖所示

第1個字節的前4位表示幀類型,各個取值的含義如下:

後4位表示視頻編碼類型,各個取值的含義如下:

字節位置 描述
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的小知識

類型 描述
SPS 序列參數集,SPS中保存了⼀組編碼視頻序列(Coded video sequence)的全局參數
PPS 圖像參數集,對應的是⼀個序列中某⼀幅圖像或者某⼏幅圖像的參數
I幀 幀內編碼幀,可獨⽴解碼⽣成完整的圖⽚
P幀 前向預測編碼幀,需要參考其前⾯的⼀個I 或者B 來⽣成⼀張完整的圖⽚
B幀 雙向預測內插編碼幀,則要參考其前⼀個I或者P幀及其後⾯的⼀個P幀來⽣成⼀張完整的圖⽚

下面是第二個video tag的截圖:

第二個字節爲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包的類型

0x27 & 0x1f = 7

NAl的類型對照表:

#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:

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構成



前兩個字節爲公共頭部

字節位置 描述
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





第二個字節爲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爲:

這裏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頭部就可以了

實現代碼:

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

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以保證這部分數據可以單獨解碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章