WebRTC學習之一:開篇

一.無外掛的實時通訊

       想像一下,如果你的手機、電視、電腦都可以通過一個平臺進行通訊,想像一下,你可以在Web應用中輕鬆地加入視訊聊天和p2p資料分享,這就是WebRTC的願景。
       想試一試嗎?WebRTC現在已經被整合到Chrome、Opera和Firefox,在apprtc.appspot.com有個簡單的視訊聊天應用可供測試。
1.在Chrome、Opera或Firefox中開啟apprtc.appspot.com
2.點選允許按鈕允許應用啟用你的攝像頭。
3.在新的選項卡中開啟頁面底部顯示的URL,當然能在另外一臺電腦上開啟該URL會更好。

關於這個應用的具體教程詳見“一個簡單的視訊聊天客戶端”章節。

二.快速開始

       如果你沒有時間閱讀這篇文章,想直接編碼,你可以這樣:
1.看一看Gooogle關於WebRTC的幻燈片(here)。
2.你果你沒有用過getUserMedia,要先學習一下它,教程:HTML5
Rocks article
,例子:simpl.info/gum
3.掌握RTCPeerConnection API,教程:本文的程式碼段,例子:simpl.info/pc,這個例子在一個單獨的網頁中實現了WebRTC。
4.瞭解一下WebRTC信令服務、防火牆和NAT轉發,教程:apprtc.appspot.com
5.實在等不及了,可以通過這20
demos
練習WebRTC的JavaScript API。
6.如果有什麼問題,可以試試問題幫助頁面test.webrtc.org
       或者你可以直接跳到這一步:在WebRTC
codelab
上一步一步的學習如何構建一個完整的視訊聊天應用程式,包括一個簡單的信令伺服器。

三.關於WebRTC的小故事

       其實一個Web開發的終極挑戰就是通過音訊和視訊進行實時通訊,視訊通訊應該像文字通訊一樣自然,如果沒有它,我們在使用者互動方面的創新能力會受到限制。
       在過去,實時通訊都比較複雜,需要非常豐富的音訊和視訊技術才能進行開發。 完整的實現實時通訊需要整合大量的資料和服務,在Web上實現尤其困難。
       2008年,Gmail視訊聊天火了。2011年穀歌釋出了Hangouts,收購了GIPS,GIPS為RTC開發了許多元件,比如編碼和回聲消除技術。谷歌開源了GIPS的相關技術,並且與IETF和W3C等標準化組織達成了行業共識。2011年5月愛立信構建了第一個WebRTC應用。
       WebRTC目前實現了實時、無外掛的音訊、視訊和資料通訊,我們迫切需要它,因為:
1.許多web service在使用RTC,但是需要下載原生app或者外掛,比如Skype、Facebook和谷歌Hangouts。
2.下載、安裝和升級外掛非常繁瑣,而且容易出錯。
3.外掛不容易發現問題,測試很困難,大部分都需要授權,開發成本太高。
       WebRTC專案的宗旨是:API是開源、免費的、標準的、可內建於瀏覽器且比其他現存的技術更加高效。

四.WebRTC使用現狀

       目前WhatsAPP、Facebook Messenger等應用都使用了WebRTC,不僅如此WebRTC還出現在其他平臺中,比如TokBox。WebRTC可以被整合到WebKitGTK 或者Qt原生應用中。
WebRTC實現了下列三個API:

1.MediaStream
(別名getUserMedia)

2.RTCPeerConnection

3.RTCDataChannel
       getUserMedia可用於Chrome、Opera、Firefox和Edge。你可以看看這個跨瀏覽器的demo和Chris
