爲 CEF/Chromium 添加 x264 編碼器

前言

衆所周知,所有基於Chromium內核開發的“標準”瀏覽器架構的項目,如CEF、Electron甚至是Google Chrome,默認提供的H.264軟編碼器都是Cisco的OpenH264。那麼,如果我們想使用其他H.264編碼器,例如x264,甚至是結合特殊芯片進行硬編碼,該如何做呢?關於OpenH264和x264的差異,網上有很多相關文章,這裏就不贅述了。

OpenH264的具體實現位於WebRTC項目中,本文是基於 CEF 76.3809.132 這個分支的源碼基礎上進行介紹。因Chromium以及WebRTC代碼變化比較頻繁,可能在不久的將來,代碼就對應不上了,所以本文儘可能只說一下通用性的適合未來版本的思路。不過從git log看,H264編碼器的實現變化的不頻繁,比較頻繁的還是WebRTC的VideoSendStream,裏面有不少關於控制編碼器行爲的代碼,並且這部分寫的非常有意思,有時間我寫篇文章簡單說一下……跑題了,言歸正傳。

H264軟編碼器代碼位於:

third_party\webrtc\modules\video_coding\codecs\h264\h264_encoder_impl.h/.cc

類改造

下面是H264EncoderImpl這個類的繼承關係:在這裏插入圖片描述
因爲懶,所以上圖中的函數參數、以及部分成員變量就不寫了。

類關係還是很清楚的,也比較簡單。H264EncoderImpl實現了VideoEncoder的5個純虛函數(InitEncode、Encode、RegisterEncodeCompleteCallback、Release、SetRates),1個虛函數(GetEncoderInfo)。並且添加了若干private方法和成員變量。

H264EncoderImpl實現的這幾個虛函數是最核心的幾個,我們添加 x264編碼器,就是要圍繞着這幾個虛函數展開。至於其他的部分,大家看到,基本上都是OpenH264的實現,並非通用的。所以,可以這麼說,H264EncoderImpl 應該叫 OpenH264EncoderImpl 更合適一點。

OK,接下來我們對H264EncoderImpl進行一下改造。改造後的類結構如下:
在這裏插入圖片描述
我新建了一個基類 H264BaseEncoder,它用來存儲及實現一些通用性的方法,而真正有差異的部分,繼續保留虛函數的形式,派生出來2個子類:OpenH264EncoderImplX264EncoderImpl 去實現它們。

改造後,原來H264EncoderImpl中只適用於OpenH264的代碼被全部轉移到OpenH264EncoderImpl,公共的部分(方法、變量)保留在H264BaseEncoder。最終,X264EncoderImpl是我們着重要編寫的類。

OK,下面講一下在實現X264EncoderImpl這個類中可能會遇到的問題。

可能遇到的問題

Simulcast

WebRTC的OpenH264支持Simulcast,這個通過閱讀源碼可以看到:

std::vector<ISVCEncoder*> encoders_;

OpenH264是有一個最大數量爲4的vector來存儲每一層對應的編碼器的。x264實現是否支持,看你的實際使用情況而定,你可以只實現一個編碼器,相應地,上層初始化PeerConnection的時候就不要設置simulcast。

兩個QP閾值的含義

OpenH264的源碼中,定義了兩個靜態常量:kLowH264QpThresholdkHighH264QpThreshold,取值分別是 24 和 37。這裏要注意,這兩個值並不是OpenH264編碼參數中設置用於編碼的QP範圍,而是用於判斷是否動態升降發送分辨率或幀率的閾值。 WebRTC會根據一段時間內的編碼平均QP,和這兩個值進行比較。小等於kLowH264QpThreshold,認爲需要升分辨率(或幀率)。而大於kHighH264QpThreshold,則會下調分辨率(或幀率)。平均QP計算的這部分實現也很有意思,它的源碼位於 \webrtc\modules\video_coding\utility\quality_scaler.cc。

至於24和37是否適用於x264,這個也需要結合你使用的x264編碼參數與實際應用情況來定。我做了一點微小的調整(26, 40)。

動態碼率/幀率

上面大家有看到那幾個重要的純虛函數中,有一個 SetRates (這個函數以前叫SetRateAllocation),這個函數的調用堆棧大概是這樣的:

→ VideoEncoder::SetRates()
↑ VideoStreamEncoder::SetEncoderRates()
↑ VideoStreamEncoder::OnBitrateUpdated()
↑ VideoSendStreamImpl::OnBitrateUpdated()
↑ BitrateAllocator::OnNetworkChanged()

其中VideoEncoder::SetRates()就會來到具體編碼器的SetRates裏來。它的參數主要包含了期望調整的目標碼率、幀率等。SetRates會調用的非常頻繁,所以在這裏,我們要不斷地重設編碼器的參數,以適應WebRTC的請求。

這裏要特別說明的一點是,x264似乎並不支持動態幀率。也就是說,如果在InitEncode的時候我們設置了一個60fps的幀率,在SetRates中使用 x264_encoder_reconfig() 嘗試改爲30fps,似乎是無效的。而動態調整碼率則沒有這個問題。我不太確定是因爲我選用了ABR編碼方式引起的,還是其他什麼原因。有知道的朋友可以告訴我。

