webrtc实现群聊系列文章(一)本地模拟视频通话

2,775 阅读5分钟

前言

  • 本文首先会介绍webrtc中的一些基本概念,并通过示例代码一步步完成一个简单的本地视频模拟通话,不通过网络转接。
  • 示例代码及其重要,是完整实践配合基础概念的分部介绍。
  • 每一段话请注意仔细理解,我会尽可能简单的介绍基础概念。
  • 请确保你的电脑是有摄像头的,台式机没有摄像头外接一个就可以

媒体设备

在开发Web时,WebRTC标准提供了API,用于访问连接到计算机或智能手机的相机和麦克风。这些设备通常称为媒体设备,可以通过实现MediaDevices接口的navigator.mediaDevices对象使用JavaScript进行访问。通过该对象,我们可以枚举所有已连接的设备,侦听设备更改(连接或断开设备时),并打开设备以检索媒体流


调用getUserMedia()将触发权限请求。如果用户接受许可,则通过包含一个视频和一个音轨的MediaStream来解决承诺。如果权限被拒绝,则抛出PermissionDeniedError 。如果没有连接匹配的设备,则会抛出NotFoundError


  • 获取媒体流示例代码

 // createMedia
 async createMedia() {
        let streamTep = null;
        // 保存本地流到全局
        streamTep = await navigator.mediaDevices.getUserMedia({audio: true, video: true})
        console.log("streamTep",streamTep)
        return streamTep;
 },

着重点:navigator.mediaDevices.getUserMedia()函数

  • 本地播放媒体流
 <div style="float: left">
      <video id="sucA"></video>
  </div>
  // 本地摄像头打开
async nativeMedia(){
        const that = this;
        that.localStream = await this.createMedia()
        let video = document.querySelector('#sucA');
        // 旧的浏览器可能没有srcObject
        if ("srcObject" in video) {
          video.srcObject = that.localStream;
        } else {
          video.src = window.URL.createObjectURL(that.localStream);
        }
        // eslint-disable-next-line no-unused-vars
        video.onloadedmetadata = function(e) {
          video.play();
        };
        that.initPeer(); // 获取到媒体流后,调用函数初始化 RTCPeerConnection
},

着重点:播放本地媒体流实质就是将上一步拿到的媒体流赋值到对应的媒体标签组件,在这里就是给video标签

  • 媒体设备约束条件
//比如设置视频窗口的范围
{
    "video": {
        "width": {
            "min": 640,
            "max": 1024
        },
        "height": {
            "min": 480,
            "max": 768
        }
    }
}
//获取手机端前置摄像头
{ audio: true, video: { facingMode: "user" } }
//后置摄像头
{ audio: true, video: { facingMode: { exact: "environment" } } }
//具有带宽限制的WebRTC传输,可能需要较低的帧速率
{ video: { frameRate: { ideal: 10, max: 15 } } }


着重点:通俗的讲就是对音频或者视频进行约束,约束条件参数入口就是上面示例中的获取媒体流的函数 getUserMedia({audio: true, video: true})


名词:ICE,STUN,NAT,TURN,SDP解释

交互式连接建立(ICE)是允许您的Web浏览器与对等方连接的框架。A到B两个客户端能够点对点通信的基础就是建立通信,但是很多情况下两个设备之间并没有公网IP,而且还有防火墙这些阻断数据传输,这个时候就需要STUN来为你提供一个独一无二的地址和TURN服务器中继数据。

更详细的介绍请看 MDN,具体不再介绍


RTCPeerConnection

该RTCPeerConnection接口表示本地计算机和远程对等方之间的WebRTC连接。它提供了连接到远程对等方,维护和监视连接以及在不再需要连接时关闭连接的方法。

RTCPeerConnection之间如何建立

  • 本地流获取(上述已解释)
  • 全局参数初始化
