本文由融雲技術團隊原創投稿,做者是融雲WebRTC高級工程師蘇道,轉載請註明出處。php
在一個典型的IM應用裏,使用實時音視頻聊天功能時,視頻首幀的顯示,是一項很重要的用戶體驗指標。html
本文主要經過對WebRTC接收端的音視頻處理過程分析,來了解和優化視頻首幀的顯示時間,並進行了總結和分享。android
(本文同步發佈於:http://www.52im.net/thread-3169-1-1.html)web
對於沒接觸過實時音視頻技術的人來講,老是看到別人在提WebRTC,那WebRTC是什麼?咱們有必要簡單介紹一下。算法
說到 WebRTC,咱們不得不提到 Gobal IP Solutions,簡稱 GIPS。這是一家 1990 年成立於瑞典斯德哥爾摩的 VoIP 軟件開發商,提供了能夠說是世界上最好的語音引擎。相關介紹詳見《訪談WebRTC標準之父:WebRTC的過去、如今和將來》。小程序
Skype、騰訊 QQ、WebEx、Vidyo 等都使用了它的音頻處理引擎,包含了受專利保護的回聲消除算法,適應網絡抖動和丟包的低延遲算法,以及先進的音頻編解碼器。微信小程序
Google 在 Gtalk 中也使用了 GIPS 的受權。Google 在 2011 年以6820萬美圓收購了 GIPS,並將其源代碼開源,加上在 2010 年收購的 On2 獲取到的 VPx 系列視頻編解碼器(詳見《即時通信音視頻開發(十七):視頻編碼H.26四、VP8的前世此生》),WebRTC 開源項目應運而生,即 GIPS 音視頻引擎 + 替換掉 H.264 的 VPx 視頻編解碼器。api
在此以後,Google 又將在 Gtalk 中用於 P2P 打洞的開源項目 libjingle 融合進了 WebRTC。目前 WebRTC 提供了包括 Web、iOS、Android、Mac、Windows、Linux 在內的全部平臺支持。數組
(以上介紹,引用自《了不得的WebRTC:生態日趨完善,或將實時音視頻技術白菜化》)緩存
雖然WebRTC的目標是實現跨平臺的Web端實時音視頻通信,但由於核心層代碼的Native、高品質和內聚性,開發者很容易進行除Web平臺外的移殖和應用。目前爲止,WebRTC幾乎是是業界能免費獲得的惟一高品質實時音視頻通信技術。
一個典型的實時音視頻處理流程大概是這樣:
以下圖所示:
本文所涉及的參數調整,談論的部分位於上圖中的第 4 步。
由於是接收端,因此會收到對方的 Offer 請求。先設置 SetRemoteDescription 再 SetLocalDescription。
以下圖藍色部分:
當收到 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 實現以下:
void 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) because of "
"SetRecvParameters";
MaybeRecreateWebRtcFlexfecStream();
}
if(video_needs_recreation) {
RTC_LOG(LS_INFO)
<< "RecreateWebRtcVideoStream (recv) because of SetRecvParameters";
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 重啓。
和上面的視頻調整相似,音頻也會有由於 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<webrtc::RtpExtension> 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_);
}
}
return 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 對應的庫文件。
WebRTC 內部有三個很是重要的線程:
調用 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<bool>(
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");
return InvokeOnWorker<bool>(
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 type,
cricket::ContentSource source) {
const SessionDescriptionInterface* sdesc =
(source == cricket::CS_LOCAL ? local_description()
: remote_description());
RTC_DCHECK(sdesc);
// Push down the new SDP media section for each audio/video transceiver.
for(const auto& transceiver : transceivers_) {
const ContentInfo* content_info =
FindMediaSectionForTransceiver(transceiver, sdesc);
cricket::ChannelInterface* channel = transceiver->internal()->channel();
if(!channel || !content_info || content_info->rejected) {
continue;
}
const MediaContentDescription* content_desc =
content_info->media_description();
if(!content_desc) {
continue;
}
std::string error;
bool success = (source == cricket::CS_LOCAL)
? channel->SetLocalContent(content_desc, type, &error)
: channel->SetRemoteContent(content_desc, type, &error);
if(!success) {
LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER, error);
}
}
...
}
AndroidVideoDecoder 是 WebRTC Android 平臺上的視頻硬解類。AndroidVideoDecoder 利用 MediaCodec API 完成對硬件解碼器的調用。
MediaCodec 有已下解碼相關的 API:
在實踐當中發現,發送端發送的視頻寬高須要 16 字節對齊,由於在某些 Android 手機上解碼器須要 16 字節對齊。
大體的原理就是:Android 上視頻解碼先是把待解碼的數據經過 queueInputBuffer 給到 MediaCodec。而後經過 dequeueOutputBuffer 反覆查看是否有解完的視頻幀。若非 16 字節對齊,dequeueOutputBuffer 會有一次MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED。而不是一上來就能成功解碼一幀。
經測試發現:幀寬高非 16 字節對齊會比 16 字節對齊的慢 100 ms 左右。
iOS 移動設備上,WebRTC App應用進入後臺後,視頻解碼由 VTDecompressionSessionDecodeFrame 返回 kVTInvalidSessionErr,表示解碼session 無效。從而會觸發觀看端的關鍵幀請求給服務器。
這裏要求服務器必須轉發接收端發來的關鍵幀請求給發送端。若服務器沒有轉發關鍵幀給發送端,接收端就會長時間沒有能夠渲染的圖像,從而出現黑屏問題。
這種狀況下只能等待發送端本身生成關鍵幀,發送個接收端,從而使黑屏的接收端恢復正常。
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 小時,分兩種狀況:
代碼以下:
auto last_decoded_frame = decoded_frames_history_.GetLastDecodedFrameId();
auto last_decoded_frame_timestamp =
decoded_frames_history_.GetLastDecodedFrameTimestamp();
if(last_decoded_frame && id <= *last_decoded_frame) {
if(AheadOf(frame->Timestamp(), *last_decoded_frame_timestamp) &&
frame->is_keyframe()) {
// If this frame has a newer timestamp but an earlier picture id then we
// assume there has been a jump in the picture id due to some encoder
// reconfiguration or some other reason. Even though this is not according
// to spec we can still continue to decode from this frame if it is a
// keyframe.
RTC_LOG(LS_WARNING)
<< "A jump in picture id was detected, clearing buffer.";
ClearFramesAndHistory();
last_continuous_picture_id = -1;
} else{
RTC_LOG(LS_WARNING) << "Frame with (picture_id:spatial_id) ("
<< id.picture_id << ":"
<< static_cast<int>(id.spatial_layer)
<< ") inserted after frame ("
<< last_decoded_frame->picture_id << ":"
<< static_cast<int>(last_decoded_frame->spatial_layer)
<< ") was handed off for decoding, dropping frame.";
return last_continuous_picture_id;
}
}
所以爲了能讓收到了流順利播放,發送端和中轉的服務端須要確保視頻幀的 picture_id, 時間戳正確性。
WebRTC 還有其餘不少丟幀邏輯,若網絡正常且有持續有接收數據,可是視頻卡頓或黑屏無顯示,多爲流自己的問題。
本文經過分析 WebRTC 音視頻接收端的處理邏輯,列舉了一些能夠優化首幀顯示的點,好比經過調整 local SDP 和 remote SDP 中與影響接收端處理的相關部分,從而避免 Audio/Video ReceiveStream 的重啓。
另外列舉了 Android 解碼器對視頻寬高的要求、服務端對關鍵幀請求處理、以及 WebRTC 代碼內部的一些丟幀邏輯等多個方面對視頻顯示的影響。 這些點都提升了融雲 SDK 視頻首幀的顯示時間,改善了用戶體驗。
因我的水平有限,文章內容或許存在必定的侷限性,歡迎回復進行討論。
《IM消息ID技術專題(三):解密融雲IM產品的聊天消息ID生成策略》
《融雲技術分享:基於WebRTC的實時音視頻首幀顯示時間優化實踐》(* 本文)
[1] 開源實時音視頻技術WebRTC的文章:
《開源實時音視頻技術WebRTC的現狀》
《簡述開源實時音視頻技術WebRTC的優缺點》
《訪談WebRTC標準之父:WebRTC的過去、如今和將來》
《良心分享:WebRTC 零基礎開發者教程(中文)[附件下載]》
《WebRTC實時音視頻技術的總體架構介紹》
《新手入門:到底什麼是WebRTC服務器,以及它是如何聯接通話的?》
《WebRTC實時音視頻技術基礎:基本架構和協議棧》
《淺談開發實時視頻直播平臺的技術要點》
《[觀點] WebRTC應該選擇H.264視頻編碼的四大理由》
《基於開源WebRTC開發實時音視頻靠譜嗎?第3方SDK有哪些?》
《開源實時音視頻技術WebRTC中RTP/RTCP數據傳輸協議的應用》
《簡述實時音視頻聊天中端到端加密(E2EE)的工做原理》
《實時通訊RTC技術棧之:視頻編解碼》
《開源實時音視頻技術WebRTC在Windows下的簡明編譯教程》
《網頁端實時音視頻技術WebRTC:看起來很美,但離生產應用還有多少坑要填?》
《了不得的WebRTC:生態日趨完善,或將實時音視頻技術白菜化》
《騰訊技術分享:微信小程序音視頻與WebRTC互通的技術思路和實踐》
《融雲技術分享:基於WebRTC的實時音視頻首幀顯示時間優化實踐》
>> 更多同類文章 ……
[2] 實時音視頻開發的其它精華資料:
《即時通信音視頻開發(一):視頻編解碼之理論概述》
《即時通信音視頻開發(二):視頻編解碼之數字視頻介紹》
《即時通信音視頻開發(三):視頻編解碼之編碼基礎》
《即時通信音視頻開發(四):視頻編解碼之預測技術介紹》
《即時通信音視頻開發(五):認識主流視頻編碼技術H.264》
《即時通信音視頻開發(六):如何開始音頻編解碼技術的學習》
《即時通信音視頻開發(七):音頻基礎及編碼原理入門》
《即時通信音視頻開發(八):常見的實時語音通信編碼標準》
《即時通信音視頻開發(九):實時語音通信的迴音及迴音消除概述》
《即時通信音視頻開發(十):實時語音通信的迴音消除技術詳解》
《即時通信音視頻開發(十一):實時語音通信丟包補償技術詳解》
《即時通信音視頻開發(十二):多人實時音視頻聊天架構探討》
《即時通信音視頻開發(十三):實時視頻編碼H.264的特色與優點》
《即時通信音視頻開發(十四):實時音視頻數據傳輸協議介紹》
《即時通信音視頻開發(十五):聊聊P2P與實時音視頻的應用狀況》
《即時通信音視頻開發(十六):移動端實時音視頻開發的幾個建議》
《即時通信音視頻開發(十七):視頻編碼H.26四、VP8的前世此生》
《即時通信音視頻開發(十八):詳解音頻編解碼的原理、演進和應用選型》
《即時通信音視頻開發(十九):零基礎,史上最通俗視頻編碼技術入門》
《實時語音聊天中的音頻處理與編碼壓縮技術簡述》
《網易視頻雲技術分享:音頻處理與壓縮技術快速入門》
《學習RFC3550:RTP/RTCP實時傳輸協議基礎知識》
《基於RTMP數據傳輸協議的實時流媒體技術研究(論文全文)》
《聲網架構師談實時音視頻雲的實現難點(視頻採訪)》
《淺談開發實時視頻直播平臺的技術要點》
《還在靠「喂喂喂」測試實時語音通話質量?本文教你科學的評測方法!》
《實現延遲低於500毫秒的1080P實時音視頻直播的實踐分享》
《移動端實時視頻直播技術實踐:如何作到實時秒開、流暢不卡》
《如何用最簡單的方法測試你的實時音視頻方案》
《技術揭祕:支持百萬級粉絲互動的Facebook實時視頻直播》
《簡述實時音視頻聊天中端到端加密(E2EE)的工做原理》
《移動端實時音視頻直播技術詳解(一):開篇》
《移動端實時音視頻直播技術詳解(二):採集》
《移動端實時音視頻直播技術詳解(三):處理》
《移動端實時音視頻直播技術詳解(四):編碼和封裝》
《移動端實時音視頻直播技術詳解(五):推流和傳輸》
《移動端實時音視頻直播技術詳解(六):延遲優化》
《理論聯繫實際:實現一個簡單地基於HTML5的實時視頻直播》
《IM實時音視頻聊天時的回聲消除技術詳解》
《淺談實時音視頻直播中直接影響用戶體驗的幾項關鍵技術指標》
《如何優化傳輸機制來實現實時音視頻的超低延遲?》
《首次披露:快手是如何作到百萬觀衆同場看直播仍能秒開且不卡頓的?》
《Android直播入門實踐:動手搭建一套簡單的直播系統》
《網易雲信實時視頻直播在TCP數據傳輸層的一些優化思路》
《實時音視頻聊天技術分享:面向不可靠網絡的抗丟包編解碼器》
《P2P技術如何將實時視頻直播帶寬下降75%?》
《專訪微信視頻技術負責人:微信實時視頻聊天技術的演進》
《騰訊音視頻實驗室:使用AI黑科技實現超低碼率的高清實時視頻聊天》
《微信團隊分享:微信每日億次實時音視頻聊天背後的技術解密》
《近期大熱的實時直播答題系統的實現思路與技術難點分享》
《福利貼:最全實時音視頻開發要用到的開源工程彙總》
《七牛雲技術分享:使用QUIC協議實現實時視頻直播0卡頓!》
《實時音視頻聊天中超低延遲架構的思考與技術實踐》
《理解實時音視頻聊天中的延時問題一篇就夠》
《實時視頻直播客戶端技術盤點:Native、HTML五、WebRTC、微信小程序》
《寫給小白的實時音視頻技術入門提綱》
《微信多媒體團隊訪談:音視頻開發的學習、微信的音視頻技術和挑戰等》
《騰訊技術分享:微信小程序音視頻技術背後的故事》
《微信多媒體團隊梁俊斌訪談:聊一聊我所瞭解的音視頻技術》
《新浪微博技術分享:微博短視頻服務的優化實踐之路》
《實時音頻的混音在視頻直播應用中的技術原理和實踐總結》
《以網遊服務端的網絡接入層設計爲例,理解實時通訊的技術挑戰》
《騰訊技術分享:微信小程序音視頻與WebRTC互通的技術思路和實踐》
《新浪微博技術分享:微博實時直播答題的百萬高併發架構實踐》
《技術乾貨:實時視頻直播首屏耗時400ms內的優化實踐》
《愛奇藝技術分享:輕鬆詼諧,講解視頻編解碼技術的過去、如今和未來》
《零基礎入門:實時音視頻技術基礎知識全面盤點》
>> 更多同類文章 ……
本文已同步發佈於「即時通信技術圈」公衆號:
本文在公衆號上的連接是:點此進入,原文連接是:http://www.52im.net/thread-3169-1-1.html