Categories
程式開發

融雲技術分享:基於WebRTC的實時音視頻首幀顯示時間優化實踐


本文由融雲技術團隊原創投稿,作者是融雲WebRTC高級工程師蘇道,轉載請註明出處。

1、引言

在一個典型的IM應用裡,使用實時音視頻聊天功能時,視頻首幀的顯示,是一項很重要的用戶體驗指標。

本文主要通過對WebRTC接收端的音視頻處理過程分析,來了解和優化視頻首幀的顯示時間,並進行了總結和分享。

(本文同步發佈於:http://www.52im.net/thread-3169-1-1.html“)

2、什麼是WebRTC?

對於沒接觸過實時音視頻技術的人來說,總是看到別人在提WebRTC,那WebRTC是什麼?我們有必要簡單介紹一下。

融雲技術分享:基於WebRTC的實時音視頻首幀顯示時間優化實踐 1

說到WebRTC,我們不得不提到Gobal IP Solutions,簡稱GIPS。這是一家1990 年成立於瑞典斯德哥爾摩的VoIP 軟件開發商,提供了可以說是世界上最好的語音引擎。相關介紹詳見《訪談WebRTC標準之父:WebRTC的過去、現在和未來“》。

Skype、騰訊QQ、WebEx、Vidyo 等都使用了它的音頻處理引擎,包含了受專利保護的迴聲消除算法,適應網絡抖動和丟包的低延遲算法,以及先進的音頻編解碼器。

Google 在Gtalk 中也使用了GIPS 的授權。 Google 在2011 年以6820萬美元收購了GIPS,並將其源代碼開源,加上在2010 年收購的On2 獲取到的VPx 系列視頻編解碼器(詳見《即時通訊音視頻開發(十七):視頻編碼H.264、VP8的前世今生“》),WebRTC 開源項目應運而生,即GIPS 音視頻引擎+ 替換掉H.264 的VPx 視頻編解碼器。

在此之後,Google 又將在Gtalk 中用於P2P 打洞的開源項目libjingle 融合進了WebRTC。目前WebRTC 提供了包括Web、iOS、Android、Mac、Windows、Linux 在內的所有平台支持。

(以上介紹,引用自《了不起的WebRTC:生態日趨完善,或將實時音視頻技術白菜化“》)

雖然WebRTC的目標是實現跨平台的Web端實時音視頻通訊,但因為核心層代碼的Native、高品質和內聚性,開發者很容易進行除Web平台外的移殖和應用。目前為止,WebRTC幾乎是是業界能免費得到的唯一高品質實時音視頻通訊技術。

3、流程介紹

一個典型的實時音視頻處理流程大概是這樣:

1)發送端採集音視頻數據,通過編碼器生成幀數據;2)這數據被打包成RTP 包,通過ICE 通道發送到接收端;3)接收端接收RTP 包,取出RTP payload,完成組幀的操作;4)之後音視頻解碼器解碼幀數據,生成視頻圖像或音頻PCM 數據。

如下圖所示:

融雲技術分享:基於WebRTC的實時音視頻首幀顯示時間優化實踐 2

本文所涉及的參數調整,談論的部分位於上圖中的第4 步。

因為是接收端,所以會收到對方的Offer 請求。先設置SetRemoteDescription 再SetLocalDescription。

如下圖藍色部分:

融雲技術分享:基於WebRTC的實時音視頻首幀顯示時間優化實踐 3

4、參數調整

4.1 視頻參數調整

當收到Signal 線程SetRemoteDescription 後,會在Worker 線程中創建VideoReceiveStream 對象。具體流程為SetRemoteDescription -> VideoChannel::SetRemoteContent_w 創建WebRtcVideoReceiveStream。

WebRtcVideoReceiveStream 包含了一個VideoReceiveStream 類型stream_ 對象, 通過webrtc::VideoReceiveStream* Call::CreateVideoReceiveStream 創建。

創建後立即啟動VideoReceiveStream 工作,即調用Start() 方法。

此時VideoReceiveStream 包含一個RtpVideoStreamReceiver 對象準備開始處理video RTP 包。

接收方創建createAnswer 後通過setLocalDescription 設置local descritpion。

對應會在Worker 線程中setLocalContent_w 方法中根據SDP 設置channel 的接收參數,最終會調用到WebRtcVideoReceiveStream::SetRecvParameters。

