Vue、WebRTC、SocketIO、Node和Redis助力多人视频会议(上)
几个月来疫情持续蔓延,大家使用视频会议应用的次数也水涨船高。无论身处何地,只要有视频应用我们就能与朋友见面。那我们为什么不试着定制一款自己的会议应用,让会话变得更加真实呢? 话不多说,我们直接开始吧!
Zoom截图
实现这个目标不需要我们从头开始。在之前的文章中,我已经介绍过构建具有一对一私人视频功能的应用所需的各个步骤。那么我们就以此为基础对其进行完善,最后添加多对多视频的功能就大功告成了。
(因为下文我们会提到一些在上一篇文章中解释过的概念,如果你之前没有读过,我建议你先移步上一篇文章。)
基于需求,视频会议应具备以下功能:
创建会议的用户自动成为管理员,只有他/她能邀请其他人进入会议;
管理员只能邀请同一公共房间的用户进入会议;
无论是启动会议还是加入会议,用户都不能与其他用户私下交谈;
已经进入某个会议的用户不能进行私聊,也就是说没有人可以和他/她对话;
如果管理员结束会议,所有用户会自动退出会议;
测试视频会议功能
在深入探讨实现细节之前,我们先来了解一下webRTC多方架构的主要方法。
网状结构( Mesh )
Mesh是最简单的一种架构。所有端之间都是相互连接的,会直接把自己的媒体发送到其他所有端上。
Mesh webRTC架构
优点:
. 基本和简单的webRTC实现
. 不需要多媒体中心服务器
缺点:
. 加载和带宽消耗(N-1上行和下行链路)过多;
. 不能扩展容纳过多端(最多4-6个端)。
混合和MCU (Multipoint Conference Unit)
每个端将其媒体发送到中心服务器,并从中心服务器接收媒体。MCU作为一个混合点,接收、解码和混合来自所有端的媒体,最后以单一流的形式发送给所有用户。
MCU混合架构
优点:
. 在客户端基本实现webRTC
. 每个端都有1个上行和下行链路
缺点:
. 需要具备强大处理能力的服务器端(解码和编码每个端的媒体)。
比如Kurento提供MCU媒体服务器来实现视频应用(下文提到的SFU拓扑结构除外)。
路由和 SFU(Selective Forward Unit)
每个端将自己的媒体发送到中心服务器,并从它那里接收所有其他的媒体流。SFU就像一个媒体的路由器,接收所有用户的媒体流,然后决定将哪些流发送给哪些用户。
SFU路由架构
优点:
. 服务器端计算成本较低(比MCU低)
. 非对称带宽(1个上行链路和N-1个下行链路)即可,适合ADSL连接。
缺点:
. 服务器端设计和实现较复杂。
SFU有三种不同的方法路由媒体:多单播、同播和SVC(可扩展视频编码)。像OpenVidu和Mediasoup等供应商都提供了这几种拓扑。
给予我们的需求,我们决定实现第一种方法——最多支持3个端口的网状拓扑(但还是能扩展到更多用户的)。接下来我们来讨论下实现的细节。
完善措施
在开始讨论构建app的细节前,我们先进一步改进之前的架构,以期获取更简洁、架构更合理的代码。
WebRTC 通信机制
如前所述,在mesh架构中,所有的端与端之间都是直接连接的,之前私聊中两端间建立连接的机制和配置都没变。因此,我们在WebRTC.js文件中把这些相关项当作一个mixin单拎出来了:
export const videoConfiguration = {
data() {
return {
constraints: {}, // Media constraints
configuration: servers, // TURN/STUN ice servers
// Offer config
offerOptions: {
offerToReceiveAudio: 1,
offerToReceiveVideo: 1
},
// Local video
myVideo: undefined,
localStream: undefined,
username: ""
}
},
created() {
this.username = this.$store.state.username
},
// Method implementations
methods: {
async getUserMedia() { ... },
getAudioVideo() { ... },
async setRemoteDescription(remoteDesc, pc) {
try {
log(`${this.username} setRemoteDescription: start`)
await pc.setRemoteDescription(remoteDesc)
log(`${this.username} setRemoteDescription: finished`)
} catch (error) {
log(`Error setting the RemoteDescription in ${this.username}. Error: ${error}`)
}
},
async createOffer(pc, to, room, conference = false) { ... },
async createAnswer(pc, to, room, conference) { ... },
async handleAnswer(desc, pc, from, room, conference = false) { ... },
sendSignalingMessage(desc, offer, to, room, conference) { ... },
addLocalStream(pc) { ... },
addCandidate(pc, candidate) { ... },
onIceCandidates(pc, to, room, conference = false) { ... },
},
}
(WebRTC mixin)