WebRTC 全稱爲:Web Real-Time Communication
。它是爲了解決 Web 端沒法捕獲音視頻的能力,而且提供了 peer-to-peer(就是瀏覽器間)的視頻交互。實際上,細分看來,它包含三個部分:git
但一般,peer-to-peer 的場景實際上應用不大。對比與去年火起來的直播
業務,這應該纔是 WebRTC 經常應用到的地方。那麼對應於 Web 直播來講,咱們一般須要兩個端:github
這裏,我就不談觀衆端了,後面另寫一篇文章介紹(由於,這是在是太多了)。這裏,主要談一下會用到 WebRTC 的主播端。 簡化一下,主播端應用技術簡單能夠分爲:錄製視頻,上傳視頻。你們先記住這兩個目標,後面咱們會經過 WebRTC 來實現這兩個目標。web
WebRTC 主要由兩個組織來制定。api
固然,咱們初級目標是先關心基本瀏覽器定義的 API 是啥?以及怎麼使用? 而後,後期目標是學習期內部的相關協議,數據格式等。這樣按部就班來,比較適合咱們的學習。瀏覽器
WebRTC 對於音視頻的處理,主要是交給 Audio/Vidoe Engineering 處理的。處理過程爲:安全
降噪
,消除迴音
,抖動/丟包隱藏
,編碼
。圖像加強
,同步
,抖動/丟包隱藏
,編碼
。最後經過 mediaStream Object 暴露給上層 API 使用。也就是說 mediaStream 是鏈接 WebRTC API 和底層物理流的中間層。因此,爲了下面更好的理解,這裏咱們先對 mediaStream 作一些簡單的介紹。服務器
MS(MediaStream)是做爲一個輔助對象存在的。它承載了音視頻流的篩選,錄製權限的獲取等。MS 由兩部分構成: MediaStreamTrack 和 MediaStream。架構
會聲會影
的話,應該對軌道
這個詞不陌生。通俗來說,你能夠認爲二者就是等價的。MediaStreamTrack
。它主要的做用就是確保幾個軌道是同時播放的。例如,聲音須要和視頻畫面同步。這裏,咱們不說太深,講講基本的 MediaStream
對象便可。一般,咱們使用實例化一個 MS 對象,就能夠獲得一個對象。socket
// 裏面還須要傳遞 track,或者其餘 stream 做爲參數。 // 這裏只爲演示方便 let ms = new MediaStream();
咱們能夠看一下 ms
上面帶有哪些對象屬性:tcp
它的原型鏈上還掛在了其餘方法,我挑幾個重要的說一下。
前面說了,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 利用的是 UDP 方式來進行傳輸視頻包。這樣作的好處是延遲性低,不用過分關注包的順序。不過,UDP 僅僅只是做爲一個傳輸層協議而已。WebRTC 還須要解決不少問題
整個架構以下:
上面那些協議,例如,ICE/STUN/TURN 等,咱們後面會慢慢講解。先來看一下,二者是如何進行信息協商的,一般這一階段,咱們叫作 signaling
。
signaling 其實是一個協商過程。由於,兩端進不進行 WebRTC 視頻交流之間,須要知道一些基本信息。
master key
用來確保安全鏈接。不過,signaling 這個過程並非寫死的,即,無論你用哪一種協議,只要能確保安全便可。爲何呢?由於,不一樣的應用有着其自己最適合的協商方法。好比:
咱們本身也能夠模擬出一個 signaling 通道。它的原理就是將信息進行傳輸而已,一般爲了方便,咱們能夠直接使用 socket.io 來創建 room
提供信息交流的通道。
假定,咱們如今已經經過 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 的協商流程。這裏有兩個基本的概念,offer
,answer
。
具體過程爲:
不過,上面只是簡單確立了兩端的鏈接信息而已,尚未涉及到視頻信息的傳輸,也就是說 UDP 傳輸。UDP 傳輸原本就是一個很是讓人蛋疼的活,若是是 client-server 的模型話還好,直接傳就能夠了,但這恰恰是 peer-to-peer 的模型。想一想,你如今是要把你的電腦當作一個服務器使用,中間還須要經歷若是突破防火牆,若是找到端口,如何跨網段進行?因此,這裏咱們就須要額外的協議,即,STUN/TURN/ICE ,來幫助咱們完成這樣的傳輸任務。
在 UDP 傳輸中,咱們不可避免的會碰見 NAT
(Network address translator)服務器。即,它主要是將其它網段的消息傳遞給它負責網段內的機器。不過,咱們的 UDP 包在傳遞時,通常只會帶上 NAT 的 host
。若是,此時你沒有目標機器的 entry
的話,那麼該次 UDP 包將不會被轉發成功。不過,若是你是 client-server 的形式的話,就不會碰見這樣的問題。但,這裏咱們是 peer-to-peer 的方式進行傳輸,沒法避免的會碰見這樣的問題。
爲了解決這樣的問題,咱們就須要創建 end-to-end 的鏈接。那辦法是什麼呢?很簡單,就是在中間設立一個 server
用來保留目標機器在 NAT 中的 entry
。經常使用協議有 STUN, TURN 和 ICE
。那他們有什麼區別嗎?
NAT traversal
服務器,保留指定機器的 entry
因此,上面三者一般是結合在一塊兒使用的。它們在 PeerConnection 中的角色以下圖:
若是,涉及到 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
。
不過,這裏爲了更好的講解 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 移除標準。不過,對於目前來講,問題不大,由於僅僅只是做爲演示使用。整個流程咱們一步一步來說解下。
pc2.addIceCandidate
方法將 pc1 添加進去。oniceconnectionstatechange
檢查 pc1 遠端 candidate 的狀態。當爲 completed
狀態時,則會觸發 pc2 onicecandidate
事件。