14、Mediasoup实战:Mediasoup架构、Worker/Router/Producer/Consumer、Demo搭建
说实话,WebRTC 的服务器端选型,我折腾过不少。从早期的 Janus、Licode,到后来火起来的 Mediasoup,每个都踩过坑。但如果你问我个人最推荐哪个作为生产环境的选择,我会毫不犹豫地说 Mediasoup。
为什么?因为它够「轻」、够「灵活」。它不像 Janus 那样给你一个完整的「黑盒」,而是把底层能力暴露给你,让你自己控制一切。说白了,Mediasoup 更像是一套乐高积木,而不是一个已经拼好的模型。
这一章,我们就来拆解这套积木。我会从架构讲起,然后深入到 Worker、Router、Producer、Consumer 这几个核心概念,最后带你把 Demo 跑起来。
14.1 Mediasoup 的整体架构
先看一张图,这是我画的 Mediasoup 核心架构图。你盯着看 30 秒,后面讲的所有东西都会围绕它展开。
这张图其实已经把 Mediasoup 的层次关系讲清楚了。从上到下依次是:你的 Node.js 业务层、Worker 进程、Router、以及最底层的 Transport/Producer/Consumer。
我个人习惯把 Mediasoup 理解成一个「媒体引擎工厂」。你给它一个 Worker,它就能生产出 Router;你在 Router 上挂载 Transport,就能收发媒体流。每个环节都是独立的,你可以按需组合。
14.2 Worker:最底层的「发动机」
Worker 是 Mediasoup 最底层的单元。它是一个独立的 C++ 子进程,内部跑着一个 libuv 事件循环。
你可能会问:为什么不用多线程,而是用多进程?
嗯,这里有个关键原因:隔离性。每个 Worker 进程都是独立的,一个 Worker 崩溃不会影响其他 Worker。我在生产环境中就遇到过这种情况——某个房间的 Worker 因为内存泄漏挂掉了,但其他房间完全不受影响。如果是多线程模型,一个线程崩了,整个进程都得挂。
核心要点:
- 每个 Worker 是一个独立的 C++ 进程
- 一个 Node.js 进程可以创建多个 Worker
- Worker 之间完全隔离,互不影响
- 推荐每个 CPU 核心创建 1 个 Worker
创建 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 创建成功,PID: ${worker.pid}`);
return worker;
}
这里有个坑,我曾经踩过:rtcMinPort 和 rtcMaxPort 一定要根据你的服务器防火墙规则来设置。默认范围是 10000-59999,但很多云厂商只开放了 40000-49999 的 UDP 端口。如果你不调整,客户端连不上 ICE 就会一直报错。
14.3 Router:房间的「核心交换机」
一个 Worker 可以创建多个 Router。每个 Router 对应一个「房间」或「会话」。说白了,Router 就是媒体流的交换机——它负责把 Producer 发来的流,转发给所有订阅了这个流的 Consumer。
创建 Router 的代码:
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 });
注意看 mediaCodecs 这个参数。它定义了 Router 支持的编解码器。你想想看,如果客户端发的是 H264,但 Router 没配 H264,那这个流就根本进不来。
我的建议: 生产环境中,尽量把 VP8、H264、VP9 都配上去。虽然 VP8 是 WebRTC 的「通用语言」,但 H264 在硬件编码器上表现更好。我遇到过不少移动端设备,VP8 软编码发热严重,切到 H264 硬编就稳了。
14.4 Producer 和 Consumer:流的「生产者」与「消费者」
这两个概念是 Mediasoup 最核心的抽象。理解它们,你就理解了 Mediasoup 的一半。
- Producer:发送端。一个 Producer 对应一个媒体轨道(音频或视频)。比如一个用户开启了摄像头,就会创建一个视频 Producer。
- Consumer:接收端。一个 Consumer 对应一个接收到的媒体轨道。比如你要看别人的视频,就会创建一个视频 Consumer。
它们之间通过 Transport 来传输数据。Transport 是 WebRTC 的 ICE/DTLS 连接通道。
创建 Producer 的流程:
// 1. 先创建 WebRTC Transport(接收端)
const webRtcTransport = await router.createWebRtcTransport({
listenIps: [{ ip: '0.0.0.0', announcedIp: '你的公网IP' }],
enableUdp: true,
enableTcp: true,
preferUdp: true,
});
// 2. 客户端通过信令交换 ICE 候选和 DTLS 指纹
// 3. 连接建立后,创建 Producer
const producer = await webRtcTransport.produce({
kind: 'video',
rtpParameters: {
codecs: [],
encodings: [],
},
});
创建 Consumer 的流程:
// 1. 同样先创建 Transport
const consumerTransport = await router.createWebRtcTransport({
listenIps: [{ ip: '0.0.0.0', announcedIp: '你的公网IP' }],
});
// 2. 创建 Consumer,订阅某个 Producer
const consumer = await consumerTransport.consume({
producerId: producer.id,
rtpCapabilities: router.rtpCapabilities,
paused: false,
});
注意: Consumer 创建时默认是 unpaused 状态,但实际媒体流并不会立即开始传输。你需要先调用 consumer.resume(),客户端才会收到真正的 RTP 包。这个细节我当年调试了整整一个下午才找到原因。
14.5 Demo 搭建:从零跑通一个视频通话
理论讲完了,我们来点实际的。下面这个 Demo 会创建一个最简单的视频通话房间,支持两个人互相看到对方。
首先,安装依赖:
mkdir mediasoup-demo
cd mediasoup-demo
npm init -y
npm install mediasoup express socket.io
然后,创建服务端代码 server.js:
const mediasoup = require('mediasoup');
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = socketIo(server);
let worker, router;
async function start() {
worker = await mediasoup.createWorker();
router = await worker.createRouter({
mediaCodecs: [
{ kind: 'audio', mimeType: 'audio/opus', clockRate: 48000, channels: 2 },
{ kind: 'video', mimeType: 'video/VP8', clockRate: 90000 },
],
});
io.on('connection', (socket) => {
socket.on('getRouterRtpCapabilities', (_, callback) => {
callback(router.rtpCapabilities);
});
socket.on('createTransport', async (_, callback) => {
const transport = await router.createWebRtcTransport({
listenIps: [{ ip: '0.0.0.0', announcedIp: '127.0.0.1' }],
});
callback({
id: transport.id,
iceParameters: transport.iceParameters,
iceCandidates: transport.iceCandidates,
dtlsParameters: transport.dtlsParameters,
});
});
socket.on('connectTransport', async ({ transportId, dtlsParameters }) => {
const transport = router.getTransport(transportId);
await transport.connect({ dtlsParameters });
});
socket.on('produce', async ({ transportId, kind, rtpParameters }, callback) => {
const transport = router.getTransport(transportId);
const producer = await transport.produce({ kind, rtpParameters });
callback({ id: producer.id });
});
socket.on('consume', async ({ transportId, producerId, rtpCapabilities }, callback) => {
const transport = router.getTransport(transportId);
const consumer = await transport.consume({
producerId,
rtpCapabilities,
paused: false,
});
callback({
id: consumer.id,
kind: consumer.kind,
rtpParameters: consumer.rtpParameters,
});
});
});
server.listen(3000, () => console.log('Server running on port 3000'));
}
start();
这个 Demo 虽然简单,但已经包含了 Mediasoup 的核心流程:
- 客户端获取 Router 的 RTP 能力
- 创建 Transport(服务端生成 ICE 候选和 DTLS 指纹)
- 客户端连接 Transport
- 创建 Producer 发送媒体
- 创建 Consumer 接收媒体
调试技巧: 如果你发现客户端连不上,先检查 announcedIp 是不是填对了。本地测试用 127.0.0.1,公网测试一定要填服务器的公网 IP。我曾经在阿里云上调试,忘了填公网 IP,结果 ICE 候选全是内网地址,客户端死活连不上。
14.6 避坑指南
最后,分享几个我实际项目中踩过的坑:
- 端口范围:Mediasoup 默认的 RTC 端口范围是 10000-59999,但很多云厂商只开放了 40000-49999。记得在创建 Worker 时指定
rtcMinPort和rtcMaxPort。 - Worker 数量:不要创建超过 CPU 核心数的 Worker。每个 Worker 跑一个 libuv 事件循环,Worker 太多反而会因为上下文切换导致性能下降。
- Consumer 的 resume:创建 Consumer 后,一定要调用
consumer.resume(),否则客户端收不到任何数据。 - 编解码器配置:Router 创建时指定的
mediaCodecs决定了它能处理哪些编码。如果客户端发的是 H264 但 Router 没配,流会被直接丢弃。
Mediasoup 的架构设计其实非常优雅。它把复杂的 WebRTC 底层封装成了几个清晰的概念:Worker、Router、Transport、Producer、Consumer。你只要理解了这几个概念之间的关系,就能搭建出稳定、高性能的实时通信系统。
下一章,我们会深入 Mediasoup 的源码,看看它内部是如何处理 RTP 包的转发和路由的。到时候你会发现,这些概念在源码层面是如何落地的。
公众号:蓝海资料掘金营,微信deep3321