WebRTC 概述
WebRTC,名稱源自網(wǎng)頁(yè)即時(shí)通信(Web Real-Time Communication的縮寫),是一個(gè)支持網(wǎng)頁(yè)瀏覽器進(jìn)行實(shí)時(shí)語(yǔ)音對(duì)話或視頻對(duì)話的API。它于2011年6月1日開(kāi)源并在Google、Mozilla、Opera支持下被納入萬(wàn)維網(wǎng)聯(lián)盟的W3C推薦標(biāo)準(zhǔn)。
WebRTC提供了視頻會(huì)議的核心技術(shù),包括音視頻的采集、編解碼、網(wǎng)絡(luò)傳輸、顯示等功能, 并且還支持跨平臺(tái):windows,linux,mac,android。

從架構(gòu)圖中我們看到,WebRTC幫我實(shí)現(xiàn)了音頻引擎,視頻引擎,網(wǎng)絡(luò)傳輸引擎,封裝了上層API,讓開(kāi)發(fā)者能夠基于瀏覽器(Chrome\FireFox...)輕易快捷開(kāi)發(fā)出豐富的實(shí)時(shí)多媒體應(yīng)用,而無(wú)需下載安裝任何插件,也無(wú)需關(guān)注多媒體的數(shù)字信號(hào)處理過(guò)程,只需編寫簡(jiǎn)單的Javascript程序即可實(shí)現(xiàn)。
WebRTC原理
瀏覽器點(diǎn)對(duì)點(diǎn)P2P的信道(peer-to-peer),信道可發(fā)送任何數(shù)據(jù)并無(wú)需經(jīng)過(guò)服務(wù)器。WebRTC的實(shí)現(xiàn)是建立客戶端之間的直接連接而無(wú)需服務(wù)器中轉(zhuǎn)的,即P2P。所以要求彼此知道對(duì)方外網(wǎng)地址,而計(jì)算機(jī)大多位于NAT之后,只有少數(shù)主機(jī)擁有外網(wǎng)地址,這就需要一種可以穿透NAT技術(shù)。


NAT打洞
在處于使用NAT設(shè)備的私有TCP/IP網(wǎng)絡(luò)中的主機(jī)之間建立連接時(shí)需使用NAT穿越。NAT的行為是非標(biāo)準(zhǔn)化的,穿越技術(shù)大多使用公共服務(wù)器,使全球任何地方都能訪問(wèn)得到IP地址, 在RTCPeerConnection中實(shí)用ICE框架來(lái)保證RTCPeerConnection實(shí)現(xiàn)NAT穿越。

常見(jiàn)的NAT穿越技術(shù)有:應(yīng)用層網(wǎng)關(guān)(ALG)方式、M1DCOM (Middle-Bax Communications)代理方式、STUN方式、TURN方式、FuIIProxy方式、ICE方式。其中ICE己經(jīng)被公認(rèn)為在非對(duì)稱性NAT環(huán)境下首選的NAT穿越解決方案。ICE本身是一種方法,它綜合運(yùn)用STUN、 TURN等協(xié)議來(lái)提供一個(gè)通用的解決方案,使之在最適合的情況下工作,以彌補(bǔ)單獨(dú)使用其中任何一種所帶來(lái)的固有缺陷。
WebRTC建立連接

1.ClientA 創(chuàng)建WebRTC對(duì)象和RTC數(shù)據(jù)通道,并綁定一些事件
// 創(chuàng)建WebRTC對(duì)象
const pc = new RTCPeerConnection();
// 創(chuàng)建WebRTC數(shù)據(jù)通道,可以傳輸自定義數(shù)據(jù)
const dc = pc.createDataChannel("channel");
// 數(shù)據(jù)通道開(kāi)啟
dc.onopen = () => console.log("Data Channel Opened");
// 數(shù)據(jù)通道收到數(shù)據(jù)的事件
dc.onmessage = (e) => console.log("Get Message:", e.data);
// SDP變化的事件
pc.onicegatheringstatechange = () => {
if (pc.iceGatheringState === "complete") { // 在SPD創(chuàng)建完成的時(shí)候打印他
console.log(JSON.stringify(pc.localDescription));
}
}
2.ClientA 創(chuàng)建Offer
使用pc.createOffer()來(lái)創(chuàng)建Offer,并且調(diào)用setLocalDescription將Offer綁定在本地SDP,執(zhí)行完成后可以看到在控制臺(tái)中打印了你的Offer,復(fù)制這端Offer,我們可以來(lái)寫ClientB的代碼了
/**
* 創(chuàng)建Offer
* @return {Promise<void>}
*/
async function createOffer() {
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
}
3.ClientB 創(chuàng)建WebRTC對(duì)象和RTC數(shù)據(jù)通道,并綁定一些事件
由于ClientA已經(jīng)有數(shù)據(jù)通道了,所以ClientB不需要自己創(chuàng)建,通過(guò)pc.ondatachannel綁定監(jiān)聽(tīng)事件
const pc = new RTCPeerConnection();
let dc = null;
// 同樣的監(jiān)聽(tīng)SDP變化的事件,在SPD創(chuàng)建完成的時(shí)候打印他
pc.onicegatheringstatechange = () => {
if (pc.iceGatheringState === "complete") {
console.log(JSON.stringify(pc.localDescription));
}
}
// 監(jiān)聽(tīng)數(shù)據(jù)通道的事件,并添加上事件,保存一下這條數(shù)據(jù)通道
pc.ondatachannel = (e) => {
console.log("data channel");
dc = e.channel;
dc.onmessage = (e) => console.log("get message", e.data);
dc.onopen = async (e) => console.log("channel opened");
}
4.ClientB 綁定Offer
通過(guò)setRemoteDescription來(lái)設(shè)置遠(yuǎn)端OfferSDP
async function setOffer() {
await pc.setRemoteDescription(JSON.parse(需要綁定的offer));
console.log("Offer Set");
}
5.ClientB 創(chuàng)建Answer
使用createAnswer來(lái)創(chuàng)建一個(gè)Answer,并且用setLocalDescription設(shè)置到本地SDP,我們?cè)谠O(shè)置完Offer后立刻創(chuàng)建Answer,經(jīng)過(guò)上面的過(guò)程,可以在控制臺(tái)中看到打印的本地Answer,復(fù)制這個(gè)Answer,我們?nèi)lientA綁定它
async function setOffer() {
await pc.setRemoteDescription(JSON.parse(需要綁定的offer));
console.log("Offer Set");
await createAnswer();
}
async function createAnswer() {
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
}
6.ClientA 綁定Answer,建立P2P連接
在通過(guò)setRemoteDescription設(shè)置遠(yuǎn)端AnswerSDP,這樣就完成了數(shù)據(jù)通道連接,然后我們就可以通過(guò)dc.send('要發(fā)送的消息')進(jìn)行消息傳遞
async function setAnswer() {
await pc.setRemoteDescription(JSON.parse(需要綁定的Answer));
console.log("Answer Set");
}
7.將音視頻流推送給WebRTC,并接收對(duì)方傳來(lái)的音視頻流
async function openVideo() {
const stream = await navigator.mediaDevices.getUserMedia({
video: true, audio: true
});
myVideo.srcObject = stream;
stream.getTracks().forEach((track) => {
pc.addTrack(track, stream);
});
}
// 通過(guò)監(jiān)聽(tīng)track,表示有音頻流進(jìn)來(lái)
pc.ontrack = (e) => {
console.log("track");
video.srcObject = e.streams[0];
}