15、Mediasoup实战:搭建Mediasoup服务器、创建Router、管理Transport、处理Producer
好,咱们今天来点真格的。前面讲了那么多WebRTC的理论,现在该上手Mediasoup了。说实话,我第一次接触Mediasoup的时候,也被它那套Router、Transport、Producer的概念绕得有点晕。但等你真正跑起来,就会发现这套设计其实非常优雅。
这一章,我会带你从零搭建一个Mediasoup服务器。咱们不搞花架子,直接上代码。我会把我在项目中踩过的坑、总结的经验都揉进去。你跟着走一遍,基本就能掌握Mediasoup的核心玩法了。
15.1 Mediasoup是什么?先搞清楚定位
Mediasoup是一个SFU(Selective Forwarding Unit)架构的WebRTC服务器。说白了,它不负责混流、不转码,只做一件事:把每个客户端的音视频流,按需转发给其他人。
为什么选它?我个人习惯用它,因为:
- 纯C++实现,性能极高,延迟低得离谱
- Node.js封装,前端同学也能快速上手
- 模块化设计,Router、Transport、Producer各司其职
- 支持Simulcast和SVC,带宽自适应做得很好
核心概念速览:
- Router:相当于一个独立的会议室,每个房间一个Router
- Transport:客户端与服务器之间的传输通道,分WebRTC Transport和Plain Transport
- Producer:客户端向服务器推送的流(音频或视频)
- Consumer:服务器向客户端转发的流
嗯,这里要注意:Mediasoup的API设计是异步事件驱动的。你写代码的时候,脑子里要时刻绷着这根弦。
15.2 搭建Mediasoup服务器
咱们先搭一个最简的服务器。你想想看,一个视频会议系统,第一步肯定是把服务器跑起来对吧?
15.2.1 安装依赖
npm install mediasoup
npm install express socket.io # 辅助用的
Mediasoup的npm包会编译原生C++代码。我第一次装的时候,卡在编译环节半小时。后来发现是Node.js版本太高了。建议你用Node.js 16.x或18.x LTS版本,稳得很。
15.2.2 启动Mediasoup Worker
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,
});
console.log(`Worker进程ID: ${worker.pid}`);
return worker;
}
这里有个坑:rtcMinPort和rtcMaxPort决定了WebRTC使用的UDP端口范围。我在项目中遇到过,如果服务器防火墙没放开这些端口,客户端死活连不上。所以部署前,一定记得检查端口策略。
15.2.3 创建Router
Router就是会议室。每个房间独立,互不干扰。
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/VP9',
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;
}
为什么定义这么多编码?说白了,客户端设备千奇百怪。有的支持VP8,有的只认H264。你提前声明好,Mediasoup才能做协商。我曾经遇到一个坑:只配了VP8,结果iPhone Safari死活不出画面——因为Safari只支持H264。
15.3 管理Transport
Transport是客户端和服务器之间的管道。每个客户端加入房间,都需要创建两个Transport:一个用于发送(Producer),一个用于接收(Consumer)。
15.3.1 创建WebRTC 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
});
console.log(`Transport ID: ${transport.id}`);
// 返回给客户端的信息
return {
id: transport.id,
iceParameters: transport.iceParameters,
iceCandidates: transport.iceCandidates,
dtlsParameters: transport.dtlsParameters,
};
}
注意:announcedIp这个字段特别容易出错。如果服务器在内网,客户端在外网,你必须填公网IP。否则ICE协商会失败。我当初调试的时候,卡在这里整整一天。
15.3.2 Transport连接流程
客户端拿到Transport参数后,会做两件事:
- 用
iceParameters和iceCandidates建立ICE连接 - 用
dtlsParameters建立DTLS加密通道
服务器端需要监听dtlsstatechange事件:
transport.on('dtlsstatechange', (dtlsState) => {
if (dtlsState === 'connected') {
console.log('DTLS连接建立成功');
} else if (dtlsState === 'failed') {
console.error('DTLS连接失败');
}
});
嗯,这里有个小技巧:DTLS连接成功后,你才能开始收发流。所以建议在connected状态后再通知客户端开始推流。
15.4 处理Producer
Producer就是客户端推上来的流。每个Transport可以创建多个Producer(比如一个音频、一个视频)。
15.4.1 接收客户端的Producer请求
async function createProducer(transport, kind, rtpParameters) {
const producer = await transport.produce({
kind, // 'audio' 或 'video'
rtpParameters, // 客户端传过来的RTP参数
});
console.log(`Producer创建成功: ${producer.id}, 类型: ${kind}`);
producer.on('transportclose', () => {
console.log('Transport关闭,Producer自动停止');
});
return producer;
}
客户端传过来的rtpParameters长什么样?大概是这样:
{
"codecs": [
{
"mimeType": "video/VP8",
"payloadType": 96,
"clockRate": 90000,
"parameters": {}
}
],
"encodings": [
{ "ssrc": 12345678 }
],
"rtcp": {
"cname": "user123"
}
}
你可能会问:这些参数哪来的?其实是客户端通过getUserMedia获取本地流后,用pc.getTransceivers()拿到的。Mediasoup的客户端SDK(比如mediasoup-client)会自动处理这些细节。
15.4.2 管理Producer的生命周期
Producer创建后,你需要把它存起来,方便后续做转发:
// 用Map存储所有Producer
const producers = new Map();
// 创建时存入
producers.set(producer.id, {
producer,
userId: socket.id,
kind: producer.kind,
transportId: transport.id,
});
// 监听关闭事件
producer.on('producerclose', () => {
console.log(`Producer ${producer.id} 已关闭`);
producers.delete(producer.id);
// 通知其他用户:有人停止推流了
});
经验之谈:我建议你在Producer创建时,顺便把它的RTP参数广播给房间里的其他人。这样其他客户端可以提前创建Consumer,减少延迟。说白了,就是「预连接」的思路。
15.5 完整流程串讲
好,咱们把整个流程串起来,看看一个用户加入房间的全过程:
- 客户端连接Socket.IO,发送
joinRoom事件 - 服务器创建Router(如果还没有)
- 服务器创建两个WebRTC Transport(发送和接收)
- 服务器把Transport参数返回给客户端
- 客户端用参数创建PeerConnection,建立ICE+DTLS连接
- 连接成功后,客户端通过Transport创建Producer推流
- 服务器收到Producer,广播给房间其他人
- 其他人创建Consumer,开始接收流
你看,其实逻辑并不复杂。但每一步都有细节,尤其是ICE和DTLS的协商,稍有不慎就会翻车。
15.6 核心架构图
下面这张图,是我自己总结的Mediasoup核心架构。你看一眼,基本就能理解各个组件的关系:
从图里你能看到:一个Worker可以创建多个Router,每个Router管理多个Transport,Transport再承载Producer和Consumer。这种分层设计,让Mediasoup能轻松支持成千上万的并发房间。
15.7 避坑指南
最后,我把自己在项目中踩过的坑总结一下,你遇到类似问题可以少走弯路:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 客户端连不上服务器 | 端口未开放或announcedIp错误 | 检查防火墙,确保UDP端口范围开放 |
| DTLS连接失败 | 证书问题或网络不稳定 | 重启Transport,或者检查TLS版本 |
| Producer创建后无流 | RTP参数不匹配 | 核对codec的mimeType和payloadType |
| 高并发下内存泄漏 | Producer/Consumer未正确清理 | 监听close事件,及时从Map中删除 |
特别提醒:Mediasoup的Worker是独立进程。如果Worker崩溃,所有房间都会挂。生产环境建议用workerObserver做进程守护,或者用PM2管理。
好了,这一章的内容就到这。代码你都拿到了,建议自己动手跑一遍。遇到问题别慌,对照着避坑指南排查,基本都能解决。
公众号:蓝海资料掘金营,微信deep3321