拒绝调用SDK!手搓WebRTC视频会议,拆解mediasoup+Spring Boot桥接秘籍

0 阅读6分钟

过年期间,做了个 WebRTC 视频会议系统(开源了)

一个程序员的自述:从「这玩意儿怎么这么难」到「原来也没那么复杂」的心路历程

项目地址NeoView - 基于 mediasoup 的 WebRTC 视频会议系统


一、故事的开始

过年期间,有个想法在我脑子里转悠:能不能自己做一个视频会议系统?

不是那种调用别人 SDK 的「假」视频会议,而是真正的、从底层到上层都自己把控的那种。

然后我打开了 Google,输入了「WebRTC」,接着陷入了长达两周的懵逼状态。

如果你也对 WebRTC 感兴趣,或者正在做类似的技术选型,这篇文章会帮你省掉很多「走弯路」的时间。


二、先聊聊「视频会议」到底难在哪

很多人觉得:不就是两个人视频通话吗?微信、钉钉不都能做到?

但当你真正去研究,就会发现事情没那么简单。

2.1 两个人的通话:简单

A 和 B 打视频电话,只需要建立一条连接:

A ←———→ B

浏览器提供了 WebRTC API,媒体流直接在两人之间传输,不需要经过服务器。完美!(是不是像小学时学的两个人那个易拉罐连根绳就能说话)

2.2 三个人的通话:开始头秃

A、B、C 三个人开会,需要建立几条连接?

A ←——→ B
A ←——→ C
B ←——→ C

3 条连接。(其实也还好,为啥,三个易拉罐呗,哈哈哈)

2.3 十个人的会议:放弃治疗

如果是 10 个人开会呢?

连接数 = n × (n - 1) / 2 = 45 条连接!

100 个人开会?4950 条连接。每增加一个人,所有人的浏览器都要创建新的连接,CPU 直接原地爆炸。(易拉罐和绳子不够用啦!)

这就是为什么我们需要「媒体服务器」


三、媒体服务器:从「互相直连」到「有个中间人」

既然每个人直接连其他人太复杂,那就找个「中间人」——所有人只跟「中间人」连,「中间人」负责把音视频流转发给其他人。

这个「中间人」,就叫 媒体服务器

graph LR
    subgraph 没有媒体服务器 - 全员互连
        A1[A] <--> B1[B]
        A1 <--> C1[C]
        B1 <--> C1[C]
    end
    
    subgraph 有媒体服务器 - 星形连接
        A2[A] --> S[媒体服务器]
        B2[B] --> S
        C2[C] --> S
        S --> A2
        S --> B2
        S --> C2
    end

这个模式有个专业名字:SFU(Selective Forwarding Unit,选择性转发单元)

听起来很高端,其实就是个「转发器」——你把视频发给我,我帮你转发给别人。不做任何处理,只管转发,所以延迟极低。

四、技术选型:为什么我选了 mediasoup?

市面上做 SFU 的媒体服务器有好几个,我调研了一圈:

名字什么写的我的感受
JitsiJava功能很全,但太重了。就像想买个自行车,结果给你推来一辆坦克
JanusC性能强悍,但 C 语言......我不配
PionGo很灵活,但需要自己实现的东西太多了
mediasoupNode.js/C++刚刚好!轻量、专注、文档清晰

mediasoup 就像那种「小而美」的工具——只做一件事,但把这件事做到极致。

它的特点是:

  • 延迟极低:毫秒级转发,开会就像面对面
  • 资源占用少:一个普通服务器就能跑几百人的会议
  • Node.js 原生支持:前端开发者友好

就它了!


五、架构设计:一个「翻译官」的故事

选好了 mediasoup,我以为可以开干了。

然后我发现了一个问题:mediasoup 用的是一种叫 protoo 的协议,而这个协议只有 Node.js 的客户端库。

我的后端是 Spring Boot(Java)啊!

