12、SFU服务器搭建:基于mediasoup的SFU部署、房间与Peer管理、媒体流转发
聊到SFU,很多刚接触WebRTC的朋友第一反应是:「这东西是不是特别重?」
其实不然。我最早接触mediasoup时,也被它的C++底层吓到过。但用下来发现,它的Node.js层封装得相当优雅。说白了,mediasoup就是一个跑在服务端的「媒体路由器」——它不负责编解码,只负责把各路媒体流高效地转发给需要的人。
12.1 mediasoup的核心架构
先看一张图,把整体逻辑理清楚。
看到这个分层结构了吗?mediasoup启动后,会创建多个Worker(C++子进程),每个Worker里可以跑多个Router。我一般把每个Router对应一个「房间」。Router内部管理着Transport、Producer、Consumer这些核心对象。
嗯,这里有个关键点:Worker是真正的媒体处理单元,它跑在独立的进程中。这样做的好处是——某个Worker挂了,不会影响其他Worker。我在生产环境里就遇到过Worker内存泄漏的情况,幸好其他房间没受影响。
12.2 部署mediasoup服务
部署其实不复杂。先装依赖,再写几行核心代码就能跑起来。
// 安装 mediasoup
npm install mediasoup
// 创建 Worker 和 Router
const mediasoup = require('mediasoup');
async function createWorker() {
const worker = await mediasoup.createWorker({
logLevel: 'warn',
logTags: ['info', 'ice', 'dtls', 'rtp', 'srtp', 'rtcp'],
rtcMinPort: 40000,
rtcMaxPort: 49999
});
worker.on('died', () => {
console.error('Worker 进程挂了,赶紧重启!');
// 我一般在这里做自动拉起
});
return worker;
}
async function createRouter(worker) {
const router = await worker.createRouter({
mediaCodecs: [
{
kind: 'audio',
mimeType: 'audio/opus',
clockRate: 48000,
channels: 2
},
{
kind: 'video',
mimeType: 'video/VP8',
clockRate: 90000,
parameters: {
'x-google-start-bitrate': 1000
}
},
{
kind: 'video',
mimeType: 'video/H264',
clockRate: 90000,
parameters: {
'packetization-mode': 1,
'profile-level-id': '42e01f',
'level-asymmetry-allowed': 1
}
}
]
});
return router;
}
12.3 房间与Peer管理
房间管理说白了就是维护一个Map。Key是房间ID,Value是Router实例。Peer管理也类似,每个房间里维护一个Peer列表。
// 房间管理器
class RoomManager {
constructor() {
this.rooms = new Map(); // roomId -> { router, peers }
}
async createRoom(roomId, worker) {
if (this.rooms.has(roomId)) {
console.warn(`房间 ${roomId} 已存在`);
return this.rooms.get(roomId);
}
const router = await createRouter(worker);
const room = {
router,
peers: new Map() // peerId -> { transports, producers, consumers }
};
this.rooms.set(roomId, room);
console.log(`房间 ${roomId} 创建成功`);
return room;
}
getRoom(roomId) {
return this.rooms.get(roomId);
}
closeRoom(roomId) {
const room = this.rooms.get(roomId);
if (room) {
room.router.close();
this.rooms.delete(roomId);
console.log(`房间 ${roomId} 已关闭`);
}
}
}
// Peer 加入房间
async function joinRoom(room, peerId) {
if (room.peers.has(peerId)) {
console.warn(`Peer ${peerId} 已在房间中`);
return room.peers.get(peerId);
}
const peer = {
id: peerId,
transports: new Map(),
producers: new Map(),
consumers: new Map()
};
room.peers.set(peerId, peer);
console.log(`Peer ${peerId} 加入房间`);
return peer;
}
12.4 媒体流转发核心逻辑
媒体流转发是SFU的灵魂。说白了就是:A发视频,B、C、D都要看,那服务器就把A的流复制三份,分别推给B、C、D。
mediasoup里,这个逻辑通过Producer和Consumer来实现:
- Producer:代表一个Peer发出的媒体流(音频或视频)
- Consumer:代表一个Peer接收的媒体流
- Transport:Peer和Router之间的连接通道(WebRTC Transport或Plain Transport)
// 创建 Transport(每个 Peer 需要创建自己的 Transport)
async function createWebRtcTransport(router) {
const transport = await router.createWebRtcTransport({
listenIps: [
{
ip: '0.0.0.0', // 监听所有网卡
announcedIp: '你的公网IP' // 生产环境一定要填公网IP
}
],
enableUdp: true,
enableTcp: true,
preferUdp: true,
initialAvailableOutgoingBitrate: 1000000 // 初始1Mbps
});
return transport;
}
// 创建 Producer(Peer 发布自己的流)
async function createProducer(transport, kind, rtpParameters) {
const producer = await transport.produce({
kind, // 'audio' 或 'video'
rtpParameters
});
return producer;
}
// 创建 Consumer(为其他 Peer 订阅流)
async function createConsumer(router, consumerTransport, producer) {
// 检查是否可以消费
const canConsume = await router.canConsume({
producerId: producer.id,
rtpCapabilities: consumerTransport.rtpCapabilities
});
if (!canConsume) {
console.warn('无法消费该流,可能是编解码器不匹配');
return null;
}
const consumer = await consumerTransport.consume({
producerId: producer.id,
rtpCapabilities: consumerTransport.rtpCapabilities,
paused: false // 是否一开始就暂停
});
return consumer;
}
核心转发逻辑:
当Peer A发布视频后,服务器需要遍历房间内所有其他Peer,为每个Peer创建一个Consumer。这样每个Peer都能看到A的画面。
但要注意——不要给发送者自己创建Consumer,否则会出现回声或画面循环。
12.5 实际项目中的避坑指南
我在做第一个mediasoup项目时,踩了不少坑。挑几个典型的说说:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 客户端连不上 | announcedIp 没填公网IP | 填写服务器的公网IP或域名 |
| 视频卡顿严重 | 初始码率设得太高 | initialAvailableOutgoingBitrate 设为 500000 |
| 内存持续增长 | Peer断开后没清理 | 监听 'close' 事件,及时释放资源 |
| 部分浏览器没画面 | H264 参数不匹配 | profile-level-id 用 42e01f |
12.6 完整的媒体流转发流程
把上面的代码串起来,一个典型的流转发流程是这样的:
- 服务端启动Worker,创建Router(每个房间一个)
- Peer加入房间,创建WebRTC Transport
- Peer发布媒体流,创建Producer
- 服务端遍历房间内其他Peer,为每个Peer创建Consumer
- 当有新的Peer加入时,服务端自动为其创建已有Producer的Consumer
- 当Peer离开时,清理其所有Transport、Producer、Consumer
嗯,这个流程看起来简单,但实际写代码时要注意并发问题。比如两个Peer同时加入房间,可能会竞争创建Transport。我一般用队列或者锁来处理,保证每个Peer的操作是串行的。
最后说一句:mediasoup的文档其实写得不错,但有些细节藏在源码里。遇到问题别慌,去GitHub的issues里搜一搜,八成能找到答案。我每次升级版本都会先看看changelog,避免踩坑。
公众号:蓝海资料掘金营,微信deep3321