--------------------全局初始化PeerConnection--------------------------
 var PeerConnection = window.RTCPeerConnection ||
         window.mozRTCPeerConnection ||
         window.webkitRTCPeerConnection;
-----------------------iceServers  stun和turn服务器配置-------------------------------------------
var iceServers = {
         iceServers: [
           { url: "stun:stun.l.google.com:19302"},// 谷歌的公共服务
   		{
           url: 'turn:numb.viagenie.ca',
           credential: 'muazkh',
           username: 'webrtc@live.com'
         }
         ]
       };

  • 初始化两个模拟客户端
      //以下pc和pc2  分别代表两个模拟客户端的链接服务简写  ( pc: 代表pc->pc2链接  pc2:代表pc2->pc链接 )
       const that = this;
       that.pc = new PeerConnection(iceServers);
       that.pc2 = new PeerConnection(iceServers);
       //将全局视频流赋给pc链接服务
       that.pc.addStream(this.localStream);
        //监听ice候选信息  具体下面会说明
       that.pc.onicecandidate = function(event) {
         console.log("pc onicecandidate",event)
         if (event.candidate) {
   	  //一般来说这个地方是通过第三方(socket后面会将网络端点对点)发送给另一个客户端,但是现在本地演示直接将候选信息发送到pc2链接服务
           that.pc2.addIceCandidate(event.candidate.toJSON());
         }
       };
   	//监听远程视频 pc充当呼叫端,所以只要监听pc2有无视频流信息进来
       that.pc2.onaddstream = (event) => {
         console.log("onaddstream",event)
   	  //监听到流后将视频流赋给另一个video标签
         let video = document.querySelector('#sucB');
         video.srcObject = event.stream;
         video.onloadedmetadata = function(e) {
           console.log(e)
           video.play();
         };
       };

onicecandidate: 候选ICE描述了WebRTC能够与远程设备进行通信所需的协议和路由。在启动WebRTC对等连接时,通常在连接的每一端都建议多个候选对象,直到他们相互同意描述他们认为最好的连接的候选对象为止。

  • 呼叫端模拟呼叫(pc充当呼叫端)
//创建offer
 async createOffer() {
        const that = this;
        //创建offer
        let offer_tep = await that.pc.createOffer(this.offerOption);
        console.log("offer_tep",offer_tep)
        //设置本地描述
        await that.pc.setLocalDescription(offer_tep)
        //接收端设置远程 offer 描述
        await that.pc2.setRemoteDescription(offer_tep)
        // 接收端创建 answer
        let answer = await that.pc2.createAnswer();
        // 接收端设置本地 answer 描述
        await that.pc2.setLocalDescription(answer);
        // 发送端 设置远程 answer 描述
        await that.pc.setRemoteDescription(answer);
      },

      //呼叫
      async callA() {
        const that = this;
        //创建offer 并保存本地描述
        await that.createOffer()
      },

为何呼叫会有这么麻烦的步骤呢?这就又涉及到webrtc的会话了,具体看下面一条

webrtc会话

“当用户(上述pc)向另一个用户(上述pc2)发起WebRTC呼叫时,会创建一个特殊的描述,称为offer。此描述包括有关呼叫者为呼叫建议的配置的所有信息。然后,接收者用一个答案来回应,这是他们通话结束的描述。以此方式,两个设备彼此共享为了交换媒体数据所需的信息。这种交换是使用交互式连接建立(ICE)处理的,该协议允许两个设备使用中介程序交换要约和答复,即使两个设备之间都被网络地址转换(NAT)隔开。然后,每个对等方都保留两个描述:本地描述(描述自己)和远程描述(描述呼叫的另一端)”


上面的话简单来说就是 A呼叫B,A创建offer,在本地保留offer,然后发送给B,B创建应答,之后本地保留应答,再将应该发送给A,A拿到后将B的应该设置为本地的远程描述。

  • 图示

后续

  • 本文是webrtc实现群聊系列文章的第一篇,持续更新中
  • 文章来源公众号 苏克分享