怎么办?有两个选择:

  1. 用 Node.js 重写后端 —— 我疯了?
  2. 找个办法让 Java 也能和 mediasoup 通信 —— 这个靠谱

于是,我想到了一个方案:找一个「翻译官」

sequenceDiagram
    participant Java as Spring Boot<br/>(只会说中文)
    participant 翻译官 as Node.js 桥接<br/>(中英文都懂)
    participant Mediasoup as mediasoup<br/>(只会说英文)
    
    Java->>翻译官: "我想创建一个传输通道"
    翻译官->>翻译官: 翻译成 protoo 协议
    翻译官->>Mediasoup: createWebRtcTransport
    Mediasoup-->>翻译官: 好的,通道已创建
    翻译官->>翻译官: 翻译成 JSON
    翻译官-->>Java: "传输通道创建成功"

这个「翻译官」就是 neoview-signal-bridge 模块——一个用 Node.js 写的小服务,专门负责把 Java 的 JSON 消息翻译成 protoo 协议,再发给 mediasoup。

桥接层的核心代码

// neoview-signal-bridge/server.js

// 每一个来自 Spring Boot 的连接,都对应一个翻译会话
class BridgeSession {
  constructor(ws) {
    this.ws = ws;           // 跟 Spring Boot 的连线
    this.protoo = null;     // 跟 mediasoup 的连线
  }

  // 建立「翻译通道」
  connect({ roomId, peerId }) {
    // 连接到 mediasoup
    const url = `wss://mediasoup-server/?roomId=${roomId}&peerId=${peerId}`;
    const transport = new protooClient.WebSocketTransport(url);
    this.protoo = new protooClient.Peer(transport);

    // mediasoup 发消息过来,翻译后发给 Spring Boot
    this.protoo.on('request', (request, accept, reject) => {
      this.ws.send(JSON.stringify({
        type: 'protooServerRequest',
        method: request.method,
        data: request.data
      }));
    });
  }
}

很简单对吧?就是个「消息中转站」,把一种语言翻译成另一种语言。


六、整体架构:长什么样?

有了「翻译官」,整个系统的架构就清晰了:

graph TB
    subgraph 用户端
        Browser["浏览器<br/>(Vue 3 前端)"]
    end

    subgraph 服务器端
        Backend["Spring Boot<br/>(大管家:用户、会议、业务逻辑)"]
        Bridge["Node.js 桥接<br/>(翻译官:协议转换)"]
        Mediasoup["mediasoup<br/>(媒体服务器:音视频转发)"]
    end

    subgraph 存储层
        DB[("PostgreSQL<br/>(数据库)")]
        Cache[("Redis<br/>(缓存)")]
    end

    Browser -->|"WebSocket 信令"| Backend
    Backend <-->|"JSON 消息"| Bridge
    Bridge <-->|"protoo 协议"| Mediasoup
    Browser -->|"WebRTC 音视频"| Mediasoup
    Backend --> DB
    Backend --> Cache

角色分工

角色职责比喻
Spring Boot用户认证、会议管理、业务逻辑公司的「行政部」
Node.js 桥接协议翻译、消息转发公司的「翻译」
mediasoup音视频流的接收和转发公司的「物流部」
Vue 前端用户界面、媒体采集和播放公司的「前台」

七、用户加入会议:完整流程

说了这么多概念,来看一个真实的场景:小明想加入一个视频会议