所以,針對這個問題,我的做法是:如果當前編碼幀率和目標幀率相差某個閾值(如5)或以上,則關閉當前 x264編碼器,重新創建一次(幀率採用本次的目標幀率,其他編碼參數使用關閉前的)

反饋每一幀的QP非常重要

這一條,不得不多說兩句。

其實x264編碼器我很快就寫完了,並且使用了常見的幾檔分辨率/幀率參數,使用一款羅技攝像頭進行了試驗,工作的比較良好。但當我把視頻源從攝像頭換成了一個1080P的mp4文件時,發生一件非常詭異的問題、踩了一個坑:

起初的發送分辨率是1920x1080,但通過webrtc-internals看到,很快發送分辨率下調至1440x810,之後又下降到960x540,在若干秒後,迅速下降到720x405,然後你猜怎麼着,x264就出錯了。原因是什麼?輸入了一個奇數分辨率。

那麼到底是什麼原因引起分辨率在短時間內迅速按照一個階梯狀下降呢?通過閱讀源碼後才發現裏面的奧祕:WebRTC會根據一定時間內的編碼平均QP,來決定是否要升降發送分辨率!也就是說,發送分辨率在不斷下調的根本原因,是目前的平均QP一直居高不下(超過我們上面提到的QP閾值上限),所以一直在不斷地請求下調分辨率。如果不是x264因爲輸入奇數分辨率出錯了,下一個可能到達的就是480x270。(注,下調分辨率是按照輸入分辨率 3/4 和 2/3 交替計算的)

OK,原因找到了,接下來問題就好解決了。最後問題的原因是因爲我沒有在編碼後,通過 webrtc::H264BitstreamParserParseBitstreamGetLastSliceQp 這兩個方法計算編碼幀的正確QP送出去。(這兩個方法的調用在OpenH264編碼後的代碼你可以找到)

RTPFragmentationHeader是個什麼玩意兒

RTPFragmentationHeader的作用其實就是標識一幀編碼後的H264數據各個NALU去掉開始碼後的數據起始位置和長度的。舉個例子:比如編碼出來的一段H264數據是下面的格式:

00 00 00 01 67 aa aa aa aa aa aa 00 00 00 01 68 bb bb bb 00 00 00 01 65 cc cc cc

那麼, RTPFragmentationHeader將會存儲3個元素:

元素1的offset指針指向67,長度是67開始到下一個開始碼之間的字節數
元素2的offset指針指向68,長度是68開始到下一個開始碼之間的字節數
元素3的offset指針指向65,長度是65開始到下一個開始碼之間的字節數

注,老版本的WebRTC源碼中還有 fragmentationPlTypefragmentationTimeDiff這兩個字段,新版本已經刪掉了。

OK,最終,RTPFragmentationHeader以及編碼後的數據,將通過 EncodedImageCallback->OnEncodedImage() 送到外面去進行後續處理流程。

H264編碼數據的排列格式要求

OpenH264編碼出來的數據,一般都是比較規整的“4字節開始碼+NALU類型+編碼數據”組成的,如下:
在這裏插入圖片描述
而x264編碼器編碼輸出的數據中,有一些是無用的,需要跳過,如下是x264編碼出來的起始部分:
在這裏插入圖片描述
這裏還要注意的是,x264編碼器送出來的NALU開始碼長度有4個的,也有3個的,在填充RTPFragmentationHeader的時候需要注意別搞錯了。另外就是有些NALU類型(如SEI、AUD)需要跳過不要。

這裏插一句,起初我在填充 RTPFragmentationHeader 的時候,去掉了NALU的開始碼。我想着, RTPFragmentationHeader 反正是按照偏移和長度來獲取數據的,開始碼有沒有不重要。但後來我發現我錯了。這個開始碼不是外面要用的,而是webrtc::H264BitstreamParser在分析流和QP值的時候要讀取的,缺少了開始碼,將無法讀取到正確的QP值,所以萬不可去掉。這部分可以自行閱讀以下H264BitstreamParser的源碼。

Streams with pred_weight_table unsupported

在替換了x264以後,我發現debug級別的日誌裏面有很多這個錯誤。它來自

third_party\webrtc\common_video\h264\h264_bitstream_parser.cc

我的解決方法是通過以下兩句代碼:

[x264_param_t].analyse.i_weighted_pred = X264_WEIGHTP_NONE;
[x264_param_t].analyse.b_weighted_bipred = X264_WEIGHTP_NONE;

講實話我不太清楚關閉它的影響是什麼,如果你知道,歡迎告訴我。

OK,差不多就是這些了。本文對改造WebRTC的H264Encoder模塊,添加 x264編碼器進行了主要思路和注意事項的說明,裏面其實有很多小的知識點,都可以專門展開寫一篇文章。例如:

  • WebRTC對視頻的發送策略,有保幀率、保分辨率、平衡模式,它們的區別是什麼?
  • 在某種發送策略下,影響調高調低(分辨率/幀率)的因素有哪些?
  • 實時碼率是從何而來?
  • 平均QP的計算方法,以及上調下調的極限
  • WebRTC是怎麼檢測CPU過載的(軟編碼、硬編碼下不同的檢測策略)

我強烈建議大家有時間多看看WebRTC的實現代碼,裏面有非常多值得我們學習的地方。

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