Web Real-Time Communication(Web實時通信,WebRTC)由一組標准、協議和JavaScript API組成,用於實現瀏覽器之間(端到端)的音頻、視頻及數據共享。
WebRTC使得實時通信變成一種標准功能,任何Web應用都無需借助第三方插件和專有軟件,而是通過簡單地JavaScript API即可完成。
在WebRTC中,有三個主要的知識點,理解了這三個知識點,也就理解了WebRTC的底層實現原理。這三個知識點分別是:
- MediaStream:獲取音頻和視頻流
- RTCPeerConnection:音頻和視頻數據通信
- RTCDataChannel:任意應用數據通信
MediaStream
如上所說,MediaStream主要是用於獲取音頻和視頻流。其JS實現也比較簡單,代碼如下:
'use strict'; navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; var constraints = { // 音頻、視頻約束 audio: true, // 指定請求音頻Track video: { // 指定請求視頻Track mandatory: { // 對視頻Track的強制約束條件 width: {min: 320}, height: {min: 180} }, optional: [ // 對視頻Track的可選約束條件 {frameRate: 30} ] } }; var video = document.querySelector('video'); function successCallback(stream) { if (window.URL) { video.src = window.URL.createObjectURL(stream); } else { video.src = stream; } } function errorCallback(error) { console.log('navigator.getUserMedia error: ', error); } navigator.getUserMedia(constraints, successCallback, errorCallback);
在JS中,我們通過getUserMedia函數來處理音頻和視頻,該函數接收三個參數,分別是音視頻的約束,成功的回調以及失敗的回調。
在底層,瀏覽器通過音頻和視頻引擎對捕獲的原始音頻和視頻流加以處理,除了對畫質和音質增強之外,還得保證音頻和視頻的同步。
由於音頻和視頻是用來傳輸的,因此,發送方還要適應不斷變化的帶寬和客戶端之間的網絡延遲調整輸出的比特率。
對於接收方來說,則必須實時解碼音頻和視頻流,並適應網絡抖動和時延。其工作原理如下圖所示:
如上成功回調的stream對象中攜帶者一個或多個同步的Track,如果你同時在約束中設置了音頻和視頻為true,則在stream中會攜帶有音頻Track和視頻Track,每個Track在時間上是同步的。
stream的輸出可以被發送到一或多個目的地:本地的音頻或視頻元素、后期處理的JavaScript代理,或者遠程另一端。如下圖所示:
RTCPeerConnection
在獲取到音頻和視頻流后,下一步要做的就是將其發送出去。但這個跟client-server模式不同,這是client-client之間的傳輸,因此,在協議層面就必須解決NAT穿透問題,否則傳輸就無從談起。
另外,由於WebRTC主要是用來解決實時通信的問題,可靠性並不是很重要,因此,WebRTC使用UDP作為傳輸層協議:低延遲和及時性才是關鍵。
在更深入講解之前,我們先來思考一下,是不是只要打開音頻、視頻,然后發送UDP包就搞定了?
當然沒那么簡單,除了要解決我們上面說的NAT穿透問題之外,還需要為每個流協商參數,對用戶數據進行加密,並且需要實現擁塞和流量控制。
我們來看一張WebRTC的分層協議圖:
ICE、STUN和TURN是通過UDP建立並維護端到端連接所必需的;SDP 是一種數據格式,用於端到端連接時協商參數;DTLS用於保障傳輸數據的安全;SCTP和SRTP屬於應用層協議,用於在UDP之上提供不同流的多路復用、擁塞和流量控制,以及部分可靠的交付和其他服務。
ICE(Interactive Connectivity Establishment,交互連接建立):由於端與端之間存在多層防火牆和NAT設備阻隔,因此我們需要一種機制來收集兩端之間公共線路的IP,而ICE則是干這件事的好幫手。
- ICE代理向操作系統查詢本地IP地址
- 如果配置了STUN服務器,ICE代理會查詢外部STUN服務器,以取得本地端的公共IP和端口
- 如果配置了TURN服務器,ICE則會將TURN服務器作為一個候選項,當端到端的連接失敗,數據將通過指定的中間設備轉發。
WebRTC使用SDP(Session Description Protocol,會話描述協議)描述端到端連接的參數。
SDP不包含媒體本身的任何信息,僅用於描述"會話狀況",表現為一系列的連接屬性:要交換的媒體類型(音頻、視頻及應用數據)、網絡傳輸協議、使用的編解碼器及其設置、帶寬及其他元數據。
DTLS對TLS協議進行了擴展,為每條握手記錄明確添加了偏移字段和序號,這樣就滿足了有序交付的條件,也能讓大記錄可以被分段成多個分組並在另一端再進行組裝。
DTLS握手記錄嚴格按照TLS協議規定的順序傳輸,順序不對就報錯。最后,DTLS還要處理丟包問題:兩端都是用計時器,如果預定時間沒有收到應答,就重傳握手記錄。
為保證過程完整,兩端都要生成自己簽名的證書,然后按照常規的TLS握手協議走。但這樣的證書不能用於驗證身份,因為沒有要驗證的信任鏈。因此,在必要情況下,
應用必須自己參與各端的身份驗證:
- 應用可以通過登錄來驗證用戶
- 每一端也可以在生成SDP提議/應答時指定各自的"身份頒發機構",等對端接收到SDP消息后,可以聯系指定的身份頒發機構驗證收到的證書
SRTP為通過IP網絡交付音頻和視頻定義了標准的分組格式。SRTP本身並不對傳輸數據的及時性、可靠性或數據恢復提供任何保證機制,
它只負責把數字化的音頻采樣和視頻幀用一些元數據封裝起來,以輔助接收方處理這些流。
SCTP是一個傳輸層協議,直接在IP協議上運行,這一點跟TCP和UDP類似。不過在WebRTC這里,SCTP是在一個安全的DTLS信道中運行,而這個信道又運行在UDP之上。
由於WebRTC支持通過DataChannel API在端與端之間傳輸任意應用數據,而DataChannel就依賴於SCTP。
以上講了這么多,終於到我們的主角RTCPeerConnection,RTCPeerConnection接口負責維護每一個端到端連接的完整生命周期:
- RTCPeerConnection管理穿越NAT的完整ICE工作流
- RTCPeerConnection發送自動(STUN)持久化信號
- RTCPeerConnection跟蹤本地流
- RTCPeerConnection跟蹤遠程流
- RTCPeerConnection按需觸發自動流協商
- RTCPeerConnection提供必要的API,以生成連接提議,接收應答,允許我們查詢連接的當前狀態,等等
我們來看一下示例代碼:
var signalingChannel = new SignalingChannel(); var pc = null; var ice = { "iceServers": [ { "url": "stun:stun.l.google.com:19302" }, //使用google公共測試服務器 { "url": "turn:user@turnserver.com", "credential": "pass" } // 如有turn服務器,可在此配置 ] }; signalingChannel.onmessage = function (msg) { if (msg.offer) { // 監聽並處理通過發信通道交付的遠程提議 pc = new RTCPeerConnection(ice); pc.setRemoteDescription(msg.offer); navigator.getUserMedia({ "audio": true, "video": true }, gotStream, logError); } else if (msg.candidate) { // 注冊遠程ICE候選項以開始連接檢查 pc.addIceCandidate(msg.candidate); } } function gotStream(evt) { pc.addstream(evt.stream); var local_video = document.getElementById('local_video'); local_video.src = window.URL.createObjectURL(evt.stream); pc.createAnswer(function (answer) { // 生成描述端連接的SDP應答並發送到對端 pc.setLocalDescription(answer); signalingChannel.send(answer.sdp); }); } pc.onicecandidate = function (evt) { if (evt.candidate) { signalingChannel.send(evt.candidate); } } pc.onaddstream = function (evt) { var remote_video = document.getElementById('remote_video'); remote_video.src = window.URL.createObjectURL(evt.stream); } function logError() { ... }
DataChannel
DataChannel支持端到端的任意應用數據交換,就像WebSocket一樣,但是是端到端的。
建立RTCPeerConnection連接之后,兩端可以打開一或多個信道交換文本或二進制數據。
其示例demo如下:
var ice = { 'iceServers': [ {'url': 'stun:stun.l.google.com:19302'}, // google公共測試服務器 // {"url": "turn:user@turnservera.com", "credential": "pass"} ] }; // var signalingChannel = new SignalingChannel(); var pc = new RTCPeerConnection(ice); navigator.getUserMedia({'audio': true}, gotStream, logError); function gotStream(stram) { pc.addStream(stram); pc.createOffer().then(function(offer){ pc.setLocalDescription(offer); }); } pc.onicecandidate = function(evt) { // console.log(evt); if(evt.target.iceGatheringState == 'complete') { pc.createOffer().then(function(offer){ // console.log(offer.sdp); // signalingChannel.send(sdp); }) } } function handleChannel(chan) { console.log(chan); chan.onerror = function(err) {} chan.onclose = function() {} chan.onopen = function(evt) { console.log('established'); chan.send('DataChannel connection established.'); } chan.onmessage = function(msg){ // do something } } // 以合適的交付語義初始化新的DataChannel var dc = pc.createDataChannel('namedChannel', {reliable: false}); handleChannel(dc); pc.onDataChannel = handleChannel; function logError(){ console.log('error'); }
原文:https://segmentfault.com/a/1190000011403597
僅供個人學習和交流