Wilson的amazing examples,這些例子使用getUserMedia作為音訊的輸入。
       RTCPeerConnection可用於Chrome、Opera和Firefox。經過幾次迭代之後RTCPeerConnection被Chrome和Opera實現為webkitRTCPeerConnection,被Firefox實現為mozRTCPeerConnection。其他的實現已經被廢棄。當標準化程序穩定之後,這兩個實現名字的字首會被移除。Chromium的一個超級簡單的RTCPeerConnection實現在GitHub上,大量的視訊聊天應用在apprtc.appspot.com
       RTCDataChannel可用於Chrome、Opera和Firefox。在GitHub上有關於資料通道的例子,可以去實踐一下。

五.我的第一個WebRTC應用

       開發WebRTC應用需要做好下列準備:
1.獲取音視訊流或者其他資料
2.得到網路資訊,如IP地址和埠,通過網路和其它WebRTC客戶端交換資料,解決NATs/防火牆穿透問題。
3.協調信令通訊來報告錯誤、啟動或關閉會話。
4.交換媒體和客戶端資訊,比如解析度和編解碼引數。
5.傳輸音視訊流或者其他資料。
       為了實現資料流通訊,WebRTC實現了下列API:
1.MediaStream從裝置獲取資料流,比如說攝像頭和麥克風。
2.RTCPeerConnection:音視訊通話,包括裝置加密和頻寬管理。
3.RTCDataChannel:p2p通訊。

六.MediaStream (別名getUserMedia)

       MediaStream
API
代表媒體流的同步。比如,從攝像頭和麥克風獲取的媒體流具有同步視訊和音訊軌道。不要將這裡的MediaStream軌道和<track>元素混淆,它們是完全不同的概念。
理解MediaStream最簡單的方法如下:
1.在Chrome或Opera中開啟例子https://webrtc.github.io/samples/src/content/getusermedia/gum
2.開啟控制檯
3.檢查stream變數,該變數是全域性的。
       每個MediaStream都有輸入,即navigator.getUserMedia();也有輸出,被傳遞到video元素或RTCPeerConnection
getUserMedia()方法有三個引數:
1.一個約束物件。
2.一個成功的回撥,如果成功會回傳一個MediaStream。
3.一個失敗的回撥,如果失敗會回傳一個error物件。
       每個MediaStream都有一個label,比如’Xk7EuLhsuHKbnjLWkW4yYGNJJ8ONsgwHBvLQ’,getAudioTradks()和getAudioTracks()方法會回傳一個MediaStreamTracks物件的陣列。
在例子https://webrtc.github.io/samples/src/content/getusermedia/gum中,stream.getAudioTracks()回傳了一個空陣列(因為沒有音訊),假設攝像頭正常工作並連線,stream.getVideoTracks()回傳一個MediaStreamTracks物件的陣列。陣列中的每個MediaStreamTracks物件包含一種媒體(‘video’或‘audio’)和一個label(比如’FaceTime
HD Camera (Built-in)’),而且還代表了一個或多個音視訊的資料通道。在這個例子中,只有一個視訊軌道,沒有音訊。當然,很容易就能擴充套件到其他情況。
       在Chrome或Opera中, URL.createObjectURL()方法將MediaStream轉換成Blob
URL
,該Blob URL可以設定為video元素的輸入(在Firefox和Opera中,視訊源可以通過資料流本身設定)。版本M25開始,基於Chromium的瀏覽器(Chrome和Opera)允許來自getUserMedia的音訊資料傳遞到aduio或video元素。
       getUserMedia還可用作Web Audio API的輸入節點

function gotStream(stream) {
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var audioContext = new AudioContext();
// Create an AudioNode from the stream
var mediaStreamSource = audioContext.createMediaStreamSource(stream);
// Connect it to destination to hear yourself
// or any other node for processing!
mediaStreamSource.connect(audioContext.destination);
}
navigator.getUserMedia({audio:true}, gotStream);

       在manifest中新增audioCapture和videoCapture許可權可以在載入的時候得到(僅一次)授權,畢竟載入之後使用者不會再有對攝像頭或麥克風的訪問請求。
       最終的目的是使MediaStream適用於任何資料來源,不僅限於攝像頭和麥克風,還包括來自磁碟或者感測器等輸入裝置二進位制資料。
       需要注意的是getUserMedia()必須在伺服器上使用,而不是本地檔案中,否則的話將會丟擲許可權的錯誤PERMISSION_DENIED。
       getUserMedia()通常和其他的JavaScript API及庫一起使用:
