Web Real-Time Communication(Web實時通訊,WebRTC)由一組標準、協議和JavaScript API組成,用於實現瀏覽器之間(端到端)的音頻、視頻及數據共享。javascript
WebRTC使得實時通訊變成一種標準功能,任何Web應用都無需藉助第三方插件和專有軟件,而是經過簡單地JavaScript API便可完成。java
在WebRTC中,有三個主要的知識點,理解了這三個知識點,也就理解了WebRTC的底層實現原理。這三個知識點分別是:web
如上所說,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在時間上是同步的。ide
stream的輸出能夠被髮送到一或多個目的地:本地的音頻或視頻元素、後期處理的JavaScript代理,或者遠程另外一端。以下圖所示:函數
在獲取到音頻和視頻流後,下一步要作的就是將其發送出去。但這個跟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則是幹這件事的好幫手。
WebRTC使用SDP(Session Description Protocol,會話描述協議)描述端到端鏈接的參數。
SDP不包含媒體自己的任何信息,僅用於描述"會話情況",表現爲一系列的鏈接屬性:要交換的媒體類型(音頻、視頻及應用數據)、網絡傳輸協議、使用的編解碼器及其設置、帶寬及其餘元數據。
DTLS對TLS協議進行了擴展,爲每條握手記錄明確添加了偏移字段和序號,這樣就知足了有序交付的條件,也能讓大記錄能夠被分段成多個分組並在另外一端再進行組裝。
DTLS握手記錄嚴格按照TLS協議規定的順序傳輸,順序不對就報錯。最後,DTLS還要處理丟包問題:兩端都是用計時器,若是預約時間沒有收到應答,就重傳握手記錄。
爲保證過程完整,兩端都要生成本身簽名的證書,而後按照常規的TLS握手協議走。但這樣的證書不能用於驗證身份,由於沒有要驗證的信任鏈。所以,在必要狀況下,
應用必須本身參與各端的身份驗證:
SRTP爲經過IP網絡交付音頻和視頻定義了標準的分組格式。SRTP自己並不對傳輸數據的及時性、可靠性或數據恢復提供任何保證機制,
它只負責把數字化的音頻採樣和視頻幀用一些元數據封裝起來,以輔助接收方處理這些流。
SCTP是一個傳輸層協議,直接在IP協議上運行,這一點跟TCP和UDP相似。不過在WebRTC這裏,SCTP是在一個安全的DTLS信道中運行,而這個信道又運行在UDP之上。
因爲WebRTC支持經過DataChannel API在端與端之間傳輸任意應用數據,而DataChannel就依賴於SCTP。
以上講了這麼多,終於到咱們的主角RTCPeerConnection,RTCPeerConnection接口負責維護每個端到端鏈接的完整生命週期:
咱們來看一下示例代碼:
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支持端到端的任意應用數據交換,就像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'); }