跳到主要内容

WebRTC 全景实战 (12):SFU/MCU/Mesh 架构与 Pion 实战

Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡
Rainy
16 MIN READ... VIEWS

"我们早期押注多播,但公网教会了我们:你需要的是 packet shufflers,不是 multicast routers。" — Serge Lachapelle,Curious 历史访谈

本系列从 P2P(Ch2)走来,现在进入多人会议的架构选型。Marratech 是瑞典最早的 Web 视频会议公司之一,2009 年被 Google 收购——Serge Lachapelle 随之加入 Google,成为 WebRTC 标准化的核心推动者。他在 Curious 访谈中回忆:Marratech 早期押注 IP 多播,后来行业转向 packet shufflers——即今天的 SFU(Selective Forwarding Unit)。

配套 Lab:examples/webrtc-lab/client/ch12-sfu-client(LiveKit 客户端)+ examples/webrtc-lab/signaling/

推荐先读

本章涉及 LiveKit 作为 SFU 参考实现。建议先阅读本站 LiveKit 介绍,了解 Room/Participant/Track 模型与 SDK 选型。


本篇术语表

术语英文解释
Mesh网状拓扑每个参与者与其他所有人建立 P2P 连接
MCUMultipoint Control Unit多点控制单元,服务端混流/转码后广播
SFUSelective Forwarding Unit选择性转发单元,服务端按订阅转发 RTP 包
Forwarder转发器SFU 核心模块,接收 RTP 并按需转发给订阅者
Room房间逻辑会话容器,包含多个 Participant
Participant参与者Room 内的一个连接实体(人、Agent、录制器)
Track轨道单条媒体流(音频/视频/屏幕共享)
Publication发布Track 的发布状态与元数据
Subscription订阅订阅者与远程 Track 的绑定关系
PionGo 语言 WebRTC 库,Curious 作者 Sean-Der 主导
StreamTracker流追踪器SFU 内部跟踪每个 Track 各层可用性的组件
LayerSelector层选择器根据带宽为订阅者选择 Simulcast/SVC 层
Control Plane控制面信令、Room 管理、Token 鉴权
Media Plane媒体面SRTP 媒体流转发,高带宽低延迟
Webhook回调Room 事件通知后端(join/leave/publish)
Node节点分布式 SFU 集群中的单台物理/虚拟服务器

一、三种架构对比

架构延迟服务端 CPU客户端 CPU客户端带宽规模典型产品
Mesh最低O(N²) 解码O(N) 上下行≤4 人1v1 通话
MCU较高(混流)极高(转码)低(1 路)低(1 路)传统会议早期 Zoom
SFU低(转发)O(N) 解码O(N) 下行10–10k+Meet/Teams/LiveKit

1.1 连接数增长对比

Mesh 在 N=6 时需要 15 条 P2P 连接,N=10 时需要 45 条——客户端和网络都无法承受。SFU 将连接数降为 O(N)。

1.2 何时仍用 Mesh?

场景原因
1v1 通话无 SFU 开销,延迟最低
2–3 人小群连接数可控
端到端加密 E2EE无服务端转发(SFU 需特殊 E2EE 方案)
超低延迟游戏避免 SFU 额外一跳

二、历史脉络:Marratech → Meet → LiveKit

Serge Lachapelle 在 Curious 访谈中强调:WebRTC 标准化的成功在于站在巨人肩膀上——RTP、SRTP、ICE 等协议早已成熟,WebRTC 做的是把它们整合成浏览器 API。SFU 架构同理:不是发明新的媒体协议,而是在正确的位置做 RTP 包调度

Marratech 被收购后,其核心工程师将 packet shuffler 经验带入 Google Meet。Sean-Der(Pion 作者)则在 Curious 一书中系统化了 SFU 的实现细节——LiveKit 正是 Pion 之上构建的生产级 SFU。


三、SFU 核心数据模型

LiveKit 的 Room / Participant / Track 模型是 SFU 领域的标准抽象:

概念说明示例
Room会话容器,有唯一 IDmeeting-123
ParticipantRoom 内的连接实体alice, bob
Track单条媒体流TR_abc123 (video), TR_def456 (audio)
PublicationTrack 的发布状态muted, simulcast, source
Subscription订阅关系P2 订阅 P1 的 video Track

详见 LiveKit 介绍——该文详细对比了 LiveKit 与 mediasoup/Janus 的选型,以及 SDK 集成路径。

3.1 Track 生命周期

3.2 JWT Access Token 结构

import { AccessToken } from "livekit-server-sdk";

const token = new AccessToken(apiKey, apiSecret, { identity: "alice" });
token.addGrant({
roomJoin: true,
room: "meeting-123",
canPublish: true,
canSubscribe: true,
canPublishData: true,
});
const jwt = await token.toJwt();

四、SFU Forwarder 内部逻辑

