一、故事的开端:你有没有想过?
当你在腾讯会议、Zoom、飞书会议里点击"加入会议"后,几秒钟内就能看到其他人的画面、听到他们的声音------这背后发生了什么?
最简单的方案是"点对点"连接,但10个人开会就需要45个连接!更好的方案是 SFU(选择性转发单元) :大家把视频发给服务器,服务器转发给其他人。Mediasoup 就是这样的服务器。本文讲基于Mediasoup讲述这背后服务之间是如何进行配合的。
二、三个角色,各司其职

| 服务 | 比喻 | 职责 |
|---|---|---|
| mediasoup-ui | 电视机 | 采集画面、播放声音、用户交互 |
| signal-bridge | 信号转换器 | 协议翻译(JSON ↔ protoo) |
| signal-server | 播控中心 | 管理房间、转发媒体流 |
三、一次视频会议的"生命旅程"
让我们跟随一个用户"小马"的视角,看看他从加入会议到看到其他人画面的完整过程:
第一步:小马打开网页 📺
sequenceDiagram
小马->>UI: 点击加入会议
UI->>Server: 建立websocket连接
Server-->>小马: 准备好接收和发送媒体流
第二步:获取"电视频道列表" 📋
js
// 小马问服务器:你们支持哪些视频格式?
const routerRtpCapabilities = await this.signaling.request('getRouterRtpCapabilities');
// 小马的浏览器检查:这些格式我支持吗?
this.device = new mediasoupClient.Device();
await this.device.load({ routerRtpCapabilities });
// 如果没有报错,说明可以正常通信!
通俗解释:就像你买了一个新电视,先要检查能不能收到当地电视台的信号格式(高清还是标清)。
第三步:铺设"信号线" 🔌
小马需要两条"线":
- 发送线:把小马的画面传给服务器
- 接收线:从服务器接收其他人的画面
js
async createTransports() {
// 📤 创建发送线
const sendInfo = await this.signaling.request('createWebRtcTransport', {
forceTcp: false,
appData: { direction: 'producer' }, // 我是生产者
});
this.sendTransport = this.device.createSendTransport({
id: sendInfo.transportId,
iceParameters: sendInfo.iceParameters, // 冰块参数(网络地址)
iceCandidates: sendInfo.iceCandidates, // 候选地址列表
dtlsParameters: sendInfo.dtlsParameters, // 加密参数
});
// 📥 创建接收线(代码类似)
const recvInfo = await this.signaling.request('createWebRtcTransport', {
appData: { direction: 'consumer' }, // 我是消费者
});
this.recvTransport = this.device.createRecvTransport({...});
}
Transport: 就像一根水管,你需要两根------一根往里注水(发送),一根往外放水(接收)。
第四步:服务器端铺设"水管" 🏗️
服务器收到请求后,在 mediasoup 里创建真正的 Transport:
js
// signal-server/Room.ts
const transport = await mediasoupRouter.createWebRtcTransport({
webRtcServer: mediasoupWebRtcServer, // 共享端口服务器
enableUdp: true, // 支持UDP(更快)
enableTcp: true, // 支持TCP(更稳定)
appData: { direction }, // 记录这是发送还是接收
});
// 返回给客户端
resolve({
transportId: transport.id,
iceParameters: transport.iceParameters,
iceCandidates: transport.iceCandidates,
dtlsParameters: transport.dtlsParameters,
});
第五步:小马打开摄像头 📹
js
async enableMic({ stream } = {}) {
// 1. 向浏览器申请摄像头/麦克风权限
const localStream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: false
});
const track = localStream.getAudioTracks()[0];
// 2. 通过发送线,把画面发出去
this.micProducer = await this.sendTransport.produce({ track });
}
关键来了! 当调用 produce() 时,会触发一个事件:
js
// 监听 'produce' 事件 - 这是 WebRTC 的核心!
this.sendTransport.on('produce', async ({ kind, rtpParameters }, callback) => {
// 通知服务器:我要发送一个媒体流
const { producerId } = await this.signaling.request('produce', {
transportId: this.sendTransport.id,
kind, // 'audio' 或 'video'
rtpParameters, // 编码参数
});
// 告诉本地 Transport:服务器已经准备好了
callback({ id: producerId });
});
第六步:服务器创建 Producer 🎙️
服务器收到请求后,创建一个"生产者"对象:
js
// signal-server/Peer.ts
case 'produce': {
const { transportId, kind, rtpParameters, appData } = data;
const transport = this.getTransport(transportId);
// 🎯 核心API:创建 Producer
const producer = await transport.produce({
kind, // 音频还是视频
rtpParameters, // 编码参数
appData: {
peerId: this.id, // 是谁发的
source: 'mic', // 来源是什么
},
});
// 🔔 重要:触发事件,通知房间里其他人
this.emit('new-producer', { producer });
// 返回 Producer ID 给客户端
accept({ producerId: producer.id });
}
第七步:其他用户收到小马的画面 👥
Room 监听到 new-producer 事件后,会为其他用户创建 Consumer:
js
// signal-server/Room.ts
peer.on('new-producer', async ({ producer }) => {
// 获取房间里除了小明以外的所有人
const otherPeers = this.getOtherPeers(peer);
// 为每个人创建 Consumer(消费者)
for (const otherPeer of otherPeers) {
await otherPeer.consume({ producer });
}
});
创建 Consumer 的详细过程:
js
// signal-server/Peer.ts
async consume({ producer }) {
const transport = this.getRecvTransport();
// 🎯 创建消费者(初始暂停状态)
const consumer = await transport.consume({
producerId: producer.id,
rtpCapabilities: this.rtpCapabilities,
paused: true, // 先暂停,等客户端准备好
});
// 📢 通知客户端:有新的媒体流可以消费
await this.request('newConsumer', {
peerId: producer.appData.peerId, // 谁发的
consumerId: consumer.id,
producerId: producer.id,
kind: consumer.kind, // 音频还是视频
rtpParameters: consumer.rtpParameters,
});
// 客户端确认后,恢复传输
await consumer.resume();
}
第八步:小王的浏览器显示小马的画面 🖥️
js
// mediasoup-ui 处理 newConsumer 请求
async handleServerRequest(request) {
if (request.method === 'newConsumer') {
const { consumerId, producerId, kind, rtpParameters } = request.data;
// 📥 消费这个媒体流
const consumer = await this.recvTransport.consume({
id: consumerId,
producerId,
kind,
rtpParameters,
});
// 🎬 获取媒体轨道,创建可播放的流
const stream = new MediaStream([consumer.track]);
// 把流绑定到 video/audio 标签
const videoElement = document.getElementById('remote-video');
videoElement.srcObject = stream;
// 接受请求,服务器开始传输
request.accept();
}
}
四、完整流程图
sequenceDiagram
participant UI as mediasoup-ui
(小马浏览器) participant Bridge as signal-bridge
(协议转换) participant Server as signal-server
(媒体服务器) Note over UI,Server: 1️⃣ 建立连接 UI->>Bridge: WebSocket 连接 Bridge->>Server: protoo 连接 Server-->>Bridge: 连接成功 Bridge-->>UI: protooOpen Note over UI,Server: 2️⃣ 获取路由能力 UI->>Bridge: getRouterRtpCapabilities Bridge->>Server: 转发请求 Server-->>Bridge: router.rtpCapabilities Bridge-->>UI: 返回能力 UI->>UI: Device.load() Note over UI,Server: 3️⃣ 创建传输通道 UI->>Bridge: createWebRtcTransport Bridge->>Server: 转发请求 Server->>Server: 创建 Transport Server-->>UI: {transportId, iceParams...} UI->>UI: 创建 SendTransport/RecvTransport Note over UI,Server: 4️⃣ 加入房间 UI->>Bridge: join {displayName, rtpCapabilities} Bridge->>Server: 转发请求 Server->>Server: 创建 Peer Server-->>UI: {peers: [已在线用户]} Note over UI,Server: 5️⃣ 打开摄像头 UI->>UI: getUserMedia() UI->>UI: sendTransport.produce() UI->>Bridge: produce {kind, rtpParameters} Bridge->>Server: 转发请求 Server->>Server: 创建 Producer Server-->>UI: {producerId} Note over UI,Server: 6️⃣ 其他用户接收 Server->>Server: 触发 new-producer 事件 Server->>Server: 为其他 Peer 创建 Consumer Server-->>UI: newConsumer 请求 UI->>UI: recvTransport.consume() UI-->>Server: accept Server->>Server: consumer.resume()
(小马浏览器) participant Bridge as signal-bridge
(协议转换) participant Server as signal-server
(媒体服务器) Note over UI,Server: 1️⃣ 建立连接 UI->>Bridge: WebSocket 连接 Bridge->>Server: protoo 连接 Server-->>Bridge: 连接成功 Bridge-->>UI: protooOpen Note over UI,Server: 2️⃣ 获取路由能力 UI->>Bridge: getRouterRtpCapabilities Bridge->>Server: 转发请求 Server-->>Bridge: router.rtpCapabilities Bridge-->>UI: 返回能力 UI->>UI: Device.load() Note over UI,Server: 3️⃣ 创建传输通道 UI->>Bridge: createWebRtcTransport Bridge->>Server: 转发请求 Server->>Server: 创建 Transport Server-->>UI: {transportId, iceParams...} UI->>UI: 创建 SendTransport/RecvTransport Note over UI,Server: 4️⃣ 加入房间 UI->>Bridge: join {displayName, rtpCapabilities} Bridge->>Server: 转发请求 Server->>Server: 创建 Peer Server-->>UI: {peers: [已在线用户]} Note over UI,Server: 5️⃣ 打开摄像头 UI->>UI: getUserMedia() UI->>UI: sendTransport.produce() UI->>Bridge: produce {kind, rtpParameters} Bridge->>Server: 转发请求 Server->>Server: 创建 Producer Server-->>UI: {producerId} Note over UI,Server: 6️⃣ 其他用户接收 Server->>Server: 触发 new-producer 事件 Server->>Server: 为其他 Peer 创建 Consumer Server-->>UI: newConsumer 请求 UI->>UI: recvTransport.consume() UI-->>Server: accept Server->>Server: consumer.resume()
五、媒体流路由示意图

