什么?你还不懂WebRTC?来带你入门

大家好,我是刘布斯。

随着我们的生活越来越数字化,实时通信变得更重要,而 WebRTC 技术正在重新定义我们对实时通信的理解。

无论是视频会议、在线游戏、远程教育还是医疗保健,WebRTC都为这些场景提供了简单、高效、安全的解决方案。

作为一名前端的开发者,需要关注 WebRTC 所带来的变革。

让我们今天一块来感受 WebRTC 的魅力!

什么是 WebRTC

WebRTC (Web Real-Time Communications) 是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。WebRTC 包含的这些标准使用户在无需安装任何插件或者第三方的软件的情况下,创建点对点(Peer-to-Peer)的数据分享和电话会议成为可能。

WebRTC 的历史和发展

  1. 2011年5月:Google发布了WebRTC项目,旨在提供浏览器之间实时音视频通信的能力,无需安装插件或第三方应用程序。
  2. 2012年1月:Google将WebRTC纳入Chromium项目,开始在Chrome浏览器中提供原生支持。
  3. 2013年6月:WebRTC标准被提交给Internet Engineering Task Force(IETF)和World Wide Web Consortium(W3C)进行标准化。
  4. 2014年3月:WebRTC标准被W3C推荐为候选推荐标准。
  5. 2015年12月:WebRTC 1.0标准被W3C正式推荐为标准。
  6. 2017年1月:WebRTC工作组开始致力于WebRTC 1.0的扩展和改进。

WebRTC 的应用场景

  • 直播
  • 游戏
  • 视频会议/在线教育
  • 屏幕共享/远程控制
  • 等等等

兼容性

WebRTC 的核心组件

注意:摄像头和麦克风属于用户的隐私设备,在调用API的时候需要满足安全源的访问(chrome官方文档),即只能是在 HTTPS 协议或 localhost 下使用

  • getUserMedia:获取媒体

以前的版本中使用 navigator.getUserMedia 来获取计算机的摄像头或者麦克风,但是现在这个接口废弃,变更为 navigator.mediaDevices.getUserMedia

getUserMedia 除了可以获取默认摄像头和麦克风,还可以控制获取到媒体的分辨率,以及其他的以一些可选项。

注意设备信息中,音频、视频的输入/输出信息里,ID字段是设备信息的核心,后续对媒体的控制都需要用到。

获取设备信息

