21、SFU服务器搭建:Mediasoup简介、Mediasoup架构、Mediasoup安装与配置、创建Router与Transport

好,咱们今天聊点硬核的——SFU服务器搭建。

前面几章我们一直在讲WebRTC的客户端逻辑,信令、媒体协商、P2P连接。但说实话,真正在生产环境里,P2P模式用得并不多。为什么?因为一旦超过4个人开会,每个客户端都要跟其他人建立连接,带宽和CPU都扛不住。这时候就需要SFU(Selective Forwarding Unit)出场了。

我个人最常用的SFU方案就是Mediasoup。它性能好、灵活度高,而且是用C++写的底层,Node.js做上层控制,非常适合二次开发。今天我们就把它拆开揉碎了讲清楚。

核心要点:Mediasoup是一个基于WebRTC的SFU服务器,它不处理信令,只负责媒体流的转发。你想想看,这其实是个很聪明的设计——把信令交给业务层,Mediasoup只做它最擅长的事:高效转发音视频数据。

Mediasoup简介

Mediasoup是什么?简单说,它是一个运行在服务端的WebRTC协议栈。你可以把它理解成一个媒体路由器——客户端把音视频流推上来,Mediasoup负责把这些流转发给其他客户端。

我记得第一次接触Mediasoup时,最让我惊讶的是它的性能。单台服务器可以轻松支撑几百路并发流。这在WebRTC的世界里,已经相当能打了。

Mediasoup的几个关键特性:

  • 纯C++核心:媒体处理层用C++编写,性能极高
  • Node.js控制层:API层用Node.js,开发效率高
  • 支持Simulcast:同一路流可以发送多个分辨率,接收端按需选择
  • 支持SVC:可伸缩视频编码,适应不同网络条件
  • 灵活的Pipe模式:支持服务器间级联,实现大规模分发

我的经验:如果你要做视频会议、在线教育、直播连麦这类场景,Mediasoup基本是首选。我曾经在一个项目中用Mediasoup支撑了200人同时在线会议,服务器CPU占用率不到40%。

Mediasoup架构

Mediasoup的架构其实不复杂,但有几个核心概念你必须搞懂。我画了一张图,帮你理清它们之间的关系。

Mediasoup 核心架构图 Worker (C++进程) 媒体处理核心 Router (路由器) 媒体路由单元 Transport (传输层) WebRTC连接 创建 创建 Producer (生产者) Consumer (消费者) 推送流 拉取流 媒体数据流 架构说明: • Worker:一个操作系统进程,可运行多个Worker实例 • Router:每个Worker下可创建多个Router,每个Router对应一个"房间" • Transport:每个Router下可创建多个Transport,每个Transport对应一个客户端连接 • Producer/Consumer:Transport上创建的媒体生产者/消费者

这张图其实已经把Mediasoup的核心逻辑讲清楚了。从上往下看:

  • Worker:最底层,一个Worker就是一个C++进程。我建议一台机器上跑多个Worker,充分利用多核CPU。
  • Router:Worker下面可以创建多个Router。每个Router相当于一个独立的"房间"或"会议"。不同Router之间的媒体流是隔离的。
  • Transport:Router下面创建Transport。每个Transport对应一个客户端的WebRTC连接。说白了,Transport就是客户端和服务器之间的管道。
  • Producer/Consumer:Transport上可以创建Producer(推流)和Consumer(拉流)。Producer把客户端的音视频数据推上来,Consumer从服务器拉取数据。

注意:Mediasoup的架构是分层的,你不能跨Worker操作Router,也不能跨Router操作Transport。我曾经见过有人试图在一个Worker里创建100个Router,结果性能反而下降了。合理的做法是:根据CPU核心数创建Worker,每个Worker下再按需创建Router。

Mediasoup安装与配置

好,理论讲完了,咱们动手装一个。Mediasoup的安装其实不复杂,但有几个坑需要注意。

环境要求

组件 版本要求 备注
Node.js >= 18.0.0 推荐使用LTS版本
Python >= 3.6 编译依赖
GCC / Clang C++17支持 Linux用GCC,macOS用Clang
操作系统 Linux / macOS Windows需要WSL2

安装步骤

安装Mediasoup其实就一行命令:

npm install mediasoup

但这里有个坑——Mediasoup需要编译原生模块。如果你的机器上没有C++编译环境,安装会失败。我个人习惯在安装前先确认一下:

# 检查C++编译器
g++ --version

# 检查Python
python3 --version

# 如果缺少,Ubuntu上可以这样装
sudo apt-get install build-essential python3

避坑指南:我曾经在一台CentOS服务器上装Mediasoup,折腾了半天都编译不过。后来发现是GCC版本太旧,不支持C++17。升级GCC后一次通过。所以,装之前一定先确认编译器版本。

基础配置

安装完成后,我们需要配置Mediasoup。配置项比较多,但核心的就几个:

const mediasoup = require('mediasoup');

