騰訊IVWEB團隊:WebRTC 點對點直播


作者:villainthr

摘自:villainhr

WebRTC 全稱為:Web Real-Time Communication。它是為了解決 Web 端無法捕獲音視頻的能力,並且提供了 peer-to-peer(就是瀏覽器間)的視頻交互。實際上,細分看來,它包含三個部分:

  • MediaStream:捕獲音視頻流
  • RTCPeerConnection:傳輸音視頻流(一般用在 peer-to-peer 的場景)
  • RTCDataChannel: 用來上傳音視頻二進制數據(一般用到流的上傳)

但通常,peer-to-peer 的場景實際上應用不大。對比與去年火起來的直播業務,這應該才是 WebRTC 常常應用到的地方。那么對應於 Web 直播來說,我們通常需要兩個端:

  • 主播端:錄制並上傳視頻
  • 觀眾端:下載並觀看視頻

這里,我就不談觀眾端了,后面另寫一篇文章介紹(因為,這是在是太多了)。這里,主要談一下會用到 WebRTC 的主播端。
簡化一下,主播端應用技術簡單可以分為:錄制視頻,上傳視頻。大家先記住這兩個目標,后面我們會通過 WebRTC 來實現這兩個目標。

WebRTC 基本了解

WebRTC 主要由兩個組織來制定。

  • Web Real-Time Communications (WEBRTC) W3C 組織:定義瀏覽器 API
  • Real-Time Communication in Web-browsers (RTCWEB) IETF 標准組織:定義其所需的協議,數據,安全性等手段。

當然,我們初級目標是先關心基本瀏覽器定義的 API 是啥?以及怎么使用?
然后,后期目標是學習期內部的相關協議,數據格式等。這樣循序漸進來,比較適合我們的學習。

WebRTC 對於音視頻的處理,主要是交給 Audio/Vidoe Engineering 處理的。處理過程為:

engineer.svg-62.3kB

  • 音頻:通過物理設備進行捕獲。然后開始進行降噪消除回音抖動/丟包隱藏編碼
  • 視頻:通過物理設備進行捕獲。然后開始進行圖像增強同步抖動/丟包隱藏編碼

最后通過 mediaStream Object 暴露給上層 API 使用。也就是說 mediaStream 是連接 WebRTC API 和底層物理流的中間層。所以,為了下面更好的理解,這里我們先對 mediaStream 做一些簡單的介紹。

MediaStream

MS(MediaStream)是作為一個輔助對象存在的。它承載了音視頻流的篩選,錄制權限的獲取等。MS 由兩部分構成: MediaStreamTrack 和 MediaStream。

  • MediaStreamTrack 代表一種單類型數據流。如果你用過會聲會影的話,應該對軌道這個詞不陌生。通俗來講,你可以認為兩者就是等價的。
  • MediaStream 是一個完整的音視頻流。它可以包含 >=0 個 MediaStreamTrack。它主要的作用就是確保幾個軌道是同時播放的。例如,聲音需要和視頻畫面同步。

這里,我們不說太深,講講基本的 MediaStream 對象即可。通常,我們使用實例化一個 MS 對象,就可以得到一個對象。

// 里面還需要傳遞 track,或者其他 stream 作為參數。 // 這里只為演示方便 let ms = new MediaStream(); 

我們可以看一下 ms 上面帶有哪些對象屬性:

  • active[boolean]:表示當前 ms 是否是活躍狀態(就是可播放狀態)。
  • id[String]: 對當前的 ms 進行唯一標識。例如:"f61641ec-ee78-4317-9415-58acac066a4d"
  • onactive: 當 active 為 true 時,觸發該事件
  • onaddtrack: 當有新的 track 添加時,觸發該事件
  • oninactive: 當 active 為 false 時,觸發該事件
  • onremovetrack: 當有 track 移除時,觸發該事件

它的原型鏈上還掛在了其他方法,我挑幾個重要的說一下。

  • clone(): 對當前的 ms 流克隆一份。該方法通常用於對該 ms 流有操作時,常常會用到。