Webcam Toy是一個photobooth應用,它使用WebGL來新增一些特效,讓使用者可以共享照片或是儲存到本地。
FaceKat是一個人臉追蹤的遊戲,使用headtrackr.js。

ASCII
Camera
使用Canvas API來生成ASCII碼的圖片。

七.約束

       Constraints已經在Chrome、FireFox和Opera中實現了。通過約束可以設定getUserMedia()和RTCPeerConnection的addStream()獲取視訊的解析度,約束的實現是為了通過applyConstraints()方法控制視訊高度和寬度的比例、幀率、和正反攝像頭模式等等……
       這裡有一個例子:https://webrtc.github.io/samples/src/content/getusermedia/resolution/
       一個陷阱:getUserMedia約束設定在瀏覽器的一個標籤中,會約束之後開啟的所有標籤。設定一個非法的值會提示以下錯誤:

navigator.getUserMedia error:
NavigatorUserMediaError {code: 1, PERMISSION_DENIED: 1}

八.螢幕和標籤捕獲

       Chrome應用可以通過chrome.tabCapture和chrome.desktopCapture這兩個API實時分享瀏覽器標籤或者整個桌面。桌面捕獲的例子:WebRTC
samples GitHub repository
。更多關於螢幕錄製、編碼的資訊和有參考:Screensharing
with WebRTC

       在Chrome中,可以將螢幕捕獲當做MediaStream的資料來源,此時使用的是實驗性的chromeMediaSource約束,一個例子:this
demo
。需要注意的是螢幕捕獲功能需要HTTPS支援,並且只用於開發中,通過一個命令列標誌來啟用。

九.信令:會話控制,網路和媒體資訊

       WebRTC使用RTCPeerConnection在瀏覽器(別名peer)之間互通資料流,但是需要一種機制去協調通訊或者傳送控制訊息,這個過程被稱為信令。WebRTC沒有指定信令方法和協議,信令不是RTCPeerConnection API的一部分。
       因此,WebRTC應用的開發者可用選擇其擅長的訊息協議,比如SIP或XMPP,或者其他合適的雙工通訊協議。
apprtc.appspot.com這個例子使用XHR和Channel
API作為信令機制。codelab是我們通過Socket.io構建,執行在Node
server
上的應用。
       信令通常用於互動三類資訊:
1.會話控制訊息;初始化或者關閉通訊,報告錯誤。
2.網路資訊:對於外部而言,我的IP地址和埠是什麼?
3.媒體資訊:什麼編碼和解析度瀏覽器可以處理,我的瀏覽器要和誰通訊。
       在p2p的流傳輸之前,必須通過信令成功的交換資訊。
       假如Alice想和Bob通訊,這裡有個簡單的例子來自WebRTC
W3C Working Draft
,展示了實際的信令處理過程。例子中假設存在某種信令機制,該機制通過createSignalingChannel()方法建立。注意在Chrome和Opera中,RTCPeerConnection是帶有字首的。