Pion(Go)和 LiveKit(Go + Pion)都实现了这套 Forwarder + StreamTracker 管线。关键设计原则:

  1. 不转码:只修改 RTP 头,payload 原样转发
  2. per-subscriber 状态:每个订阅者独立的 SSRC/seq/timestamp 空间
  3. per-subscriber BWE:每个订阅者独立的 TWCC 和 LayerSelector
  4. Simulcast 层选择:根据 Ch11 的 rid/DD 选层

4.1 RTP 头重写

SFU 必须为每个订阅者维护独立的 RTP 序列号空间,否则订阅者的 jitter buffer 和 TWCC 会混乱。

4.2 StreamTracker 职责

检测项作用
各 rid 层是否有 Keyframe层切换前提
各层最后活跃时间Dynacast 停发依据
MID/SSRC 映射变化Re-negotiation 处理
DD 空间/时间层可用性SVC 选层输入

五、开源 SFU 选型

项目语言定位特点
PionGoWebRTC for the Curious 作者 Sean-Der 主导,最大灵活度
mediasoupC++/Node框架灵活,需自建信令/Room/鉴权
JanusC框架插件化,SIP 集成强
LiveKitGo产品完整栈:SFU + SDK + Agents + Egress
Jitsi VideobridgeJava产品Jitsi Meet 后端,成熟但 Java 栈

LiveKit 介绍 从控制面/媒体面分离、SDK 生态、Agents 扩展等维度做了更完整的选型分析——本章聚焦 SFU 媒体面原理,LiveKit 作为贯穿 Ch12–Ch15 的参考实现。

5.1 选型决策树

5.2 mediasoup vs LiveKit 模型对比

概念mediasoupLiveKit
路由单元RouterRoom
传输通道WebRtcTransportParticipant PC
媒体流Producer / ConsumerTrack Publication / Subscription
信令自建内置 WebSocket
鉴权自建JWT Token

六、Pion 最小 SFU 概念代码

// 概念示意 — 非完整可运行代码
// 完整示例见 github.com/pion/example-webrtc-applications

package main

import (
"github.com/pion/webrtc/v4"
)

func main() {
api := webrtc.NewAPI()
publisherPC, _ := api.NewPeerConnection(webrtc.Configuration{})
subscribers := make(map[string]*webrtc.PeerConnection)

publisherPC.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
for _, subPC := range subscribers {
localTrack, _ := webrtc.NewTrackLocalStaticRTP(
track.Codec().RTPCodecCapability,
track.ID(),
track.StreamID(),
)
subPC.AddTrack(localTrack)

go func(t *webrtc.TrackRemote, lt *webrtc.TrackLocalStaticRTP) {
buf := make([]byte, 1500)
for {
n, _, err := t.Read(buf)
if err != nil { return }
lt.Write(buf[:n])
}
}(track, localTrack)
}
})
}
生产 SFU 远不止这些

上述代码缺少 Simulcast 层选择、TWCC、SRTP 密钥管理、Room 状态、断线重连等。这就是为什么大多数团队选择 LiveKit 而非从零用 Pion 构建——详见 LiveKit 介绍


七、实战:LiveKit 三人会议

7.1 启动 LiveKit Server

brew install livekit
livekit-server --dev
# 输出: LiveKit server started on :7880

7.2 生成 Access Token

brew install livekit-cli
lk token create \
--api-key devkey \
--api-secret secret \
--join \
--room meeting \
--identity user1 \
--valid-for 24h

7.3 客户端连接

// examples/webrtc-lab/client/ch12-sfu-client
import { Room, RoomEvent, Track } from "livekit-client";

const room = new Room();

room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
if (track.kind === Track.Kind.Video) {
const element = track.attach();
document.getElementById("remote-videos").appendChild(element);
}
});

room.on(RoomEvent.ParticipantConnected, (p) => {
console.log("[Layer1] joined:", p.identity);
});

await room.connect("ws://localhost:7880", token);
await room.localParticipant.enableCameraAndMicrophone();
console.log("Participants:", room.numParticipants);

React 组件方式:

import { LiveKitRoom, VideoConference } from "@livekit/components-react";

function Meeting() {
return (
<LiveKitRoom token={token} serverUrl="ws://localhost:7880">
<VideoConference />
</LiveKitRoom>
);
}

7.4 验证三人会议


八、分布式 SFU Mesh

LiveKit 默认启用分布式 Mesh——单个 Room 可跨多台物理服务器:

组件作用
RedisRoom 路由表、Node 注册、Participant 位置
Node Selector新 Participant 分配到最优 Node
Relay跨 Node 的 RTP 转发(当发布者和订阅者在不同 Node)

跨 Node Relay 会增加一跳延迟——生产环境应通过 GeoDNS + 区域亲和性 让同一 Room 的参与者尽量在同一 Node(Ch14 详述)。

8.1 Webhook 事件

// 后端接收 LiveKit Webhook
app.post("/webhook/livekit", (req, res) => {
const event = req.body;
switch (event.event) {
case "participant_joined":
console.log(JSON.stringify({ layer: "signaling", event, identity: event.participant.identity }));
break;
case "track_published":
console.log(JSON.stringify({ layer: "media", track: event.track }));
break;
case "room_finished":
// 清理资源
break;
}
res.sendStatus(200);
});

