webrtc

什麼是 WebRTC ?

WebRTC 是由一家名爲 Gobal IP Solutions,簡稱 GIPS 的瑞典公司開發的。Google 在 2011 年收購了 GIPS,並將其源代碼開源。而後又與 IETF 和 W3C 的相關標準機構合做,以確保行業達成共識。其中:css

  • Web Real-Time Communications (WEBRTC) W3C 組織:定義瀏覽器 API。
  • Real-Time Communication in Web-browsers (RTCWEB) IETF 標準組織:定義其所需的協議,數據,安全性等手段。

簡單來講,WebRTC 是一個能夠在 Web 應用程序中實現音頻,視頻和數據的實時通訊的開源項目。在實時通訊中,音視頻的採集和處理是一個很複雜的過程。好比音視頻流的編解碼、降噪和回聲消除等,可是在 WebRTC 中,這一切都交由瀏覽器的底層封裝來完成。咱們能夠直接拿到優化後的媒體流,而後將其輸出到本地屏幕和揚聲器,或者轉發給其對等端。html

WebRTC 的音視頻處理引擎:前端

WebRTC 的音視頻處理引擎

因此,咱們能夠在不須要任何第三方插件的狀況下,實現一個瀏覽器到瀏覽器的點對點(P2P)鏈接,從而進行音視頻實時通訊。固然,WebRTC 提供了一些 API 供咱們使用,在實時音視頻通訊的過程當中,咱們主要用到如下三個:vue

  • getUserMedia:獲取音頻和視頻流(MediaStream)
  • RTCPeerConnection:點對點通訊
  • RTCDataChannel:數據通訊

不過,雖然瀏覽器給咱們解決了大部分音視頻處理問題,可是從瀏覽器請求音頻和視頻時,咱們仍是須要特別注意流的大小和質量。由於即使硬件可以捕獲高清質量流,CPU 和帶寬也不必定能夠跟上,這也是咱們在創建多個對等鏈接時,不得不考慮的問題。node

實現

接下來,咱們經過分析上文提到的 API,來逐步弄懂 WebRTC 實時通訊實現的流程。git

getUserMedia

  • MediaStream

getUserMedia 這個 API 你們可能並不陌生,由於常見的 H5 錄音等功能就須要用到它,主要就是用來獲取設備的媒體流(即 MediaStream)。它能夠接受一個約束對象 constraints 做爲參數,用來指定須要獲取到什麼樣的媒體流。es6

navigator.mediaDevices.getUserMedia({ audio: true, video: true })      // 參數表示須要同時獲取到音頻和視頻         .then(stream => {           // 獲取到優化後的媒體流           let video = document.querySelector('#rtc');           video.srcObject = stream;         })         .catch(err => {           // 捕獲錯誤         });

咱們簡單看一下獲取到的 MediaStream。github

能夠看到它有不少屬性,咱們只須要了解一下就好,更多信息能夠查看 MDNweb

* id [String]: 對當前的 MS 進行惟一標識。因此每次刷新瀏覽器或是從新獲取 MS,id 都會變更。 * active [boolean]: 表示當前 MS 是不是活躍狀態(就是是否能夠播放)。 * onactive: 當 active 爲 true 時,觸發該事件。

結合上圖,咱們順便複習一下上期講的原型和原型鏈。MediaStream 的 __proto__ 指向它的構造函數所對應的原型對象,在原型對象中又有一個 constructor 屬性指向了它所對應的構造函數。也就是說 MediaStream 的構造函數是一個名爲 MediaStream 的函數。可能說得有一點繞,對原型還不熟悉的同窗,能夠去看一下上期文章 JavaScript 原型和原型鏈及 canvas 驗證碼實踐 2mongodb

這裏也能夠經過 getAudioTracks()、getVideoTracks() 來查看獲取到的流的某些信息,更多信息查看 MDN

* kind: 是當前獲取的媒體流類型(Audio/Video)。 * label: 是媒體設備,我這裏用的是虛擬攝像頭。 * muted: 表示媒體軌道是否靜音。
  • 兼容性

    繼續來看 getUserMedia,navigator.mediaDevices.getUserMedia 是新版的 API,舊版的是 navigator.getUserMedia。爲了不兼容性問題,咱們能夠稍微處理一下(其實說到底,如今 WebRTC 的支持率還不算高,有須要的能夠選擇一些適配器,如 adapter.js)。