var signalingChannel = createSignalingChannel();
var pc;
var configuration = ...;
// run start(true) to initiate a call
function start(isCaller) {
pc = new RTCPeerConnection(configuration);
// send any ice candidates to the other peer
pc.onicecandidate = function (evt) {
signalingChannel.send(JSON.stringify({ "candidate": evt.candidate }));
};
// once remote stream arrives, show it in the remote video element
pc.onaddstream = function (evt) {
remoteView.src = URL.createObjectURL(evt.stream);
};
// get the local stream, show it in the local video element and send it
navigator.getUserMedia({ "audio": true, "video": true }, function (stream) {
selfView.src = URL.createObjectURL(stream);
pc.addStream(stream);
if (isCaller)
pc.createOffer(gotDescription);
else
pc.createAnswer(pc.remoteDescription, gotDescription);
function gotDescription(desc) {
pc.setLocalDescription(desc);
signalingChannel.send(JSON.stringify({ "sdp": desc }));
}
});
}
signalingChannel.onmessage = function (evt) {
if (!pc)
start(false);
var signal = JSON.parse(evt.data);
if (signal.sdp)
pc.setRemoteDescription(new RTCSessionDescription(signal.sdp));
else
pc.addIceCandidate(new RTCIceCandidate(signal.candidate));
};

       首先,Alice和Bob交換網路資訊,‘finding candidates’表示通過ICE
framework
查詢網路介面和埠。
1.Alice建立一個RTCPeerConnection物件,該物件內建onicecandidate處理器。
2.這個處理器在網路candidate生效時開始執行。
3.Alice通過信令通道傳送序列化的資料給Bob,信令通道可以是WebSocket或者其他機制。
4.當Bob收到Alice的candidate訊息後,呼叫addIceCandidate將candidate新增到遠端描述。
        WebRTC客戶端(別名peer,這裡指Alice和Bob)需要明確並交換本地和遠端音視訊媒體資訊,比如解析度和編碼格式。交換媒體資訊的信令,是通過交換會話描述協議(SDP)來實現的。
1.Alice呼叫了RTCPeerConnection的createOffer()方法,它的回撥引數傳入的是RTCSessionDescription(Alice的本地會話描述)。
2.在回撥中,Alice呼叫setLocalDescription()方法設定了本地會話描述,然後將該會話描述通過信令通道傳送給Bob。注意,RTCPeerConnection並不會採集candidate直到setLocalDescription()被呼叫。
3.Bob使用setRemoteDescription()方法將Alice傳送給他的會話描述設定為遠端會話描述。
4.Bob呼叫了RTCPeerConnection的createAnswer()方法,並傳入它從Alice接收到的遠端會話描述,此時一個與Alice相容的本地會話產生了。createAnswer()的回撥引數傳入的是RTCSessionDescription(Bob將它設定為本地會話描述併傳送給Alice)。
5.當Alice收到Bob的會話描述,她使用setRemoteDescription()方法將其設定為遠端會話描述。
6.Ping
       RTCSessionDescription物件遵從SDP(Session Description Protocol),一個SDP物件看起來如下所示:

v=0
o=- 3883943731 1 IN IP4 127.0.0.1
s=
t=0 0
a=group:BUNDLE audio video
m=audio 1 RTP/SAVPF 103 104 0 8 106 105 13 126
// ...
a=ssrc:2223794119 label:H4fjnMzxy3dPIgQ7HxuCTLb4wLLLeRHnFxh810

       交換網路和媒體資訊可以同時進行,但這兩個過程必須在音視訊流開始傳輸之前完成。

      上述的offer/answer架構被稱為JSEP(JavaScript
Session Establishment Protocol),JSEP架構如下所示:

      一旦信令過程成功,就可以直接進行Caller和callee之間p2p的資料流傳輸了。

十.RTCPeerConnection

RTCPeerConnection是WebRTC的元件,用來穩定高效的處理端對端的資料流通訊。

下圖是WebRTC的架構圖,標明瞭RTCPeerConnection扮演的角色。你可能注意到了,綠色部分是相當複雜的。

       從JavaScript的角度來看,理解這個圖最重要的是理解RTCpeerConnection這一部分。WebRTC對編解碼器和協議做了大量的工作,使實時通訊成為可能,甚至在一些不可靠的網路中:
1.包補償
2.回聲消除
3.自適應頻寬
4.視訊抖動緩衝
5.自動增益控制
6.噪聲抑制
7.影象清除
       章節九中的例子從信令的角度進行了講解,下面我們將學習兩個WebRTC應用;一個簡單的演示了RTCPeerConnection,另一個是功能齊全的視訊聊天客戶端。

