自研SFU设计要点
面向打算自研或深度改造 SFU(Selective Forwarding Unit)的工程团队。所有结论尽量回答:为什么、什么时候适用、什么时候不适用、失败时的样子。
1. SFU 与 MCU / P2P 网状的对比
1.1 三种拓扑的本质差异
| 维度 | P2P Mesh | MCU | SFU |
|---|---|---|---|
| 媒体路径 | 端到端直连 | 端 → 服务器解码混流再编码 → 端 | 端 → 服务器仅转发 RTP → 端 |
| 服务器是否解码 | 否 | 是 | 否(只看 RTP 头与 RTCP) |
| 上行带宽 | N-1 路 | 1 路 | 1 路 |
| 下行带宽 | N-1 路 | 1 路 | N-1 路 |
| 端侧解码路数 | N-1 | 1 | N-1 |
| CPU 集中度 | 端侧 | 服务器 | 端侧(解码)+ 服务器(转发) |
| 端到端延迟 | 最低(一跳) | 最高(编解码两次) | 中等(一次中转) |
| E2EE 可行性 | 天然 | 不可行(需明文混流) | 可行(Insertable Streams / SFrame) |
| 单服务器承载规模 | 不适用 | 几十路顶天 | 单核数百路上行、千路下行 |
1.2 为什么主流选 SFU
- 为什么:浏览器算力足够并行解码 9 ~ 16 路 720p;服务器只搬 RTP 包不动 payload,CPU 留给路由与拥塞控制。
- 什么时候适用:多人会议、互动直播、连麦、教育大班课、云游戏多人观战。
- 什么时候不适用:
- 终端 CPU/带宽极差(IoT、低端 Android)— MCU 更合适
- 强 E2EE 且不能信任服务端做合流 — 仍选 SFU + SFrame
- 仅 1v1 通话且双方网络对等 — P2P 直连更省钱
- 失败时的样子:参会人数一上来端侧 CPU 100% 风扇狂转、热度报警、低端机解码丢帧到打码、电池迅速发烫掉电。
1.3 SFU 的核心承诺
SFU 不解码、不重编码,只做四件事:
- 解析 RTP/RTCP 头部,按 SSRC / MID / RID 路由
- 在订阅者维度选择 Simulcast 层或 SVC 子层
- 聚合 NACK/PLI/FIR/TWCC 反馈并向源端回报
- 通过 Pacer 平滑出包,避免瞬时拥塞
任何超出这四件事的逻辑(转码、混音、合屏)都应放到独立 MCU 或边缘合流服务里,不污染 SFU 主路径。
2. 整体架构与数据流
关键点:
- 上行只解 RTP 头,payload 仍是 SRTP 加密块,转发时只重写头部字段(SSRC、序号偏移、时间戳偏移)
- 反馈通道独立:订阅端的 RTCP 不能直接透传给发布端,必须先在 SFU 聚合,否则发布端会被 N 路反馈打爆
- BWE 双向:发布端的拥塞估计只看上行链路;下行各订阅者的拥塞估计反过来影响 SFU 给该订阅者下发哪一层
3. 转发决策核心:按 SSRC / MID / RID 路由
3.1 标识体系
WebRTC 媒体标识有三层,初学者最容易把它们混成一团:
- SSRC:RTP 头中的 32 位同步源标识,传输层概念,每条独立编码流一个
- MID:SDP
a=mid描述的逻辑 m-line 标识,应用层概念,跨 SSRC 稳定 - RID:
a=ridSimulcast 时区分同一 m-line 内多层的字符串标签(如qhf)
在 SFU 内部,主键应该是 (transportId, MID, RID),SSRC 只是当前这一次会话的运行时映射。原因是 SSRC 在 ICE restart、reconnect、网络变化时可能变化,而 MID/RID 由 SDP 协商锁定。
3.2 转发决策伪代码(Go 版,基于 Pion 风格)
// 为什么这样写:把"路由匹配"和"层选择"解耦
// 路由匹配:决定包从哪个发布者流来,要发给哪些订阅者
// 层选择:决定每个订阅者拿哪一层(Simulcast/SVC)
type Forwarder struct {
pubs map[SSRC]*PublishedStream // 上行流表
subs map[SubID]*Subscription // 下行订阅表
pacer *Pacer
bwe map[SubID]*SendSideBWE
}
// 入口:每个 SRTP 解密后的 RTP 包都走这里
func (f *Forwarder) OnRTP(pkt *rtp.Packet, from TransportID) {
pub, ok := f.pubs[SSRC(pkt.SSRC)]
if !ok {
return // 未知 SSRC,丢弃,不要无条件转发
}
layer := pub.LayerOf(pkt) // 通过 RID/SVC 描述符识别层级
for _, sub := range pub.Subscribers() {
// 1. 匹配订阅意图
if !sub.Wants(pub.MID) {
continue
}
// 2. 层选择:根据该订阅者的下行带宽估计 + 用户期望分辨率
target := sub.PickLayer(pub.AvailableLayers(), f.bwe[sub.ID].Estimate())
if !layer.Matches(target) {
continue // 这一层不发给他
}
// 3. SVC 时域层裁剪(VP9/AV1)
if pub.IsSVC() && !temporalLayerAllowed(pkt, target.TemporalID) {
continue
}
// 4. 重写 SSRC / SeqNum / Timestamp,避免下游重复
out := f.rewrite(pkt, sub)
// 5. 交给 Pacer 平滑发送
f.pacer.Enqueue(sub.ID, out, priorityOf(layer))
}
}
// 层选择:保守降档,激进升档要等 keyframe
func (s *Subscription) PickLayer(avail []Layer, bps int64) Layer {
sort.Slice(avail, func(i, j int) bool { return avail[i].Bitrate < avail[j].Bitrate })
target := avail[0]
for _, l := range avail {
if l.Bitrate <= bps*9/10 { // 留 10% 余量
target = l
}
}
if target.SpatialID > s.currentLayer.SpatialID && !s.canSwitchUp() {
return s.currentLayer // 升档需 keyframe,否则继续旧层
}
return target
}
为什么要在层选择上"降档激进、升档保守":
- 升档需要等关键帧,否则订阅者收到无法独立解码的高层 P 帧会绿屏。常见做法是切层时主动发 PLI 给发布端
- 降档可以立即生效,因为低层独立编码(Simulcast 三路独立 / SVC 基础层独立),随时切换都能解
3.3 SSRC 重写:必做不可省
// C++ 关键结构:每个订阅维度维护偏移表
struct SsrcContext {
uint32_t out_ssrc; // 下游看到的 SSRC(SFU 自己分配)
uint16_t seq_offset; // 切层时序号补偿
uint32_t ts_offset; // 时间戳补偿,避免抖动
uint16_t last_seq;
uint32_t last_ts;
bool key_frame_seen; // 升档屏障
};
bool RewriteForSubscriber(rtp::Packet& pkt, SsrcContext& ctx) {
pkt.set_ssrc(ctx.out_ssrc);
pkt.set_sequence_number(static_cast<uint16_t>(pkt.sequence_number() + ctx.seq_offset));
pkt.set_timestamp(pkt.timestamp() + ctx.ts_offset);
ctx.last_seq = pkt.sequence_number();
ctx.last_ts = pkt.timestamp();
return true;
}
不重写 SSRC 的后果(见反模式 § 11.1):订阅端同时收到来自不同发布者的相同 SSRC,jitter buffer 错乱,画面定格或解码崩溃。
4. 订阅模型设计
4.1 Pub/Sub 解耦
SFU 不应该让订阅端直接挑 SSRC,而应该挑 (roomID, peerID, mediaKind, preferredLayer)。这样:
- 重连后 SSRC 变了订阅意图不变
- 端侧不需要理解服务端的内部映射
- 切层、降级、断流自动在 SFU 内部完成
4.2 三种订阅意图
| 模式 | 描述 | 适用场景 |
|---|---|---|
| Auto | SFU 根据下行 BWE + 容器尺寸自动选层 | 大班课、会议网格 |
| Pinned | 固定订阅最高层 | 主讲人、舞台位 |
| Manual | 端侧 API 指定 RID / spatialId | 调试、特殊业务 |
实现层面只需在 Subscription 上维护一个 LayerPolicy 接口,三种模式实现不同的 PickLayer 即可,转发主路径不动。
4.3 何时不适合 Pub/Sub 模型
如果业务是 基于 ROI 的局部裁剪(例如远程协作只看屏幕的某一区域),SFU 模式无法做局部裁剪,必须降级到 MCU 或在端侧裁剪。
5. TWCC 反馈处理
5.1 TWCC 协议要点(RFC 8888 / draft-holmer)
- 发送端在每个 RTP 包附带 transport-wide sequence number(RTP header extension)
- 接收端按时间窗口聚合 (seq, recv_time) 写入 RTCP TWCC 反馈包
- 发送端据此计算 per-packet delay,输入 GCC / TrendLine 估带宽
5.2 SFU 在 TWCC 链路中的角色
关键点:SFU 不是直通管道,而是"两段独立链路"。
- 上行段:发布端 → SFU。SFU 充当接收端,自己生成 TWCC 反馈给发布端
- 下行段:SFU → 订阅端。SFU 充当发送端,给每个出包打 transport-wide seq,订阅端回 TWCC 给 SFU
Publisher ──RTP+TWCC seq──> SFU // 上行
Publisher <──TWCC FB────── SFU // 上行带宽估计
SFU ──RTP+TWCC seq───────> Subscriber A // 下行
SFU <──TWCC FB──────────── Subscriber A // 下行带宽估计
5.3 聚合策略
- 窗口大小:50 ~ 100ms,太小反馈过密占带宽,太大估计滞后
- Chunk 编码:Run Length / Status Vector 两种 chunk 自适应选择,连续相同状态时用 Run Length 省字节
- 乱序与丢失:丢失的包在 TWCC 中标记为
NotReceived,必须保留,否则发送端无法估丢包率
5.4 与 REMB 的兼容
- REMB(Receiver Estimated Maximum Bitrate)是老协议,只回带宽数字,不带 per-packet delay
- 现代浏览器默认协商 TWCC,REMB 仅在对端不支持 TWCC 时回退
- SFU 实现需要:协商时优先 TWCC,回落到 REMB 时只能用粗粒度估计,BWE 精度下降
什么时候不适用:低版本 Android WebView 或某些嵌入式 SDK 仍只发 REMB,此时 SFU 的下行 BWE 颗粒度会变粗,需要降低升档敏感度避免抖动。
6. NACK / PLI / FIR 处理
6.1 NACK(Negative Acknowledgement,丢包重传)
- 订阅端发现 seq 跳跃,发 RTCP NACK 给 SFU
- SFU 维护 每订阅者一份的发送缓冲(默认 500ms ~ 1s)
- 命中缓冲则用 RTX SSRC 重传,未命中则透传 NACK 给发布端
// 简化的 NACK 处理
func (s *Subscription) OnNACK(seqs []uint16) {
for _, seq := range seqs {
if pkt := s.sendBuffer.Get(seq); pkt != nil {
s.sendRTX(pkt) // 命中本地缓存
} else {
s.upstreamNACK <- seq // 透传给上游
}
}
}
为什么需要本地缓冲:每个订阅者的丢包是独立的,把 NACK 全透传会让发布端被 N 路放大轰炸。
6.2 PLI / FIR(关键帧请求)
- PLI(Picture Loss Indication):编码器层信号,请求一个 keyframe
- FIR(Full Intra Request):要求立即生成 IDR,更强制
- 现代实现 99% 用 PLI
SFU 触发 PLI 的时机:
- 新订阅者加入(首帧必须是关键帧才能解码)
- Simulcast / SVC 升档
- 长时间丢包恢复后
- 收到订阅端 PLI 透传
关键:PLI 必须节流。一秒内收到 10 个订阅者的 PLI,只发 1 个给发布端即可,否则发布端被迫频繁 IDR,码率剧烈波动。
class PliThrottler {
std::chrono::steady_clock::time_point last_;
static constexpr auto kMinInterval = std::chrono::milliseconds(500);
public:
bool Allow() {
auto now = std::chrono::steady_clock::now();
if (now - last_ < kMinInterval) return false;
last_ = now;
return true;
}
};
7. Pacer:发送端平滑
7.1 为什么必须有 Pacer
视频编码器吐包是 burst 的:一帧 1080p I 帧可能有 100KB+ 数据被切成 80 个 RTP 包,瞬时几乎同时进发送队列。直接送出去会导致:
- 链路瞬时拥塞,路由器尾丢
- TWCC delay 测出虚假尖峰,BWE 误降码率
- 接收端 jitter buffer 被冲垮
Pacer 把这种 burst 按目标码率(通常是 BWE 估计的 1.0 ~ 2.5x,留 retransmit 余量)平滑成均匀流。
7.2 实现要点
type Pacer struct {
queues map[Priority]*PacketQueue
budget int64 // 字节预算
rate int64 // 当前目标 bps
ticker *time.Ticker // 5ms 粒度
}
func (p *Pacer) tick() {
// 每 5ms 补充预算
p.budget += p.rate * 5 / 1000 / 8
for p.budget > 0 {
pkt := p.dequeueByPriority() // 先音频后视频,先低层后高层
if pkt == nil { break }
p.send(pkt)
p.budget -= int64(len(pkt.payload))
}
}
优先级建议:
- 音频(必须最低延迟)
- 重传 RTX 包
- 视频低层(基础层不能丢)
- 视频高层
- 探测包(probe,用于试探更高带宽)
7.3 失败的样子
不做 Pacer 时:发布端发关键帧瞬间,下行所有订阅者都收到一个抖动尖峰,TWCC 反馈让 BWE 在 1 秒内从 3Mbps 估到 800kbps,画面立刻打码,1 ~ 2 秒后慢慢恢复。这种锯齿状码率曲线是没有 Pacer 的典型征兆。
8. 真实性能数据参考
以下数据来自一台 Intel Xeon E5-2680 v4(2.4GHz 单核基准)+ 25Gbps 网卡 + Linux 5.15,上游 720p30 H.264 baseline 1.5Mbps,下游不转码:
8.1 单核 100 路 720p30 转发的画像
| 指标 | 数值 |
|---|---|
| 上行包速率 | ~12,500 pkt/s(100 路 × 125 pkt/s) |
| 下行包速率 | ~125,000 pkt/s(每路平均 10 个订阅者) |
| 单核 CPU 占用 | 65% ~ 80% |
| 内存占用 | ~450 MB(含每订阅者 1s 发送缓冲) |
| 端到端中转延迟(P50) | 3 ~ 5 ms |
| 端到端中转延迟(P99) | 12 ~ 18 ms |
8.2 CPU 分布(perf 采样近似比例)
| 模块 | 占比 |
|---|---|
| SRTP 解密 + 加密 | 35% ~ 40% |
| RTP 解析 / 路由匹配 | 15% |
| Pacer 调度 | 10% |
| RTCP 聚合(TWCC/NACK) | 12% |
| BWE 计算 | 5% |
| 网络 syscall(recvmmsg/sendmmsg) | 15% |
| 其他 | 余量 |
启示:
- SRTP 是大头,开 AES-NI 指令集能省 40%;用 GCM 比 SHA1 快但要看版本协商
- syscall 用 recvmmsg/sendmmsg 批量收发,单包 syscall 在 100k pkt/s 下 syscall 自身就吃 10%+ CPU
- BWE 不是瓶颈,但要避免每个包都跑一次完整估计,按窗口聚合即可
8.3 横向扩展
实战中通常每物理核留 1 ~ 2 个 SFU worker(pin CPU),按 NUMA 分组,单台 32 核机器可承载 3000+ 路 720p30 上行、30000+ 路下行订阅。
8.4 什么时候不适用这些数据
- ARM 服务器(如 Graviton):SRTP 路径若没用 ARMv8 Crypto 扩展会慢 1.5 ~ 2x
- 大量 1080p / 4K:包速率不变但 payload 大,瓶颈从 CPU 转到网卡 PPS / 带宽
- 高丢包链路:NACK 重传放大 1.3 ~ 1.5x,需预留 buffer
9. 级联(Cascading):Origin SFU + Edge SFU
9.1 为什么要级联
- 跨地域:北京发布、东京订阅,单 SFU 无法兼顾两端 RTT
- 超大房间:单 SFU 下行扇出 5000+,单机带宽顶不住
- 就近接入:边缘 SFU 离用户近,端到端延迟低
9.2 拓扑
9.3 关键设计
- Origin → Edge 永远传送全部层,因为不知道 Edge 下游需要什么。一旦剥离了高层,下游用户即使有带宽也升不上去
- Edge 独立做 BWE 与 Pacer,对 Origin 表现为一个特殊订阅者
- 跨级 RTCP 不能透传:Edge 侧的 NACK 应优先在 Edge 自身缓冲命中,未命中再向 Origin 申请,否则跨地域重传 RTT 太长(300ms+)已经不实用
- TWCC 双段独立:每段链路独立估带宽,不能合并
9.4 SVC 多层选转
SVC(VP9 / AV1)天然支持级联:
- Origin 只发一份完整 SVC 流到 Edge
- Edge 根据下游订阅者带宽剥离不需要的 spatial / temporal 层
- 比 Simulcast 省上行带宽,但端侧编码 CPU 高
9.5 失败的样子
- Origin 误剥层:Edge 下游用户网络好转想升档,发现 Origin 早把高层丢了,永远卡在低分辨率
- Edge 缓冲不够:跨地域链路抖动大,1s 缓冲不够命中 NACK,端侧出现周期性卡顿
- 环路:双向级联配置错误,A 把流转给 B、B 又转回 A,CPU 飙满到崩溃
10. 负载均衡
10.1 房间粒度的一致性 Hash
WebRTC 业务的天然粒度是 房间,因为同一房间内成员必须落在同一个 SFU(或同一组级联 SFU)才能互通。
sfuNode = consistentHash(roomID) % activeSFUs
- 为什么用一致性 Hash:节点扩缩时只迁移一小部分房间,绝大多数房间的现有连接不受影响
- 加权:按节点剩余 CPU/带宽配额加权,避免新加入的节点空载
- 虚拟节点:每个物理 SFU 映射 100 ~ 200 个虚拟节点,平滑负载
10.2 热点房间扩缩
突发热点(明星连麦、突发新闻)导致单房间几万人,单 SFU 扛不住。处理:
- 预探测:会前就知道是大房间的(直播开播预约),直接走级联拓扑
- 运行时检测:监控单房间订阅数 / 带宽,超过阈值触发自动级联,新订阅者落到 Edge SFU
- 平滑迁移:现有订阅者不动,只把新订阅者引到 Edge,老连接自然消亡
10.3 信令与媒体分离
- 信令服务器:无状态,纯轮询负载均衡
- SFU 选择:信令在
joinRoom时通过 ConsistentHash + 节点健康度选出 SFU,把 ICE 候选返回给客户端 - 客户端拿到的 ICE 候选直连 SFU,不走信令链路
10.4 失败的样子
- 简单轮询:同房间成员落到不同 SFU,互相看不到,必须靠 SFU 间转发,复杂度炸裂
- Hash 不一致:节点扩缩时所有房间重洗,海量重连
- 不做加权:新节点上线瞬间被空房间塞满,老节点继续过载
11. STUN / TURN 部署
11.1 STUN 和 TURN 的角色区别
- STUN:帮客户端发现公网映射地址,自身不中转媒体,资源消耗极低
- TURN:当 P2P 打洞失败时中转媒体,吃带宽和 CPU
11.2 部署位置策略
| 场景 | 推荐部署 |
|---|---|
| STUN | 与 SFU 同机房或独立小机器,全球散布 |
| TURN | 边缘节点(CDN POP 类)尽量靠近客户端 |
| TURN over TLS 443 | 必备,绕过企业防火墙限制 UDP / 非常用端口 |
11.3 SFU 与 TURN 的关系
理论上客户端连到 SFU 不需要 TURN(SFU 公网可达就够),但实际场景要部署 TURN:
- 企业内网只允许 TCP/443 出站
- 运营商对 UDP 严格 QoS,导致质量差
- 客户端在对称型 NAT 下打洞失败
此时客户端 → TURN → SFU 形成两跳,TURN 仅做包透传,不解 SRTP。
11.4 TURN over TLS 443 的工程要点
- 证书:用合法 CA 证书(Let's Encrypt 即可),自签证书在企业 SSL 中间盒下会被拦
- 端口:必须 443,避免 5349 等"看起来像 TURN"的端口被防火墙识别
- SNI:开启 SNI,服务器多租户更省机器
- TCP vs UDP:TLS over TCP 必须开 Nagle off + TCP_QUICKACK,否则小包延迟高
11.5 TURN REST API 鉴权(draft-uberti-rtcweb-turn-rest)
不能把长期凭证写死在客户端,否则被反编译就裸奔。
username = unix_timestamp_expiry + ":" + userId
password = base64(HMAC-SHA1(secret, username))
工作流:
- 客户端登录后请求业务后端的
/turn-credential接口 - 业务后端用预共享 secret 签发短期(10 分钟)凭证
- 客户端把凭证作为 ICE server 配置传给 RTCPeerConnection
- TURN 服务器收到 BIND/ALLOCATE 请求时校验 HMAC
什么时候不适用:完全内网部署且客户端可信时可以用静态长期凭证,但生产环境强烈建议永远用 REST API 模式。
11.6 失败的样子
- TURN 部署在远端中心机房:跨洋客户端中转 RTT 增加 200ms+,体验崩溃
- 不开 TLS 443:5% ~ 15% 企业用户进不来连接全失败,且无法定位原因
- 写死长期凭证:被恶意用户白嫖带宽,月底账单炸裂
- TURN 与 SFU 强绑定:TURN 故障导致 SFU 全部不可用,两者应独立扩缩
12. 反模式(必读)
12.1 SFU 直接转发不重写 SSRC
症状:订阅端拉多个发布者时,浏览器收到不同流但 SSRC 冲突,jitter buffer 把它们当成一路,画面定格 / 乱序 / 解码错。
正确做法:每订阅维度独立分配输出 SSRC,重写头部,建立 (in_ssrc, sub_id) → out_ssrc 映射表。
12.2 TWCC 反馈漏聚合
症状:订阅端的 TWCC 反馈每包一个直接转发给发布端,发布端 BWE 接收 N 倍噪声,估带宽剧烈震荡。
正确做法:SFU 必须对每段链路独立做 TWCC 接收/发送,不要透传。
12.3 不做 Pacer 导致瞬时拥塞
症状:码率曲线呈锯齿状,每 1 ~ 2 秒一个尖峰对应关键帧,BWE 频繁误降。
正确做法:所有出包必须经过 Pacer,按 BWE 估计的 1.5 ~ 2.5x 平滑发送,关键帧切片下发。
12.4 级联时不剥离 RTX 浪费带宽
症状:Edge 把上游的 RTX 包原样转给下游,下游早就靠 Edge 本地缓冲恢复了,纯粹双倍带宽浪费。
正确做法:Origin 到 Edge 链路只传原包,Edge 根据下游的 NACK 独立维护 RTX,不级联 RTX SSRC。
12.5 PLI 不节流
症状:每个新订阅者加入都触发一个 PLI,发布端被迫连续打 IDR,码率曲线满是巨型关键帧,普通用户体验抖动。
正确做法:500ms ~ 1s PLI 节流窗口,新订阅者在窗口内复用最近一次 keyframe 即可。
12.6 NACK 透传
症状:发布端收到 N 个订阅端的 NACK 重传请求,被迫为不同丢包重发,上行带宽放大数倍。
正确做法:SFU 维护每订阅者的发送缓冲,NACK 优先本地命中,不命中才向上游申请且做去重。
12.7 SVC 升档不等关键帧
症状:订阅者带宽好转切到高层,下游收到无法解码的高层 P 帧,画面绿屏 / 黑屏一两秒。
正确做法:升档前主动发 PLI 给发布端,等下一个 keyframe 到来才完成切层。
12.8 SFU 内部解码做"小操作"
症状:业务想加个角标 / 文字水印,于是在 SFU 解码加水印再编码,CPU 直接 10x 增长。
正确做法:水印在端侧或独立的转码服务做,SFU 主路径绝不解 payload。
12.9 房间负载均衡用轮询
症状:同一房间用户落到不同 SFU,要么互通失败,要么被迫做 SFU 间媒体桥接,复杂度爆炸。
正确做法:以 roomID 为单位的一致性 Hash,房间作为最小调度单元。
12.10 TURN 凭证写死
症状:客户端被反编译,TURN 凭证泄漏,被恶意用户长期占用带宽。
正确做法:TURN REST API 模式签发短期凭证。
13. 自研决策清单
在动手前确认以下问题,每个都关系到代码量与运维难度:
- 单房间最大并发:决定是否需要级联
- 端侧设备分布:低端设备多则 Simulcast 优先,高端多则 SVC 更香
- 跨地域需求:决定 Origin/Edge 拓扑
- E2EE 需求:决定是否需要 SFrame 集成
- 录制需求:是否需要 SFU 旁路录制(不解码原始 RTP 落盘)
- 转码需求:明确说"SFU 不转码",转码独立做
- 团队对 RTP/RTCP/拥塞控制的理解程度:不够则建议先用 mediasoup / Pion ion-sfu 改造
- 监控指标:至少 per-room / per-stream / per-subscriber 的丢包、RTT、码率、BWE 估计、PLI/NACK 速率
14. 权威资料
14.1 RFC
- RFC 3550 — RTP: A Transport Protocol for Real-Time Applications
- RFC 3711 — SRTP
- RFC 4585 — RTP/AVPF (Feedback)
- RFC 5104 — Codec Control Messages (PLI / FIR)
- RFC 8285 — RTP Header Extensions
- RFC 8853 — Using Simulcast in SDP and RTP Sessions
- RFC 8854 — RID
- RFC 8888 — RTP Congestion Control Feedback
- draft-holmer-rmcat-transport-wide-cc-extensions — TWCC(事实标准)
- RFC 7635 — TURN REST API
- RFC 8656 — TURN
14.2 开源参考实现
- Pion WebRTC(Go) — 模块化,最适合学习
- ion-sfu(Pion 上的 SFU 实现)
- mediasoup(C++/Node.js) — 工业级 SFU
- Janus Gateway(C)
- LiveKit(Go) — 现代级联 SFU 参考
- Jitsi Videobridge(Java)
- Galene(Go,单文件 SFU 学习)
- WebRTC.org native code(C++) — BWE / Pacer 工业级源码
14.3 重要文献与文章
- Google "Send-Side Bandwidth Estimation"(GCC 论文)
- "Improving WebRTC Video Quality" — bloggeek.me 系列
- "Cascading SFUs" — mediasoup / LiveKit 官方博客
核对日期:2026-06-22