// 判斷是否有 navigator.mediaDevices,沒有賦成空對象     if (navigator.mediaDevices === undefined) {         navigator.mediaDevices = {};     }          // 繼續判斷是否有 navigator.mediaDevices.getUserMedia,沒有就採用 navigator.getUserMedia     if (navigator.mediaDevices.getUserMedia) {         navigator.mediaDevices.getUserMedia = function(prams) {             let getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;             // 兼容獲取             if (!getUserMedia) {                 return Promise.reject(new Error('getUserMedia is not implemented in this browser'));             }             return new Promise(function(resolve, reject) {                 getUserMedia.call(navigator, prams, resolve, reject);             });         };     }     navigator.mediaDevices.getUserMedia(constraints)         .then(stream => {             let video = document.querySelector('#Rtc');             if ('srcObject' in video) { // 判斷是否支持 srcObject 屬性                 video.srcObject = stream;             } else {                 video.src = window.URL.createObjectURL(stream);             }             video.onloadedmetadata = function(e) {                 video.play();             };         })         .catch((err) => { // 捕獲錯誤             console.error(err.name + ': ' + err.message);         });
  • constraints

對於 constraints 約束對象,咱們能夠用來指定一些和媒體流有關的屬性。好比指定是否獲取某種流:

navigator.mediaDevices.getUserMedia({ audio: false, video: true });     // 只須要視頻流,不要音頻

指定視頻流的寬高、幀率以及理想值:

// 獲取指定寬高,這裏須要注意:在改變視頻流的寬高時,     // 若是寬高比和採集到的不同,會直接截掉某部分     { audio: false,        video: { width: 1280, height: 720 }      }     // 設定理想值、最大值、最小值     {       audio: true,       video: {         width: { min: 1024, ideal: 1280, max: 1920 },         height: { min: 776, ideal: 720, max: 1080 }       }     }

對於移動設備來講,還能夠指定獲取前攝像頭,或者後置攝像頭:

{ audio: true, video: { facingMode: "user" } } // 前置     { audio: true, video: { facingMode: { exact: "environment" } } } // 後置     // 也能夠指定設備 id,     // 經過 navigator.mediaDevices.enumerateDevices() 能夠獲取到支持的設備     { video: { deviceId: myCameraDeviceId } }

還有一個比較有意思的就是設置視頻源爲屏幕,可是目前只有火狐支持了這個屬性。

{ audio: true, video: {mediaSource: 'screen'} }

這裏就不接着作搬運工了,更多精彩盡在 MDN,^_^!

RTCPeerConnection

