上一篇文章中我們已經可以使用 MediaStream
擷取本地端的多媒體串流,現在我們要利用 RTCPeerConnection
建立連線,準備將串流傳送出去。
在建立連線之前,我們要先討論一下,peer-to-peer 連線建立上的問題,理論上來說只要電腦都有連上網路,就可以透過網路建立一條連線直接溝通,不過很多時候因為 NAT 或是防火牆等問題,會讓您無法直接建立這樣的連線,這時候可以使用 ICE 的架構來幫助我們建立一個 peer-to-peer 的連線。
ICE 靠著 STUN 與 TURN 協定來處理 NAT 穿透(NAT traversal)與其他棘手的問題。
如果 UDP 的方式失敗了,ICE 會接著嘗試以 TCP 的方式連線,先嘗試 HTTP,若不行則再嘗試 HTTPS。如果直接連線都失敗了,則改以 TURN 伺服器作為中繼站,讓所有的資料都透過 TURN 伺服器來轉送。
而這裡各種不同的連線組態(IP 位址與連接埠),就稱為 ICE candidates,當雙方要建立 peer-to-peer 連線時,就會先進行這樣的流程,找到一個可用且最好的 ICE candidate 來使用。
關於 ICE, STUN 與 TURN 可以參考 2013 Google I/O WebRTC 的解說。
WebRTC 的 RTCPeerConnection
會負責多媒體串流的傳送,不過除此之外,我們還會需要一個額外的機制來傳送一些控制與建立連線用的信令(signaling),而這個信令主要包含下面這些資訊:
在真正的視訊連線要建立之前,都一定要使用這樣的信令機制讓兩方進行一些必要的設定,接著才能建立起視訊連線,然而傳送信令的方式與協定並沒有定義在 RTCPeerConnection
API 當中,所以我們要另外找一個方式來負責信令的傳送。
開發者可以自由選擇傳送信令的方式與協定,例如 SIP、XMPP 或任何可以進行雙向溝通的協定都可以,apprtc.appspot.com 是利用 XHR 與 Channel API 來傳送,codelab 則是使用 Node.js 的 Socket.io 來傳送信令。
這裡我們使用 WebRTC W3C Working Draft 的範例來解說,這裡已經假設信令的傳送機制已經有了(透過 SignalingChannel()
建立)。
var signalingChannel = new SignalingChannel(); var configuration = { "iceServers": [{ "url": "stun:stun.example.org" }] }; var pc; // 呼叫 start() 開始建立連線 function start() { pc = new RTCPeerConnection(configuration); // 當有任何 ICE candidates 可用時, // 透過 signalingChannel 將 candidate 傳送給對方 pc.onicecandidate = function (evt) { if (evt.candidate) signalingChannel.send(JSON.stringify({ "candidate": evt.candidate })); }; // let the "negotiationneeded" event trigger offer generation pc.onnegotiationneeded = function () { pc.createOffer(localDescCreated, logError); } // once remote stream arrives, show it in the remote video element pc.onaddstream = function (evt) { remoteView.src = URL.createObjectURL(evt.stream); }; // get a local stream, show it in a self-view and add it to be sent navigator.getUserMedia({ "audio": true, "video": true }, function (stream) { selfView.src = URL.createObjectURL(stream); pc.addStream(stream); }, logError); } function localDescCreated(desc) { pc.setLocalDescription(desc, function () { signalingChannel.send(JSON.stringify({ "sdp": pc.localDescription })); }, logError); } signalingChannel.onmessage = function (evt) { if (!pc) start(); var message = JSON.parse(evt.data); if (message.sdp) { pc.setRemoteDescription(new RTCSessionDescription(message.sdp), function () { // 當接收到 offer 時,要回應一個 answer if (pc.remoteDescription.type == "offer") pc.createAnswer(localDescCreated, logError); }, logError); } else { // 接收對方的 candidate 並加入自己的 RTCPeerConnection pc.addIceCandidate(new RTCIceCandidate(message.candidate)); } }; function logError(error) { log(error.name + ": " + error.message); }
在開始建立連線時,我們要先呼叫上面定義的 start()
函數,首先建立 RTCPeerConnection
物件,接著進行以下的步驟:
設定 RTCPeerConnection
物件的 onicecandidate
handler,讓它可以在取得 ICE candidates 時可以直接透過 signalingChannel
傳送給對方。
傳送的機制就依照上面 SignalingChannel()
的實作方式而定,例如 WebSocket 等。
而在對方接到 candidate 的資訊時,會呼叫 addIceCandidate
把這個 candidate 加入它的 RTCPeerConnection
中。
使用 Session Description Protocol(SDP)協定的 offer 與 answer 來交換多媒體相關的資訊(例如解析度與 codec 等),傳輸的通道同樣是使用上面建立的信令傳輸管道。
首先呼叫 createOffer()
並傳入兩個回呼函數,它會建立一個 RTCSessionDescription
物件(包含了多媒體相關的設定資訊),當這個物件建立時會直接傳入第一個回呼函數中(localDescCreated()
),而其第二個回呼函數只是單純的輸出錯誤訊息而已。
當 RTCSessionDescription
物件建立好的時候,我們在 localDescCreated()
函數中透過 setLocalDescription() 將該物件設定為 local description,再將其傳送給對方。
對方接收到這個 description 資料之後,透過 new RTCSessionDescription(message.sdp)
重建 RTCSessionDescription
物件,並呼叫 setRemoteDescription()
設定 remote description。
當對方接收到 offer 的資料,必須回傳一個 answer 作為回應,而其建立與傳送的過程與 offer 大同小異,只不過是從遠端傳回本地端而已。
當本地端接收到 answer 的回應時,同樣呼叫 setRemoteDescription()
設定 remote description,到這裡整個連線的前置作業就完成了。
網路與多媒體的資訊交換可以同時進行,只是這兩種資訊都要在真正建立視訊連線之前完成。
上述 offer 與 answer 的交換機制稱為 JavaScript Session Establishment Protocol(JSEP),當雙方都透過這樣的方式取得對方的資訊之後,就可以開始傳送多媒體的串流,進行視訊連線了。
關於 JSEP 的機制,這裡有比較清楚的講解影片:
參考資料:HTML5 ROCKS