Web Real-Time Communication(Web實時通信,WebRTC)由一組標準煌恢、協(xié)議和JavaScript API組成骇陈,用于實現(xiàn)瀏覽器之間(端到端)的音頻、視頻及數(shù)據(jù)共享瑰抵。
WebRTC使得實時通信變成一種標準功能你雌,任何Web應用都無需借助第三方插件和專有軟件,而是通過簡單地JavaScript API即可完成二汛。
在WebRTC中婿崭,有三個主要的知識點,理解了這三個知識點肴颊,也就理解了WebRTC的底層實現(xiàn)原理逛球。這三個知識點分別是:
- MediaStream:獲取音頻和視頻流
- RTCPeerConnection:音頻和視頻數(shù)據(jù)通信
- RTCDataChannel:任意應用數(shù)據(jù)通信
MediaStream
如上所說,MediaStream主要是用于獲取音頻和視頻流苫昌。其JS實現(xiàn)也比較簡單颤绕,代碼如下:
'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函數(shù)來處理音頻和視頻奥务,該函數(shù)接收三個參數(shù),分別是音視頻的約束袜硫,成功的回調以及失敗的回調氯葬。
在底層,瀏覽器通過音頻和視頻引擎對捕獲的原始音頻和視頻流加以處理婉陷,除了對畫質和音質增強之外帚称,還得保證音頻和視頻的同步。
由于音頻和視頻是用來傳輸?shù)幕喟模虼舜扯茫l(fā)送方還要適應不斷變化的帶寬和客戶端之間的網(wǎng)絡延遲調整輸出的比特率。
對于接收方來說担神,則必須實時解碼音頻和視頻流楼吃,并適應網(wǎng)絡抖動和時延。其工作原理如下圖所示:
如上成功回調的stream對象中攜帶者一個或多個同步的Track妄讯,如果你同時在約束中設置了音頻和視頻為true孩锡,則在stream中會攜帶有音頻Track和視頻Track,每個Track在時間上是同步的亥贸。
stream的輸出可以被發(fā)送到一或多個目的地:本地的音頻或視頻元素躬窜、后期處理的JavaScript代理,或者遠程另一端炕置。如下圖所示:
RTCPeerConnection
在獲取到音頻和視頻流后荣挨,下一步要做的就是將其發(fā)送出去溜族。但這個跟client-server模式不同,這是client-client之間的傳輸垦沉,因此煌抒,在協(xié)議層面就必須解決NAT穿透問題,否則傳輸就無從談起厕倍。
另外寡壮,由于WebRTC主要是用來解決實時通信的問題,可靠性并不是很重要讹弯,因此况既,WebRTC使用UDP作為傳輸層協(xié)議:低延遲和及時性才是關鍵。
在更深入講解之前组民,我們先來思考一下棒仍,是不是只要打開音頻、視頻臭胜,然后發(fā)送UDP包就搞定了莫其?
當然沒那么簡單,除了要解決我們上面說的NAT穿透問題之外耸三,還需要為每個流協(xié)商參數(shù)乱陡,對用戶數(shù)據(jù)進行加密,并且需要實現(xiàn)擁塞和流量控制仪壮。
我們來看一張WebRTC的分層協(xié)議圖:
ICE憨颠、STUN和TURN是通過UDP建立并維護端到端連接所必需的;SDP 是一種數(shù)據(jù)格式积锅,用于端到端連接時協(xié)商參數(shù)爽彤;DTLS用于保障傳輸數(shù)據(jù)的安全;SCTP和SRTP屬于應用層協(xié)議,用于在UDP之上提供不同流的多路復用、擁塞和流量控制,以及部分可靠的交付和其他服務。
ICE(Interactive Connectivity Establishment斯稳,交互連接建立):由于端與端之間存在多層防火墻和NAT設備阻隔,因此我們需要一種機制來收集兩端之間公共線路的IP建车,而ICE則是干這件事的好幫手傀顾。
- ICE代理向操作系統(tǒng)查詢本地IP地址
- 如果配置了STUN服務器,ICE代理會查詢外部STUN服務器薄货,以取得本地端的公共IP和端口
- 如果配置了TURN服務器翁都,ICE則會將TURN服務器作為一個候選項,當端到端的連接失敗谅猾,數(shù)據(jù)將通過指定的中間設備轉發(fā)柄慰。
WebRTC使用SDP(Session Description Protocol,會話描述協(xié)議)描述端到端連接的參數(shù)坐搔。
SDP不包含媒體本身的任何信息,僅用于描述"會話狀況"概行,表現(xiàn)為一系列的連接屬性:要交換的媒體類型(音頻、視頻及應用數(shù)據(jù))凳忙、網(wǎng)絡傳輸協(xié)議业踏、使用的編解碼器及其設置、帶寬及其他元數(shù)據(jù)勤家。
DTLS對TLS協(xié)議進行了擴展柳恐,為每條握手記錄明確添加了偏移字段和序號伐脖,這樣就滿足了有序交付的條件,也能讓大記錄可以被分段成多個分組并在另一端再進行組裝乐设。
DTLS握手記錄嚴格按照TLS協(xié)議規(guī)定的順序傳輸晓殊,順序不對就報錯伤提。最后,DTLS還要處理丟包問題:兩端都是用計時器肿男,如果預定時間沒有收到應答舶沛,就重傳握手記錄嘹承。
為保證過程完整如庭,兩端都要生成自己簽名的證書,然后按照常規(guī)的TLS握手協(xié)議走坪它。但這樣的證書不能用于驗證身份,因為沒有要驗證的信任鏈蒙揣。因此开瞭,在必要情況下罩息,
應用必須自己參與各端的身份驗證:
- 應用可以通過登錄來驗證用戶
- 每一端也可以在生成SDP提議/應答時指定各自的"身份頒發(fā)機構"个扰,等對端接收到SDP消息后,可以聯(lián)系指定的身份頒發(fā)機構驗證收到的證書
SRTP為通過IP網(wǎng)絡交付音頻和視頻定義了標準的分組格式递宅。SRTP本身并不對傳輸數(shù)據(jù)的及時性、可靠性或數(shù)據(jù)恢復提供任何保證機制茅主,
它只負責把數(shù)字化的音頻采樣和視頻幀用一些元數(shù)據(jù)封裝起來土榴,以輔助接收方處理這些流。
SCTP是一個傳輸層協(xié)議玷禽,直接在IP協(xié)議上運行,這一點跟TCP和UDP類似糯笙。不過在WebRTC這里撩银,SCTP是在一個安全的DTLS信道中運行,而這個信道又運行在UDP之上够庙。
由于WebRTC支持通過DataChannel API在端與端之間傳輸任意應用數(shù)據(jù)抄邀,而DataChannel就依賴于SCTP。
以上講了這么多境肾,終于到我們的主角RTCPeerConnection,RTCPeerConnection接口負責維護每一個端到端連接的完整生命周期:
- RTCPeerConnection管理穿越NAT的完整ICE工作流
- RTCPeerConnection發(fā)送自動(STUN)持久化信號
- RTCPeerConnection跟蹤本地流
- RTCPeerConnection跟蹤遠程流
- RTCPeerConnection按需觸發(fā)自動流協(xié)商
- RTCPeerConnection提供必要的API偶宫,以生成連接提議衫嵌,接收應答,允許我們查詢連接的當前狀態(tài)结闸,等等
我們來看一下示例代碼:
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) { // 監(jiān)聽并處理通過發(fā)信通道交付的遠程提議
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應答并發(fā)送到對端
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支持端到端的任意應用數(shù)據(jù)交換蔫耽,就像WebSocket一樣,但是是端到端的匙铡。
建立RTCPeerConnection連接之后,兩端可以打開一或多個信道交換文本或二進制數(shù)據(jù)黑毅。
其示例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');
}