作者:楊成功
簡(jiǎn)介:專注前端工程與架構(gòu)產(chǎn)出
來(lái)源:SegmentFault 思否社區(qū)
最近需要搭建一個(gè)在線課堂的直播平臺(tái),考慮到清晰度和延遲性,我們一致認(rèn)為使用 WebRTC 最合適。
原因有兩點(diǎn):首先是“點(diǎn)對(duì)點(diǎn)通信”非常吸引我們,不需要中間服務(wù)器,客戶端直連,通信非常方便;再者是 WebRTC 瀏覽器原生支持,其他客戶端支持也很好,不像傳統(tǒng)直播用 flv.js 做兼容,可以實(shí)現(xiàn)標(biāo)準(zhǔn)統(tǒng)一。
然而令我非常尷尬的是,社區(qū)看了好幾篇文章,理論架構(gòu)寫了一堆,但沒(méi)一個(gè)能跑起來(lái)。WebRTC 里面概念很新也很多,理解它的通信流程才是最關(guān)鍵,這點(diǎn)恰恰很少有描述。
于是我就自己搗鼓吧。搗鼓了幾天,可算是整明白了。下面我結(jié)合自己的實(shí)踐經(jīng)驗(yàn),按照我理解的關(guān)鍵步驟,帶大家從應(yīng)用場(chǎng)景的角度認(rèn)識(shí)這個(gè)厲害的朋友 —— WebRTC。
線上預(yù)覽本地通信 Demo:https://example.ruims.top/local/
大綱預(yù)覽
本文介紹的內(nèi)容包括以下方面:
什么是 WebRTC?
WebRTC (Web Real-Time Communications) 是一項(xiàng)實(shí)時(shí)通訊技術(shù),它允許網(wǎng)絡(luò)應(yīng)用或者站點(diǎn),在不借助中間媒介的情況下,建立瀏覽器之間點(diǎn)對(duì)點(diǎn)(Peer-to-Peer)的連接,實(shí)現(xiàn)視頻流和音頻流或者其他任意數(shù)據(jù)的傳輸。簡(jiǎn)單的說(shuō),就是 WebRTC 可以不借助媒體服務(wù)器,通過(guò)瀏覽器與瀏覽器直接連接(點(diǎn)對(duì)點(diǎn)),即可實(shí)現(xiàn)音視頻傳輸。如果你接觸過(guò)直播技術(shù),你就會(huì)知道“沒(méi)有媒體服務(wù)器”多么令人驚訝。以往的直播技術(shù)大多是基于推流/拉流的邏輯實(shí)現(xiàn)的。要想做音視頻直播,則必須有一臺(tái)流媒體服務(wù)器做為中間站做數(shù)據(jù)轉(zhuǎn)發(fā)。但是這種推拉流的方案有兩個(gè)問(wèn)題:
因?yàn)閮啥送ㄐ哦家冗^(guò)服務(wù)器,就好比本來(lái)是一條直路,你偏偏“繞了半個(gè)圈”,這樣肯定會(huì)花更多的時(shí)間,因此直播必然會(huì)有延遲,即使延遲再低也要 1s 以上。清晰度高低的本質(zhì)是數(shù)據(jù)量的大小。你想象一下,每天乘地鐵上班,早高峰人越多,進(jìn)站的那條道就越容易堵,堵你就會(huì)走走停停,再加上還繞了路,是不是到公司就更晚了。把這個(gè)例子聯(lián)系到高清晰度的直播:因?yàn)閿?shù)據(jù)量大就容易發(fā)生網(wǎng)絡(luò)擁堵,擁堵就會(huì)導(dǎo)致播放卡頓,同時(shí)延遲性也會(huì)更高。但是 WebRTC 就不一樣了,它不需要媒體服務(wù)器,兩點(diǎn)一線直連,首先延遲性一定大大縮短。再者因?yàn)閭鬏斅肪€更短,所以清晰度高的數(shù)據(jù)流也更容易到達(dá),相對(duì)來(lái)說(shuō)不易擁堵,因此播放端不容易卡頓,這樣就兼顧了清晰度與延遲性。當(dāng)然 WebRTC 也是支持中間媒體服務(wù)器的,有些場(chǎng)景下確實(shí)少不了服務(wù)器轉(zhuǎn)發(fā)。我們這篇只探討點(diǎn)對(duì)點(diǎn)的模式,旨在幫助大家更容易的了解并上手 WebRTC。獲取媒體流
點(diǎn)對(duì)點(diǎn)通信的第一步,一定是發(fā)起端獲取媒體流。常見(jiàn)的媒體設(shè)備有三種:攝像機(jī),麥克風(fēng) 和 屏幕。其中攝像機(jī)和屏幕可以轉(zhuǎn)化為視頻流,而麥克風(fēng)可轉(zhuǎn)化為音頻流。音視頻流結(jié)合起來(lái)就組成了常見(jiàn)的媒體流。以 Chrome 瀏覽器為例,攝像頭和屏幕的視頻流獲取方式不一樣。對(duì)于攝像頭和麥克風(fēng),使用如下 API 獲取:var stream = await navigator.mediaDevices.getUserMedia()
對(duì)于屏幕錄制,則會(huì)用另外一個(gè) API。限制是這個(gè) API 只能獲取視頻,不能獲取音頻:var stream = await navigator.mediaDevices.getDisplayMedia()
注意:這里我遇到過(guò)一個(gè)問(wèn)題,編輯器里提示 navigator.mediaDevices == undefined,原因是我的 typescript 版本小于 4.4,升級(jí)版本即可。這兩個(gè)獲取媒體流的 API 有使用條件,必須滿足以下兩種情況之一:如果不滿足,則 navigator.mediaDevices 的值就是 undefined。以上方法都有一個(gè)參數(shù) constraints,這個(gè)參數(shù)是一個(gè)配置對(duì)象,稱為 媒體約束。這里面最有用的是可以配置只獲取音頻或視頻,或者音視頻同時(shí)獲取。let stream = await navigator.mediaDevices.getDisplayMedia({
audio: false,
video: true
})
除了簡(jiǎn)單的配置獲取視頻之外,還可以對(duì)視頻的清晰度,碼率等涉及視頻質(zhì)量相關(guān)的參數(shù)做配置。比如我需要獲取 1080p 的超清視頻,我就可以這樣配:var stream = await navigator.mediaDevices.getDisplayMedia({
audio: false,
video: {
width: 1920,
height: 1080
}
})
當(dāng)然了,這里配置視頻的分辨率 1080p,并不代表實(shí)際獲取的視頻一定是 1080p。比如我的攝像頭是 720p 的,那即便我配置了 2k 的分辨率,實(shí)際獲取的最多也是 720p,這個(gè)和硬件與網(wǎng)絡(luò)有關(guān)系。上面說(shuō)了,媒體流是由音頻流和視頻流組成的。再說(shuō)的嚴(yán)謹(jǐn)一點(diǎn),一個(gè)媒體流(MediaStream)會(huì)包含多條媒體軌道(MediaStreamTrack),因此我們可以從媒體流中單獨(dú)獲取音頻和視頻軌道:// 視頻軌道
let videoTracks = stream.getVideoTracks()
// 音頻軌道
let audioTracks = stream.getAudioTracks()
// 全部軌道
stream.getTracks()
單獨(dú)獲取軌道有什么意義呢?比如上面的獲取屏幕的 API getDisplayMedia 無(wú)法獲取音頻,但是我們直播的時(shí)候既需要屏幕也需要聲音,此時(shí)就可以分別獲取音頻和視頻,然后組成一個(gè)新的媒體流。實(shí)現(xiàn)如下:const getNewStream = async () => {
var stream = new MediaStream()
let audio_stm = await navigator.mediaDevices.getUserMedia({
audio: true
})
let video_stm = await navigator.mediaDevices.getDisplayMedia({
video: true
})
audio_stm.getAudioTracks().map(row => stream.addTrack(row))
video_stm.getVideoTracks().map(row => stream.addTrack(row))
return stream
}
對(duì)等連接流程
要說(shuō) WebRTC 有什么不優(yōu)雅的地方,首先要提的就是連接步驟復(fù)雜。很多同學(xué)就因?yàn)榭偸沁B接不成功,結(jié)果被成功勸退。對(duì)等連接,也就是上面說(shuō)的點(diǎn)對(duì)點(diǎn)連接,核心是由 RTCPeerConnection 函數(shù)實(shí)現(xiàn)。兩個(gè)瀏覽器之間點(diǎn)對(duì)點(diǎn)的連接和通信,本質(zhì)上是兩個(gè) RTCPeerConnection 實(shí)例的連接和通信。用 RTCPeerConnection 構(gòu)造函數(shù)創(chuàng)建的兩個(gè)實(shí)例,成功建立連接之后,可以傳輸視頻、音頻或任意二進(jìn)制數(shù)據(jù)(需要支持 RTCDataChannel API )。同時(shí)也提供了連接狀態(tài)監(jiān)控,關(guān)閉連接的方法。不過(guò)兩點(diǎn)之間數(shù)據(jù)單向傳輸,只能由發(fā)起端向接收端傳遞。我們現(xiàn)在根據(jù)核心 API,梳理一下具體連接步驟。第一步:創(chuàng)建連接實(shí)例
首先創(chuàng)建兩個(gè)連接實(shí)例,這兩個(gè)實(shí)例就是互相通信的雙方。var peerA = new RTCPeerConnection()
var peerB = new RTCPeerConnection()
下文統(tǒng)一將發(fā)起直播的一端稱為 發(fā)起端,接收觀看直播的一端稱為 接收端現(xiàn)在的這兩個(gè)連接實(shí)例都還沒(méi)有數(shù)據(jù)。假設(shè) peerA 是發(fā)起端,peerB 是接收端,那么 peerA 的那端就要像上一步一樣獲取到媒體流數(shù)據(jù),然后添加到 peerA 實(shí)例,實(shí)現(xiàn)如下:var stream = await navigator.mediaDevices.getUserMedia()
stream.getTracks().forEach(track => {
peerA.addTrack(track, stream)
})
當(dāng) peerA 添加了媒體數(shù)據(jù),那么 peerB 必然會(huì)在后續(xù)連接的某個(gè)環(huán)節(jié)接收到媒體數(shù)據(jù)。因此還要為 peerB 設(shè)置監(jiān)聽(tīng)函數(shù),獲取媒體數(shù)據(jù):peerB.ontrack = async event => {
let [ remoteStream ] = event.streams
console.log(remoteStream)
})
這里要注意:必須 peerA 添加媒體數(shù)據(jù)之后,才能進(jìn)行下一步! 否則后續(xù)環(huán)節(jié)中 peerB 的 ontrack 事件就不會(huì)觸發(fā),也就不會(huì)拿到媒體流數(shù)據(jù)。第二步:建立對(duì)等連接
添加數(shù)據(jù)之后,兩端就可以開(kāi)始建立對(duì)等連接。建立連接最重要的角色是 SDP(RTCSessionDescription),翻譯過(guò)來(lái)就是 會(huì)話描述。連接雙方需要各自建立一個(gè) SDP,但是他們的 SDP 是不同的。發(fā)起端的 SDP 被稱為 offer,接收端的 SDP 被稱為 answer。其實(shí)兩端建立對(duì)等連接的本質(zhì)就是互換 SDP,在互換的過(guò)程中相互驗(yàn)證,驗(yàn)證成功后兩端的連接才能成功。現(xiàn)在我們?yōu)閮啥藙?chuàng)建 SDP。peerA 創(chuàng)建 offer,peerB 創(chuàng)建 answer:var offer = await peerA.createOffer()
var answer = await peerB.createAnswer()
創(chuàng)建之后,首先接收端 peerB 要將 offset 設(shè)置為遠(yuǎn)程描述,然后將 answer 設(shè)置為本地描述:await peerB.setRemoteDescription(offer)
await peerB.setLocalDescription(answer)
注意:當(dāng) peerB.setRemoteDescription 執(zhí)行之后,peerB.ontrack 事件就會(huì)觸發(fā)。當(dāng)然前提是第一步為 peerA 添加了媒體數(shù)據(jù)。這個(gè)很好理解。offer 是 peerA 創(chuàng)建的,相當(dāng)于是連接的另一端,因此要設(shè)為“遠(yuǎn)程描述”。answer 是自己創(chuàng)建的,自然要設(shè)置為“本地描述”。同樣的邏輯,peerB 設(shè)置完成后,peerA 也要將 answer 設(shè)為遠(yuǎn)程描述,offer 設(shè)置為本地描述。await peerA.setRemoteDescription(answer)
await peerA.setLocalDescription(offer)
到這里,互相交換 SDP 已完成。但是通信還未結(jié)束,還差最后一步。當(dāng) peerA 執(zhí)行 setLocalDescription 函數(shù)時(shí)會(huì)觸發(fā) onicecandidate 事件,我們需要定義這個(gè)事件,然后在里面為 peerB 添加 candidate:peerA.onicecandidate = event => {
if (event.candidate) {
peerB.addIceCandidate(event.candidate)
}
}
至此,端對(duì)端通信才算是真正建立了!如果過(guò)程順利的話,此時(shí) peerB 的 ontrack 事件內(nèi)應(yīng)該已經(jīng)接收到媒體流數(shù)據(jù)了,你只需要將媒體數(shù)據(jù)渲染到一個(gè) video 標(biāo)簽上即可實(shí)現(xiàn)播放。還要再提一次:這幾步看似簡(jiǎn)單,實(shí)際順序非常重要,一步都不能出錯(cuò),否則就會(huì)連接失敗!如果你在實(shí)踐中遇到問(wèn)題,一定再回頭檢查一下步驟有沒(méi)有出錯(cuò)。最后我們?cè)贋?peerA 添加狀態(tài)監(jiān)聽(tīng)事件,檢測(cè)連接是否成功:peerA.onconnectionstatechange = event => {
if (peerA.connectionState === 'connected') {
console.log('對(duì)等連接成功!')
}
if (peerA.connectionState === 'disconnected') {
console.log('連接已斷開(kāi)!')
}
}
本地模擬通信源碼
上一步我們梳理了點(diǎn)對(duì)點(diǎn)通信的流程,其實(shí)主要代碼也就這么多。這一步我們?cè)侔堰@些知識(shí)點(diǎn)串起來(lái),簡(jiǎn)單實(shí)現(xiàn)一個(gè)本地模擬通信的 Demo,運(yùn)行起來(lái)讓大家看效果。
首先是頁(yè)面布局,非常簡(jiǎn)單。兩個(gè) video 標(biāo)簽,一個(gè)播放按鈕:<div class="local-stream-page">
<video autoplay controls muted id="elA"></video>
<video autoplay controls muted id="elB"></video>
<button onclick="onStart()">播放</button>
</div>
var peerA = null
var peerB = null
var videoElA = document.getElementById('elA')
var videoElB = document.getElementById('elB')
按鈕綁定了一個(gè) onStart 方法,在這個(gè)方法內(nèi)獲取媒體數(shù)據(jù):const onStart = async () => {
try {
var stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true
})
if (videoElA.current) {
videoElA.current.srcObject = stream // 在 video 標(biāo)簽上播放媒體流
}
peerInit(stream) // 初始化連接
} catch (error) {
console.log('error:', error)
}
}
onStart 函數(shù)里調(diào)用了 peerInit 方法,在這個(gè)方法內(nèi)初始化連接:const peerInit = stream => {
// 1. 創(chuàng)建連接實(shí)例
var peerA = new RTCPeerConnection()
var peerB = new RTCPeerConnection()
// 2. 添加視頻流軌道
stream.getTracks().forEach(track => {
peerA.addTrack(track, stream)
})
// 添加 candidate
peerA.onicecandidate = event => {
if (event.candidate) {
peerB.addIceCandidate(event.candidate)
}
}
// 檢測(cè)連接狀態(tài)
peerA.onconnectionstatechange = event => {
if (peerA.connectionState === 'connected') {
console.log('對(duì)等連接成功!')
}
}
// 監(jiān)聽(tīng)數(shù)據(jù)傳來(lái)
peerB.ontrack = async event => {
const [remoteStream] = event.streams
videoElB.current.srcObject = remoteStream
}
// 互換sdp認(rèn)證
transSDP()
}
初始化連接之后,在 transSDP 方法中互換 SDP 建立連接:const transSDP = async () => {
// 1. 創(chuàng)建 offer
let offer = await peerA.createOffer()
await peerB.setRemoteDescription(offer)
// 2. 創(chuàng)建 answer
let answer = await peerB.createAnswer()
await peerB.setLocalDescription(answer)
// 3. 發(fā)送端設(shè)置 SDP
await peerA.setLocalDescription(offer)
await peerA.setRemoteDescription(answer)
}
注意:這個(gè)方法里的代碼順序非常重要,如果改了順序多半會(huì)連接失敗!如果順利的話,此時(shí)已經(jīng)連接成功。截圖如下:我們用兩個(gè) video 標(biāo)簽和三個(gè)方法,實(shí)現(xiàn)了本地模擬通信的 demo。其實(shí) “本地模擬通信” 就是模擬 peerA 和 peerB 通信,把兩個(gè)客戶端放在了一個(gè)頁(yè)面上,當(dāng)然實(shí)際情況不可能如此,這個(gè) demo 只是幫助我們理清通信流程。https://github.com/ruidoc/blog-codes/tree/master/src/webrtc-web拉代碼直接打開(kāi) index.html 即可看到效果。接下來(lái)我們探索真實(shí)場(chǎng)景 —— 局域網(wǎng)如何通信。局域網(wǎng)兩端通信
上一節(jié)實(shí)現(xiàn)了本地模擬通信,在一個(gè)頁(yè)面模擬了兩個(gè)端連接。現(xiàn)在思考一下:如果 peerA 和 peerB 是一個(gè)局域網(wǎng)下的兩個(gè)客戶端,那么本地模擬通信的代碼需要怎么改呢?本地模擬通信我們用了 兩個(gè)標(biāo)簽 和 三個(gè)方法 來(lái)實(shí)現(xiàn)。如果分開(kāi)的話,首先 peerA 和 peerB 兩個(gè)實(shí)例,以及各自綁定的事件,肯定是分開(kāi)定義的,兩個(gè) video 標(biāo)簽也同理。然后獲取媒體流的 onStart 方法一定在發(fā)起端 peerA,也沒(méi)問(wèn)題,但是互換 SDP 的 transSDP 方法此時(shí)就失效了。// peerA 端
let offer = await peerA.createOffer()
await peerA.setLocalDescription(offer)
await peerA.setRemoteDescription(answer)
這里設(shè)置遠(yuǎn)程描述用到了 answer,那么 answer 從何而來(lái)?本地模擬通信我們是在同一個(gè)文件里定義變量,可以互相訪問(wèn)。但是現(xiàn)在 peerB 在另一個(gè)客戶端,answer 也在 peerB 端,這樣的話就需要在 peerB 端創(chuàng)好 answer 之后,傳到 peerA 端。相同的道理,peerA 端創(chuàng)建好 offer 之后,也要傳到 peerB 端。這樣就需要兩個(gè)客戶端遠(yuǎn)程交換 SDP,這個(gè)過(guò)程被稱作 信令。沒(méi)錯(cuò),信令是遠(yuǎn)程交換 SDP 的過(guò)程,并不是某種憑證。兩個(gè)客戶端需要互相主動(dòng)交換數(shù)據(jù),那么就需要一個(gè)服務(wù)器提供連接與傳輸。而“主動(dòng)交換”最適合的實(shí)現(xiàn)方案就是 WebSocket,因此我們需要基于 WebSocket 搭建一個(gè) 信令服務(wù)器 來(lái)實(shí)現(xiàn) SDP 互換。不過(guò)本篇不會(huì)詳解信令服務(wù)器,我會(huì)單獨(dú)出一篇搭建信令服務(wù)器的文章。現(xiàn)在我們用兩個(gè)變量 socketA 和 socketB 來(lái)表示 peerA 和 peerB 兩端的 WebSocket 連接,然后改造對(duì)等連接的邏輯。首先修改 peerA 端 SDP 的傳遞與接收代碼:// peerA 端
const transSDP = async () => {
let offer = await peerA.createOffer()
// 向 peerB 傳輸 offer
socketA.send({ type: 'offer', data: offer })
// 接收 peerB 傳來(lái)的 answer
socketA.onmessage = async evt => {
let { type, data } = evt.data
if (type == 'answer') {
await peerA.setLocalDescription(offer)
await peerA.setRemoteDescription(data)
}
}
}
這個(gè)邏輯是發(fā)起端 peerA 創(chuàng)建 offer 之后,立即傳給 peerB 端。當(dāng) peerB 端執(zhí)行完自己的代碼并創(chuàng)建 answer 之后,再回傳給 peerA 端,此時(shí) peerA 再設(shè)置自己的描述。此外,還有 candidate 的部分也需要遠(yuǎn)程傳遞:// peerA 端
peerA.onicecandidate = event => {
if (event.candidate) {
socketA.send({ type: 'candid', data: event.candidate })
}
}
peerB 端稍有不同,必須是接收到 offer 并設(shè)置為遠(yuǎn)程描述之后,才可以創(chuàng)建 answer,創(chuàng)建之后再發(fā)給 peerA 端,同時(shí)也要接收 candidate 數(shù)據(jù):// peerB 端,接收 peerA 傳來(lái)的 offer
socketB.onmessage = async evt => {
let { type, data } = evt.data
if (type == 'offer') {
await peerB.setRemoteDescription(data)
let answer = await peerB.createAnswer()
await peerB.setLocalDescription(answer)
// 向 peerA 傳輸 answer
socketB.send({ type: 'answer', data: answer })
}
if (type == 'candid') {
peerB.addIceCandidate(data)
}
}
這樣兩端通過(guò)遠(yuǎn)程互傳數(shù)據(jù)的方式,就實(shí)現(xiàn)了局域網(wǎng)內(nèi)兩個(gè)客戶端的連接通信。總結(jié)一下,兩個(gè)客戶端監(jiān)聽(tīng)對(duì)方的 WebSocket 發(fā)送消息,然后接收對(duì)方的 SDP,互相設(shè)置為遠(yuǎn)程描述。接收端還要獲取 candidate 數(shù)據(jù),這樣“信令”這個(gè)過(guò)程就跑通了。一對(duì)多通信
前面我們講的,不管是本地模擬通信,還是局域網(wǎng)兩端通信,都屬于“一對(duì)一”通信。然而在很多場(chǎng)景下,比如在線教育班級(jí)直播課,一個(gè)老師可能要面對(duì) 20 個(gè)學(xué)生,這是典型的一對(duì)多場(chǎng)景。但是 WebRTC 只支持點(diǎn)對(duì)點(diǎn)通信,也就是一個(gè)客戶端只能與一個(gè)客戶端建立連接,那這種情況該怎么辦呢?記不記得前面說(shuō)過(guò):兩個(gè)客戶端之間點(diǎn)對(duì)點(diǎn)的連接和通信,本質(zhì)上是兩個(gè) RTCPeerConnection 實(shí)例的連接和通信。那我們變通一下,比如現(xiàn)在接收端可能是 peerB,peerC,peerD 等等好幾個(gè)客戶端,建立連接的邏輯與之前的一樣不用變。那么發(fā)起端能否從“一個(gè)連接實(shí)例”擴(kuò)展到“多個(gè)連接實(shí)例”呢?也就是說(shuō),發(fā)起端雖然是一個(gè)客戶端,但是不是可以同時(shí)創(chuàng)建多個(gè) RTCPeerConnection 實(shí)例。這樣的話,一對(duì)一連接的本質(zhì)沒(méi)有變,只不過(guò)把多個(gè)連接實(shí)例放到了一個(gè)客戶端,每個(gè)實(shí)例再與其他接收端連接,變相的實(shí)現(xiàn)了一對(duì)多通信。具體思路是:發(fā)起端維護(hù)一個(gè)連接實(shí)例的數(shù)組,當(dāng)一個(gè)接收端請(qǐng)求建立連接時(shí),發(fā)起端新建一個(gè)連接實(shí)例與這個(gè)接收端通信,連接成功后,再將這個(gè)實(shí)例 push 到數(shù)組里面。當(dāng)連接斷開(kāi)時(shí),則會(huì)從數(shù)組里刪掉這個(gè)實(shí)例。這種方式我親測(cè)有效,下面我們對(duì)發(fā)起端的代碼改造。其中類型為 join 的消息,表示連接端請(qǐng)求連接。// 發(fā)起端
var offer = null
var Peers = [] // 連接實(shí)例數(shù)組
// 接收端請(qǐng)求連接,傳來(lái)標(biāo)識(shí)id
const newPeer = async id => {
// 1. 創(chuàng)建連接
let peer = new RTCPeerConnection()
// 2. 添加視頻流軌道
stream.getTracks().forEach(track => {
peer.addTrack(track, stream)
})
// 3. 創(chuàng)建并傳遞 SDP
offer = await peerA.createOffer()
socketA.send({ type: 'offer', data: { id, offer } })
// 5. 保存連接
Peers.push({ id, peer })
}
// 監(jiān)聽(tīng)接收端的信息
socketA.onmessage = async evt => {
let { type, data } = evt.data
// 接收端請(qǐng)求連接
if (type == 'join') {
newPeer(data)
}
if (type == 'answer') {
let index = Peers.findIndex(row => row.id == data.id)
if (index >= 0) {
await Peers[index].peer.setLocalDescription(offer)
await Peers[index].peer.setRemoteDescription(data.answer)
}
}
}
這個(gè)就是核心邏輯了,其實(shí)不難,思路理順了就很簡(jiǎn)單。因?yàn)樾帕罘?wù)器我們還沒(méi)有詳細(xì)介紹,實(shí)際的一對(duì)多通信需要信令服務(wù)器參與,所以這里我只介紹下實(shí)現(xiàn)思路和核心代碼。更詳細(xì)的實(shí)現(xiàn),我會(huì)在下一篇介紹信令服務(wù)器的文章再次實(shí)戰(zhàn)一對(duì)多通信,到時(shí)候完整源碼一并奉上。
點(diǎn)擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開(kāi)更多互動(dòng)和交流,掃描下方”二維碼“或在“公眾號(hào)后臺(tái)“回復(fù)“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~