十一.無伺服器的RTCPeerConnection

       下面的程式碼來自https://webrtc.github.io/samples/src/content/peerconnection/pc1,包含基於網頁的本地和遠端RTCPeerConnection。這個例子中caller和callee在同一個網頁中,能更加清晰的展示RTCPeerConnection
API的工作流程,因為RTCPeerConnection物件之間可以直接交換資料和訊息,不需要通過中繼通道機制。
       一個陷阱:RTCPeerConnection()第二個約束型別的引數是可選的,它與getUserMedia()中使用的約束型別不同。
       本例中pc1表示本地端(caller),pc2表示遠端端(callee)
caller
1.建立一個RTCPeerConnection,並通過getUserMedia()新增資料流。

// servers is an optional config file (see TURN and STUN discussion below)
pc1 = new webkitRTCPeerConnection(servers);
// ...
pc1.addStream(localStream); 

2.建立一個offer,並將它設定為pc1的本地會話描述,設定為pc2的遠端會話描述。可以直接在程式碼中設定,不需要使用信令,因為caller和callee在同一個網頁中。

pc1.createOffer(gotDescription1);
//...
function gotDescription1(desc){
pc1.setLocalDescription(desc);
trace("Offer from pc1 \n"   desc.sdp);
pc2.setRemoteDescription(desc);
pc2.createAnswer(gotDescription2);
}

callee
1.建立pc2,接收pc1的資料流,並顯示到video元素中

pc2 = new webkitRTCPeerConnection(servers);
pc2.onaddstream = gotRemoteStream;
//...
function gotRemoteStream(e){
vid2.src = URL.createObjectURL(e.stream);
}

十二.有伺服器的RTCPeerConnection

       實際應用中,WebRTC需要伺服器,無論多簡單,下面四步是必須的:
1.使用者通過交換名字之類的資訊發現對方。
2.WebRTC客戶端應用交換網路資訊。
3.客戶端交換媒體資訊包括視訊格式和解析度。
4.WebRTC客戶端穿透NAT閘道器和伺服器。
       換句話說,WebRTC需要四種型別的服務端功能。
1.使用者發現和通訊
2.信令
3.NAT/防火牆穿透
4.中繼伺服器,防止端到端的通訊失敗
       以上這些不在本文討論範圍之內。可以說基於STUNTURN協議的ICE框架,使得RTCPeerConnection處理NAT穿透和其他網路難題成為可能。
       ICE框架用於端到端的連線,比如說兩個視訊聊天客戶端。起初,ICE嘗試通過UDP直接連線兩端,這樣可以保證低延遲。在這個過程中,STUN伺服器有一個簡單的任務:使NAT後邊的端能找到它的公網地址和埠(谷歌有多個STUN伺服器,其中一個用在了apprtc.appspot.com例子)。

       如果UDP傳輸失敗,ICE會嘗試TCP:首先是HTTP,然後才會選擇 HTTPS。如果直接連線失敗,通常因為企業的NAT穿透和防火牆,此時ICE使用中繼(Relay)伺服器。換句話說,ICE首先使用STUN和UDP直接連線兩端,失敗之後返回中繼伺服器。‘finding cadidates’就是尋找網路介面和埠的過程。

       WebRTC工程師Justin Uberti在幻燈片2013
Google I/O WebRTC presentation
中提供了許多關於ICE、STUN和TURN的資訊。

十三.一個簡單的視訊聊天客戶端

       如果你覺得這個例子比較難,你也行會喜歡上我們的WebRTC
codelab
。那裡一步步的介紹瞭如何建立一個完整的視訊聊天應用,包括一個執行於Node
server
上基於Socket.io的信令伺服器。
       apprtc.appspot.com是一個測試WebRTC的好地方,裡面有視訊聊天的例子,它實現了信令和基於STUN伺服器的NAT/防火牆穿透。這個例子使用adapter.js處理不同的RTCPeerConnection和getUserMedia()實現。

       下面我們詳細的過一遍程式碼。