sequenceDiagram
    participant 小明 as 小明(浏览器)
    participant 后端 as Spring Boot
    participant 翻译官 as Node.js 桥接
    participant 媒体 as mediasoup

    小明->>后端: 1. 我要加入会议 room123
    后端->>翻译官: 2. 帮我连上 mediasoup
    翻译官->>媒体: 3. 建立 protoo 连接
    媒体-->>翻译官: 4. 连上了
    翻译官-->>后端: 5. 准备就绪
    
    小明->>后端: 6. 给我服务器支持的编码格式
    后端->>翻译官: 7. getRouterRtpCapabilities
    翻译官->>媒体: 8. 你支持什么编码?
    媒体-->>翻译官: 9. H264、VP8、Opus...
    翻译官-->>后端: 10. 返回编码列表
    后端-->>小明: 11. 这些编码你可以用
    
    小明->>后端: 12. 帮我创建传输通道
    Note over 小明,媒体: 创建「发送通道」和「接收通道」
    
    小明->>媒体: 13. 开始发送视频流
    媒体->>媒体: 14. 转发给房间里的其他人
    媒体-->>小明: 15. 接收其他人的视频流

用大白话解释

  1. 小明点击「加入会议」
  2. 后端收到请求,通知翻译官去连接 mediasoup
  3. 翻译官连接成功后,告诉后端「可以开始了」
  4. 小明的浏览器问服务器:「你支持什么视频编码?」
  5. 服务器通过翻译官问 mediasoup,得到答案后告诉小明
  6. 小明的浏览器创建「传输通道」(可以理解为建立一条管道)
  7. 小明开始发送视频流,mediasoup 收到后转发给房间里的其他人
  8. 同时,小明也开始接收其他人的视频流

整个过程在 1 秒内完成,用户几乎感觉不到延迟。(在没有cdn加速域名的情况下,上海和山东以及北京用户没有感受到有什么延迟)


八、前端是怎么接入的?

前端使用 mediasoup-client 这个库,它帮我们封装了 WebRTC 的复杂操作。

核心代码(简化版)

import * as mediasoupClient from 'mediasoup-client';

class VideoMeeting {
  async join(roomId) {
    // 1. 连接后端
    await this.signaling.connect();

    // 2. 获取服务器的编码能力
    const { routerRtpCapabilities } = 
      await this.signaling.request('getRouterRtpCapabilities');

    // 3. 创建 Device(可以理解为「浏览器端的代理」)
    this.device = new mediasoupClient.Device();
    await this.device.load({ routerRtpCapabilities });

    // 4. 创建传输通道
    await this.createTransports();

    // 5. 正式加入房间
    await this.signaling.request('join', {
      displayName: '小明',
      rtpCapabilities: this.device.rtpCapabilities
    });

    // 6. 开始发送摄像头画面
    await this.publishCamera();
  }

  async publishCamera() {
    // 获取摄像头
    const stream = await navigator.mediaDevices.getUserMedia({ 
      video: true, 
      audio: true 
    });

    // 通过发送通道发送视频
    const producer = await this.sendTransport.produce({
      track: stream.getVideoTracks()[0]
    });
  }
}

几个关键概念

概念是什么比喻
Device浏览器端的「代理」前台接待员
Transport传输通道数据管道
Producer发送音视频流的「生产者」发货员
Consumer接收音视频流的「消费者」收货员

九、最后想说的话

之前就一直对WebRTC 感兴趣,对音视频会议充满了interest,但确实对 WebRTC 一无所知。

经过此次,我做出了一个能用的视频会议系统,还把它开源了。

这个过程教会我的

  1. 技术没那么可怕:很多概念听起来高大上,拆解开来其实就那么回事
  2. 架构设计很重要:好的架构让每个模块职责清晰,出了问题也好排查
  3. 开源的力量:mediasoup、protoo-client 这些开源库,帮我省了大量的时间

如果你也想做一个 WebRTC 项目,希望这篇文章能给你一些启发。


十、项目信息

GitHub / GiteeNeoView

技术栈:Vue 3 + Spring Boot 3.2 + Node.js + mediasoup + PostgreSQL + Redis

功能

  • 多人视频会议
  • 屏幕共享
  • 实时语音识别
  • AI 降噪
  • 3D虚拟头像(规划中)

如果觉得有帮助,欢迎 Star ⭐ 支持一下!


作者:椰子皮 本文首发于稀土掘金,转载请注明出处