前端播放webrtc

2,679 阅读8分钟

webrtc简介

  WebRTC (Web Real-Time Communications),是一个由 Google 发起的实时通讯解决方案,其中包含视频音频采集,编解码,数据传输,音视频展示等功能,通过它,我们可以非常方便且快速地构建出一个音视频通讯应用。

  它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。所以它的重点在于通信~ 这次我们主要讲解如何使用 WebRTC一些 api 配合 信令服务建立连接,以及如何将获取到的媒体流进行传输

image.png

了解几个概念

1、SDP

  SDPSession Description Protocol,它是一种用于描述多媒体会话的协议,它可以帮助我们描述媒体流的信息,比如媒体流的类型,编码格式,分辨率等等。WebRTC 通过SDP来交换端与端之间的网络媒体信息。下图中就是一个SDP信息的示例:从中你能大概的看到一些你的内网 IP 信息,外网 IP 信息,以及一些媒体流的信息。

v=0 # SDP版本号
o=- 0 0 IN IP4 120.24.99.xx # 会话标识信息
s=- # 会话名称
t=0 0 # 会话的有效时间
a=group:BUNDLE audio video # 媒体流类型
a=msid-semantic: WMS * # 媒体流标识符
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 126 # 音频媒体流
c=IN IP4 120.24.99.xx # 连接信息
a=rtcp:9 IN IP4 0.0.0.0 # RTCP 的 IP 地址
a=candidate:0 1 UDP 2122252543 120.24.99.xx 9 typ host # 候选 IP 地址
# ...等等等

2、NAT

  NATNetwork Address Translation,网络地址转换,它可以将私有 IP 地址转换为公共 IP 地址,从而实现私有网络与公共网络之间的通信。

  因为 IPv4 的地址空间比较有限,所以我们大多数设备都部署在 NAT 网络内部。(比较贴切的例子就是当你连接 wifi 的时候,我们就处于 NAT 网络内,或者网吧,公司的电脑大多都处在 NAT 网络内,它们共用一个路由器,然后在这些机子的上层,或上 n 层才会有一个有效的全球 IP 公网地址)。

image.png

  IPv6 正在逐步普及,等我们彻底用上了 IPv6,NAT 存在的意义就不大了。

  总而言之,NAT 的存在就是因为 IPv4 的地址数量有限,太多的设备因此只能部署在 NAT 网络内部,所以就引出了我们的内网 IP 地址是不可被外网访问的问题。只需要把它理解为一个可以帮助我们实现内网与外网通信的工具就行。常说的 NAT 打洞内网穿透,就是利用 NAT 的这些特性和一些相关技术来实现的。

3、ICE

  ICEInteractive Connectivity Establishment,交互式连接建立协议,用于在两个主机之间建立连接,它可以在两个主机之间建立连接,即使它们之间的防火墙阻止了直接连接。(可以不借助一个公网 server 完成端到端(Peer to peer,P2P)的通信)。

如何建立连接

  一般来说,一个 WebRTC 应用架构是这样的:

image.png

1. 创建本地的 RTCPeerConnection 对象

  在 WebRTC 中 ,由RTCPeerConnection api 负责创建,保持,监控,关闭连接。它是 WebRTC 的核心 api。

  为了让 WebRTC 的相关 api 在各个浏览器中都能够正常的运行,强烈推荐使用 adapter.js,它是一个垫片,用于将应用程序与 WebRTC 中的规范更改和前缀差异隔离开来。如今,前缀差异大多消失了,但浏览器之间的行为差异仍然存在。 而且,WebRTC 仍在快速发展,因此 adapter.js 是非常有用的。

# 安装它
npm install webrtc-adapter
// 你只需要引入它即可,不需要做任何配置和多余的操作。
import 'webrtc-adapter'

  如果你的应用需要在公网中使用,那么你需要在创建 RTCPeerConnection 对象的时候,传入一个配置对象,配置对象中包含了 STUN 和 TURN 服务器的地址。

  这两个服务器的作用是什么呢?我们先来看看 STUN 服务器。
STUNSession Traversal Utilities for NAT,用来帮助我们获取本地计算机的公网 IP 地址,以及端口号。
TURNTraversal Using Relays around NAT,用来帮助我们穿越 NAT 网关,实现公网中的 WebRTC 连接。一般来说它用来做兜底的,当所有方法都无法穿越 NAT 网关或者无法直接建立 P2P 连接的时候,我们才会使用到它。这时,TURN 服务器会作为一个中继服务器,然后用户的媒体流会通过它来中转传输。

// 内网中使用
const pc = new RTCPeerConnection();
// 公网中使用
const pc = new RTCPeerConnection({
  iceServers: [
    // 免费STUN 服务器
    {
      urls: 'stun:stun.voipbuster.com ',
    },
    // TURN 服务器,这里看实际情况搭建自己的TURN服务器
    {
      urls: 'turn:turn.xxxx.org',
      username: 'webrtc',
      credential: 'turnserver',
    },
  ],
})

2、采集媒体流

  用于建立点对点视频通话、音频通话。当然,如果是仅需要播放远端视频,则本地无需采集媒体流,只需要设置好媒体流需要挂载的音视频元素即可。