RTCPeerConnection 接口表明一個由本地計算機到遠端的 WebRTC 鏈接。該接口提供了建立,保持,監控,關閉鏈接的方法的實現。—— MDN

  • 概述

    RTCPeerConnection 做爲建立點對點鏈接的 API,是咱們實現音視頻實時通訊的關鍵。在點對點通訊的過程當中,須要交換一系列信息,一般這一過程叫作 — 信令(signaling)。在信令階段須要完成的任務:

    • 爲每一個鏈接端建立一個 RTCPeerConnection,並添加本地媒體流。
    • 獲取並交換本地和遠程描述:SDP 格式的本地媒體元數據。
    • 獲取並交換網絡信息:潛在的鏈接端點稱爲 ICE 候選者。

    咱們雖然把 WebRTC 稱之爲點對點的鏈接,但並不意味着,實現過程當中不須要服務器的參與。由於在點對點的信道創建起來以前,兩者之間是沒有辦法通訊的。這也就意味着,在信令階段,咱們須要一個通訊服務來幫助咱們創建起這個鏈接。WebRTC 自己沒有指定信令服務,因此,咱們能夠但不限於使用 XMPP、XHR、Socket 等來作信令交換所需的服務。我在工做中採用的方案是基於 XMPP 協議的Strophe.js來作雙向通訊,可是在本例中則會使用Socket.io以及 Koa 來作項目演示。

  • NAT 穿越技術

    咱們先看鏈接任務的第一條:爲每一個鏈接端建立一個 RTCPeerConnection,並添加本地媒體流。事實上,若是是通常直播模式,則只須要播放端添加本地流進行輸出,其餘參與者只須要接受流進行觀看便可。

    由於各瀏覽器差別,RTCPeerConnection 同樣須要加上前綴。

    let PeerConnection = window.RTCPeerConnection ||                          window.mozRTCPeerConnection ||                          window.webkitRTCPeerConnection;     let peer = new PeerConnection(iceServers);

    咱們看見 RTCPeerConnection 也一樣接收一個參數 — iceServers,先來看看它長什麼樣:

    {       iceServers: [         { url: "stun:stun.l.google.com:19302"}, // 谷歌的公共服務         {           url: "turn:***",           username: ***, // 用戶名           credential: *** // 密碼         }       ]     }

    參數配置了兩個 url,分別是 STUN 和 TURN,這即是 WebRTC 實現點對點通訊的關鍵,也是通常 P2P 鏈接都須要解決的問題:NAT穿越。

    NAT(Network Address Translation,網絡地址轉換)簡單來講就是爲了解決 IPV4 下的 IP 地址匱乏而出現的一種技術,也就是一個 公網 IP 地址通常都對應 n 個內網 IP。這樣也就會致使不是同一局域網下的瀏覽器在嘗試 WebRTC 鏈接時,沒法直接拿到對方的公網 IP 也就不能進行通訊,因此就須要用到 NAT 穿越(也叫打洞)。如下爲 NAT 穿越基本流程:

    通常狀況下會採用 ICE 協議框架進行 NAT 穿越,ICE 的全稱爲 Interactive Connectivity Establishment,即交互式鏈接創建。它使用 STUN 協議以及 TURN 協議來進行穿越。關於 NAT 穿越的更多信息能夠參考 ICE協議下NAT穿越的實現(STUN&TURN)P2P通訊標準協議(三)之ICE

    到這裏,咱們能夠發現,WebRTC 的通訊至少須要兩種服務配合:

    • 信令階段須要雙向通訊服務輔助信息交換。
    • STUN、TURN輔助實現 NAT 穿越。
  • 創建點對點鏈接

    WebRTC 的點對點鏈接究竟是什麼樣的過程呢,咱們經過結合圖例來分析鏈接。

    顯而易見,在上述鏈接的過程當中:

    • 呼叫端(在這裏都是指代瀏覽器)須要給 接收端 發送一條名爲 offer 的信息。
    • 接收端 在接收到請求後,則返回一條 answer 信息給 呼叫端

    這即是上述任務之一 ,SDP 格式的本地媒體元數據的交換。sdp 信息通常長這樣:

    v=0     o=- 1837933589686018726 2 IN IP4 127.0.0.1     s=-     t=0 0     a=group:BUNDLE audio video     a=msid-semantic: WMS yvKeJMUSZzvJlAJHn4unfj6q9DMqmb6CrCOT     m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126     ...     ...

    可是任務不只僅是交換,還須要分別保存本身和對方的信息,因此咱們再加點料:

    • 呼叫端 建立 offer 信息後,先調用 setLocalDescription 存儲本地 offer 描述,再將其發送給 接收端
    • 接收端 收到 offer 後,先調用 setRemoteDescription 存儲遠端 offer 描述;而後又建立 answer 信息,一樣須要調用 setLocalDescription 存儲本地 answer 描述,再返回給 接收端
    • 呼叫端 拿到 answer 後,再次調用 setRemoteDescription 設置遠端 answer 描述。

    到這裏點對點鏈接還缺一步,也就是網絡信息 ICE 候選交換。不過這一步和 offer、answer 信息的交換並無前後順序,流程也是同樣的。即:在呼叫端接收端的 ICE 候選信息準備完成後,進行交換,並互相保存對方的信息,這樣就完成了一次鏈接。

     

     

    這張圖是我認爲比較完善的了,詳細的描述了整個鏈接的過程。正好咱們再來小結一下:

    • 基礎設施:必要的信令服務和 NAT 穿越服務
    • clientA 和 clientB 分別建立 RTCPeerConnection 併爲輸出端添加本地媒體流。若是是視頻通話類型,則意味着,兩端都須要添加媒體流進行輸出。
    • 本地 ICE 候選信息採集完成後,經過信令服務進行交換。
    • 呼叫端(比如 A 給 B 打視頻電話,A 爲呼叫端)發起 offer 信息,接收端接收並返回一個 answer 信息,呼叫端保存,完成鏈接。

本地 1 v 1 對等鏈接

基礎流程講完了,那麼是騾子是馬拉出來溜溜。咱們先來實現一個本地的對等鏈接,藉此熟悉一下流程和部分 API。本地鏈接,意思就是不通過服務,在本地頁面的兩個 video 之間進行鏈接。算了,仍是上圖吧,一看就懂。

明確一下目標,A 做爲輸出端,須要獲取到本地流並添加到本身的 RTCPeerConnection;B 做爲呼叫端,並無輸出的需求,所以只須要接收流。

  • 建立媒體流

頁面佈局很簡單,就是兩個 video 標籤,分別表明 A 和 B。因此咱們直接看代碼,雖然源碼是用 Vue 構建的,可是並無用到特別的 API,總體上和 es6 的 class 語法相差不大,並且都有詳細的註釋,因此建議沒有 Vue 基礎的同窗能夠直接當成 es6 來閱讀。示例 源碼庫 webrtc-stream 14

async createMedia() {       // 保存本地流到全局       this.localstream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true })       let video = document.querySelector('#rtcA');       video.srcObject = this.localstream;       this.initPeer(); // 獲取到媒體流後,調用函數初始化 RTCPeerConnection   }
  • 初始化 RTCPeerConnection
initPeer() {       ...       this.peerA.addStream(this.localstream); // 添加本地流       this.peerA.onicecandidate = (event) => {       // 監聽 A 的ICE候選信息 若是收集到,就添加給 B 鏈接狀態           if (event.candidate) {               this.peerB.addIceCandidate(event.candidate);           }       };       ...       // 監聽是否有媒體流接入,若是有就賦值給 rtcB 的 src       this.peerB.onaddstream = (event) => {           let video = document.querySelector('#rtcB');           video.srcObject = event.stream;       };       this.peerB.onicecandidate = (event) => { 鏈接狀態       // 監聽 B 的ICE候選信息 若是收集到,就添加給 A           if (event.candidate) {               this.peerA.addIceCandidate(event.candidate);           }       };   }

這部分主要就是分別建立 peer 實例,並互相交換 ICE 信息。不過有一個屬性須要在這裏提一下,就是 iceConnectionState。

peer.oniceconnectionstatechange = (evt) => {       console.log('ICE connection state change: ' + evt.target.iceConnectionState);   };

咱們能夠經過 oniceconnectionstatechange 方法來監測 ICE 鏈接的狀態,它一共有七種狀態:

new        ICE代理正在收集候選人或等待提供遠程候選人。 checking   ICE代理已經在至少一個組件上接收了遠程候選者,而且正在檢查候選但還沒有找到鏈接。除了檢查,它可能還在收集。 connected  ICE代理已找到全部組件的可用鏈接,但仍在檢查其餘候選對以查看是否存在更好的鏈接。它可能還在收集。 completed  ICE代理已完成收集和檢查,並找到全部組件的鏈接。 failed     ICE代理已完成檢查全部候選對,但未能找到至少一個組件的鏈接。可能已找到某些組件的鏈接。 disconnected ICE 鏈接斷開 closed      ICE代理已關閉,再也不響應STUN請求。

咱們須要注意的是 completed 和 disconnected,一個是完成鏈接時觸發,一個在斷開鏈接時觸發。

  • 建立鏈接
async call() {       if (!this.peerA || !this.peerB) { // 判斷是否有對應實例,沒有就從新建立           this.initPeer();       }       try {           let offer = await this.peerA.createOffer(this.offerOption); // 建立 offer           await this.onCreateOffer(offer);       } catch (e) {           console.log('createOffer: ', e);       }   }

這裏須要判斷是否有對應實例,是爲了掛斷以後又從新呼叫作的處理。

async onCreateOffer(desc) {       try {           await this.peerB.setLocalDescription(desc); // 呼叫端設置本地 offer 描述       } catch (e) {           console.log('Offer-setLocalDescription: ', e);       }       try {           await this.peerA.setRemoteDescription(desc); // 接收端設置遠程 offer 描述       } catch (e) {           console.log('Offer-setRemoteDescription: ', e);       }       try {           let answer = await this.peerA.createAnswer(); // 接收端建立 answer           await this.onCreateAnswer(answer);       } catch (e) {           console.log('createAnswer: ', e);       }   },   async onCreateAnswer(desc) {       try {           await this.peerA.setLocalDescription(desc); // 接收端設置本地 answer 描述       } catch (e) {           console.log('answer-setLocalDescription: ', e);       }       try {           await this.peerB.setRemoteDescription(desc); // 呼叫端端設置遠程 answer 描述       } catch (e) {           console.log('answer-setRemoteDescription: ', e);       }   }

這基本就是以前重複過好幾回的流程用代碼寫出來而已,看到這裏,思路應該比較清晰了。不過有一點須要說明一下,就是如今這種狀況,A 做爲呼叫端,B 同樣是能夠拿到 A 的媒體流的。由於鏈接一旦創建了,就是雙向的,只不過 B 初始化 peer 的時候沒有添加本地流,因此 A 不會有 B 的媒體流。

網絡 1 v 1 對等鏈接

想必基本流程你們都已經熟悉了,經過圖解、實例來來回回講了好幾遍。因此趁熱打鐵,咱們此次把服務加上,作一個真正的點對點鏈接。在看下面的文章以前,我但願你有一點點 Koa 和 Scoket.io 的基礎,瞭解一些基本 API 便可。不熟悉的同窗也沒關係,如今看也來得及,KoaSocke.io,或者能夠參考我以前的文章 Vchat - 一個社交聊天系統(vue + node + mongodb) 1

  • 需求

    仍是老規矩,先了解一下需求。圖片加載慢,能夠直接看演示地址 5

    鏈接過程涉及到多個環節,這裏就不一一截圖了,能夠直接去演示地址查看。簡單分析一下咱們要作的事情:

    • 加入房間後,獲取到房間的全部在線成員。
    • 選擇任一成員進行通話,也就是呼叫動做。這時候就有一些細節問題要處理:不能呼叫本身、同一時刻只容許呼叫一我的且須要判斷對方是不是通話中、呼叫後回覆須要有相應判斷(贊成、拒絕以及通話中)
    • 拒絕或通話中,都沒有後續動做,能夠換我的再呼叫。贊成以後,就要開始創建點對點鏈接。
  • 加入房間

    簡單看一下加入房間的流程:

    // 前端   join() {       if (!this.account) return;       this.isJoin = true; // 輸入框彈層邏輯       window.sessionStorage.account = this.account; // 刷新判斷是否登陸過       socket.emit('join', {roomid: this.roomid, account: this.account}); // 發送加入房間請求   }      // 後端   const sockS = {}; // 不一樣客戶端對應的 sock 實例   const users = {}; // 成員列表   sock.on('join', data=>{       sock.join(data.roomid, () => {           if (!users[data.roomid]) {               users[data.roomid] = [];           }           let obj = {               account: data.account,               id: sock.id           };           let arr = users[data.roomid].filter(v => v.account === data.account);           if (!arr.length) {               users[data.roomid].push(obj);           }           sockS[data.account] = sock; // 保存不一樣客戶端對應的 sock 實例            // 將房間內成員列表發給房間內全部人           app._io.in(data.roomid).emit('joined', users[data.roomid], data.account, sock.id);       });   });

    後端成員列表的處理,是由於作了多房間的邏輯,按每一個房間的成員表返回的。大家若是作的時候沒有多房間,則不須要這麼考慮。sockS 的處理,是爲了發送私聊消息。

  • 呼叫

    前面已經說了呼叫的注意事項,因此這裏就一塊兒來說。須要注意的就是消息中須要帶有本身和對方的 account,由於這是判斷成員 sock 的標識,也就是以前存儲在 socks 中的用來發私聊消息的。而後是前面說的三種狀態,在這裏用 type 值 1, 2, 3 來區分,而後給出不一樣的回覆。

    // 前端   apply(account) { // 發送請求       // account 對方account self 是本身的account       this.loading = true;       this.loadingText = '呼叫中'; // 呼叫中 loading       socket.emit('apply', {account: account, self: this.account});   },   reply(account, type) { // 處理回覆       socket.emit('reply', {account: account, self: this.account, type: type});   }   // 收到請求   socket.on('apply', data => {       if (this.isCall) { // 判斷是否在通話中           this.reply(data.self, '3');           return;       }       this.$confirm(data.self + ' 向你請求視頻通話, 是否贊成?', '提示', {           confirmButtonText: '贊成',           cancelButtonText: '拒絕',           type: 'warning'       }).then(async () => {           this.isCall = data.self;           this.reply(data.self, '1');       }).catch(() => {           this.reply(data.self, '2');       });   });      // 後端   sock.on('apply', data=>{ // 轉發申請       sockS[data.account].emit('apply', data);   });

    後端比較簡單,僅僅是轉發一下請求,給對應的客戶端。其實咱們這個例子的後端,基本都是這個操做,因此後面的後端代碼就不貼了,能夠去源碼直接看。

  • 回覆

    回覆和和呼叫是同樣的邏輯,分別處理不一樣的回覆就行了。

    // 前端    socket.on('reply', async data =>{ // 收到回覆       this.loading = false;       switch (data.type) {           case '1': // 贊成               this.isCall = data.self; // 存儲通話對象               break;           case '2': //拒絕               this.$message({                   message: '對方拒絕了你的請求!',                   type: 'warning'               });               break;           case '3': // 正在通話中               this.$message({                   message: '對方正在通話中!',                   type: 'warning'               });               break;       }   });
  • 建立鏈接

    呼叫和回覆的邏輯基本清楚了,那咱們繼續思考,應該在什麼時機建立 P2P 鏈接呢?咱們以前說的,拒絕和通話中都不須要處理,只有贊成須要,那就應該在贊成請求的位置開始建立。須要注意的是,贊成請求有兩個地方:一個是你點了贊成,另外一個是對方知道你點了贊成以後。

    本例採起的是呼叫方發送 Offer,這個地方必定得注意,只要有一方建立 Offer 就能夠了,由於一旦鏈接就是雙向的。

    socket.on('apply', data => { // 你點贊成的地方       ...       this.$confirm(data.self + ' 向你請求視頻通話, 是否贊成?', '提示', {           confirmButtonText: '贊成',           cancelButtonText: '拒絕',           type: 'warning'       }).then(async () => {           await this.createP2P(data); // 贊成以後建立本身的 peer 等待對方的 offer           ... // 這裏不發 offer       })       ...   });   socket.on('reply', async data =>{ // 對方知道你點了贊成的地方       switch (data.type) {           case '1': // 只有這裏發 offer               await this.createP2P(data); // 對方贊成以後建立本身的 peer               this.createOffer(data); // 並給對方發送 offer               break;           ...       }   });

    和微信等視頻通話同樣,雙方都須要進行媒體流輸出,由於大家都要看見對方。因此這裏和以前本地對等鏈接的區別就是都須要給本身的 RTCPeerConnection 實例添加媒體流,而後鏈接後各自都能拿到對方的視頻流。在 初始化 RTCPeerConnection 時,記得加上 onicecandidate 函數,用以給對方發送 ICE 候選。

    async createP2P(data) {       this.loading = true; // loading動畫       this.loadingText = '正在創建通話鏈接';       await this.createMedia(data);   },   async createMedia(data) {       ... // 獲取並將本地流賦值給 video 同以前       this.initPeer(data); // 獲取到媒體流後,調用函數初始化 RTCPeerConnection   },   initPeer(data) {       // 建立輸出端 PeerConnection       ...       this.peer.addStream(this.localstream); // 都須要添加本地流       this.peer.onicecandidate = (event) => {       // 監聽ICE候選信息 若是收集到,就發送給對方           if (event.candidate) { // 發送 ICE 候選               socket.emit('1v1ICE',               {account: data.self, self: this.account, sdp: event.candidate});           }       };       this.peer.onaddstream = (event) => {       // 監聽是否有媒體流接入,若是有就賦值給 rtcB 的 src,改變相應loading狀態,賦值省略           this.isToPeer = true;           this.loading = false;           ...       };   }

    createOffer 等信息交換和以前同樣,只是須要經過 Socket 轉發給對應的客戶端。而後各自接收到消息後分別採起對應的措施。

    socket.on('1v1answer', (data) =>{ // 接收到 answer       this.onAnswer(data);   });   socket.on('1v1ICE', (data) =>{ // 接收到 ICE       this.onIce(data);   });   socket.on('1v1offer', (data) =>{ // 接收到 offer       this.onOffer(data);   });      // 這裏只貼一個 createOffer 的代碼,由於和以前的思路都同樣,只是寫法有些區別   // 建議你們都本身敲一遍,有問題能夠交流,也能夠去源碼查看。   async createOffer(data) { // 建立併發送 offer       try {           // 建立offer           let offer = await this.peer.createOffer(this.offerOption);           // 呼叫端設置本地 offer 描述           await this.peer.setLocalDescription(offer);           // 給對方發送 offer           socket.emit('1v1offer', {account: data.self, self: this.account, sdp: offer});       } catch (e) {           console.log('createOffer: ', e);       }   }
  • 掛斷

    掛斷的思路依然是將各自的 peer 關閉,可是這裏掛斷方還須要藉助 Socket 告訴對方,你已經掛電話了,否則對方還在癡癡地等。

    hangup() { // 掛斷通話 並作相應處理 對方收到消息後同樣須要關閉鏈接       socket.emit('1v1hangup', {account: this.isCall, self: this.account});       this.peer.close();       this.peer = null;       this.isToPeer = false;       this.isCall = false;   }
相關文章
相關標籤/搜索