WebRtcVideoReceiveStream::SetRecvParameters 實現如下:

無效的WebRtcVideoChannel :: WebRtcVideoReceiveStream :: SetRecvParameters(const ChangedRecvParameters&params){bool video_needs_recreation = false; bool flexfec_needs_recreation = false; if(params.codec_settings){ConfigureCodecs(* params.codec_settings); video_needs_recreation = true; } if(params.rtp_header_extensions){config_.rtp.extensions = * params.rtp_header_extensions; flexfec_config_.rtp_header_extensions = * params.rtp_header_extensions; video_needs_recreation = true; flexfec_needs_recreation = true; } if(params.flexfec_payload_type){ConfigureFlexfecCodec(* params.flexfec_payload_type); flexfec_needs_recreation = true; } if(flexfec_needs_recreation){RTC_LOG(LS_INFO)<<“ MaybeRecreateWebRtcFlexfecStream(recv)因為”“ SetRecvParameters”; 也許RecreateWebRtcFlexfecStream(); } if(video_needs_recreation){RTC_LOG(LS_INFO)<<“由於SetRecvParameters而重新創建WebRtcVideoStream(recv)”; RecreateWebRtcVideoStream(); }}

根據上面SetRecvParameters 代碼,如果codec_settings 不為空、rtp_header_extensions 不為空、flexfec_payload_type 不為空都會重啟VideoReceiveStream。

video_needs_recreation 表示是否要重啟VideoReceiveStream。

重啟過程為:把先前創建的釋放掉,然後重建新的VideoReceiveStream。

以codec_settings 為例:初始video codec 支持H264 和VP8。若對端只支持H264,協商後的codec 僅支持H264。 SetRecvParameters 中的codec_settings 為H264 不空。其實前後VideoReceiveStream 的都有H264 codec,沒有必要重建VideoReceiveStream。可以通過配置本地支持的video codec 初始列表和rtp extensions,從而生成的local SDP 和remote SDP 中影響接收參數部分調整一致,並且判斷codec_settings 是否相等。如果不相等再video_needs_recreation 為true。

這樣設置就會使SetRecvParameters 避免觸發重啟VideoReceiveStream 邏輯。

在debug 模式下,修改後,驗證沒有“RecreateWebRtcVideoStream (recv) because of SetRecvParameters” 的打印, 即可證明沒有VideoReceiveStream 重啟。

4.2 音頻參數調整

和上面的視頻調整類似,音頻也會有因為rtp extensions 不一致導致重新創建AudioReceiveStream,也是釋放先前的AudioReceiveStream,再重新創建AudioReceiveStream。

參考代碼:

bool WebRtcVoiceMediaChannel :: SetRecvParameters(const AudioRecvParameters&params){TRACE_EVENT0(“ webrtc”,“ WebRtcVoiceMediaChannel :: SetRecvParameters”); RTC_DCHECK(worker_thread_checker_.CalledOnValidThread()); RTC_LOG(LS_INFO)<<“ WebRtcVoiceMediaChannel :: SetRecvParameters:”< params.ToString();  // TODO(pthatcher): Refactor this to be more clean now that we have  // all the information at once.   if(!SetRecvCodecs(params.codecs)) {    return false;  }   if(!ValidateRtpExtensions(params.extensions)) {    return false;  }  std::vector filtered_extensions = FilterRtpExtensions(      params.extensions, webrtc::RtpExtension::IsSupportedForAudio, false);  if(recv_rtp_extensions_ != filtered_extensions) {    recv_rtp_extensions_.swap(filtered_extensions);    for(auto& it : recv_streams_) {      it.second->SetRtpExtensionsAndRecreateStream(recv_rtp_extensions_); }返回true;}

AudioReceiveStream 的構造方法會啟動音頻設備,即調用AudioDeviceModule 的StartPlayout。

AudioReceiveStream 的析構方法會停止音頻設備,即調用AudioDeviceModule 的StopPlayout。

因此重啟AudioReceiveStream 會觸發多次StartPlayout/StopPlayout。

經測試,這些不必要的操作會導致進入視頻會議的房間時,播放的音頻有一小段間斷的情況。

解決方法:同樣是通過配置本地支持的audio codec 初始列表和rtp extensions,從而生成的local SDP 和remote SDP 中影響接收參數部分調整一致,避免AudioReceiveStream 重啟邏輯。

另外audio codec 多為WebRTC 內部實現,去掉一些不用的Audio Codec,可以減小WebRTC 對應的庫文件。

4.3 音視頻相互影響

WebRTC 內部有三個非常重要的線程:

1)woker 線程;2)signal 線程;3)network 線程。

