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 举报,一经查实,本站将立刻删除。