WebRTC怎么获取媒体流及对等连接流程

WebRTC (Web Real-Time Communications) 是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和音频流或者其他任意数据的传输。

简单的说,就是 WebRTC 可以不借助媒体服务器,通过浏览器与浏览器直接连接(点对点),即可实现音视频传输。

WebRTC获取媒体流

点对点通信的第一步,一定是发起端获取媒体流。

常见的媒体设备有三种:摄像机麦克风屏幕。其中摄像机和屏幕可以转化为视频流,而麦克风可转化为音频流。音视频流结合起来就组成了常见的媒体流。

以 Chrome 浏览器为例,摄像头和屏幕的视频流获取方式不一样。对于摄像头和麦克风,使用如下 API 获取:

var stream = await navigator.mediaDevices.getUserMedia()

对于屏幕录制,则会用另外一个 API。限制是这个 API 只能获取视频,不能获取音频:

var stream = await navigator.mediaDevices.getDisplayMedia()

注意:这里我遇到过一个问题,编辑器里提示 navigator.mediaDevices == undefined,原因是我的 typescript 版本小于 4.4,升级版本即可。

这两个获取媒体流的 API 有使用条件,必须满足以下两种情况之一:

  • 域名是 localhost
  • 协议是 https

如果不满足,则 navigator.mediaDevices 的值就是 undefined

以上方法都有一个参数 constraints,这个参数是一个配置对象,称为 媒体约束。这里面最有用的是可以配置只获取音频或视频,或者音视频同时获取。

比如我只要视频,不要音频,就可以这样:

let stream = await navigator.mediaDevices.getDisplayMedia({
  audio: false,
  video: true
})

除了简单的配置获取视频之外,还可以对视频的清晰度,码率等涉及视频质量相关的参数做配置。比如我需要获取 1080p 的超清视频,我就可以这样配:

var stream = await navigator.mediaDevices.getDisplayMedia({
  audio: false,
  video: {
    width: 1920,
    height: 1080
  }
})

当然了,这里配置视频的分辨率 1080p,并不代表实际获取的视频一定是 1080p。比如我的摄像头是 720p 的,那即便我配置了 2k 的分辨率,实际获取的最多也是 720p,这个和硬件与网络有关系。

上面说了,媒体流是由音频流和视频流组成的。再说的严谨一点,一个媒体流(MediaStream)会包含多条媒体轨道(MediaStreamTrack),因此我们可以从媒体流中单独获取音频和视频轨道:

// 视频轨道
let videoTracks = stream.getVideoTracks()
// 音频轨道
let audioTracks = stream.getAudioTracks()
// 全部轨道
stream.getTracks()

单独获取轨道有什么意义呢?比如上面的获取屏幕的 API getDisplayMedia 无法获取音频,但是我们直播的时候既需要屏幕也需要声音,此时就可以分别获取音频和视频,然后组成一个新的媒体流。实现如下:

const getNewStream = async () => {
  var stream = new MediaStream()
  let audio_stm = await navigator.mediaDevices.getUserMedia({
    audio: true
  })
  let video_stm = await navigator.mediaDevices.getDisplayMedia({
    video: true
  })
  audio_stm.getAudioTracks().map(row => stream.addTrack(row))
  video_stm.getVideoTracks().map(row => stream.addTrack(row))
  return stream
}

对等连接流程

要说 WebRTC 有什么不优雅的地方,首先要提的就是连接步骤复杂。很多同学就因为总是连接不成功,结果被成功劝退。

对等连接,也就是上面说的点对点连接,核心是由 RTCPeerConnection 函数实现。两个浏览器之间点对点的连接和通信,本质上是两个 RTCPeerConnection 实例的连接和通信。

用 RTCPeerConnection 构造函数创建的两个实例,成功建立连接之后,可以传输视频、音频或任意二进制数据(需要支持 RTCDataChannel API )。同时也提供了连接状态监控,关闭连接的方法。不过两点之间数据单向传输,只能由发起端向接收端传递。

我们现在根据核心 API,梳理一下具体连接步骤。

第一步:创建连接实例

首先创建两个连接实例,这两个实例就是互相通信的双方。

var peerA = new RTCPeerConnection()
var peerB = new RTCPeerConnection()

下文统一将发起直播的一端称为 发起端,接收观看直播的一端称为 接收端

现在的这两个连接实例都还没有数据。假设 peerA 是发起端,peerB 是接收端,那么 peerA 的那端就要像上一步一样获取到媒体流数据,然后添加到 peerA 实例,实现如下:

var stream = await navigator.mediaDevices.getUserMedia()
stream.getTracks().forEach(track => {
  peerA.addTrack(track, stream)
})

当 peerA 添加了媒体数据,那么 peerB 必然会在后续连接的某个环节接收到媒体数据。因此还要为 peerB 设置监听函数,获取媒体数据:

peerB.ontrack = async event => {
  let [ remoteStream ] = event.streams
  console.log(remoteStream)
})

这里要注意:必须 peerA 添加媒体数据之后,才能进行下一步! 否则后续环节中 peerB 的 ontrack 事件就不会触发,也就不会拿到媒体流数据。

第二步:建立对等连接

添加数据之后,两端就可以开始建立对等连接。

建立连接最重要的角色是 SDP(RTCSessionDescription),翻译过来就是 会话描述。连接双方需要各自建立一个 SDP,但是他们的 SDP 是不同的。发起端的 SDP 被称为 offer,接收端的 SDP 被称为 answer。

其实两端建立对等连接的本质就是互换 SDP,在互换的过程中相互验证,验证成功后两端的连接才能成功。

现在我们为两端创建 SDP。peerA 创建 offer,peerB 创建 answer:

var offer = await peerA.createOffer()
var answer = await peerB.createAnswer()

创建之后,首先接收端 peerB 要将 offset 设置为远程描述,然后将 answer 设置为本地描述:

await peerB.setRemoteDescription(offer)
await peerB.setLocalDescription(answer)

注意:当
peerB.setRemoteDescription 执行之后,peerB.ontrack 事件就会触发。当然前提是第一步为 peerA 添加了媒体数据。

这个很好理解。offer 是 peerA 创建的,相当于是连接的另一端,因此要设为“远程描述”。answer 是自己创建的,自然要设置为“本地描述”。

同样的逻辑,peerB 设置完成后,peerA 也要将 answer 设为远程描述,offer 设置为本地描述。

await peerA.setRemoteDescription(answer)
await peerA.setLocalDescription(offer)

到这里,互相交换 SDP 已完成。但是通信还未结束,还差最后一步。

当 peerA 执行 setLocalDescription 函数时会触发 onicecandidate 事件,我们需要定义这个事件,然后在里面为 peerB 添加 candidate

peerA.onicecandidate = event => {
  if (event.candidate) {
    peerB.addIceCandidate(event.candidate)
  }
}

至此,端对端通信才算是真正建立了!如果过程顺利的话,此时 peerB 的 ontrack 事件内应该已经接收到媒体流数据了,你只需要将媒体数据渲染到一个 video 标签上即可实现播放。

还要再提一次:这几步看似简单,实际顺序非常重要,一步都不能出错,否则就会连接失败!如果你在实践中遇到问题,一定再回头检查一下步骤有没有出错。

最后我们再为 peerA 添加状态监听事件,检测连接是否成功:

peerA.onconnectionstatechange = event => {
  if (peerA.connectionState === 'connected') {
    console.log('对等连接成功!')
  }
  if (peerA.connectionState === 'disconnected') {
    console.log('连接已断开!')
  }
}

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(1)

相关推荐

发表回复

登录后才能评论