<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          音視頻通信加餐 —— WebRTC一肝到底

          共 15022字,需瀏覽 31分鐘

           ·

          2022-03-16 13:45

          作者:楊成功

          簡(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?
          • 獲取媒體流
          • 對(duì)等連接流程
          • 本地模擬通信源碼
          • 局域網(wǎng)兩端通信
          • 一對(duì)多通信
          • 我想學(xué)更多


          什么是 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)題:

          1. 較高的延遲
          2. 清晰度難以保證

          因?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 有使用條件,必須滿足以下兩種情況之一:

          • 域名是 localhost
          • 協(xié)議是 https

          如果不滿足,則 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>

          然后設(shè)置全局變量:

          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 只是幫助我們理清通信流程。

          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 端:

          // 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ù)文章~

          - END -


          瀏覽 37
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲成人娱乐网 | 无码群交东京热 | 伊人伊成久久 | 色福利视频 | 色香蕉视频在线 |