第一個WebRPC應用
WebRPC 需要做以下的幾件事:
- 獲取音頻,視頻或者其他數據
- 獲取網絡信息比如IP地址,端口,並與其他的WebRTC客戶端進行交換,穿過NAT合防火牆進行連接.
- 處理信號以便發起請求報告錯誤或者關閉會話
- 交換客戶端支持的媒體信息,比如分辨率,解碼器
- 傳輸音頻視頻流或者數據
爲了獲得流數據的 WebRTC實現了以下API
MediaStream
獲取數據流,比如從用戶的攝像頭或者麥克風RPCPeerConnection
音頻或者視頻的調用,具有加密和帶寬管理RTCDataChannel
點到點的通用數據傳輸.
MediaStream(即:getUserMedia)
MediaStream用來表示同步的媒體流,例如:從攝像頭和麥克風獲取的輸入包含了視頻合音頻流.(MediaStreamTrack和<track>
元素是完全不同的,不要混淆)
爲了更好的理解 MediaStream
你可以打開DEMO , webrtc.github.io/samples/src/content/getusermedia/gum..在JS控制檯檢查 stream
變量.
每一個MediaStram
對象都有一個輸入,可能是由getUserMedia()
生成的MediaStream
,也有一個輸出,輸出可能是一個video
對象或者一個RTCPeerConnection
.
每個MediaStream
都有一個標籤,比如 Xk7EuLhsuHKbnjLWkW4yYGNJJ8ONsgwHBvLQ
(通過 stream.id
可以獲取).getAudioTracks()
和getVideoTracks()
方法會返回一個 MediaStreamTrack
數組.
剛纔的示例中 stream.getAudioTracks()
返回了一個空數組,因爲我們沒有獲取音頻.如果連接了攝像頭 stream.getVideoTracks()
會返回包含一個 MediaStreamTrack
元素的數組.MediaStreamTrack
的 kind
屬性 (值 video
或者audio
)來標識媒體的類型.以及一個label
, (比如 FaceTime HD Camera (05ac:8514)
) 來對一個或者多個的音視頻頻道進行描述.當前例子我們只有個視頻頻道沒有音頻頻道.但是我們想象出現多個頻道的情況,比如: 使用前置攝像頭,後置攝像頭以及麥克風和屏幕分享應用.
MediaStream
可以通過設置 srcObject
屬性來綁定到video
.
MediaStramTrack
會激活攝像頭,如果不用了要使用track.stop()
來進行關閉.
getUserMedia
也可以作爲 Web Audio API
的輸入節點.
navigator.mediaDevices.getUserMedia({audio: true}, (stream) => {
// Create an AudioNode from the stream
const mediaStreamSource =
audioContext.createMediaStreamSource(stream);
mediaStreamSource.connect(filterNode);
filterNode.connect(gainNode);
// connect the gain node to the destination (i.e. play the sound)
gainNode.connect(audioContext.destination);
});
基於Chromium的應用程序合擴展可以將audioCapture
和videoCapture
權限加入到manifest文件,這樣就只會在安裝的時候要求用戶授權.使用的時候就不用再點擊進行授權了.
Constraints 約束
約束用來設置getUserMedia()
的分辨率.還支持更多約束設置,比如 縱橫比,前後攝像頭,幀率,高度,寬度.以及 applyConstraints()
方法.
有一個問題,getUserMedia
的約束參數會影響共享的資源的配置.比如:一個攝像頭在一個頁卡中以640X480的分辨率打開,那麼他就不能在另外的頁卡以更高的分辨率打開.因爲他只能以一種模式打開.
設置一個不允許的約束值會拋出一個DOMException
或者 如果設置了一個不支持的分辨率會拋出一個 OverconstrainedError
.查看DEMO
屏幕和頁卡捕捉
Chrome 應用可以通過 chrome.tabCapture
和 chrome.desktopCapture
API 來進行實時的桌面分享. 也可以使用Chrome的實驗性API chromeMediaSource
約束來獲取.注意分享屏幕需要HTTPS連接.
2 使用RTCPeerConnection
建立連接
信號 會話控制,網絡和媒體信息
WebRTC使用RTCPeerConnection
在瀏覽器之間傳輸流數據.需要一個機制來進行傳輸的協調和控制消息的發送,這個過程叫做信號處理.信號處理的方法和協議未包含在WebRTC中.
WebRTC應用的開發者可以選擇自己喜歡的消息協議,比如 SIP 或者XMPP,任何適合的雙向通行信道.appr.tc的示例使用了XHR和Channel API 作爲信令機制.Codelab使用 Node運行的Socket.io 庫來做.
信號用於交換以下三類信息:
- 會話控制消息:用來初始化或者關閉通訊和報告錯誤
- 網絡配置:我面向外部世界的IP地址合端口
- 媒體能力:什麼樣的解碼器合分辨率可以被我的瀏覽器支持和瀏覽器想要什麼樣的數據.
在p2p流開始之前我們必須成功通過信號交換這些信息.
例如: 假定Alice想與Bob進行通信.下面是一些代碼示例.展示了信號處理的動作.代碼架設已經有一些信號處理機制.通過createSignalingChannel()
方法進行創建.注意在Chrome合Opera中RTCPeerConnection
已經存在了.
// handles JSON.stringify/parse
const signaling = new SignalingChannel();
const constraints = {audio: true, video: true};
const configuration = {iceServers: [{urls: 'stuns:stun.example.org'}]};
const pc = new RTCPeerConnection(configuration);
// send any ice candidates to the other peer
pc.onicecandidate = ({candidate}) => signaling.send({candidate});
// let the "negotiationneeded" event trigger offer generation
pc.onnegotiationneeded = async () => {
try {
await pc.setLocalDescription(await pc.createOffer());
// send the offer to the other peer
signaling.send({desc: pc.localDescription});
} catch (err) {
console.error(err);
}
};
// once remote track media arrives, show it in remote video element
pc.ontrack = (event) => {
// don't set srcObject again if it is already set.
if (remoteView.srcObject) return;
remoteView.srcObject = event.streams[0];
};
// call start() to initiate
async function start() {
try {
// get local stream, show it in self-view and add it to be sent
const stream =
await navigator.mediaDevices.getUserMedia(constraints);
stream.getTracks().forEach((track) =>
pc.addTrack(track, stream));
selfView.srcObject = stream;
} catch (err) {
console.error(err);
}
}
signaling.onmessage = async ({desc, candidate}) => {
try {
if (desc) {
// if we get an offer, we need to reply with an answer
if (desc.type === 'offer') {
await pc.setRemoteDescription(desc);
const stream =
await navigator.mediaDevices.getUserMedia(constraints);
stream.getTracks().forEach((track) =>
pc.addTrack(track, stream));
await pc.setLocalDescription(await pc.createAnswer());
signaling.send({desc: pc.localDescription});
} else if (desc.type === 'answer') {
await pc.setRemoteDescription(desc);
} else {
console.log('Unsupported SDP type.');
}
} else if (candidate) {
await pc.addIceCandidate(candidate);
}
} catch (err) {
console.error(err);
}
};
首先,Alice合Bob交換了網絡信息,使用 ICE框架查找網絡接和端口.
- Alice使用
onicecandidate
回調,創建了一個RTCPeerConnection
對象. - 這個回調會在收到網絡候選信息後會被調用
- Alice發送序列號之後的網絡候選信息給Bob,通過信號信道, 比如 WebSocket或者其他的
- 當Bob獲取到網絡候選信息之後,他調用
addIceCandidate
來添加遠端的描述.
WebRTC 客戶端(比如:Bob Alice) 也需要探明和交換本地以及遠端的音頻合視頻信息.比如分辨力合解碼能力.信令通過使用會話描述協議(SDP)交換和應答Offer來進行媒體配置信息的信交換:
- Alice運行
RTCPeerConnection
的createOffer()
方法,這個方法返回ALice的本地會話描述 - 在回調中Alice使用
setLocalDescription()
然後通過他們的信號信道把會話描述發送給Bob.請注意,在調用setLocalDescription()之前,RTCPeerConnection不會開始收集候選者:這是在JSEP IETF草案中編寫的。 - Bob使用
setRemoteDescription()
將Alice發送給他的描述設置未遠端的描述. - Bob運行
RTCPeerConnection.createAnswer()
方法,傳遞他從Alice得到的描述信息.這樣一個匹配他本地的會話就創建好了. createAnswer()回調傳遞給RTCSessionDescription:Bob將其設置爲本地描述並將其發送給Alice。 - 當Alice獲取到Bob的會話描述信息,他使用
setRemoteDescription
設置爲遠端的描述信息. - 完成連接
Webrtc 調用圖
如果不使用
RTCPeerConnection
了要調用close()
進行關閉.不然會佔用較多資源
RTCSessionDescription
對象是符合SDP規格的序列號二進制對象.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架構被叫做 JESP JavaScript Session Establishment Protocol .
一旦信號處理流程成功,數據可以直接在點到點的在調用和被調用者之間進行傳輸.如果這樣操作失敗了則通過一箇中介服務器進行傳輸.流的傳輸是RPCPeerConnection
的主要工作.
RTCPeerConnection
RTCPeerConnection
是RPC中處理對端流數據的穩定性和效率的組件. 如下圖的架構,可以看到RTCPeerConnection
在架構中扮演的角色,綠色部分很複雜.
從JS的視覺看RTCPeerConnection
講開發人員從無法複雜性中解脫出來.WebRTC使用編碼器和協議做了大量工作使得即使在不可靠的網絡下也可以進行實時通訊.
- 丟包隱藏
- 回聲消除
- 帶寬適應性
- 動態抖動緩衝
- 自動增益控制
- 降噪和抑制
- 圖像清理
上面的W3C代碼從信令角度展示了WebRTC的簡化示例。下面是兩個正在運行的WebRTC應用程序的演練:第一個是演示RTCPeerConnection的簡單示例;第二個是完全可操作的視頻聊天客戶端。
不通過服務器的 RTCPeerConnection
連接
https://webrtc.github.io/samples/src/content/peerconnection/pc1/ 示例代碼演示從單個頁面實現一個 RTCPeerConnection
連接.
示例中 pc1
扮演本地端,pc2
扮演遠端.
調用方
- 創建一個新的
RTCPeerConnection
對象並使用getUserMedia()
添加流.
pc1 = new RTCPeerConnection(servers);
//..
localStream.getTracks().forEach((track) => {
pc1.addTrack(track,localStream);
}
- 創建有個
offer
設置爲pc1
的本地描述.作爲pc2
的遠端描述.這些可以在一段代碼裏搞定,不用用到信號系統.因爲調用和被調用方都在一個頁面.
pc1.setLocalDescription(desc).then(()=>{
onSetLocalSuccess(pc1);
} ,
onSetSessionDescriptionError
);
trace('pc2 setRemoteDescription start')
pc2.setRemoteDescription(desc).then(()=>{
onSetRemoteSuccess(pc2);
},onSetSessionDescriptionError);
被調用方
- 創建
pc2
,當pc1
有流過來就顯示到video
元素
pc2 = new RTCPeerConnection(servers);
pc2.ontrack = gotRemoteStream;
//...
function gotRemoteStream(e){
vid2.srcObject = e.stream;
}
RTCPeerConnection
加服務器
在現實世界中,WebRTC需要服務器,雖然很簡單,但是也包含以下步驟:
- 用戶發現彼此並交換真實世界的信息
- WebRTC 客戶端應用交換網絡信息
- 端之間交換數據,比如分辨率,視頻格式和分辨率
- WebRTC客戶端穿透NAT和防火牆
換句話說,WebRTC 需要四類服務器端的功能.
- 用戶發現和交流
- 信號處理
- NAT和防火牆穿透
- 當p2p鏈接失敗時的中繼服務
RTCPeerConnection
使用的ICE框架,通過STUN協議,以及STUN的擴展 TURN 協議來進行P2P網絡的穿透.
ICE
是一個連接對等端的框架,比如視頻聊天的兩個客戶端.最初ICE嘗試通過UDP直接連接對等端,以儘可能的降低延遲.在這個過程中,STUN服務器的作用是讓處於NAT後面的端找出他的公開地址以及端口(瞭解更多STUN和TURN的內容)
如果UDP連接失敗,ICE 嘗試TCP,如果直接連接失敗.ICE會使用一個TURN的中繼服務器進行連接, 通常無法連接的情況是由於NAT穿透和防火牆的原因.換句話說 ICE會首先通過UDP使用STUN直接連接端.如果失敗則使用TURN的中繼服務.上圖展示了這個查找網絡地址和端口的過程.
WebRTC大牛Justin Uberti 有個Slide,詳細講解了ICE,STUN和TURN,地址是 https://www.youtube.com/watch?v=p2HzZkd2A40&t=21m12s 示例中還包含一個TURN和STUN的實現.
一個簡單的視頻聊天客戶端
體驗WebRTC的完整功能,包含信令,防火牆穿透使用STUN服務器等功能,訪問 https://appr.tc/.這個APP使用 adapter.js 一個適配層以屏蔽一些差異.訪問更多信息 可以查看 https://webrtc.org/web-apis/interop
代碼記錄了比較詳細的日誌.以便讓大家通過代碼瞭解更多細節.
如果上面的看不懂可以看 https://codelabs.developers.google.com/codelabs/webrtc-web/ ,這個教程教大家一步步建立一個完整的視頻聊天應用.
網絡結構
WebRTC 目前實現爲只支持單個點到點的通訊.但是也可以被用於更復雜的場景:比如,多個點與點之間直接連接的點到點方式.或者通過一個 多點控制單元(MCU),通過服務器來處理大量的參與者進行選擇性的流轉發,音視頻的混合和錄製.