如何開始

這個例子從initialize()函式開始執行。

function initialize() {
console.log("Initializing; room=99688636.");
card = document.getElementById("card");
localVideo = document.getElementById("localVideo");
miniVideo = document.getElementById("miniVideo");
remoteVideo = document.getElementById("remoteVideo");
resetStatus();
openChannel('AHRlWrqvgCpvbd9B-Gl5vZ2F1BlpwFv0xBUwRgLF/* ...*/');
doGetUserMedia();
}

       需要注意的是,變數room和openChannel()引數的值都是由Google App Engine應用自身提供的。檢視一下index.html
template
 就知道該賦什麼值了。
       這段程式碼初始化HTML video元素的相關變數,video元素播放來自本地攝像頭(localVieo)和遠端攝像頭(remoteVideo)的視訊流。resetStatus()設定了一條狀態訊息。
       openChannel()函式建立了WebRTC客戶端間的訊息通道。

function openChannel(channelToken) {
console.log("Opening channel.");
var channel = new goog.appengine.Channel(channelToken);
var handler = {
'onopen': onChannelOpened,
'onmessage': onChannelMessage,
'onerror': onChannelError,
'onclose': onChannelClosed
};
socket = channel.open(handler);
}

關於信令,本例使用的是Google App Engine
Channel API
,這使得JavaScritp客戶端無需輪詢就能實現訊息傳輸。

       使用Channel API建立通道的流程大致如下:
1.客戶端A生成一個唯一ID。
2.客戶端A向Google App Engine應用請求一個通道標識(即openChannel()的引數),並將它的ID傳給Google App Engine應用。
3.Google App Engine應用會呼叫Channel API為客戶端ID分配一個通道和一個通道標識。
4.Google App Engine應用將通道標識發給客戶端A。

5.客戶端A開啟socket並監聽伺服器上建立的通道。

       傳送訊息的流程大致如下:
1.客戶端B給Google App Engine應用傳送了一個POST請求,要求升級程式。
2.Google App Engine應用給通道傳送一個請求訊息。
3.訊息經通道傳遞給客戶端A

4.客戶端A的onmessage回撥函式被呼叫。

       重申一次,信令傳輸機制是由開發者選擇的。WebRTC並沒有指定信令機制。本例的Channel API能被其他的方式取代,比如WebSocket。
       initialize()呼叫完openChannel()之後,緊接著呼叫getUserMedia(),這個函式可以檢測出瀏覽器是否支援getUserMedia API。如果一切順利,onUserMediaSuccess會被呼叫。

function onUserMediaSuccess(stream) {
console.log("User has granted access to local media.");
// Call the polyfill wrapper to attach the media stream to this element.
attachMediaStream(localVideo, stream);
localVideo.style.opacity = 1;
localStream = stream;
// Caller creates PeerConnection.
if (initiator) maybeStart();
}

       這樣一來,本地攝像頭就能顯示在localVideo元素中了。
       此時,initiator被設定成1(直到caller的會話終止),maybeStart()被呼叫。

function maybeStart() {
if (!started && localStream && channelReady) {
// ...
createPeerConnection();
// ...
pc.addStream(localStream);
started = true;
// Caller initiates offer to peer.
if (initiator)
doCall();
}
}

       該函式使用了一種巧妙的結構,可以工作於多個非同步回撥:maybeStart()可能被任何函式呼叫,但是隻有當localStream被定義、channelReady為true且通訊還未開始的情況下,maybeStart()才會執行。因此,當連線還未建立,本地流已經可用,且信令通道已經準備好時,連線才會建立並載入本地視訊流。接著started被設定為true。所以連線不會被建立多次

RTCPeerConnection: 發起通話
       在maybeStart()中被呼叫的createPeerConnection(),才是關鍵所在。

