【从头到脚】撸一个多人视频聊天 — 前端 WebRTC 实战(一)

前言

从头到脚 】会做为一个系列文章来发布,它包括但不限于 WebRTC 多人视频,预计会有:javascript

  • WebRTC 实战(一):也就是本期,主要是基础讲解以及一对一的本地对等链接,网络对等链接。
  • WebRTC 实战(二):主要讲解数据传输以及多端本地对等链接、网络对等链接。
  • WebRTC 实战(三):实现一个一对一的视频聊天项目,包括但不限于截图、录制等。
  • WebRTC + Canvas 实现一个共享画板项目。
  • 做者开源做品 💘🍦🙈Vchat — 一个社交聊天系统(vue + node + mongodb) 的系列文章

由于文章输出确实要耗费很大的精力,因此上面计划不必定是这个发布顺序,中间也会穿插发布其它方向的文章,如 Vue、JavaScript 或者其余学习的主题。在文章里,会把我本身遇到过的一些坑点重点提示你们注意,尽可能让你们在学习过程当中少走弯路。固然,个人也并非标准答案,只是我我的的思路。若是你们有更好的方法,能够互相交流,也但愿你们都能有所收获。css

在这里也但愿你们能够 关注 一波,大家的关注支持,也能激励我输出更好的文章。html

先放个效果图,这期的目标是实现一个 1 V 1 的视频通话(笔记本摄像头不能用了,用的虚拟摄像头)。文章比较长,能够 mark 之后慢慢看。前端

文章末尾有 交流群公众号,但愿你们支持,感谢🍻。vue

什么是 WebRTC ?

WebRTC 是由一家名为 Gobal IP Solutions,简称 GIPS 的瑞典公司开发的。Google 在 2011 年收购了 GIPS,并将其源代码开源。而后又与 IETF 和 W3C 的相关标准机构合做,以确保行业达成共识。其中:java

  • Web Real-Time Communications (WEBRTC) W3C 组织:定义浏览器 API。
  • Real-Time Communication in Web-browsers (RTCWEB) IETF 标准组织:定义其所需的协议,数据,安全性等手段。

简单来讲,WebRTC 是一个能够在 Web 应用程序中实现音频,视频和数据的实时通讯的开源项目。在实时通讯中,音视频的采集和处理是一个很复杂的过程。好比音视频流的编解码、降噪和回声消除等,可是在 WebRTC 中,这一切都交由浏览器的底层封装来完成。咱们能够直接拿到优化后的媒体流,而后将其输出到本地屏幕和扬声器,或者转发给其对等端。node

WebRTC 的音视频处理引擎:git

WebRTC 的音视频处理引擎

因此,咱们能够在不须要任何第三方插件的状况下,实现一个浏览器到浏览器的点对点(P2P)链接,从而进行音视频实时通讯。固然,WebRTC 提供了一些 API 供咱们使用,在实时音视频通讯的过程当中,咱们主要用到如下三个:es6

  • getUserMedia:获取音频和视频流(MediaStream)
  • RTCPeerConnection:点对点通讯
  • RTCDataChannel:数据通讯

不过,虽然浏览器给咱们解决了大部分音视频处理问题,可是从浏览器请求音频和视频时,咱们仍是须要特别注意流的大小和质量。由于即使硬件可以捕获高清质量流,CPU 和带宽也不必定能够跟上,这也是咱们在创建多个对等链接时,不得不考虑的问题。github

实现

接下来,咱们经过分析上文提到的 API,来逐步弄懂 WebRTC 实时通讯实现的流程。

getUserMedia

MediaStream

getUserMedia 这个 API 你们可能并不陌生,由于常见的 H5 录音等功能就须要用到它,主要就是用来获取设备的媒体流(即 MediaStream)。它能够接受一个约束对象 constraints 做为参数,用来指定须要获取到什么样的媒体流。

navigator.mediaDevices.getUserMedia({ audio: true, video: true }) 
    // 参数表示须要同时获取到音频和视频
        .then(stream => {
          // 获取到优化后的媒体流
          let video = document.querySelector('#rtc');
          video.srcObject = stream;
        })
        .catch(err => {
          // 捕获错误
        });
复制代码

咱们简单看一下获取到的 MediaStream。

能够看到它有不少属性,咱们只须要了解一下就好,更多信息能够查看 MDN

* id [String]: 对当前的 MS 进行惟一标识。因此每次刷新浏览器或是从新获取 MS,id 都会变更。
* active [boolean]: 表示当前 MS 是不是活跃状态(就是是否能够播放)。
* onactive: 当 active 为 true 时,触发该事件。
复制代码

结合上图,咱们顺便复习一下上期讲的原型和原型链。MediaStream 的 __proto__ 指向它的构造函数所对应的原型对象,在原型对象中又有一个 constructor 属性指向了它所对应的构造函数。也就是说 MediaStream 的构造函数是一个名为 MediaStream 的函数。可能说得有一点绕,对原型还不熟悉的同窗,能够去看一下上期文章 JavaScript 原型和原型链及 canvas 验证码实践

这里也能够经过 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

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)

需求

仍是老规矩,先了解一下需求。图片加载慢,能够直接看演示地址

链接过程涉及到多个环节,这里就不一一截图了,能够直接去演示地址查看。简单分析一下咱们要作的事情: * 加入房间后,获取到房间的全部在线成员。 * 选择任一成员进行通话,也就是呼叫动做。这时候就有一些细节问题要处理:不能呼叫本身、同一时刻只容许呼叫一我的且须要判断对方是不是通话中、呼叫后回复须要有相应判断(赞成、拒绝以及通话中) * 拒绝或通话中,都没有后续动做,能够换我的再呼叫。赞成以后,就要开始创建点对点链接。

加入房间

简单看一下加入房间的流程:

// 前端
  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;
  }
复制代码

参考文章

交流群

qq前端交流群:960807765,欢迎各类技术交流,期待你的加入

后记

若是你看到了这里,且本文对你有一点帮助的话,但愿你能够动动小手支持一下做者,感谢🍻。文中若有不对之处,也欢迎你们指出,共勉。

往期文章:

欢迎关注公众号 前端发动机,第一时间得到做者文章推送,还有各种前端优质文章,但愿在将来的前端路上,与你一同成长。

相关文章
相关标签/搜索