webRTC 基礎介紹

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

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

但一般,peer-to-peer 的場景實際上應用不大。對比與去年火起來的直播業務,這應該纔是 WebRTC 經常應用到的地方。那麼對應於 Web 直播來講,咱們一般須要兩個端:github

  • 主播端:錄製並上傳視頻
  • 觀衆端:下載並觀看視頻

這裏,我就不談觀衆端了,後面另寫一篇文章介紹(由於,這是在是太多了)。這裏,主要談一下會用到 WebRTC 的主播端。 簡化一下,主播端應用技術簡單能夠分爲:錄製視頻,上傳視頻。你們先記住這兩個目標,後面咱們會經過 WebRTC 來實現這兩個目標。web

WebRTC 基本瞭解

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

  • 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 對象,就能夠獲得一個對象。socket

// 裏面還須要傳遞 track,或者其餘 stream 做爲參數。 // 這裏只爲演示方便 let ms = new MediaStream();

咱們能夠看一下 ms 上面帶有哪些對象屬性:tcp

  • 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。
相關文章
相關標籤/搜索