5.通過RTCPeerConnection傳輸流媒體視訊
本節內容
在本節課程中, 我們將學習以下內容:
- 使用WebRTC相容庫: adapter.js, 來抹平各瀏覽器間的差異。
- 通過 RTCPeerConnection API 傳輸流媒體視訊。
- 控制 media 的捕捉和傳輸。
本節的完整版程式碼位於 step-02
資料夾中。
RTCPeerConnection 簡介
在WebRTC規範中, RTCPeerConnection
用於視訊流/音訊流、以及資料的傳輸。
下面的示例程式, 將會在一個頁面上, 通過兩個 RTCPeerConnection 物件建立一個連線通道。
這個demo本身沒什麼實用價值, 目的只是為了理解 RTCPeerConnection 的原理。
新增video
元素及控制按鈕
在 index.html 檔案中, 配置兩個 video 元素, 以及三個按鈕:
<video id="localVideo" autoplay playsinline></video> <video id="remoteVideo" autoplay playsinline></video> <div> <button id="startButton">Start</button> <button id="callButton">Call</button> <button id="hangupButton">Hang Up</button> </div>
第一個 video 元素(id="localVideo"
)用於展示通過 getUserMedia()
獲取到的本地視訊流, 第二個 video 元素(id="remoteVideo"
)則通過RTCPeerconnection, 接收並顯示同樣的視訊。在實際應用中, 頁面中一般都有兩個 video 元素: 一個用來展示本地視訊, 另一個用來播放遠端傳輸過來的視訊( 可以參考微信視訊聊天介面, 其中有一大一小,兩個視訊展示視窗 )。
新增 adapter.js 相容庫
在 main.js 引用的前面, 引入 adapter.js 的最新版本:
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
adapter.js 是一個適配程式, 隔離了應用程式和規範之間的變更、字首等差異.(當然, WebRTC實現所使用的標準和協議都已經是穩定版了, 有字首的API也沒幾個。)
在本節課程中, 我們引入了 adapter.js 的最新版本。這個庫對於實驗和教程來說足夠用了, 但如果想用於生產環境, 可能還需要進一步完善。 adapter.js
的地址為: https://github.com/webrtc/adapter, Github提供的服務讓我們可以使用到最新的版本。
現在, Index.html 的內容如下:
<!DOCTYPE html>
<html>
<head>
<title>Realtime communication with WebRTC</title>
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<h1>Realtime communication with WebRTC</h1>
<video id="localVideo" autoplay playsinline></video>
<video id="remoteVideo" autoplay playsinline></video>
<div>
<button id="startButton">Start</button>
<button id="callButton">Call</button>
<button id="hangupButton">Hang Up</button>
</div>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="js/main.js"></script>
</body>
</html>
呼叫 RTCPeerConnection
使用 step-02/main.js
檔案替換 work/main.js
檔案。
按道理來說,在demo教程中,是不應該存在這種大量的複製貼上行為的, 但沒辦法, RTCPeerConnection 相關的程式碼是一個整體, 要跑起來就必須全部配置好才行。
下面我們對程式碼進行詳細的講解。
撥打視訊
瀏覽器中通過http協議開啟 index.html頁面, 單擊 Start 按鈕獲取攝像頭的視訊, 之後點選 Call 按鈕來建立對等連線(peer connection)。 如果連線成功, 那麼就可以在兩個 video 中中看到同樣的畫面. 請開啟瀏覽器的控制檯, 檢視 WebRTC 相關的日誌資訊。
原理簡介
這一步做了很多的操作…
具體的內容比較複雜, 如果不關心實現過程, 可直接跳到下一節。 跳過下面的步驟, 依然可以進行該教程的學習!
通過 RTCPeerConnection, 可以在 WebRTC 客戶端之間建立連線, 來傳輸流媒體視訊, 每個客戶端就是一個端點(peer)。
本節的示例中, 兩個 RTCPeerConnection 物件在同一個頁面中: 即 pc1
和 pc2
。 所以並沒有什麼實際價值, 只是用來演示api的使用。
在 WebRTC 客戶端之間建立視訊通話, 需要執行三個步驟:
- 為每個客戶端建立一個 RTCPeerConnection 例項, 並且通過
getUserMedia()
獲取本地媒體流。 - 獲取網路資訊併發送給對方: 有可能成功的連線點(endpoint), 被稱為 ICE 候選。
- 獲取本地和遠端描述資訊並分享: SDP 格式的本地 media 元資料。
假設 Alice 和 Bob 想通過 RTCPeerConnection 進行視訊聊天。
首先, Alice 和 Bob 需要交換雙方的網路資訊。 “尋找候選” 指的是通過 ICE 框架來查詢可用網路和埠資訊的過程。 可以分為以下步驟:
-
Alice 建立一個 RTCPeerConnection 物件, 設定好
onicecandidate
回撥 [即addEventListener('icecandidate', XXX)
] 。 在 main.js 中對應的程式碼為:let localPeerConnection;
以及,
localPeerConnection = new RTCPeerConnection(servers); localPeerConnection.addEventListener('icecandidate', handleConnection); localPeerConnection.addEventListener( 'iceconnectionstatechange', handleConnectionChange);
在本例中, RTCPeerConnection 建構函式的
servers
引數為 null。servers
引數中, 可以指定 STUN 和 TURN 伺服器相關的資訊。
WebRTC 是為 peer-to-peer 網路設計的, 所以使用者可以在大部分可以直連的網路中使用. 但現實情況非常複雜, WebRTC面臨的真實環境是: 客戶端程式需要穿透 NAT閘道器 ,以及各類防火牆。 所以在直連失敗的情況下, peer-to-peer 網路需要一種回退措施。
為了解決 peer-to-peer 直連通訊失敗的問題, WebRTC 通過 STUN 服務來獲取客戶端的公網IP, 並使用 TURN 作為中繼伺服器。 詳細資訊請參考: WebRTC in the real world 。
-
Alice 呼叫
getUserMedia()
, 將獲取到的本地 stream 傳給 localVideo:navigator.mediaDevices.getUserMedia(mediaStreamConstraints). then(gotLocalMediaStream). catch(handleLocalMediaStreamError);
function gotLocalMediaStream(mediaStream) { localVideo.srcObject = mediaStream; localStream = mediaStream; trace('Received local stream.'); callButton.disabled = false; // Enable call button. }
localPeerConnection.addStream(localStream); trace('Added local stream to localPeerConnection.');
-
在網路候選者變為可用時, 步驟1中引入的
onicecandidate
回撥函式, 會被執行。 -
Alice 將序列化之後的候選者資訊傳送給 Bob。這個過程被稱為 signaling(信令), 實際應用中, 會通過訊息服務來傳遞。 在後面的教程中會看到. 當然,在本節中, 因為兩個 RTCPeerConnection 例項處於同一個頁面, 所以可以直接通訊, 不再需要外部訊息服務。
-
Bob從Alice處獲得候選者資訊後, 呼叫
addIceCandidate()
方法, 將候選資訊傳給 remote peer description:
function handleConnection(event) {
const peerConnection = event.target;
const iceCandidate = event.candidate;
if (iceCandidate) {
const newIceCandidate = new RTCIceCandidate(iceCandidate);
const otherPeer = getOtherPeer(peerConnection);
otherPeer.addIceCandidate(newIceCandidate)
.then(() => {
handleConnectionSuccess(peerConnection);
}).catch((error) => {
handleConnectionFailure(peerConnection, error);
});
trace(`${getPeerName(peerConnection)} ICE candidate:\n` +
`${event.candidate.candidate}.`);
}
}
WebRTC客戶端還需要獲取本地和遠端的音訊/視訊媒體資訊, 比如解析度、編碼/解碼器的能力等等. 交換媒體配置資訊的信令過程, 是通過交換元資料的blob資料進行的, 即一次 offer 與一次 answer, 使用會話描述協議(Session Description Protocol), 簡稱 SDP:
-
Alice 執行 RTCPeerConnection 的
createOffer()
方法。返回的 promise 中提供了一個 RTCSessionDescription 物件: 其中包含 Alice 本地的會話描述資訊:trace('localPeerConnection createOffer start.'); localPeerConnection.createOffer(offerOptions) .then(createdOffer).catch(setSessionDescriptionError);
-
如果執行成功, Alice 通過
setLocalDescription()
方法將本地會話資訊儲存, 接著通過信令通道, 將這些資訊傳送給Bob。 -
Bob使用RTCPeerConnection的
setRemoteDescription()
方法, 將Alice傳過來的遠端會話資訊填進去。 -
Bob執行RTCPeerConnection的
createAnswer()
方法, 傳入獲取到的遠端會話資訊, 然後就會生成一個和Alice適配的本地會話。createAnswer()
方法返回的 promise 會傳入一個 RTCSessionDescription 物件: Bob將它設定為本地描述, 當然也需要傳送給Alice。 -
當Alice獲取到Bob的會話描述資訊之後, 使用
setRemoteDescription()
方法將遠端會話資訊設定進去。// Logs offer creation and sets peer connection session descriptions. function createdOffer(description) { trace(`Offer from localPeerConnection:\n${description.sdp}`); trace('localPeerConnection setLocalDescription start.'); localPeerConnection.setLocalDescription(description) .then(() => { setLocalDescriptionSuccess(localPeerConnection); }).catch(setSessionDescriptionError); trace('remotePeerConnection setRemoteDescription start.'); remotePeerConnection.setRemoteDescription(description) .then(() => { setRemoteDescriptionSuccess(remotePeerConnection); }).catch(setSessionDescriptionError); trace('remotePeerConnection createAnswer start.'); remotePeerConnection.createAnswer() .then(createdAnswer) .catch(setSessionDescriptionError); } // Logs answer to offer creation and sets peer connection session descriptions. function createdAnswer(description) { trace(`Answer from remotePeerConnection:\n${description.sdp}.`); trace('remotePeerConnection setLocalDescription start.'); remotePeerConnection.setLocalDescription(description) .then(() => { setLocalDescriptionSuccess(remotePeerConnection); }).catch(setSessionDescriptionError); trace('localPeerConnection setRemoteDescription start.'); localPeerConnection.setRemoteDescription(description) .then(() => { setRemoteDescriptionSuccess(localPeerConnection); }).catch(setSessionDescriptionError); }
-
然後, 視訊會話就接通了!
練習與實踐
- 在一個新標籤頁中開啟
chrome://webrtc-internals
。 該頁面提供了 WebRTC 相關的統計資料和除錯資訊。(Chrome 相關的功能url列舉在chrome://about
之中) - 修改頁面的CSS樣式:
- 將視訊並排在一起。
- 統一按鈕的寬高, 使用更大的字號。
- 適配移動端。
- 在Chrome控制檯中(Chrome Dev Tools console), 檢視
localStream
,localPeerConnection
和remotePeerConnection
物件。 - 在控制檯中, 檢視
localPeerConnectionpc1.localDescription
。看看 SDP 格式具體是什麼樣的?
知識點回顧
在本節課程中, 我們學習了:
- 使用WebRTC相容庫: adapter.js, 來抹平各瀏覽器間的差異。
- 通過 RTCPeerConnection API 傳輸流媒體視訊。
- 控制 media 的捕捉和傳輸。
- 在兩個端點(peer)間共享 media 和網路資訊, 以接通WebRTC視訊通話。
本節的完整版程式碼位於 step-02
資料夾中。
提示
- 本節涉及的知識點很多! 關於 RTCPeerConnection 的更多資訊, 請參考 webrtc.org/start. 裡面有一些對 JavaScript 框架的建議, 如果想使用WebRTC, 也想深入瞭解API細節的話。
- 如果想要體驗當下最先進的WebRTC視訊聊天應用, 可以看看 AppRTC, 這也是WebRTC專案的標準實現: app訪問地址: https://appr.tc/, 程式碼地址 https://github.com/webrtc/apprtc。 建立通話的時間可以控制在 500 ms以內。
最佳實踐
- 想要讓程式碼跟上時代的部分, 請使用基於Promise的API, 並通過 adapter.js 來相容各種瀏覽器。
後續內容
本節演示了在兩個WebRTC端點之間傳輸視訊流 —— 後續小節將會展示如何傳輸資料!
接下來, 我們將學習 RTCDataChannel, 並用它來傳輸任意的資料內容。
翻譯日期: 2018年07月12日