function initInnerLocalDevice({
  const that = this
  const localDevice = {
    audioIn: [],
    videoIn: [],
    audioOut: [],
  }
  const constraints = { videotrueaudiotrue }
  
  if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
    console.log('浏览器不支持获取媒体设备')
    return
  }
  
  navigator.mediaDevices
    .getUserMedia(constraints)
    .then(function (stream{
      stream.getTracks().forEach((trick) => {
        trick.stop()
      })
      // List cameras and microphones.
      navigator.mediaDevices
        .enumerateDevices()
        .then(function (devices{
          console.log('devices', devices)
          devices.forEach(function (device{
            const obj = { id: device.deviceId, kind: device.kind, label: device.label }
            if (device.kind === 'audioinput') {
              if (localDevice.audioIn.filter((e) => e.id === device.deviceId).length === 0) {
                localDevice.audioIn.push(obj)
              }
            }
            if (device.kind === 'audiooutput') {
              if (localDevice.audioOut.filter((e) => e.id === device.deviceId).length === 0) {
                localDevice.audioOut.push(obj)
              }
            } else if (device.kind === 'videoinput') {
              if (localDevice.videoIn.filter((e) => e.id === device.deviceId).length === 0) {
                localDevice.videoIn.push(obj)
              }
            }
          })
        })
        .catch(handleError)
    })
    .catch(handleError)
}

function handleError(error{
  alert('摄像头无法正常使用,请检查是否占用或缺失')
  console.error('navigator.MediaDevices.getUserMedia error: ', error.message, error.name)
}

媒体约束 constraints

// 获取音频与摄像头
const constraints = { videotrueaudiotrue }

function handleError(error{
  console.error('navigator.MediaDevices.getUserMedia error: ', error.message, error.name)
}

/**
 * 获取设备 stream
 * @param constraints
 * @returns {Promise<MediaStream>}
 */

async function getLocalUserMedia(constraints{
  return await navigator.mediaDevices.getUserMedia(constraints)
}

const stream = await this.getLocalUserMedia(constraints).catch(handleError)
console.log(stream)

使用特定的网络摄像头或者麦克风

/**
 * 获取指定媒体设备id对应的媒体流
 * @param videoId
 * @param audioId
 * @returns {Promise<void>}
 */

async function getTargetIdStream(videoId, audioId{
  const constraints = {
    audio: { deviceId: audioId ? { exact: audioId } : undefined },
    video: {
      deviceId: videoId ? { exact: videoId } : undefined,
      width1920,
      height1080,      
    },
  }
  if (window.stream) {
    window.stream.getTracks().forEach((track) => {
      track.stop()
    })
  }
  
  const stream = await this.getLocalUserMedia(constraints).catch(handleError)
}

getDisplayMedia 分享屏幕

注意:这里的 constraints 配置和前面getUserMedia的约束配置是有差别的。在屏幕分享的约束中,video 是不能设置为 false 的,但是可以设置指定的分辨率

/**
 * 获取屏幕分享的媒体流
 * @returns {Promise<void>}
 */

async function getShareMedia({
  const constraints = {
    video: { width1920height1080 },
    audiofalse,
  }
  if (window.stream) {
    window.stream.getTracks().forEach((track) => {
      track.stop()
    })
  }
  return await navigator.mediaDevices.getDisplayMedia(constraints).catch((error) => {
    console.error('navigator.MediaDevices.getUserMedia error: ', error.message, error.name)
  })
}

RTCPeerConnection:建立点对点连接

RTCPeerConnection是一个由本地计算机到远端的 WebRTC 连接,该接口提供创建,保持,监控,关闭连接的方法的实现,可以简单理解为功能强大的 socket 连接。

RTCPeerConnection 的一些主要用途:音频和视频通信、数据通道、屏幕共享、多对多通信、网络穿透

至于它是如何保障端与端之间的连通性,如何保证音视频的服务质量,又如何确定使用的是哪个编解码器等问题,作为应用者的我们大可不必关心,因为所有的这些问题都已经在 RTCPeerConnection 对象的底层实现好了。

const localPc = new RTCPeerConnection(rtcConfig)
// 将音视频流添加到 RTCPeerConnection 对象中
localStream.getTracks().forEach((track) => {  localPc.addTrack(track, localStream)})

在第一步获取音视频流后,需要将流添加到创建的 RTCPeerConnection 对象中,当 RTCPeerConnection 对象获得音视频流后,就可以开始与对端进行媒协体协商。

什么是媒体协商

媒体协商的作用是找到双方共同支持的媒体能力,如双方各自支持的编解码器,音频的参数采样率,采样大小,声道数、视频的参数分辨率,帧率等等。

上述说到的这些音频/视频的信息都会在SDP(Session Description Protocal:即使用文本描述各端的“能力”) 中进行描述。

一对一的媒体协商大致如下:首先自己在 SDP 中记录自己支持的音频/视频参数和传输协议,然后进行信令交互,交互的过程会同时传递 SDP 信息,另一方接收后与自己的 SDP 信息比对,并取出它们之间的交集,这个交集就是它们协商的结果,也就是它们最终使用的音视频参数及传输协议。

媒体协商过程

一对一通信中,发起方发送的 SDP 称为Offer(提议),接收方发送的 SDP 称为Answer(应答)。

每端保持两个描述:描述本身的本地描述LocalDescription,描述呼叫的远端的远程描述RemoteDescription

当通信双方 RTCPeerConnection 对象创建完成后,就可以进行媒体协商了,大致过程如下:

  1. 发起方创建 Offer 类型的 SDP,保存为本地描述后再通过信令服务器发送到对端;
  2. 接收方接收到 Offer 类型的 SDP,将 Offer 保存为远程描述;
  3. 接收方创建 Answer 类型的 SDP,保存为本地描述,再通过信令服务器发送到发起方,此时接收方已知道连接双方的配置;
  4. 发起方接收到 Answer 类型的 SDP 后保存到远程描述,此时发起方也已知道连接双方的配置;
  5. 整个媒体协商过程处理完毕。

更详细的步骤请参考 MDN 中对会话描述讲解。

什么是信令服务器

信令可以简单理解为消息,在协调通讯的过程中,为了建立一个 WebRTC 的通讯过程,在通信双方彼此连接、传输媒体数据之前,它们要通过信令服务器交换一些信息,如加入房间、离开房间及媒体协商等,而这个过程在 WebRTC 里面是没有实现的,需要自己搭建信令服务。

代码实现媒体协商过程

通过 MDN 先了解下我们需要用到的 API:

  • createOffer用于创建 Offer;
  • createAnswer用于创建 Answer;
  • setLocalDescription用于设置本地 SDP 信息;
  • setRemoteDescription用于设置远端的 SDP 信息。

下面来看下代码:

  • 发起方创建 RTCPeerConnection
// 配置
export const rtcConfig = null
const localPc = new RTCPeerConnection(rtcConfig)
  • 发起方/接收方创建 Offer 保存为本地描述
let offer = await localPc.createOffer()
// 保存为本地描述
await localPc.setLocalDescription(offer)
// 通过信令服务器发送到对端
socket.emit('offer', offer)
  • 接受 Offer 后 创建 Answer 并发送
socket.on('offer', offer) => {  
    // 将 Offer 保存为远程描述;  
    remotePc = new RTCPeerConnection(rtcConfig)  
    await remotePc.setRemoteDescription(offer)  
    let remoteAnswer = await remotePc.createAnswer()  
    await remotePc.setLocalDescription(remoteAnswer)  
    socket.emit('answer', remoteAnswer)
});
  • 接受 Answer 存储为远程描述
// 发起方接收到 Answer 类型的 SDP 后保存到远程描述,此时发起方也已知道连接双方的配置;
socket.on('answer', answer) => {  
    // 将 Answer 保存为远程描述;  
    await localPc.setRemoteDescription(answer);
});

至此,媒体协商结束,紧接着在 WebRTC 底层会收集Candidate,并进行连通性检测,最终在通话双方之间建立起一条链路来。

RTCDataChannel:建立和管理数据通道

这个数据通道可以用来发送任何类型的数据,包括文本、文件、二进制数据等。

RTCDataChannel 的工作方式与 WebSocket 非常相似,但是它是在已经存在的 RTCPeerConnection 上建立的,这意味着它继承了 RTCPeerConnection 的连接特性。

RTCDataChannel 的主要用途包括:文本聊天、文件共享、游戏、实时控制。

WebRTC 的安全性

  1. 端到端加密:WebRTC 的所有类型的通信,包括音频、视频和数据通道,都使用安全的实时传输协议 (SRTP) 进行端到端加密。这意味着只有发送方和接收方可以解密和查看数据,即使数据经过了中间的服务器,服务器也无法解密数据。这是一个非常重要的特性,它保护了用户的隐私和数据安全。
  2. 身份验证:WebRTC 使用 DTLS(数据报文传输层安全协议,是 TLS 的 UDP 版本)进行身份验证。这确保了连接的另一端是你期望的服务器或客户端,防止了中间人攻击。
  3. 完全的 P2P 连接:一旦建立了连接,数据就直接在用户之间传输,没有中间服务器。这降低了数据被拦截或篡改的风险。
  4. 同源策略:WebRTC 遵守同源策略,这意味着一个网页只能访问与其在同一源(协议、域名和端口)的 WebRTC 组件。这防止了跨站点脚本攻击。
  5. 用户权限:获取用户的媒体设备(如麦克风和摄像头)需要用户的明确许可。浏览器通常会在请求这些设备时显示一个提示,让用户选择是否允许访问。

WebRTC 的优缺点

优点

  1. 实时通信:WebRTC 支持浏览器之间的实时音频、视频和数据通信,无需任何插件或第三方软件。
  2. 高质量的音视频通信:WebRTC 使用最先进的音频和视频编解码器,如 Opus 和 VP8/VP9,可以提供高质量的通信体验。
  3. 端到端加密:WebRTC 的所有通信都是端到端加密的,保护了用户的隐私和数据安全。
  4. P2P 连接:WebRTC 支持直接的 P2P 连接,可以减少延迟和带宽消耗,提高通信效率。
  5. 跨平台和跨浏览器:WebRTC 是一个开源的标准,被大多数现代浏览器和平台支持。

缺点

  1. 复杂的信令过程:WebRTC 本身并不包含信令协议,开发者需要自己实现信令过程,增加了开发的复杂性。
  2. 防火墙和 NAT 问题:在某些网络环境下,建立 P2P 连接可能会受到防火墙和 NAT 的限制。
  3. 隐私问题:虽然 WebRTC 的通信是加密的,但仍有可能泄露用户的 IP 地址,可能会引发一些隐私问题。
  4. 资源消耗:实时音视频通信需要消耗大量的 CPU 和带宽资源,可能会影响设备的性能。

WebRTC 应用

最后

还没有使用过我们刷题网站(https://fe.ecool.fun/)或者刷题小程序的同学,如果近期准备或者正在找工作,千万不要错过,题库主打无广告和更新快哦~。

老规矩,也给我们团队的辅导服务打个广告。