30、项目实战二:多人视频会议系统
终于到了最后一个实战项目。说实话,多人视频会议系统是WebRTC应用里最典型的场景,也是最考验架构能力的。我当年在创业公司做第一个视频会议产品时,踩过的坑能写满一本笔记本。今天咱们就把这些经验一次性梳理清楚。
一、系统架构设计
多人视频会议,说白了就是让N个人同时看到彼此的画面和声音。这里有个核心问题:如果每个人都给其他人发一路流,那N个人就需要N×(N-1)路连接。10个人就是90路,20个人就是380路——客户端根本扛不住。
所以我们需要一个中心节点来做转发。这就是SFU(Selective Forwarding Unit)架构。
核心架构:SFU模式
每个客户端只上传一路视频流到服务器,服务器根据订阅关系,把流转发给其他参与者。客户端只接收自己需要的流,大大降低了带宽和CPU压力。
我的经验:早期我尝试过MCU(多点控制单元)架构,服务器端混流,客户端只收一路。但MCU的CPU消耗太高,而且灵活性差。后来全面转向SFU,配合Simulcast( simulcast ),效果好了不止一个档次。
二、功能模块实现
一个完整的多人视频会议系统,至少需要以下几个核心模块。我按实现顺序来讲。
1. 房间管理模块
用户进入会议前,必须先加入一个房间。房间就是逻辑上的会议空间。
// 房间管理核心逻辑
class RoomManager {
constructor() {
this.rooms = new Map(); // roomId -> Room
}
createRoom(roomId, hostId) {
const room = {
id: roomId,
host: hostId,
participants: new Map(),
createdAt: Date.now()
};
this.rooms.set(roomId, room);
return room;
}
joinRoom(roomId, participant) {
const room = this.rooms.get(roomId);
if (!room) throw new Error('房间不存在');
room.participants.set(participant.id, participant);
// 通知其他用户有新成员加入
this.broadcastToRoom(roomId, {
type: 'user-joined',
userId: participant.id
});
return room;
}
leaveRoom(roomId, userId) {
const room = this.rooms.get(roomId);
if (!room) return;
room.participants.delete(userId);
// 如果房间没人了,销毁房间
if (room.participants.size === 0) {
this.rooms.delete(roomId);
}
}
}
注意:房间ID一定要做防冲突处理。我曾经在生产环境遇到过两个团队同时创建了相同ID的房间,结果两拨人串会了。后来我改用UUID+时间戳的组合方式,再也没出过问题。
2. 媒体协商模块
每个新加入的用户,都需要和SFU服务器建立PeerConnection。这里有个关键点:不是用户之间直连,而是用户和SFU连接。
// 客户端与SFU建立连接
async function connectToSFU(sfuUrl, roomId, userId) {
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
// 获取本地媒体流
const localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
// 添加本地轨道
localStream.getTracks().forEach(track => {
pc.addTrack(track, localStream);
});
// 创建offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 通过信令发送offer到SFU
const answer = await signaling.sendOffer({
roomId,
userId,
sdp: offer
});
await pc.setRemoteDescription(answer);
return { pc, localStream };
}
3. 订阅管理模块
用户进入房间后,不能一股脑把所有流都拉下来。我建议默认只订阅大画面和音频,其他用户用小画面展示。用户点击某个小画面时,再切换成大流。
| 订阅策略 | 说明 | 适用场景 |
|---|---|---|
| 全量订阅 | 订阅所有参与者的所有流 | 2-4人小会议 |
| 按需订阅 | 只订阅当前活跃说话者的高清流 | 5-10人中型会议 |
| 分层订阅 | 根据屏幕布局动态调整订阅质量 | 10人以上大型会议 |
避坑指南:我曾经在20人会议上默认全量订阅,结果用户的MacBook风扇直接起飞,CPU冲到100%。后来改成按需订阅+Simulcast分层,CPU降到了30%以下。
三、性能测试与优化
性能测试这块,我建议分三步走:单机压测、网络模拟、真实场景验证。
1. 单机压测
用一台高性能服务器模拟多个客户端,同时连接SFU。重点看这几个指标:
- CPU使用率:SFU进程的CPU占用,超过80%就要警惕
- 内存占用:每个连接大约消耗2-5MB内存
- 带宽吞吐:服务器网卡的上行/下行流量
- 连接数上限:系统能同时维持多少路PeerConnection
2. 网络模拟
用tc(Linux流量控制)模拟各种网络环境:
# 模拟丢包5%
tc qdisc add dev eth0 root netem loss 5%
# 模拟延迟100ms
tc qdisc add dev eth0 root netem delay 100ms
# 模拟带宽限制1Mbps
tc qdisc add dev eth0 root tbf rate 1mbit burst 32kbit latency 400ms
我习惯在三种网络条件下测试:
- 良好网络:延迟<30ms,丢包<1%
- 一般网络:延迟50-100ms,丢包1-3%
- 差网络:延迟>200ms,丢包5%以上
3. 关键优化手段
经过多次实战,我总结出几个最有效的优化点:
- Simulcast分层编码:发送端同时编码多个分辨率,接收端按需选择。这是最核心的优化,没有之一。
- 动态码率调整:根据网络状况实时调整视频码率。我用的是Google Congestion Control算法,效果不错。
- 音频优先:在网络拥塞时,优先保证音频的传输。视频可以降分辨率,但音频不能断。
- 空闲连接清理:定期检查长时间没有数据传输的连接,主动断开释放资源。
我的小技巧:在SFU端做一层视频流缓存。当新用户加入时,不用等I帧,直接发送缓存的最近一个关键帧,能减少50%以上的加入延迟。
四、总结
多人视频会议系统,说白了就是三个核心:架构选SFU、订阅按需来、优化看网络。我做了这么多项目,发现最容易出问题的不是技术本身,而是对网络状况的预估不足。你想想看,用户可能用4G、可能用WiFi、甚至可能用卫星网络,每种场景下的表现都不一样。
所以我的建议是:先跑通基础功能,然后立刻上性能测试。别等到上线了才发现扛不住并发。嗯,今天就聊到这里,希望这些实战经验能帮你少走弯路。
公众号:蓝海资料掘金营,微信deep3321