function createPeerConnection() {
var pc_config = {"iceServers": [{"url": "stun:stun.l.google.com:19302"}]};
try {
// Create an RTCPeerConnection via the polyfill (adapter.js).
pc = new RTCPeerConnection(pc_config);
pc.onicecandidate = onIceCandidate;
console.log("Created RTCPeerConnnection with config:\n"   "  \""  
JSON.stringify(pc_config)   "\".");
} catch (e) {
console.log("Failed to create PeerConnection, exception: "   e.message);
alert("Cannot create RTCPeerConnection object; WebRTC is not supported by this browser.");
return;
}
pc.onconnecting = onSessionConnecting;
pc.onopen = onSessionOpened;
pc.onaddstream = onRemoteStreamAdded;
pc.onremovestream = onRemoteStreamRemoved;
}

       這段程式碼的目的是使用STUN伺服器建立一個連線,並將onIceCandidate()作為回撥函式。然後給RTCPeerConnection每個事件指定處理器(函式):當會話連線或開啟,當遠端流被載入或移除。在本例中,這些處理器只是記錄了狀態訊息——除了onRemoteStreamAdded(),它給remoteVideo元素設定了資料來源。

function onRemoteStreamAdded(event) {
// ...
miniVideo.src = localVideo.src;
attachMediaStream(remoteVideo, event.stream);
remoteStream = event.stream;
waitForRemoteVideo();
}

       一旦createPeerConnection()在maybeStart()中被呼叫,就會發起通話,建立Offer併傳送訊息給對端。

function doCall() {
console.log("Sending offer to peer.");
pc.createOffer(setLocalAndSendMessage, null, mediaConstraints);
}

       這裡的offer建立過程類似於上面無信令的例子。但是,除此之外,一條訊息被髮送到了對端,詳見setLocalAndSendMessage():

function setLocalAndSendMessage(sessionDescription) {
// Set Opus as the preferred codec in SDP if Opus is present.
sessionDescription.sdp = preferOpus(sessionDescription.sdp);
pc.setLocalDescription(sessionDescription);
sendMessage(sessionDescription);
}

用Channel API傳輸信令
        當RTCPeerConnection在createPeerConnection()中成功建立的時候,onIceCandidate()回撥函式會觸發,併傳送關於candidate的資訊。

function onIceCandidate(event) {
if (event.candidate) {
sendMessage({type: 'candidate',
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate});
} else {
console.log("End of candidates.");
}
}

        從客戶端到伺服器的訊息外傳,是通過sendMessage()方法內的XHR請求實現的。

function sendMessage(message) {
var msgString = JSON.stringify(message);
console.log('C->S: '   msgString);
path = '/message?r=99688636'   '&u=92246248';
var xhr = new XMLHttpRequest();
xhr.open('POST', path, true);
xhr.send(msgString);
}

        XHR多用於從客戶端傳送信令訊息到服務端,但是某些機制需要用來實現服務端到客戶端的訊息傳輸:本例用的是Google App Engine Channel API。來自此API的訊息會傳遞到processSignalingMessage():

function processSignalingMessage(message) {
var msg = JSON.parse(message);
if (msg.type === 'offer') {
// Callee creates PeerConnection
if (!initiator && !started)
maybeStart();
pc.setRemoteDescription(new RTCSessionDescription(msg));
doAnswer();
} else if (msg.type === 'answer' && started) {
pc.setRemoteDescription(new RTCSessionDescription(msg));
} else if (msg.type === 'candidate' && started) {
var candidate = new RTCIceCandidate({sdpMLineIndex:msg.label,
candidate:msg.candidate});
pc.addIceCandidate(candidate);
} else if (msg.type === 'bye' && started) {
onRemoteHangup();
}
}

       如果訊息是來自對端的answer(offer的迴應),RTCPeerConnection設定遠端會話描述,通訊開始。如果訊息是offer(來自callee),RTCPeerConnection設定遠端會話描述,傳送answer給callee,然後呼叫RTCPeerConnection的startIce()方法發起連線。

