ICE 架構
在建立連線之前,我們要先討論一下,peer-to-peer 連線建立上的問題,理論上來說只要電腦都有連上網路,就可以透過網路建立一條連線直接溝通,不過很多時候因為 NAT 或是防火牆等問題,會讓您無法直接建立這樣的連線,這時候可以使用 ICE 的架構來幫助我們建立一個 peer-to-peer 的連線。ICE 靠著 STUN 與 TURN 協定來處理 NAT 穿透(NAT traversal)與其他棘手的問題。
一開始 ICE 會嘗試以 UDP 的方式直接連線到另一方,在這個過程中,STUN 伺服器只有提供一個簡單的功能,就是讓在 NAT 中的 client 獲取自己本身公開的 IP 位址與連接埠。
如果 UDP 的方式失敗了,ICE 會接著嘗試以 TCP 的方式連線,先嘗試 HTTP,若不行則再嘗試 HTTPS。如果直接連線都失敗了,則改以 TURN 伺服器作為中繼站,讓所有的資料都透過 TURN 伺服器來轉送。
而這裡各種不同的連線組態(IP 位址與連接埠),就稱為 ICE candidates,當雙方要建立 peer-to-peer 連線時,就會先進行這樣的流程,找到一個可用且最好的 ICE candidate 來使用。
關於 ICE, STUN 與 TURN 可以參考 2013 Google I/O WebRTC 的解說。
信令(signaling)
WebRTC 的 RTCPeerConnection 會負責多媒體串流的傳送,不過除此之外,我們還會需要一個額外的機制來傳送一些控制與建立連線用的信令(signaling),而這個信令主要包含下面這些資訊:- 連線 session 控制資訊:用於建立與關閉連線,錯誤訊息處理。
- 網路資訊:提供對方本地端之 IP 位址與連接埠(port)。
- 多媒體格式資訊:記錄瀏覽器可使用的 codecs 與解析度等資訊。
開發者可以自由選擇傳送信令的方式與協定,例如 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 的機制,這裡有比較清楚的講解影片:
基本上在這裡我們只需要處理視訊連線的前置設定,RTCPeerConnection 會負責解決其餘的多媒體串流問題,而信令管道的實作可以參考這裡。
參考資料:HTML5 ROCKS
沒有留言:
張貼留言