前言
“它(WebRTC)允許網絡應用或者站點,在不借助中間媒介的情況下,建立瀏覽器之間點對點(Peer-to-Peer)的連接,實現視頻流和(或)音頻流或者其他任意數據的傳輸”。
這是 MDN 上對 WebRTC 的描述,初次接觸時無法理解 WebRTC 爲什麼要和 WebSocket 搭配,明明說的很清楚 不借助中間媒介 ,那 WebSocket 充當的是什麼角色?整個 WebRTC 通話建立的流程又是怎樣的?
開始
先看一下最終效果:
1.相關技術
本示例主要使用了 WebRTC
和 WebSocket
:
-
WebRTC
(Web Real-Time Communication)即網頁即時通信,是一個支持網頁瀏覽器進行實時語音對話或視頻對話的API。 -
WebSocket
是一種在單個TCP連接上進行全雙工通信的協議。在 WebSocket 中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,並進行雙向數據傳輸。
2.通話建立流程
簡單說一下流程,如瀏覽器 A 想和瀏覽器 B 進行音視頻通話:
- A、B 都連接信令服務器(ws);
- A 創建本地視頻,並獲取會話描述對象(
offer sdp
)信息; - A 將
offer sdp
通過 ws 發送給 B; - B 收到信令後,B 創建本地視頻,並獲取會話描述對象(
answer sdp
)信息; - B 將
answer sdp
通過 ws 發送給 A; - A 和 B 開始打洞,收集並通過 ws 交換 ice 信息;
- 完成打洞後,A 和 B 開始爲安全的媒體通信協商祕鑰;
- 至此, A 和 B 可以進行音視頻通話。
引用網上的有關WebRTC
建立的時序圖,可能更加直觀:
從上述流程,可以發現通信雙方在建立連接前需要交換信息,這也就是開頭提到的 WebSocket
充當的角色:信令服務器,用於轉發信息。而 WebRTC 不借助中間媒介 的意思是,在建立對等連接後,不需要藉助第三方服務器中轉,而是直接在兩個實體(瀏覽器)間進行傳輸。
3.代碼
第一步
獲取視頻標籤,連接信令服務器,創建 RTCPeerConnection
對象。其中 RTCPeerConnection 的作用是在兩個對等端之間建立連接,其構造函數支持傳一個配置對象,包含ICE“打洞”(由於本示例在本機進行測試,故不需要)。
const localVideo = document.querySelector('#local-video');
const remoteVideo = document.querySelector('#remote-video');
const socket = new WebSocket('ws://localhost:8080');
const peer = new RTCPeerConnection();
socket.onmessage = () => { // todo }
peer.ontrack = () => { // todo }
peer.onicecandidate = () => { // todo }
第二步
獲取本地攝像頭/麥克風(需要允許使用權限),拿到本地媒體流(MediaStream)後,需要將其中所有媒體軌道(MediaStreamTrack)添加到軌道集,這些軌道將被髮送到另一對等方。
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
localVideo.srcObject = stream;
stream.getTracks().forEach(track => {
peer.addTrack(track, stream);
});
});
第三步
創建發起方會話描述對象(createOffer),設置本地SDP(setLocalDescription),並通過信令服務器發送到對等端,以啓動與遠程對等端的新WebRTC連接。
peer.createOffer().then(offer => {
peer.setLocalDescription(offer);
socket.send(JSON.stringify(offer));
});
當調用 setLocalDescription 方法,PeerConnection 開始收集候選人(ice信息),併發送offer_ice到對等方。這邊補充第一步中的peer.onicecandidate
和socket.onmessage
對等方收到ice信息後,通過調用 addIceCandidate 將接收的候選者信息傳遞給瀏覽器的ICE代理。
peer.onicecandidate = e => {
if (e.candidate) {
socket.send(JSON.stringify({
type: 'offer_ice',
iceCandidate: e.candidate
}));
}
};
socket.onmessage = e => {
const { type, sdp, iceCandidate } = JSON.parse(e.data);
if (type === 'offer_ice') {
peer.addIceCandidate(iceCandidate);
}
}
第四步
接收方收到了offer
信令後,開始獲取攝像頭/麥克風,與發起方操作一致。同時將收到offer SDP
指定爲連接的遠程對等方屬性(setRemoteDescription),並創建應答SDP(createAnswer),發送到對等端。這邊補充第一步中的socket.onmessage
。
socket.onmessage = e => {
const { type, sdp, iceCandidate } = JSON.parse(e.data);
if (type === 'offer') {
navigator.mediaDevices.getUserMedia(); // 與發起方一致,省略
const offerSdp = new RTCSessionDescription({ type, sdp });
peer.setRemoteDescription(offerSdp).then(() => {
peer.createAnswer(answer => {
socket.send(JSON.stringify(answer));
peer.setLocalDescription(answer)
});
});
}
}
注意:當 setLocalDescription 方法調用後,開始收集候選人信息,併發送 answer_ice 到對等方。與發送方同理,不贅述。
第五步
通過不斷收集ICE信息(onicecandidate),發起方和應答方最終將建立一條最優的連接方式,此時會觸發 ontrack 回調,即可獲取到對等方的媒體流。
peer.ontrack = e => {
if (e && e.streams) {
remoteVideo.srcObject = e.streams[0];
}
};
4.最後
整合發起方和應答方的代碼,差不多50行,不算標題黨!哈哈哈哈哈哈...
完整示例相關代碼已上傳 github.com/shushushv/webrtc-p2p ,⭐🦆~