30、项目实战:从零搭建多人视频会议系统
说实话,走到这一章,前面那些理论、协议、信令、NAT穿透,终于要拧成一股绳了。
我经常被问到:“WebRTC学了一大堆,到底怎么落地?”
嗯,今天我们就亲手搭一个多人视频会议系统。从需求分析开始,到架构设计,再到编码实现,最后部署上线。一条龙走完。
30.1 需求分析:我们要做什么?
先别急着写代码。我有个习惯——动手前,先想清楚“到底要解决什么问题”。
多人视频会议,核心需求其实就几条:
- 多人实时音视频通话:至少支持4-6人同时开摄像头、麦克风。
- 屏幕共享:演示PPT、代码、设计稿。
- 文字聊天:边开会边发消息,不打断发言。
- 房间管理:创建房间、加入房间、离开房间。
- 基础控制:静音、关闭摄像头、切换布局。
你想想看,这些需求其实对应着WebRTC的几个核心模块:
| 需求 | 技术对应 |
|---|---|
| 多人音视频 | WebRTC PeerConnection + SFU/Mesh |
| 屏幕共享 | getDisplayMedia API |
| 文字聊天 | WebSocket / DataChannel |
| 房间管理 | 信令服务器 + 房间状态维护 |
| 控制功能 | MediaStreamTrack.enabled 切换 |
我个人建议,第一版不要贪多。先把核心通话跑通,再慢慢加功能。
30.2 架构设计:选Mesh还是SFU?
这是架构设计里最关键的决策点。
我简单解释一下:
- Mesh架构:每个客户端直接跟其他所有客户端建立P2P连接。4个人开会,每人要建3个连接。8个人?每人7个连接。上行带宽爆炸。
- SFU架构:客户端只跟服务器建一个连接。服务器负责转发音视频流。客户端只发一路,收多路。
我在早期一个项目里试过Mesh方案,4个人还行,加到6个人就开始卡顿、掉帧。后来全改成SFU了。
所以,我们选SFU。
SFU服务器我们选用 mediasoup。为什么?
- 纯Node.js实现,部署简单
- 性能好,支持几百路转发
- 文档清晰,社区活跃
整体架构图如下:
30.3 编码实现:核心模块拆解
好,架构定下来了。我们开始编码。
整个项目分成三块:
- 信令服务器(Node.js + WebSocket)
- SFU媒体服务器(Node.js + mediasoup)
- 客户端(React + WebRTC API)
30.3.1 信令服务器
信令服务器说白了就是个“中间人”。它不碰音视频数据,只负责传递控制信息。
// server/signaling.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 3001 });
const rooms = new Map(); // 房间管理
wss.on('connection', (ws) => {
ws.on('message', (message) => {
const data = JSON.parse(message);
switch(data.type) {
case 'join-room':
handleJoinRoom(ws, data.roomId, data.userId);
break;
case 'offer':
relayToRoom(data.roomId, data, ws);
break;
case 'answer':
relayToRoom(data.roomId, data, ws);
break;
case 'ice-candidate':
relayToRoom(data.roomId, data, ws);
break;
case 'leave-room':
handleLeaveRoom(ws, data.roomId);
break;
}
});
});
嗯,这里要注意:信令服务器必须维护好房间内所有用户的连接列表。我曾经踩过一个坑——用户断线重连后,房间状态没清理干净,导致别人收到一堆无效的ICE候选。
30.3.2 SFU媒体服务器
mediasoup的使用其实不复杂。核心就三步:
- 创建Worker(一个Worker就是一个CPU核心)
- 创建Router(一个Router对应一个房间)
- 创建Producer/Consumer(生产者和消费者)
// server/mediasoup.js
const mediasoup = require('mediasoup');
let worker;
let router;
async function createWorker() {
worker = await mediasoup.createWorker({
logLevel: 'warn',
rtcMinPort: 40000,
rtcMaxPort: 49999,
});
router = await worker.createRouter({
mediaCodecs: [
{
kind: 'video',
mimeType: 'video/VP8',
clockRate: 90000,
parameters: {},
},
{
kind: 'audio',
mimeType: 'audio/opus',
clockRate: 48000,
channels: 2,
},
],
});
return router;
}
我个人的习惯是,每个房间创建一个独立的Router。这样隔离性好,一个房间出问题不会影响其他房间。
30.3.3 客户端实现
客户端这边,主要工作是:
- 获取本地媒体流(getUserMedia)
- 创建PeerConnection
- 通过信令服务器交换SDP和ICE
- 把远程流渲染到video标签
// client/webrtc.js
async function startCall(roomId) {
// 1. 获取本地流
const localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
// 2. 创建PeerConnection
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});
// 3. 添加本地流
localStream.getTracks().forEach(track => {
pc.addTrack(track, localStream);
});
// 4. 监听远程流
pc.ontrack = (event) => {
const remoteVideo = document.getElementById('remote-video');
remoteVideo.srcObject = event.streams[0];
};
// 5. 创建Offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 6. 通过信令发送Offer
signaling.send({
type: 'offer',
roomId: roomId,
sdp: offer,
});
}
小技巧: 调试阶段可以把ICE候选日志打开,看看是不是卡在“连通性检查”这一步。很多时候连不上,就是STUN/TURN配置的问题。
30.4 部署上线:从本地到云端
代码写完了,怎么让全世界的人都能用?
我推荐用 Docker + Nginx + Let's Encrypt 这套组合拳。
- Docker化:把信令服务器、SFU服务器、前端静态文件分别打包成镜像。
- Nginx反向代理:统一入口,处理HTTPS和WebSocket升级。
- Let's Encrypt:免费SSL证书。WebRTC强制要求HTTPS,这个不能省。
# nginx.conf
server {
listen 443 ssl;
server_name meeting.example.com;
ssl_certificate /etc/letsencrypt/live/meeting.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/meeting.example.com/privkey.pem;
# 前端静态文件
location / {
root /usr/share/nginx/html;
index index.html;
}
# WebSocket信令
location /ws {
proxy_pass http://signaling:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# mediasoup的UDP端口
location /mediasoup {
proxy_pass http://mediasoup:3002;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
注意: mediasoup使用UDP端口传输媒体数据。云服务器安全组一定要开放UDP端口范围(比如40000-49999)。我见过有人折腾半天连不上,最后发现是安全组只开了TCP。
30.5 避坑指南:我踩过的那些坑
- ICE候选不完整:我曾经在部署时忘了配置TURN服务器,结果有些用户死活连不上。后来加了coturn,问题解决。
- 音频回声:多人会议最烦的就是回声。记得在客户端启用回声消除:
audio: { echoCancellation: true }。 - 视频布局:4个人以上,全铺开太乱。我建议用“演讲者模式”——谁说话谁放大。
- 带宽控制:默认情况下WebRTC会尽量用满带宽。在SFU端做一下码率限制,避免某个用户把服务器带宽吃光。
好了,到这里,一个完整的多人视频会议系统就搭起来了。
从需求分析到架构设计,从编码实现到部署上线,每一步都有坑,但也每一步都有收获。
你想想看,当你在浏览器里打开页面,看到自己和几个朋友的脸同时出现在屏幕上,那种感觉——嗯,挺爽的。