<!-- 如果仅播放远端视频流,无需设置local -->
<video id="local" autoplay playsinline muted></video>
<video id="remote" autoplay playsinline></video>

  然后通过 navigator.mediaDevices.getUserMedia 方法来获取媒体流。一般这里的逻辑都是在我们项目加载完成的时候,或者让用户自己手动点击按钮来采集媒体流。采集完后,我们就可以通过 RTCPeerConnection 对象的 addTrack 方法来添加媒体流。将媒体流添加到 RTCPeerConnection 对象的方法还有一个,就是 addStream 方法,但是这个方法已经被废弃了,虽然现在很多教程仍然是使用的它,这里我们不推荐使用(仅播放远端媒体流,无需此步)。

  添加完后我们也需要监听远程的媒体流是否也添加进来,当远程的媒体流也在这样我们才能够将远程的媒体流播放出来。 这里 WebRTC 为我们提供了一个 ontrack 事件,当远程的媒体流添加进来的时候,就会触发这个事件。

  第二部的初始化完整代码如下:

// 初始化
async function init(params: type) {
  // 获取本地端视频标签
  const localVideo = document.getElementById('local') as HTMLVideoElement
  // 获取远程端视频标签
  const remoteVideo = document.getElementById('remote') as HTMLVideoElement

  // 采集本地媒体流
  const localStream = await navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true,
  })
  // 设置本地视频流
  localVideo.srcObject = localStream

  // 不推荐使用:已经过时的方法 [addStream API](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream)
  // pc.addStream(localStream);

  // 添加本地媒体流的轨道都添加到 RTCPeerConnection 中
  localStream.getTracks().forEach((track) => {
    pc.addTrack(track, localStream)
  })

  // 监听远程流,方法一:
  pc.ontrack = (event) => {
    remoteVideo.srcObject = event.streams[0]
  }

  // 方法二:你也可以使用 addStream API,来更加详细的控制流的添加
  // const remoteStream: MediaStream = new MediaStream()
  // pc.ontrack = (event) => {
  //   event.streams[0].getTracks().forEach((track) => {
  //     remoteStream.addTrack(track)
  //   })
  //   // 设置远程视频流
  //   remoteVideo.srcObject = remoteStream
  // }
}

3. 建立连接

  这是实现媒体流播放最重要的一步,建立连接的主要过程就是通过 RTCPeerConnection 对象的 createOffer 方法来创建本地的 SDP 描述,然后通过 RTCPeerConnection 对象的 setLocalDescription 方法来设置本地的 SDP 描述,最后通过 RTCPeerConnection 对象的 setRemoteDescription 方法来设置远程的 SDP 描述。

  • 创建 offer(提案)
// 创建本地/远程 SDP 描述, 用于描述本地/远程的媒体流
let offerSdp = ''
let answerSdp = ''

const createOffer = async () => {
  // 创建 offer
  const offer = await pc.createOffer()
  // 设置本地描述
  await pc.setLocalDescription(offer)
  // await pc.setLocalDescription()
  // 到这里,我们本地的 offer 就创建好了,一般在这里通过信令服务器发送 offerSdp 给远端

  // 监听 RTCPeerConnection 的 onicecandidate 事件,当 ICE 服务器返回一个新的候选地址时,就会触发该事件
  pc.onicecandidate = async (event) => {
    if (event.candidate) {
      offerSdp.value = JSON.stringify(pc.localDescription)
    }
  }
}

  其中的 onicecandidate 事件,是用来监听 ICE 服务器返回的候选地址,当 ICE 服务器返回一个新的候选地址时,就会触发该事件,这里我们需要将这个候选地址发送给远端,这样远端才能够和我们建立连接。

  当然,你也可以在peerConnection.setLocalDescription()时,不传参数,这样 WebRTC 会默认调用peerConnection.createOffer()来创建 offer。

const offer = await peerConnection.createOffer()
await peerConnection.setLocalDescription(offer)
console.log(peerConnection)
// 不传参数,默认调用 peerConnection.createOffer()
await peerConnection.setLocalDescription()
console.log(peerConnection)

  我们可以把它打印一下,可以看到一样是能够创建出 offer 并设置到本地描述中的。但是不推荐这么做,WebRTC 还在不断的更新中,本身就存在很多实验性的 API,所以我们还是要遵循规范来。该传就传。😅

  ok,下一步就到了我们的第 3 步,也就是创建 offer 并发送给远程端。并监听远程端的 answer。拿到 answer 后,我们就可以设置到远程端的描述中。

  • 创建 answer

  作为接收方,在拿到 offer 后,我们就可以创建 answer 并设置到本地描述中,然后通过信令服务器发送 answer 给对端。

const createAnswer = async () => {
  // 解析字符串
  const offer = JSON.parse(offerSdp)
  pc.onicecandidate = async (event) => {
    // Event that fires off when a new answer ICE candidate is created
    if (event.candidate) {
      answerSdp = JSON.stringify(pc.localDescription)
    }
  }
  await pc.setRemoteDescription(offer)
  const answer = await pc.createAnswer()
  await pc.setLocalDescription(answer)
}
  • 添加 answer

接收方拿到 answer 后,就可以设置到远程端的描述中。

// 添加 answer(应答)
const addAnswer = async () => {
  const answer = JSON.parse(answerSdp)
  if (!pc.currentRemoteDescription) {
    pc.setRemoteDescription(answer)
  }
}

  一个最简单的 WebRTC 通信流程就完成了。是不是感觉形似 TCP 的三次握手? 这样,我们就完成了从采集媒体流,建立连接,传输的全过程。如果仅播放远端视频,这里的建立连接,可以通过发送alax请求,向服务器拿到远端的offer和answer。