18、SFU服务器搭建(mediasoup):mediasoup简介、安装与配置、创建Router和Transport、处理Producer和Consumer
聊到SFU服务器,我第一个想到的就是mediasoup。说实话,在WebRTC的SFU方案里,它是我个人最偏爱的一个。为什么?因为它够轻、够快、够灵活。你想想看,很多SFU框架封装得特别重,改个参数都要翻半天文档。mediasoup不一样,它把底层逻辑暴露给你,让你自己掌控一切。
我在项目中遇到过好几次这样的场景:客户要求低延迟、高并发,还要支持动态推流。试了一圈方案,最后还是mediasoup扛住了。嗯,今天我们就来把它拆开看看。
mediasoup是什么?
mediasoup是一个基于C++实现的SFU引擎,上层通过Node.js暴露API。它不依赖任何第三方媒体库,自己实现了RTP/RTCP协议栈。说白了,它就是一个专门处理音视频流的转发器。
它的核心特点我列一下:
- 纯SFU架构:不混入MCU功能,专注转发
- Worker进程模型:每个Worker是一个独立进程,跑在单独的CPU核心上
- Pipe传输:支持Router之间的流透传,方便做分布式部署
- Simulcast/SVC:原生支持多码率流,接收端按需订阅
我个人习惯把mediasoup比作一个「智能交换机」——它不处理媒体内容,只负责把数据包从生产者送到消费者手里,而且知道怎么走最省带宽。
核心概念速览
- Worker:运行媒体处理的进程,一个Node.js应用可以启动多个Worker
- Router:媒体路由单元,一个Worker下可以有多个Router,每个Router相当于一个独立的房间
- Transport:传输通道,分为WebRTC Transport和Plain Transport
- Producer:生产者,往Transport上推送音视频流
- Consumer:消费者,从Transport上拉取音视频流
安装与配置
安装mediasoup其实不复杂,但有几个坑要注意。我直接说步骤:
# 1. 安装Node.js(推荐v16以上)
# 2. 创建项目目录
mkdir mediasoup-demo
cd mediasoup-demo
npm init -y
# 3. 安装mediasoup
npm install mediasoup
# 4. 安装开发依赖(TypeScript用户需要)
npm install --save-dev @types/node typescript
注意:mediasoup在安装时会编译C++代码,所以你的机器上需要安装C++编译工具链。
- Linux:
sudo apt install build-essential python3 - macOS:
xcode-select --install - Windows:安装Visual Studio Build Tools
我曾经在Windows上踩过这个坑——编译到一半报错,查了半天发现是Python版本不对。mediasoup需要Python 3.6以上,而且环境变量要配好。嗯,这里要注意。
配置方面,mediasoup提供了默认配置,但生产环境我建议你手动调一下:
const mediasoup = require('mediasoup');
const workerSettings = {
logLevel: 'warn', // 日志级别:debug, warn, error
logTags: ['info', 'ice', 'dtls', 'rtp', 'srtp', 'rtcp'],
rtcMinPort: 40000, // RTC端口范围(防火墙要放开)
rtcMaxPort: 49999,
};
async function createWorker() {
const worker = await mediasoup.createWorker(workerSettings);
console.log('Worker创建成功,PID:', worker.pid);
return worker;
}
个人建议:开发阶段可以把logLevel设为'debug',方便排查问题。上线前改成'warn',不然日志量太大。
创建Router和Transport
Worker创建好了,接下来就是Router。Router你可以理解为一个「房间」,每个房间里的流是隔离的。
async function createRouter(worker) {
const 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,
},
},
];
const router = await worker.createRouter({ mediaCodecs });
return router;
}
这里有个细节:mediaCodecs数组里定义的编码,是Router支持的编码列表。客户端推流时,只能使用这里列出的编码。我一般会把VP8和H264都加上,兼容性最好。
Transport是流进出的通道。WebRTC Transport是最常用的,它封装了ICE、DTLS这些协议。
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
maxSctpMessageSize: 262144,
});
console.log('Transport创建成功');
console.log('ICE参数:', transport.iceParameters);
console.log('DTLS参数:', transport.dtlsParameters);
return transport;
}
关键点:announcedIp这个字段特别重要。如果你的服务器在内网,客户端在外网,不填这个的话,ICE协商会失败。我曾经因为这个排查了整整一下午。
处理Producer和Consumer
有了Transport,客户端就可以推流了。客户端通过信令把rtpParameters发过来,服务端创建Producer。
// 服务端接收推流
async function handleProduce(transport, rtpParameters, kind) {
const producer = await transport.produce({
kind, // 'audio' 或 'video'
rtpParameters, // 客户端发来的RTP参数
});
producer.on('transportclose', () => {
console.log('Transport关闭,Producer停止');
producer.close();
});
console.log(`Producer创建成功,ID: ${producer.id}`);
return producer;
}
Consumer是拉流端。当有新的消费者加入时,服务端需要根据已有的Producer创建Consumer。
async function handleConsume(transport, producer, rtpCapabilities) {
// 检查消费者是否支持该编码
if (!router.canConsume({ producerId: producer.id, rtpCapabilities })) {
console.error('消费者不支持该编码');
return null;
}
const consumer = await transport.consume({
producerId: producer.id,
rtpCapabilities,
paused: false, // 是否一开始就暂停
});
consumer.on('transportclose', () => {
console.log('Transport关闭,Consumer停止');
consumer.close();
});
console.log(`Consumer创建成功,ID: ${consumer.id}`);
return consumer;
}
你想想看,一个房间里如果有10个人同时推流,每个人都要看到其他9个人的画面。那就需要为每个观看者创建9个Consumer。这就是SFU的核心逻辑——一对多转发。
避坑指南:Consumer创建后,记得把consumer.rtpParameters通过信令发给客户端。客户端需要用这个参数来初始化接收端的PeerConnection。我刚开始做的时候忘了这一步,结果客户端一直收不到流。
最后,Producer和Consumer的生命周期管理也很重要。当用户离开房间时,要记得关闭对应的Transport,所有关联的Producer和Consumer会自动清理。
// 关闭Transport
transport.close();
// 或者单独关闭Producer/Consumer
producer.close();
consumer.close();
嗯,到这里mediasoup的核心流程就串起来了。从Worker到Router,从Transport到Producer和Consumer,每一步都有它的设计意图。我个人觉得mediasoup最妙的地方在于,它把复杂的媒体协商和传输细节都封装好了,你只需要关注业务逻辑——谁推流、谁拉流、怎么转发。
实际项目中,你还需要搭配信令服务器(比如Socket.IO)来协调客户端和服务端的交互。不过那是另一块内容了,今天我们先把mediasoup的骨架搭好。
公众号:蓝海资料掘金营,微信deep3321