這段時間在搗騰基於 RTMP 協議的流媒體直播框架,其間參考了衆多博主的文章,剩下一些細節問題自行琢磨也算摸索出個門道,現將自己認爲比較惱人的 AAC 音頻幀的推送和解析、H264 碼流的推送和解析以及網上沒說清楚的地方分享給各位。
RTMP 協議棧的實現,Bill 直接使用的 libRTMP,關於 libRTMP 的編譯、基本使用方法,以及簡單的流媒體直播框架,請參見博文[C++實現RTMP協議發送H.264編碼及AAC編碼的音視頻],言簡意賅,故不再贅述。
言歸正傳,我們首先來看看 AAC 以及 H264 的推送。
不論向 RTMP 服務器推送音頻還是視頻,都需要按照 FLV 的格式進行封包。因此,在我們向服務器推送第一個 AAC 或 H264 數據包之前,需要首先推送一個音頻 Tag [AAC Sequence Header] 以下簡稱“音頻同步包”,或者視頻 Tag [AVC Sequence Header] 以下簡稱“視頻同步包”。
AAC 音頻幀的推送
我們首先來看看音頻 Tag,根據 FLV 標準 Audio Tags 一節的描述:
我們可以將其簡化並得到 AAC 音頻同步包的格式如下:
音頻同步包大小固定爲 4 個字節。前兩個字節被稱爲 [AACDecoderSpecificInfo],用於描述這個音頻包應當如何被解析。後兩個字節稱爲 [AudioSpecificConfig],更加詳細的指定了音頻格式。
[AACDecoderSpecificInfo] 倆字節可以直接使用 FAAC 庫的 faacEncGetDecoderSpecificInfo 函數來獲取,也可以根據自己的音頻源進行計算。一般情況下,雙聲道,44kHz 採樣率的 AAC 音頻,其值爲 0xAF00,示例代碼:
根據 FLV 標準 不難得知,[AACDecoderSpecificInfo] 第 1 個字節高 4 位 |1010| 代表音頻數據編碼類型爲 AAC,接下來 2 位 |11| 表示採樣率爲 44kHz,接下來 1 位 |1| 表示採樣點位數 16bit,最低 1 位 |1| 表示雙聲道。其第二個字節表示數據包類型,0 則爲 AAC 音頻同步包,1 則爲普通 AAC 數據包。
音頻同步包的後兩個字節 [AudioSpecificConfig] 的結構,援引其他博主圖如下:
我們只需參照上述結構計算出對應的值即可。至此,4 個字節的音頻同步包組裝完畢,便可推送至 RTMP 服務器,示例代碼如下:
網上有博主說音頻採樣率小於等於 44100 時 SamplingFrequencyIndex 應當選擇 3(48kHz),Bill 測試發現採樣率等於 44100 時設置標記爲 3 或 4 均能正常推送並在客戶端播放,不過我們還是應當按照標準規定的行事,故此處的 SamplingFrequencyIndex 選 4。
完成音頻同步包的推送後,我們便可向服務器推送普通的 AAC 數據包,推送數據包時,[AACDecoderSpecificInfo] 則變爲 0xAF01,向服務器說明這個包是普通 AAC 數據包。後面的數據爲 AAC 原始數據去掉前 7 個字節(若存在 CRC 校驗,則去掉前 9 個字節),我們同樣以一張簡化的表格加以闡釋:
推送普通 AAC 數據包的示例代碼:
至此,我們便完成了 AAC 音頻的推送流程。此時可嘗試使用 VLC 或其他支持 RTMP 協議的播放器連接到服務器測試正在直播的 AAC 音頻流。
H264 碼流的推送
前面提到過,向 RTMP 服務器發送 H264 碼流,需要按照 FLV 格式進行封包,並且首先需要發送視頻同步包 [AVC Sequence Header]。我們依舊先閱讀 FLV 標準 Video Tags 一節:
由於視頻同步包前半部分比較簡單易懂,仔細閱讀上述標準便可明白如何操作,故 Bill 不另作圖闡釋。由上圖可知,我們的視頻同步包 FrameType == 1,CodecID == 7,VideoData == AVCVIDEOPACKET,繼續展開 AVCVIDEOPACKET,我們可以得到 AVCPacketType == 0x00,CompositionTime == 0x000000,Data == AVCDecoderConfigurationRecord。
因此構造視頻同步包的關鍵點便是構造 AVCDecoderConfigurationRecord。同樣,我們援引其他博主的圖片來闡釋這個結構的細節:
其中需要額外計算的是 H264 碼流的 Sps 以及 Pps,這兩個關鍵數據可以在開始編碼 H264 的時候提取出來並加以保存,在需要時直接使用即可。具體做法請讀者自行 Google 或參見 參考博文[2],在此不再贅述。
當我們得到本次 H264 碼流的 Sps 以及 Pps 的相關信息後,我們便可以完成視頻同步包的組裝,示例代碼如下:
至此,視頻同步包便構造完畢並推送給 RTMP 服務器。接下來只需要將普通 H264 碼流稍加封裝便可實現 H264 直播,下面我們來看一下普通視頻包的組裝過程。
回顧 FLV 標準 的 Video Tags 一節,我們可以得到 H264 普通數據包的封包信息,FrameType == (H264 I 幀 ? 1 : 2),CodecID == 7,VideoData == AVCVIDEOPACKET,繼續展開,我們可以得到 AVCPacketType == 0x01,CompositionTime 此處仍然設置爲 0x000000,具體原因 TODO(billhoo),Data == H264 NALU Size + NALU Raw Data。
構造視頻數據包的示例代碼如下:
至此 H264 碼流的整個推送流程便已完成,我們可以使用 VLC 或其他支持 RTMP 協議的播放器進行測試。
關於 AAC 音頻幀及 H264 碼流的時間戳
通過前文的步驟我們已經能夠將 AAC 音頻幀以及 H264 碼流正常推送到 RTMP 直播服務器,並能夠使用相關播放器進行播放。但播放的效果如何還取決於時間戳的設定。
在網絡良好的情況下,自己最開始使用的音頻流時間戳爲 AAC 編碼器剛輸出一幀的時間,視頻流時間戳爲 H264 編碼器剛編碼出來一幀的時間,VLC 播放端就頻繁報異常,要麼是重新緩衝,要麼直接沒聲音或花屏。在排除了推送步驟實現有誤的問題後,Bill 發現問題出在時間戳上。
之後有網友說直播流的時間戳不論音頻還是視頻,在整體時間線上應當呈現遞增趨勢。由於 Bill 最開始的時間戳計算方法是按照音視頻分開計算,而音頻時戳和視頻時戳並不是在一條時間線上,這就有可能出現音頻時戳在某一個時間點比對應的視頻時戳小, 在某一個時間點又跳變到比對應的視頻時戳大,導致播放端無法對齊。
目前採用的時間戳爲底層發送 RTMP 包的時間,不區分音頻流還是視頻流,統一使用即將發送 RTMP 包的系統時間作爲該包的時間戳。目前局域網測試播放效果良好,音視頻同步且流暢。
參考博文
[1][C++實現RTMP協議發送 H.264 編碼及 AAC 編碼的音視頻]
[2][使用 libRtmp 進行 H264 與 AAC 直播]
[3][RTMP直播到FMS中的AAC音頻直播]