騰訊雲技術社區-掘金主頁持續爲你們呈現雲計算技術文章,歡迎你們關注!javascript
做者:villainthrjava
摘自:villainhrgit
WebRTC 全稱爲:Web Real-Time Communication
。它是爲了解決 Web 端沒法捕獲音視頻的能力,而且提供了 peer-to-peer(就是瀏覽器間)的視頻交互。實際上,細分看來,它包含三個部分:github
但一般,peer-to-peer 的場景實際上應用不大。對比與去年火起來的直播
業務,這應該纔是 WebRTC 經常應用到的地方。那麼對應於 Web 直播來講,咱們一般須要兩個端:web
這裏,我就不談觀衆端了,後面另寫一篇文章介紹(由於,這是在是太多了)。這裏,主要談一下會用到 WebRTC 的主播端。
簡化一下,主播端應用技術簡單能夠分爲:錄製視頻,上傳視頻。你們先記住這兩個目標,後面咱們會經過 WebRTC 來實現這兩個目標。api
WebRTC 主要由兩個組織來制定。瀏覽器
固然,咱們初級目標是先關心基本瀏覽器定義的 API 是啥?以及怎麼使用?
而後,後期目標是學習期內部的相關協議,數據格式等。這樣按部就班來,比較適合咱們的學習。安全
WebRTC 對於音視頻的處理,主要是交給 Audio/Vidoe Engineering 處理的。處理過程爲:性能優化
降噪
,消除迴音
,抖動/丟包隱藏
,編碼
。圖像加強
,同步
,抖動/丟包隱藏
,編碼
。最後經過 mediaStream Object 暴露給上層 API 使用。也就是說 mediaStream 是鏈接 WebRTC API 和底層物理流的中間層。因此,爲了下面更好的理解,這裏咱們先對 mediaStream 作一些簡單的介紹。服務器
MS(MediaStream)是做爲一個輔助對象存在的。它承載了音視頻流的篩選,錄製權限的獲取等。MS 由兩部分構成: MediaStreamTrack 和 MediaStream。
會聲會影
的話,應該對軌道
這個詞不陌生。通俗來說,你能夠認爲二者就是等價的。MediaStreamTrack
。它主要的做用就是確保幾個軌道是同時播放的。例如,聲音須要和視頻畫面同步。這裏,咱們不說太深,講講基本的 MediaStream
對象便可。一般,咱們使用實例化一個 MS 對象,就能夠獲得一個對象。
// 裏面還須要傳遞 track,或者其餘 stream 做爲參數。
// 這裏只爲演示方便
let ms = new MediaStream();複製代碼
咱們能夠看一下 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 利用的是 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
事件。此外,還有另一個概念,RTCDataChannel
我這裏就不過多涉及了。若是有興趣的能夠參閱 webrtc,web 性能優化 進行深刻的學習。
相關推薦:
【騰訊雲的1001種玩法】 Laravel 整合微視頻上傳管理能力,輕鬆打造視頻App後臺
闡述騰訊雲直播視頻解決方案