function doAnswer() {
console.log("Sending answer to peer.");
pc.createAnswer(setLocalAndSendMessage, null, mediaConstraints);
}

       於是乎,caller和callee都發現了對方並交換相關資訊,會話被初始化,實時資料通訊可以開始了。
網路技術

       WebRTC目前只實現了一對一的通訊,但是可用於更復雜的網路環境:比如,多個peer各自直接通訊,即p2p;或者通過MCU(Multipoint
Control Unit
)伺服器來實現流的轉發、合成或音視訊的錄製。

       許多WebRTC應用只演示了瀏覽器間的通訊,但是通過閘道器伺服器可以實現WebRTC與telephones(別名PSTN)和VOIP系統直接的通訊。2012年5月,Doubango
Telecom開源了sipml5 SIP client,該客戶端基於WebRTC和WebSocket,能實現瀏覽器和IOS或Android應用之間的視訊通話。

十四.RTCDataChannel

       除了音訊和視訊,WebRTC支援其他型別資料的實時通訊。
       TCDataChannel API支援p2p低延遲和高吞吐量的二進位制資料流交換,這裡有個例子:http://webrtc.github.io/samples/src/content/datachannel/datatransfer
       很多領域都潛在地使用到了這個API,比如:
1.遊戲
2.遠端桌面應用
3.實時文字聊天
4.檔案傳輸
5.分散網路
       充分利用了RTCPeerConnection的多個特性,能實現強大而靈活的p2p通訊。
1.利用RTCPeerConnection進行會話設定。
2.通過優先順序設定多個同步的channel。
3.可靠和非可靠的語義傳遞。
4.內建立安全的DTLS和擁塞控制。
5.能用於音視訊或其他方面
TCDataChannel API語法與WebSocket類似,包括send()方法和message事件。

var pc = new webkitRTCPeerConnection(servers,
{optional: [{RtpDataChannels: true}]});
pc.ondatachannel = function(event) {
receiveChannel = event.channel;
receiveChannel.onmessage = function(event){
document.querySelector("div#receive").innerHTML = event.data;
};
};
sendChannel = pc.createDataChannel("sendDataChannel", {reliable: false});
document.querySelector("button#send").onclick = function (){
var data = document.querySelector("textarea#send").value;
sendChannel.send(data);
};

       因為是瀏覽器間的直接通訊,所以RTCDataChannel要比WebSocket快得多,即使通訊用到了中繼伺服器。
       RTCDataChannel可用於Chrome、Opera和Firefox。出色的Cube
Slam
遊戲使用TCDataChannel API來交換遊戲狀態:是敵還是友!Sharefest演示了通過RTCDataChannel分享檔案,peerCDN提供了WebRTC如何實現p2p內容分發的一種思路。

       更多關於RTCDataChannel的資訊,可以參考IETF的draft
protocol spec

十五.安全

       實時通訊應用或外掛會在許多方面忽視了安全性:

1.瀏覽器之間、瀏覽器與伺服器之間的音視訊或其他資料沒有加密。
2.應用在使用者沒有察覺的情況下錄製和分發音視訊。
3.惡意軟體或病毒可能入侵了正常的外掛或應用。
       WebRTC的許多特性可以避免這些問題:
1.WebRTC採用類似DTLSSRTP的安全協議。
2.所有的WebRTC元件強制加密,包括信令機制。
3.WebRTC不是外掛:它的元件執行於瀏覽器沙盒,不是獨立的一個程序,這些元件不需要單獨安裝,且隨著瀏覽器更新。
4.攝像頭和麥克風的訪問必須經過明確准許,當攝像頭和麥克風執行時,介面上會清楚的顯示出來。

       關於流媒體安全的討論超出了本文的範疇。更多資訊可參考IETF的WebRTC
Security Architecture

原文連結:https://www.html5rocks.com/en/tutorials/webrtc/basics/