調用PeerConnection 的API 的調用會由signal 線程進入到worker 線程。

worker 線程內完成媒體數據的處理,network 線程處理網絡相關的事務,channel.h 文件中有說明,以_w 結尾的方法為worker 線程的方法,signal 線程的到worker 線程的調用是同步操作。

如下面代碼中的InvokerOnWorker 是同步操作,setLocalContent_w 和setRemoteContent_w 是worker 線程中的方法。

bool BaseChannel :: SetLocalContent(const MediaContentDescription * content,SdpType type,std :: string * error_desc){TRACE_EVENT0(“ webrtc”,“ BaseChannel :: SetLocalContent”); returnI nvokeOnWorker(RTC_FROM_HERE,Bind(&BaseChannel :: SetLocalContent_w,this,content,type,error_desc));} bool BaseChannel :: SetRemoteContent(const MediaContentDescription * content,SdpType type,std :: string * error_desc){TRACE_EVENT0(“ webrtc” ,“ BaseChannel :: SetRemoteContent”); 返回InvokeOnWorker(RTC_FROM_HERE,Bind(&BaseChannel :: SetRemoteContent_w,this,content,type,error_desc));}

setLocalDescription 和setRemoteDescription 中的SDP 信息都會通過PeerConnection 的PushdownMediaDescription 方法依次下發給audio/video RtpTransceiver 設置SDP 信息。

舉例:執行audio 的SetRemoteContent_w 執行很長(比如音頻AudioDeviceModule 的InitPlayout 執行耗時), 會影響後面的video SetRemoteContent_w 的設置時間。

PushdownMediaDescription 代碼:

RTCError PeerConnection :: PushdownMediaDescription(SdpType類型,cricket :: ContentSource源){const SessionDescriptionInterface * sdesc =(source == cricket :: CS_LOCAL?local_description():remote_description()); RTC_DCHECK(sdesc); //按下每個音頻/視頻收發器的新SDP媒體部分。 for(常量自動&收發器:收發器_){常量ContentInfo * content_info = FindMediaSectionForTransceiver(收發器,sdesc); cket :: ChannelInterface * channel =收發器-> internal()-> channel(); if(!channel ||!content_info || content_info-> rejected){繼續; } const MediaContentDescription * content_desc = content_info-> media_description(); if(!content_desc){繼續; } std :: string錯誤; bool成功=(來源==板球:: CS_LOCAL)? channel-> SetLocalContent(content_desc,type,&error):channel-> SetRemoteContent(content_desc,type,&error); if(!success){LOG_AND_RETURN_ERROR(RTCErrorType :: INVALID_PARAMETER,錯誤); }} …}

5、其他影響首幀顯示的問題

5.1 Android圖像寬高16字節對齊

AndroidVideoDecoder 是WebRTC Android 平台上的視頻硬解類。 AndroidVideoDecoder 利用 MediaCodec” API 完成對硬件解碼器的調用。

MediaCodec” 有已下解碼相關的API:

1)dequeueInputBuffer:若大於0,則是返回填充編碼數據的緩衝區的索引,該操作為同步操作;2)getInputBuffer:填充編碼數據的ByteBuffer 數組,結合dequeueInputBuffer 返回值,可獲取一個可填充編碼數據的ByteBuffer;3)queueInputBuffer:應用將編碼數據拷貝到ByteBuffer 後,通過該方法告知MediaCodec 已經填寫的編碼數據的緩衝區索引;4)dequeueOutputBuffer:若大於0,則是返回填充解碼數據的緩衝區的索引,該操作為同步操作;5)getOutputBuffer:填充解碼數據的ByteBuffer 數組,結合dequeueOutputBuffer 返回值,可獲取一個可填充解碼數據的ByteBuffer;6)releaseOutputBuffer:告訴編碼器數據處理完成,釋放ByteBuffer 數據。