// 创建Worker的配置
const workerSettings = {
  logLevel: 'warn',      // 日志级别:debug, warn, error
  logTags: ['info'],     // 日志标签
  rtcMinPort: 40000,     // RTC端口范围(最小值)
  rtcMaxPort: 49999      // RTC端口范围(最大值)
};

// 创建Worker
const worker = await mediasoup.createWorker(workerSettings);
console.log('Worker创建成功,PID:', worker.pid);

这里要注意端口范围。Mediasoup需要一大段UDP端口用于媒体传输。我建议至少分配10000个端口。如果端口不够,客户端连接会失败。

重要:生产环境中,一定要确保防火墙开放了这些UDP端口。我曾经排查过一个线上问题,客户端一直连不上服务器,最后发现是防火墙把UDP端口全封了。嗯,这种低级错误,犯过一次就记住了。

创建Router与Transport

Worker创建好了,接下来就是创建Router和Transport。这部分是Mediasoup的核心操作,也是你写业务逻辑时最常打交道的地方。

创建Router

Router的创建很简单:

// 在Worker下创建一个Router
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/H264',
      clockRate: 90000,
      parameters: {
        'packetization-mode': 1,
        'profile-level-id': '42e01f',
        'level-asymmetry-allowed': 1
      }
    }
  ]
});

console.log('Router创建成功,ID:', router.id);

这里有个关键点——mediaCodecs配置。它告诉Mediasoup这个Router支持哪些编解码器。我建议至少配置opus(音频)和VP8/H264(视频)。为什么?因为大部分浏览器都支持这些编码。

创建Transport

Transport是客户端和服务器之间的桥梁。创建Transport需要客户端先发来一些信息:

// 服务端创建WebRTC Transport
const transport = await router.createWebRtcTransport({
  listenIps: [
    {
      ip: '0.0.0.0',      // 监听所有网卡
      announcedIp: '你的公网IP'  // 公网IP,客户端用这个IP连接
    }
  ],
  enableUdp: true,
  enableTcp: true,
  preferUdp: true,
  initialAvailableOutgoingBitrate: 1000000  // 初始发送码率 1Mbps
});

// 返回给客户端的信息
const clientTransportInfo = {
  id: transport.id,
  iceParameters: transport.iceParameters,
  iceCandidates: transport.iceCandidates,
  dtlsParameters: transport.dtlsParameters
};

// 客户端用这些信息创建本地的RTCPeerConnection
// 然后与服务端建立连接

关键点:announcedIp这个配置很容易被忽略。如果你的服务器在内网,客户端在外网,必须配置公网IP。否则客户端拿到的ICE Candidate是内网IP,连不上。我刚开始做的时候就在这里栽过跟头。

建立连接后的操作

Transport创建完成后,客户端会发来DTLS握手信息。服务端需要处理:

// 客户端发来DTLS参数后,服务端调用
await transport.connect({ dtlsParameters: clientDtlsParameters });

// 连接建立后,客户端可以创建Producer推流
const producer = await transport.produce({
  kind: 'video',
  rtpParameters: clientRtpParameters  // 客户端的RTP参数
});

// 其他客户端可以创建Consumer拉流
const consumer = await transport.consume({
  producerId: producer.id,
  rtpCapabilities: router.rtpCapabilities
});

// 把Consumer的RTP参数发给拉流客户端
const consumerRtpParams = {
  id: consumer.id,
  kind: consumer.kind,
  rtpParameters: consumer.rtpParameters
};

到这里,一个基本的SFU通信链路就建立起来了。客户端A推流到Mediasoup,客户端B从Mediasoup拉流,实现了媒体转发。

我的建议:在实际项目中,不要把所有的Transport创建逻辑都写在一个文件里。我习惯把Worker管理、Router管理、Transport管理分别封装成独立的模块。这样代码清晰,也方便后期维护。

嗯,Mediasoup的核心内容基本就这些。你想想看,其实它的设计思路很清晰——Worker管进程,Router管房间,Transport管连接,Producer/Consumer管媒体流。每一层各司其职,互不干扰。

这种分层设计的好处是,你可以根据业务需求灵活扩展。比如,一个会议一个Router,每个Router下管理多个Transport。如果需要支持大规模并发,就多开几个Worker,用负载均衡把客户端分散到不同的Worker上。

总结一下今天的内容:

  • Mediasoup是一个高性能的SFU服务器,C++核心+Node.js控制层
  • 架构分层:Worker → Router → Transport → Producer/Consumer
  • 安装时注意C++编译环境和端口配置
  • 创建Router时要配置支持的编解码器
  • 创建Transport时要注意公网IP的配置

好了,这一章的内容就到这里。代码部分你可以直接拿去用,但建议根据你的实际场景调整配置参数。下一章我们会聊Mediasoup的高级特性,比如Simulcast和SVC的处理,以及如何做服务器级联。


公众号:蓝海资料掘金营,微信deep3321