一次视频会议的“生命旅程”:从点击加入到大屏相见,Mediasoup 背后发生了什么?

一、故事的开端:你有没有想过?

当你在腾讯会议、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()

五、媒体流路由示意图

六、信令 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 的关键点:

  1. SFU 架构:服务器只转发,不编解码,所以延迟低
  2. Transport 是核心:一切媒体传输都通过 Transport
  3. Producer/Consumer 模式:一人生产,多人消费
  4. 信令与媒体分离:WebSocket 传控制消息,WebRTC 传媒体数据
  5. 事件驱动new-producer 事件触发 consume,形成完整链路
相关推荐
itslife2 小时前
前端架构模式思考
前端·架构
Maxkim2 小时前
前端工程化落地指南:pnpm workspace + Monorepo 核心用法与实践
前端·javascript·架构
Lee川18 小时前
深度拆解:基于面向对象思维的“就地编辑”组件全模块解析
javascript·架构
勤劳打代码18 小时前
Flutter 架构日记 — 状态管理
flutter·架构·前端框架
子兮曰1 天前
后端字段又改了?我撸了一个 BFF 数据适配器,从此再也不怕接口“屎山”!
前端·javascript·架构
卓卓不是桌桌1 天前
如何优雅地处理 iframe 跨域通信?这是我的开源方案
javascript·架构
Qlly1 天前
DDD 架构为什么适合 MCP Server 开发?
人工智能·后端·架构
用户881586910912 天前
AI Agent 协作系统架构设计与实践
架构
鹏北海2 天前
Qiankun 微前端实战踩坑历程
前端·架构