1. 前言
前段時間在處理公司屏幕共享功能的時候遇到一個問題, 視頻拉流渲染的時候偶爾會出現(xiàn)灰屏, 下面是個例子.
出現(xiàn)問題是有偶現(xiàn)的, 隨機的, 但頻率并不低, 嚴(yán)重的影響了觀看的體驗.
針對灰屏問題進行了一些調(diào)研, 最終解決了這個問題(目前沒有復(fù)現(xiàn)), 通過解決這個問題還是發(fā)現(xiàn)了很多知識盲區(qū)和沒掌握的細節(jié)問題, 特此做一個總結(jié).
2. 概念同步
2.1 H264
2.1.1 SPS
Sequence Paramater Set - 序列參數(shù)集
SPS 中保存了一組編碼視頻序列 (Coded Video Sequence)的全局參數(shù), 因此該類型保存的是和編碼序列相關(guān)的參數(shù)
2.1.2 PPS
Picture Paramater Set - 圖像參數(shù)集
該類型保存了整體圖像相關(guān)的參數(shù)
2.1.3 IDR
Instantaneous Decoding Refresh - 即時解碼刷新
IDR幀實質(zhì)也是I幀, 使用幀內(nèi)預(yù)測. IDR幀的作用是立即刷新缰犁,會導(dǎo)致 DPB(Decoded Picture Buffer參考幀列表) 清空枷遂,而 I幀不會. 所以 IDR幀承擔(dān)了隨機訪問功能, 一個新的 IDR幀開始, 可以重新算一個新的 GOP 開始編碼, 播放器永遠可以從一個 IDR幀播放, 因為在它之后沒有任何幀引用之前的幀.
如果一個視頻中沒有 IDR幀, 這個視頻是不能隨機訪問的. 所有位于 IDR幀后的 B幀和 P幀都不能參考 IDR幀以前的幀, 而普通 I幀后的 B幀和 P幀仍然可以參考 I幀之前的其他幀. IDR幀阻斷了誤差的積累,而 I幀并沒有阻斷誤差的積累.
2.1.3 GOP
Group of picture - 圖像組, 通常指兩個 I幀之間的幀數(shù)
一個 GOP 序列的第一個圖像叫做 IDR 圖像(立即刷新圖像, IDR 圖像都是 I 幀圖像劈榨,但 I幀不一定都是 IDR幀
2.2 WebRTC 拉流邏輯
WebRTC 接收到媒體數(shù)據(jù)的 udp 包后, 會經(jīng)過 packet_buffer, 這里負責(zé)組幀成完整幀的邏輯判斷, 只有完整幀才會繼續(xù)走下面的解碼渲染邏輯.
3. 發(fā)現(xiàn)問題
3.1 問題分析
當(dāng)看到渲染出現(xiàn)灰屏的時候首先懷疑是不是推流的問題, 但推流通常會因為碼率過低而導(dǎo)致圖片編碼質(zhì)量很低導(dǎo)致的模糊, 基本不會出現(xiàn)這種還有局部很清晰的情況, 所以從拉流一端繼續(xù)排查.
拉流端可能出現(xiàn)這類問題無非兩種問題: 數(shù)據(jù)錯誤, 數(shù)據(jù)丟失.
數(shù)據(jù)錯誤
當(dāng)出現(xiàn)錯誤的數(shù)據(jù)大概率是因為程序 bug, 導(dǎo)致交給解碼器的數(shù)據(jù)并不正確, 但通常這這會出現(xiàn)大面積色塊的問題, 并不會出現(xiàn)類似灰屏這種問題.數(shù)據(jù)丟失
組幀邏輯如果有 bug 會導(dǎo)致不完整的幀交給解碼器, 出現(xiàn)異常情況.
WebRTC 的組幀判斷的邏輯還是比較健壯的, 應(yīng)該不會出現(xiàn)丟部分?jǐn)?shù)據(jù)的問題.
繼續(xù)觀察顯現(xiàn), 出現(xiàn)的灰屏的時長基本符合我們設(shè)置的 GOP 時長, 那么問題大概率出現(xiàn)在關(guān)鍵幀刷新的地方 ( 結(jié)合拉流邏輯里對 H264 判定 IDR 幀 ).
為了更好的控制碼率, 我在 WebRTC 里集成了 x264 編碼器, 和默認的 openh264 有很多參數(shù)配置還是有區(qū)別的, 然后對比了一下兩個編碼器關(guān)于關(guān)鍵幀的一些設(shè)置發(fā)現(xiàn)了一些問題, 下面具體針對問題展開.
3.2 對比編碼器配置
3.2.1 OpenH264
typedef enum {
CONSTANT_ID = 0, ///< constant id in SPS/PPS
INCREASING_ID = 0x01, ///< SPS/PPS id increases at each IDR
SPS_LISTING = 0x02, ///< using SPS in the existing list if possible
SPS_LISTING_AND_PPS_INCREASING = 0x03,
SPS_PPS_LISTING = 0x06,
} EParameterSetStrategy;
// Reuse SPS id if possible. This helps to avoid reset of chromium HW decoder
// on each key-frame.
// Note that WebRTC resets encoder on resolution change which makes all
// EParameterSetStrategy modes except INCREASING_ID (default) essentially
// equivalent to CONSTANT_ID.
encoder_params.eSpsPpsIdStrategy = SPS_LISTING;
OpenH264 的編碼器對于Sps/Pps 的設(shè)置比較豐富, 具體使用上是對 Sps/Pps 采用盡量重用的方式.
3.2.2 x264
int b_repeat_headers; /* put SPS/PPS before each keyframe */
param.b_repeat_headers = 1;
因為我們有可能在推流過程中改變分辨率, 所以采用的是每個關(guān)鍵幀都需要攜帶 Sps/Pps 才能完成解碼.
3.2.3 WebRTC 組幀邏輯
// modules/video_coding/packet_buffer.cc
...
// sps_pps_idr_is_h264_keyframe_ 開關(guān), 默認是 false.
// 當(dāng)缺失 Sps/Pps 的時候也有可能會被認為是 IDR幀.
if ((sps_pps_idr_is_h264_keyframe_ && has_h264_idr && has_h264_sps &&
has_h264_pps) ||
(!sps_pps_idr_is_h264_keyframe_ && has_h264_idr)) {
is_h264_keyframe = true;
// Store the resolution of key frame which is the packet with
// smallest index and valid resolution; typically its IDR or SPS
// packet; there may be packet preceeding this packet, IDR's
// resolution will be applied to them.
if (buffer_[start_index]->width() > 0 &&
buffer_[start_index]->height() > 0) {
idr_width = buffer_[start_index]->width();
idr_height = buffer_[start_index]->height();
}
}
...
// 如果通過上面的邏輯判定不是關(guān)鍵幀才會判斷是否存在丟包情況
// 假如一個 IDR幀的 Sps/Pps 包發(fā)生丟包, 在這樣的邏輯下是有可能進行解碼
// 因為缺少 Sps/Pps 信息, 解碼器內(nèi)部會以普通的 I幀進行處理, 不會清空 DPB(Decoded Picture Buffer參考幀列表)
// If this is not a keyframe, make sure there are no gaps in the packet
// sequence numbers up until this point.
if (!is_h264_keyframe && missing_packets_.upper_bound(start_seq_num) !=
missing_packets_.begin()) {
return found_frames;
}
到這里可以大概率的懷疑是因為這個邏輯導(dǎo)致的灰屏. 主要原因:
- x264 的 Sps/Pps 邏輯和 OpenH264 不同
- 拉流渲染的時候關(guān)鍵幀的 Sps/Pps 包發(fā)生丟包或者亂序, 組幀的地方正好符合組幀邏輯, 進行了解碼.
下面針對這個猜測進行修改嘗試.
4. 嘗試解決
既然有了猜測, 那主要的修改就是在于如何打開這個開關(guān).
// video\/rtp_video_stream_receiver2.cc
...
if (codec_params.count(cricket::kH264FmtpSpsPpsIdrInKeyframe) ||
field_trial::IsEnabled("WebRTC-SpsPpsIdrIsH264Keyframe")) {
packet_buffer_.ForceSpsPpsIdrIsH264Keyframe();
}
可以看到這里有兩種打開方式:
- 通過 sdp 的的音頻 fmtp 增加 sps-pps-idr-in-keyframe
- 通過 WebRTC 的全局控制開關(guān)
為了能更好的兼容各種情況, 我們采用在 sdp 里攜帶動態(tài)控制開關(guān), 這樣可以針對不同的推流選擇性的開啟這個功能.
經(jīng)過線上的測試, 打開開關(guān)后確實沒有再發(fā)現(xiàn)有灰屏的問題, 說明這個控制是有效的.
5. TODO
雖然看上去是修改了這個問題, 但其實還是靠猜測和一些無法100% 可控的驗證手段, 有幾個方面還可以繼續(xù)展開調(diào)研, 可以放倒后面繼續(xù)做.
- 通過自己模擬丟包或者亂序去復(fù)現(xiàn)問題
- 解碼器針對丟失 Sps/Pps 后的處理邏輯
- 編碼器的設(shè)置是否也可以規(guī)避這個問題
- 編碼器的 Sps/Pps 的變化原理是什么