在實踐當中發現,發送端發送的視頻寬高需要16 字節對齊,因為在某些Android 手機上解碼器需要16 字節對齊。

大致的原理就是:Android 上視頻解碼先是把待解碼的數據通過queueInputBuffer 給到MediaCodec。然後通過dequeueOutputBuffer 反複查看是否有解完的視頻幀。若非16 字節對齊,dequeueOutputBuffer 會有一次MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED。而不是一上來就能成功解碼一幀。

經測試發現:幀寬高非16 字節對齊會比16 字節對齊的慢100 ms 左右。

5.2 服務器需轉發關鍵幀請求

iOS 移動設備上,WebRTC App應用進入後台後,視頻解碼由VTDecompressionSessionDecodeFrame 返回kVTInvalidSessionErr,表示解碼session 無效。從而會觸發觀看端的關鍵幀請求給服務器。

這裡要求服務器必須轉發接收端發來的關鍵幀請求給發送端。若服務器沒有轉發關鍵幀給發送端,接收端就會長時間沒有可以渲染的圖像,從而出現黑屏問題。

這種情況下只能等待發送端自己生成關鍵幀,發送個接收端,從而使黑屏的接收端恢復正常。

5.3 WebRTC內部的一些丟棄數據邏輯舉例

Webrtc從接受報數據到、給到解碼器之間的過程中也會有很多驗證數據的正確性。

舉例1:

PacketBuffer 中記錄著當前緩存的最小的序號first_seq_num_(這個值也是會被更新的)。當PacketBuffer 中InsertPacket 時候,如果即將要插入的packet 的序號seq_num 小於first_seq_num,這個packet 會被丟棄掉。如果因此持續丟棄packet,就會有視頻不顯示或卡頓的情況。

舉例2:

正常情況下FrameBuffer 中幀的picture id,時間戳都是一直正增長的。

如果FrameBuffer 收到picture_id 比最後解碼幀的picture id 小時,分兩種情況:

1)時間戳比最後解碼幀的時間戳大,且是關鍵幀,就會保存下來。 2)除情況1 之外的幀都會丟棄掉。

代碼如下:

自動last_decoded_frame = encoded_frames_history_.GetLastDecodedFrameId(); 自動last_decoded_frame_timestamp = encoded_frames_history_.GetLastDecodedFrameTimestamp(); if(last_decoded_frame && id Timestamp(),* last_decoded_frame_timestamp)&& frame-> is_keyframe()){//如果此幀的時間戳較新,但圖片ID較早,則//我們假定圖片ID發生了跳躍某種編碼器//重新配置或其他原因。 即使這不符合規範,//如果它是關鍵幀,我們仍然可以繼續從該幀解碼。 “; RTC_LOG(LS_WARNING)<<”檢測到圖像ID的跳躍,正在清除緩衝區。 ClearFramesAndHistory(); last_continuous_picture_id = -1; } else {RTC_LOG(LS_WARNING)<<“在幀(”之後插入(picture_id:spatial_id)(“ << id.picture_id <<” ::“ << static_cast(id.spatial_layer)<<”)的幀< static_cast(last_decoded_frame->; space_layer)<<“)移交用於解碼,刪除幀。”; 返回last_continuous_picture_id; }}

因此為了能讓收到了流順利播放,發送端和中轉的服務端需要確保視頻幀的picture_id, 時間戳正確性。

WebRTC 還有其他很多丟幀邏輯,若網絡正常且有持續有接收數據,但是視頻卡頓或黑屏無顯示,多為流本身的問題。

6、本文小結

本文通過分析WebRTC 音視頻接收端的處理邏輯,列舉了一些可以優化首幀顯示的點,比如通過調整local SDP 和remote SDP 中與影響接收端處理的相關部分,從而避免Audio/Video ReceiveStream 的重啟。

另外列舉了Android 解碼器對視頻寬高的要求、服務端對關鍵幀請求處理、以及WebRTC 代碼內部的一些丟幀邏輯等多個方面對視頻顯示的影響。這些點都提高了融雲SDK 視頻首幀的顯示時間,改善了用戶體驗。

因個人水平有限,文章內容或許存在一定的局限性,歡迎回復進行討論。

(本文同步發佈於:http://www.52im.net/thread-3169-1-1.html“)