六、信令 vs 媒体
flowchart TB
subgraph Signaling[信令通道 - 控制面]
S1[WebSocket]
S2[JSON/protoo 协议]
S3[传输控制消息]
end
subgraph Media[媒体通道 - 数据面]
M1[WebRTC]
M2[ICE/DTLS/SRTP]
M3[传输音视频数据]
end
Client[客户端] --> S1
Client --> M1
S1 --> Server[服务器]
M1 --> Server
| 类型 | 协议 | 传输内容 |
|---|---|---|
| 信令 | WebSocket + JSON | 控制消息(加入房间、创建Transport等) |
| 媒体 | WebRTC (ICE/DTLS/SRTP) | 音视频数据流 |
七 关键 API 速查表
mediasoup-client(浏览器端)
| API | 说明 | 使用场景 |
|---|---|---|
new Device() |
创建设备对象 | 初始化时 |
device.load({ routerRtpCapabilities }) |
加载服务器能力 | 加入房间前 |
device.createSendTransport() |
创建发送通道 | 准备发送媒体 |
device.createRecvTransport() |
创建接收通道 | 准备接收媒体 |
transport.produce({ track }) |
生产媒体流 | 打开摄像头/麦克风 |
transport.consume({ id, ... }) |
消费媒体流 | 接收远程媒体 |
mediasoup(服务器端)
| API | 说明 | 使用场景 |
|---|---|---|
worker.createRouter({ mediaCodecs }) |
创建路由器 | 创建房间时 |
router.createWebRtcTransport() |
创建传输通道 | 用户加入时 |
transport.produce({ kind, rtpParameters }) |
创建生产者 | 用户发送媒体 |
transport.consume({ producerId, rtpCapabilities }) |
创建消费者 | 分发媒体给其他人 |
router.pipeToRouter({ producerId, router }) |
跨路由传输 | 高级场景,分离生产/消费 |
八、写在最后
理解 Mediasoup 的关键点:
- SFU 架构:服务器只转发,不编解码,所以延迟低
- Transport 是核心:一切媒体传输都通过 Transport
- Producer/Consumer 模式:一人生产,多人消费
- 信令与媒体分离:WebSocket 传控制消息,WebRTC 传媒体数据
- 事件驱动 :
new-producer事件触发consume,形成完整链路