九、SFU vs 信令的关系

关键认知:

  1. 信令不承载媒体——SDP/ICE 走 WebSocket,SRTP 走 UDP 直连 SFU
  2. LiveKit 内置信令——room.connect() 同时完成信令 + 媒体协商
  3. 自研信令 + SFU——mediasoup/Pion 场景需自行实现 Room 管理(Ch3

examples/webrtc-lab/signaling/server.js 实现了最小 P2P 信令;SFU 场景下信令由 LiveKit 接管,但 Join API 仍可在同一 Node 服务中提供 Token。


十、常见陷阱

#陷阱现象修复
1Mesh 用于大会议客户端 CPU/带宽爆炸N>4 切换 SFU
2SFU 做转码延迟高、CPU 爆SFU 只转发,混流用 MCU/Egress
3忽略 per-subscriber SSRC订阅者 jitter buffer 乱Forwarder 重写 SSRC/seq
4单 Node 部署跨区延迟高分布式 Mesh + GeoDNS
5Token 无过期安全风险JWT 短期 Token + 刷新
6未启用 Simulcast所有订阅者同质量发布端开启 Simulcast
7信令与媒体混淆带宽打满信令服务器媒体必须直连 SFU
8跨 Node Relay 未监控延迟飙升Prometheus 监控 relay 延迟
9Webhook 未验签伪造 join 事件验证 LiveKit Webhook 签名
10Room 无空房间清理内存泄漏emptyTimeout 配置

十一、实战 Lab

Lab 1:LiveKit 三人会议

livekit-server --dev
lk token create --api-key devkey --api-secret secret \
--join --room lab --identity user1 --valid-for 1h
  1. 3 个浏览器 Tab 加入 room=lab
  2. 验证互相看到视频
  3. chrome://webrtc-internals 确认仅 1 条 PeerConnection 到 SFU

Lab 2:Pion 示例运行

git clone https://github.com/pion/example-webrtc-applications
cd example-webrtc-applications/sfu-ws
go run main.go
# 浏览器打开 http://localhost:8080

Lab 3:Simulcast 层观察

  1. LiveKit 三人会议中,一端 DevTools 限速 300kbps
  2. 在 LiveKit Dashboard 观察该订阅者的下行码率
  3. 确认 LayerSelector 切换到 rid=l

Lab 4:Participant 事件

room.on(RoomEvent.ParticipantConnected, (p) => console.log("joined:", p.identity));
room.on(RoomEvent.ParticipantDisconnected, (p) => console.log("left:", p.identity));
room.on(RoomEvent.TrackMuted, (pub, p) => console.log("muted:", p.identity));

Lab 5:对比 Mesh vs SFU 连接数

  1. 修改 examples/webrtc-lab/client/ch02-p2p-basic 为 3 人 Mesh
  2. chrome://webrtc-internals 数 PeerConnection 数量(应为 2)
  3. 切换 LiveKit SFU(应为 1)

Lab 6:Token 权限最小化

  1. 生成 canPublish: false 的 Token
  2. 尝试 enableCameraAndMicrophone() → 应失败
  3. 验证仅观看模式

Lab 7:Webhook 接收

  1. 配置 LiveKit webhook.urls
  2. 加入/离开 Room,观察后端日志

十二、本章小结

概念要点
Mesh无服务器,O(N²) 连接,≤4 人
MCU混流转码,低客户端开销,高服务端 CPU
SFU选择性转发,不转码,10k+ 规模
ForwarderRTP 头重写 + LayerSelector + per-subscriber BWE
LiveKitRoom/Participant/Track 标准模型,推荐入门 SFU
选型快速上线 LiveKit,深度定制 Pion/mediasoup
历史Marratech → packet shuffler → Meet → LiveKit

下一篇(Ch13)调试工具链与可观测性


系列导航

章节主题状态
0架构全景与协议栈地图✅ 已发布
1浏览器媒体 API 与设备管理✅ 已发布
2第一个 P2P 视频通话✅ 已发布
3信令服务器设计与会话状态机✅ 已发布
4SDP 解剖与媒体协商✅ 已发布
5ICE、STUN、TURN 与 NAT 穿透✅ 已发布
6Data Channel 与 SCTP over DTLS✅ 已发布
7DTLS 握手与 SRTP 加密体系✅ 已发布
8RTP/RTCP 媒体传输与 QoS✅ 已发布
9音视频编解码与 Simulcast 入门✅ 已发布
10带宽估计与拥塞控制 GCC✅ 已发布
11Simulcast、SVC 与选择性订阅✅ 已发布
12SFU/MCU/Mesh 架构与 Pion 实战✅ 已发布
13调试工具链与可观测性✅ 已发布
14TURN 集群部署与多区域扩展✅ 已发布
15Capstone 生产级视频会议系统✅ 已发布

References

Logo
RainLib

探索技术、设计与分布式系统的边界。构建面向未来的开发者工具。

留言与建议

© 2026 RainLib. 为未来构建。(Built for the Future)
版权所有。
系统正常