前面說了,MS 還可以其他篩選的作用,那么它是如何做到的呢?
在 MS 中,還有一個重要的概念叫做: Constraints。它是用來規范當前采集的數據是否符合需要。因為,我們采集視頻時,不同的設備有不同的參數設置。常用的為:

{
    "audio": true, // 是否捕獲音頻 "video": { // 視頻相關設置 "width": { "min": "381", // 當前視頻的最小寬度 "max": "640" }, "height": { "min": "200", // 最小高度 "max": "480" }, "frameRate": { "min": "28", // 最小幀率 "max": "10" } } } 

那我怎么知道我的設備支持的哪些屬性的調優呢?
這里,可以直接使用 navigator.mediaDevices.getSupportedConstraints() 來獲取可以調優的相關屬性。不過,這一般是對 video 進行設置。了解了 MS 之后,我們就要開始真正接觸 WebRTC 的相關 API。我們先來看一下 WebRTC 基本API。

WebRTC 的常用 API 如下,不過由於瀏覽器的緣故,需要加上對應的 prefix:

W3C Standard           Chrome                   Firefox
-------------------------------------------------------------- getUserMedia webkitGetUserMedia mozGetUserMedia RTCPeerConnection webkitRTCPeerConnection RTCPeerConnection RTCSessionDescription RTCSessionDescription RTCSessionDescription RTCIceCandidate RTCIceCandidate RTCIceCandidate 

不過,你可以簡單的使用下列的方法來解決。不過嫌麻煩的可以使用 adapter.js 來彌補

navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia 

這里,我們循序漸進的來學習。如果想進行視頻的相關交互,首先應該是捕獲音視頻。

捕獲音視頻

在 WebRTC 中捕獲音視頻,只需要使用到一個 API,即,getUserMedia()。代碼其實很簡單:

navigator.getUserMedia = navigator.getUserMedia ||
    navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

var constraints = { // 設置捕獲的音視頻設置 audio: false, video: true }; var video = document.querySelector('video'); function successCallback(stream) { window.stream = stream; // 這就是上面提到的 mediaStream 實例 if (window.URL) { video.src = window.URL.createObjectURL(stream); // 用來創建 video 可以播放的 src } else { video.src = stream; } } function errorCallback(error) { console.log('navigator.getUserMedia error: ', error); } // 這是 getUserMedia 的基本格式 navigator.getUserMedia(constraints, successCallback, errorCallback); 

詳細 demo 可以參考:WebRTC。不過,上面的寫法比較古老,如果使用 Promise 來的話,getUserMedia 可以寫為:

navigator.mediaDevices.getUserMedia(constraints). then(successCallback).catch(errorCallback); 

上面的注釋大概已經說清楚基本的內容。需要提醒的是,你在捕獲視頻的同時,一定要清楚自己需要捕獲的相關參數。

有了自己的視頻之后,那如何與其他人共享這個視頻呢?(可以理解為直播的方式)
在 WebRTC 中,提供了 RTCPeerConnection 的方式,來幫助我們快速建立起連接。不過,這僅僅只是建立起 peer-to-peer 的中間一環。這里包含了一些復雜的過程和額外的協議,我們一步一步的來看下。

WebRTC 基本內容

WebRTC 利用的是 UDP 方式來進行傳輸視頻包。這樣做的好處是延遲性低,不用過度關注包的順序。不過,UDP 僅僅只是作為一個傳輸層協議而已。WebRTC 還需要解決很多問題

  1. 遍歷 NATs 層,找到指定的 peer
  2. 雙方進行基本信息的協商以便雙方都能正常播放視頻
  3. 在傳輸時,還需要保證信息安全性

整個架構如下:

WebRTC_stack.svg-39.5kB

上面那些協議,例如,ICE/STUN/TURN 等,我們后面會慢慢講解。先來看一下,兩者是如何進行信息協商的,通常這一階段,我們叫做 signaling

signaling 任務

signaling 實際上是一個協商過程。因為,兩端進不進行 WebRTC 視頻交流之間,需要知道一些基本信息。

  • 打開/關閉連接的指令
  • 視頻信息,比如解碼器,解碼器的設置,帶寬,以及視頻的格式等。
  • 關鍵數據,相當於 HTTPS 中的 master key 用來確保安全連接。
  • 網關信息,比如雙方的 IP,port

不過,signaling 這個過程並不是寫死的,即,不管你用哪種協議,只要能確保安全即可。為什么呢?因為,不同的應用有着其本身最適合的協商方法。比如:

  • 單網關協議(SIP/Jingle/ISUP)適用於呼叫機制(VoIP,voice over IP)。
  • 自定義協議
  • 多網關協議

signaling.svg-59.5kB

我們自己也可以模擬出一個 signaling 通道。它的原理就是將信息進行傳輸而已,通常為了方便,我們可以直接使用 socket.io 來建立 room 提供信息交流的通道。

PeerConnection 的建立

假定,我們現在已經通過 socket.io 建立起了一個信息交流的通道。那么我們接下來就可以進入 RTCPeerConnection 一節,進行連接的建立。我們首先應該利用 signaling 進行基本信息的交換。那這些信息有哪些呢?
WebRTC 已經在底層幫我們做了這些事情-- Session Description Protocol (SDP)。我們利用 signaling 傳遞相關的 SDP,來確保雙方都能正確匹配,底層引擎會自動解析 SDP (是 JSEP 幫的忙),而不需要我們手動進行解析,突然感覺世界好美妙。。。我們來看一下怎么傳遞。

// 利用已經創建好的通道。 var signalingChannel = new SignalingChannel(); // 正式進入 RTC connection。這相當於創建了一個 peer 端。 var pc = new RTCPeerConnection({}); navigator.getUserMedia({ "audio": true }) .then(gotStream).catch(logError); function gotStream(stream) { pc.addStream(stream); // 通過 createOffer 來生成本地的 SDP pc.createOffer(function(offer) { pc.setLocalDescription(offer); signalingChannel.send(offer.sdp); }); } function logError() { ... } 

那 SDP 的具體格式是啥呢?
看一下格式就 ok,這不用過多了解:

v=0
o=- 1029325693179593971 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
a=msid-semantic: WMS
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:nHtT
a=ice-pwd:cuwglAha5fBmGljFXWntH1VN
a=fingerprint:sha-256 24:63:EB:DD:18:1B:BB:5E:B3:E8:C5:D7:92:F7:0B:44:EC:22:96:63:64:76:1A:56:64:DE:6B:CE:85:C6:64:78
a=setup:active
a=mid:audio
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=inactive
a=rtcp-mux
...

上面的過程,就是 peer-to-peer 的協商流程。這里有兩個基本的概念,offeranswer

  • offer: 主播端向其他用戶提供其本省視頻直播的基本信息
  • answer: 用戶端反饋給主播端,檢查能否正常播放

具體過程為:

webRTC (1).png-7.7kB

  1. 主播端通過 createOffer 生成 SDP 描述
  2. 主播通過 setLocalDescription,設置本地的描述信息
  3. 主播將 offer SDP 發送給用戶
  4. 用戶通過 setRemoteDescription,設置遠端的描述信息
  5. 用戶通過 createAnswer 創建出自己的 SDP 描述
  6. 用戶通過 setLocalDescription,設置本地的描述信息
  7. 用戶將 anwser SDP 發送給主播
  8. 主播通過 setRemoteDescription,設置遠端的描述信息。

不過,上面只是簡單確立了兩端的連接信息而已,還沒有涉及到視頻信息的傳輸,也就是說 UDP 傳輸。UDP 傳輸本來就是一個非常讓人蛋疼的活,如果是 client-server 的模型話還好,直接傳就可以了,但這偏偏是 peer-to-peer 的模型。想想,你現在是要把你的電腦當做一個服務器使用,中間還需要經歷如果突破防火牆,如果找到端口,如何跨網段進行?所以,這里我們就需要額外的協議,即,STUN/TURN/ICE ,來幫助我們完成這樣的傳輸任務。

NAT/STUN/TURN/ICE

在 UDP 傳輸中,我們不可避免的會遇見 NAT(Network address translator)服務器。即,它主要是將其它網段的消息傳遞給它負責網段內的機器。不過,我們的 UDP 包在傳遞時,一般只會帶上 NAT 的 host。如果,此時你沒有目標機器的 entry 的話,那么該次 UDP 包將不會被轉發成功。不過,如果你是 client-server 的形式的話,就不會遇見這樣的問題。但,這里我們是 peer-to-peer 的方式進行傳輸,無法避免的會遇見這樣的問題。

NAT_error.svg-30.4kB

為了解決這樣的問題,我們就需要建立 end-to-end 的連接。那辦法是什么呢?很簡單,就是在中間設立一個 server 用來保留目標機器在 NAT 中的 entry。常用協議有 STUN, TURN 和 ICE。那他們有什么區別嗎?

  • STUN:作為最基本的 NAT traversal 服務器,保留指定機器的 entry
  • TURN:當 STUN 出錯的時候,作為重試服務器的存在。
  • ICE:在眾多 STUN + TURN 服務器中,選擇最有效的傳遞通道。

所以,上面三者通常是結合在一起使用的。它們在 PeerConnection 中的角色如下圖:

ICE.svg-39.2kB

如果,涉及到 ICE 的話,我們在實例化 Peer Connection 時,還需要預先設置好指定的 STUN/TRUN 服務器。

var ice = {"iceServers": [ {"url": "stun:stun.l.google.com:19302"}, // TURN 一般需要自己去定義 { 'url': 'turn:192.158.29.39:3478?transport=udp', 'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=', 'username': '28224511:1379330808' }, { 'url': 'turn:192.158.29.39:3478?transport=tcp', 'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=', 'username': '28224511:1379330808' } ]}; var signalingChannel = new SignalingChannel(); var pc = new RTCPeerConnection(ice); // 在實例化 Peer Connection 時完成。 navigator.getUserMedia({ "audio": true }, gotStream, logError); function gotStream(stream) { pc.addStream(stream); // 將流添加到 connection 中。 pc.createOffer(function(offer) { pc.setLocalDescription(offer); }); } // 通過 ICE,監聽是否有用戶連接 pc.onicecandidate = function(evt) { if (evt.target.iceGatheringState == "complete") { local.createOffer(function(offer) { console.log("Offer with ICE candidates: " + offer.sdp); signalingChannel.send(offer.sdp); }); } } ... 

在 ICE 處理中,里面還分為 iceGatheringState 和 iceConnectionState。在代碼中反應的就是:

  pc.onicecandidate = function(e) { evt.target.iceGatheringState; pc.iceGatheringState }; pc.oniceconnectionstatechange = function(e) { evt.target.iceConnectionState; pc.iceConnectionState; }; 

當然,起主要作用的還是 onicecandidate

  • iceGatheringState: 用來檢測本地 candidate 的狀態。其有以下三種狀態:
    • new: 該 candidate 剛剛被創建
    • gathering: ICE 正在收集本地的 candidate
    • complete: ICE 完成本地 candidate 的收集
  • iceConnectionState: 用來檢測遠端 candidate 的狀態。遠端的狀態比較復雜,一共有 7 種: new/checking/connected/completed/failed/disconnected/closed

不過,這里為了更好的講解 WebRTC 建立連接的基本過程。我們使用單頁的連接來模擬一下。現在假設,有兩個用戶,一個是 pc1,一個是 pc2。pc1 捕獲視頻,然后,pc2 建立與 pc1 的連接,完成偽直播的效果。直接看代碼吧:

  var servers = null; // Add pc1 to global scope so it's accessible from the browser console window.pc1 = pc1 = new RTCPeerConnection(servers); // 監聽是否有新的 candidate 加入 pc1.onicecandidate = function(e) { onIceCandidate(pc1, e); }; // Add pc2 to global scope so it's accessible from the browser console window.pc2 = pc2 = new RTCPeerConnection(servers); pc2.onicecandidate = function(e) { onIceCandidate(pc2, e); }; pc1.oniceconnectionstatechange = function(e) { onIceStateChange(pc1, e); }; pc2.oniceconnectionstatechange = function(e) { onIceStateChange(pc2, e); }; // 一旦 candidate 添加成功,則將 stream 播放 pc2.onaddstream = gotRemoteStream; // pc1 作為播放端,先將 stream 加入到 Connection 當中。 pc1.addStream(localStream); pc1.createOffer( offerOptions ).then( onCreateOfferSuccess, error ); function onCreateOfferSuccess(desc) { // desc 就是 sdp 的數據 pc1.setLocalDescription(desc).then( function() { onSetLocalSuccess(pc1); }, onSetSessionDescriptionError ); trace('pc2 setRemoteDescription start'); // 省去了 offer 的發送通道 pc2.setRemoteDescription(desc).then( function() { onSetRemoteSuccess(pc2); }, onSetSessionDescriptionError ); trace('pc2 createAnswer start'); pc2.createAnswer().then( onCreateAnswerSuccess, onCreateSessionDescriptionError ); } 

看上面的代碼,大家估計有點迷茫,來點實的,大家可以參考 單頁直播。在查看該網頁的時候,可以打開控制台觀察具體進行的流程。會發現一個現象,即,onaddstream 會在 SDP 協商還未完成之前就已經開始,這也是,該 API 設計的一些不合理之處,所以,W3C 已經將該 API 移除標准。不過,對於目前來說,問題不大,因為僅僅只是作為演示使用。整個流程我們一步一步來講解下。

  1. pc1 createOffer start
  2. pc1 setLocalDescription start // pc1 的 SDP
  3. pc2 setRemoteDescription start // pc1 的 SDP
  4. pc2 createAnswer start
  5. pc1 setLocalDescription complete // pc1 的 SDP
  6. pc2 setRemoteDescription complete // pc1 的 SDP
  7. pc2 setLocalDescription start // pc2 的 SDP
  8. pc1 setRemoteDescription start // pc2 的 SDP
  9. pc2 received remote stream,此時,接收端已經可以播放視頻。接着,觸發 pc2 的 onaddstream 監聽事件。獲得遠端的 video stream,注意此時 pc2 的 SDP 協商還未完成。
  10. 此時,本地的 pc1 candidate 的狀態已經改變,觸發 pc1 onicecandidate。開始通過 pc2.addIceCandidate 方法將 pc1 添加進去。
  11. pc2 setLocalDescription complete // pc2 的 SDP
  12. pc1 setRemoteDescription complete // pc2 的 SDP
  13. pc1 addIceCandidate success。pc1 添加成功
  14. 觸發 oniceconnectionstatechange 檢查 pc1 遠端 candidate 的狀態。當為 completed 狀態時,則會觸發 pc2 onicecandidate 事件。
  15. pc2 addIceCandidate success。

此外,還有另外一個概念,RTCDataChannel 我這里就不過多涉及了。如果有興趣的可以參閱 webrtc,web 性能優化 進行深入的學習。

原文鏈接:http://ivweb.io/topic/58aae3fa59edf3683ec76c07

相關閱讀:

【騰訊雲的1001種玩法】 Laravel 整合微視頻上傳管理能力,輕松打造視頻App后台

闡述騰訊雲直播視頻解決方案


 

此文已由作者授權騰訊雲技術社區發布,轉載請注明文章出處,獲取更多雲計算技術干貨,可請前往騰訊雲技術社區

原文鏈接:https://www.qcloud.com/community/article/522911001489391620

歡迎大家關注騰訊雲技術社區-博客園官方主頁,我們將持續在博客園為大家推薦技術精品文章哦~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM