WebRTC 全景实战 (11):Simulcast、SVC 与选择性订阅
"从多播幻想到 packet shufflers——SFU 是 WebRTC 多人会议的必然归宿。" — WebRTC for the Curious 历史
Ch9 Simulcast 入门 介绍了三档发布。本章深入 SFU 如何根据订阅者带宽选择转发层——这是从 P2P 跃迁到多人会议的核心机制。
Marratech 早期押注 IP 多播(Multicast),Serge Lachapelle 在 Curious 访谈 中回忆:公网多播从未真正落地,行业最终转向 packet shufflers(SFU)——Simulcast + 选择性订阅是 SFU 的标配能力。Google 2010 年收购 Marratech 后,这套架构在 Meet 中大规模验证。
配套 Lab:examples/webrtc-lab/client/ch02-p2p-basic 扩展 Simulcast + client/ch12-sfu-client(LiveKit)。
本篇术语表
| 术语 | 英文 | 解释 |
|---|---|---|
| Simulcast | 联播 | 同一视频源独立编码多个分辨率/码率层,同时发送 |
| SVC | Scalable Video Coding | 可伸缩视频编码,单流内含多层,层间有依赖关系 |
| RID | RTP Stream Identifier | Simulcast 层的标识符(h / m / l) |
| SSRC | Synchronization Source | RTP 同步源标识,Simulcast 每层独立 SSRC |
| MID | Media Identification | Unified Plan 下 m-line 与 transceiver 的绑定标识 |
| LayerSelector | 层选择器 | SFU 根据订阅者带宽选择转发的 Simulcast/SVC 层 |
| Dynacast | 动态联播 | LiveKit 按需发布——无订阅者时不发送高层 |
| Dependency Descriptor | 依赖描述符 | AV1/VP9 SVC 的层依赖关系 RTP 头扩展 |
| Temporal Layer | 时间层 | SVC 中按帧率分层的子流(T0/T1/T2) |
| Spatial Layer | 空间层 | SVC 中按分辨率分层的子流(L0/L1/L2) |
| Selective Subscription | 选择性订阅 | 订阅者或 SFU 按需求选择接收的层/Track |
| Adaptive Stream | 自适应流 | LiveKit 客户端自动调整订阅分辨率 |
| Content Hint | 内容提示 | motion / detail / text 影响编码策略 |
| Pause Video | 暂停视频 | 带宽不足时 SFU 停止转发视频但保持音频 |
| Active Speaker | 活跃说话者 | 大会议中优先订阅当前发言人的高清层 |
一、为什么需要多层视频?
Ch10 GCC 可以在单流内降码率,但无法在不转码的情况下降分辨率。Simulcast/SVC 让 SFU 在不做转码的前提下,为不同带宽的订阅者转发不同质量的层——这是 SFU 的核心价值。
Serge Lachapelle 在 Curious 访谈中解释:Meet 早期尝试过 MCU 混流,但转码延迟和 CPU 成本不可接受;packet shuffler + Simulcast 才是可扩展路径。
二、Simulcast vs SVC 架构对比
| 特性 | Simulcast | SVC |
|---|---|---|
| 编码次数 | N 次独立编码 | 1 次编码,多层输出 |
| 上行带宽 | 高(各层码率之和) | 低(仅最高层码率) |
| 下行灵活性 | SFU 选 SSRC/rid 转发 | SFU 解析 Dependency Descriptor 选层 |
| SFU 复杂度 | 低(按 SSRC 切换) | 中(需理解层依赖) |
| 编解码器 | VP8/H.264/VP9/AV1 均可 | VP9 / AV1 为主 |
| CPU 消耗 | 高(多编码器) | 低(单编码器) |
| 典型框架 | mediasoup / Janus | LiveKit Dynacast |
2.1 选型决策树
三、Simulcast 的 SDP 与 RTP 细节
3.1 Unified Plan + RID
SDP 关键行:
a=simulcast:send h;m;l
a=rid:h send
a=rid:m send
a=rid:l send
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
| 字段 | 作用 |
|---|---|
a=simulcast:send h;m;l | 声明发送三个 Simulcast 层 |
a=rid:h send | 定义 rid 标识与方向 |
mid:0 | 绑定到 Unified Plan 的 transceiver |
rtp-stream-id | RTP 头扩展携带 rid |
3.2 发布端代码
// examples/webrtc-lab/client/ch02-p2p-basic 扩展
const transceiver = pc.addTransceiver("video", {
direction: "sendonly",
sendEncodings: [
{ rid: "h", maxBitrate: 1_500_000, scaleResolutionDownBy: 1, maxFramerate: 30 },
{ rid: "m", maxBitrate: 500_000, scaleResolutionDownBy: 2, maxFramerate: 24 },
{ rid: "l", maxBitrate: 150_000, scaleResolutionDownBy: 4, maxFramerate: 15 },
],
});
// 验证 Simulcast 是否生效
const sender = transceiver.sender;
const params = sender.getParameters();
console.log("Encodings:", params.encodings.map((e) => e.rid));
// 期望: ["h", "m", "l"]
plan-b 已废弃。确保 RTCPeerConnection 使用默认 Unified Plan,且 addTransceiver 而非 addStream。
3.3 屏幕共享 vs 摄像头 Simulcast
// 屏幕共享:更高码率,较少层
const screenTransceiver = pc.addTransceiver(screenTrack, {
direction: "sendonly",
sendEncodings: [
{ rid: "h", maxBitrate: 3_000_000, maxFramerate: 15 },
{ rid: "l", maxBitrate: 500_000, maxFramerate: 5 },
],
});
screenTrack.contentHint = "detail"; // 保文字清晰
四、SVC 层结构与 Dependency Descriptor
SVC 的关键优势:丢弃高层不影响低层解码。SFU 可以在 RTP 级别丢弃 L2/T2 包,订阅者仍能解码 L0/T0。
4.1 VP9 SVC 模式
| 模式 | 空间层 | 时间层 | 典型用途 |
|---|---|---|---|
| L1T1 | 1 | 1 | 最低开销 |
| L1T3 | 1 | 3 | 帧率自适应 |
| L3T3 | 3 | 3 | 全自适应(LiveKit 默认) |
4.2 AV1 Dependency Descriptor
AV1 的 Dependency Descriptor(DD)头扩展让 SFU 无需解码即可知道每个 RTP 包属于哪一层:
a=extmap:11 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension
五、SFU 选择性转发流程
5.1 SFU Forwarder 内部逻辑
Pion(Go)和 LiveKit(Go + Pion)都实现了 Forwarder + StreamTracker 管线。LiveKit 的 LayerSelector 会综合考虑:
- 订阅者的 TWCC 带宽估计(Ch10)
- 订阅者请求的视频质量(
HIGH/MEDIUM/LOW) - 发布者当前可用的 Simulcast 层
- CPU/带宽保护阈值
5.2 层切换时序
层切换不是瞬时的——SFU 需要等待新层 Keyframe(PLI/FIR 触发)。
六、Dynacast:按需发布
LiveKit 的 Dynacast 进一步优化上行带宽:若无人订阅高层,停止发送该层:
Dynacast 对大型会议(100+ 参与者,大部分静音/小窗)节省的上行带宽非常可观。详见 LiveKit 介绍。
6.1 Dynacast 与 Active Speaker
七、大会议带宽模型
| 架构 | 6 人会议每人连接数 | 6 人每人上行 | 6 人每人下行 |
|---|---|---|---|
| Full Mesh | 5 条 P2P | 5 × 码率 | 5 × 码率 |
| SFU | 1 条到 SFU | 1 × Simulcast 码率 | (N-1) × 单流码率 |
| MCU | 1 条到 MCU | 1 × 码率 | 1 × 混流码率 |
7.1 带宽估算公式
Simulcast 上行 = rid_h + rid_m + rid_l(无 Dynacast)
≈ rid_active_layers(有 Dynacast)
SFU 下行/订阅者 = Σ(每个远程 participant 的选中层码率)
SFU 总转发量 = Σ(每个订阅者的下行之和)
示例:10 人会议,3 档 Simulcast(1.5M + 0.5M + 0.15M = 2.15M 上行/人),SFU 为每个订阅者转发 9 路 × 选中层(假设平均 500kbps)= 4.5Mbps/订阅者。
7.2 100 人会议优化策略
| 策略 | 节省 | 实现 |
|---|---|---|
| Active Speaker 高清 | 下行 80%+ | 仅 1 人 rid=h |
| 视频 Pause | 下行 50%+ | 非可见 tile 不订阅 |
| Dynacast | 上行 60%+ | 无订阅者停发高层 |
| 音频 always-on | — | 音频独立 Track,~64kbps/人 |
八、代码:Simulcast 发布与订阅控制
8.1 发布端完整示例
async function publishWithSimulcast(room, localTrack) {
await room.localParticipant.publishTrack(localTrack, {
simulcast: true,
videoEncoding: {
maxBitrate: 1_500_000,
maxFramerate: 30,
},
videoSimulcastLayers: [
{ rid: "h", scaleResolutionDownBy: 1, maxBitrate: 1_500_000 },
{ rid: "m", scaleResolutionDownBy: 2, maxBitrate: 500_000 },
{ rid: "l", scaleResolutionDownBy: 4, maxBitrate: 150_000 },
],
});
}
LiveKit SDK 封装了 room.setVideoQuality(participant, quality) 等高层 API,见 LiveKit 介绍。
8.2 订阅质量控制
import { VideoQuality } from "livekit-client";
// 为特定参与者设置订阅质量
room.setVideoQuality(remoteParticipant.identity, VideoQuality.HIGH);
room.setVideoQuality(otherParticipant.identity, VideoQuality.LOW);
// 暂停某参与者视频(保留音频)
room.setVideoSubscription(remoteParticipant.identity, false);
8.3 原生 WebRTC 订阅层控制
const receiver = pc.getReceivers().find((r) => r.track?.kind === "video");
const params = receiver.getParameters();
params.degradationPreference = "maintain-framerate";
await receiver.setParameters(params);
8.4 getStats 验证 Simulcast 层
async function logSimulcastStats(pc) {
const stats = await pc.getStats();
stats.forEach((r) => {
if (r.type === "outbound-rtp" && r.kind === "video") {
console.log({
rid: r.rid,
ssrc: r.ssrc,
bytesSent: r.bytesSent,
framesEncoded: r.framesEncoded,
targetBitrate: r.targetBitrate,
frameWidth: r.frameWidth,
frameHeight: r.frameHeight,
});
}
});
}
// 期望看到 3 个 outbound-rtp,rid 分别为 h/m/l
九、Marratech → Google Meet 的架构启示
Serge Lachapelle 在 Curious 访谈中的核心观点:SFU 不是对 P2P 的妥协,而是公网多人会议的唯一可行架构。Simulcast 解决了 SFU「不转码就无法适配不同带宽」的问题;SVC 和 Dynacast 则是 Simulcast 上行带宽代价的后续优化。
十、常见陷阱
| # | 陷阱 | 现象 | 修复 |
|---|---|---|---|
| 1 | Simulcast 未在 SDP 生效 | 仅 1 个 SSRC | 检查 Unified Plan + sendEncodings |
| 2 | rid 命名不规范 | SFU 无法选层 | 使用 h/m/l 或 f/h/q 约定 |
| 3 | 三层码率之和超上行 | 全部层丢包 | 启用 Dynacast 或降低 maxBitrate |
| 4 | SVC 层依赖解析错误 | 订阅者花屏 | 检查 DD 头扩展协商 |
| 5 | 忽略音频独立转发 | 视频降层但音频占带宽 | 音频始终独立 Track |
| 6 | SFU 转码误用 | 延迟高、CPU 爆 | SFU 应只转发不转码 |
| 7 | 大会议全量订阅 | 下行带宽爆炸 | 仅订阅 active speaker + 可见 tile |
| 8 | 层切换无 Keyframe | 切换后黑屏 2s | SFU 发 PLI 请求 I 帧 |
| 9 | H.264 Simulcast profile 不一致 | 解码失败 | 统一 profile-level-id |
| 10 | 屏幕共享用摄像头三档 | 文字模糊/带宽浪费 | 屏幕共享单独 2 档配置 |
十一、实战 Lab
Lab 1:验证 Simulcast 三档
cd examples/webrtc-lab/signaling && npm start
npx serve examples/webrtc-lab/client/ch02-p2p-basic
- 修改
main.js添加sendEncodings三档 - 通话后运行
logSimulcastStats(pc) - 确认 3 个
outbound-rtp报告
Lab 2:带宽限制与层切换
- Chrome DevTools 限速 300kbps
- 观察
rid=l的bytesSent持续增长,rid=h/m停滞 - 取消限速,观察
rid=h恢复
Lab 3:LiveKit SFU 选择性订阅
livekit-server --dev
# 使用 examples/webrtc-lab/client/ch12-sfu-client
- 3 人加入同一 Room
- 订阅者 A 设置
VideoQuality.HIGH,订阅者 B 设置VideoQuality.LOW - 在 LiveKit Dashboard 观察不同下行码率
Lab 4:Simulcast vs SVC 上行对比
- 发布者 A:Simulcast 三档 VP8
- 发布者 B:SVC VP9(LiveKit 默认)
- 对比
getStats中总bytesSent速率
Lab 5:Dynacast 验证
- 发布者加入 Room 但不订阅任何远程 Track
- 观察 LiveKit 是否停止发送 h/m 层(通过发布者
getStats)
Lab 6:层切换延迟测量
- 通话中 DevTools 从 Unlimited 切到 300kbps
- 记录
rid变化时间戳 - 对比 SFU 层切换 vs 单流 GCC 降分辨率的速度
Lab 7:屏幕共享 Simulcast
- 发布屏幕共享 +
contentHint: "detail" - 订阅者限速 500kbps
- 确认文字仍可读(l 层保分辨率)
十二、本章小结
| 概念 | 要点 |
|---|---|
| Simulcast | 独立编码多层,SFU 按 rid 转发,上行代价高 |
| SVC | 单流多层,上行高效,SFU 需解析层依赖 |
| Dynacast | 按需发布,节省上行 |
| SFU 选层 | TWCC BWE + 订阅偏好 → LayerSelector |
| 大会议 | Active Speaker + Pause + Dynacast 三件套 |
| 历史 | Marratech 多播 → packet shuffler → Meet/LiveKit |
下一篇(Ch12):SFU 架构与 Pion 实战
系列导航
章节 主题 状态 0 架构全景与协议栈地图 ✅ 已发布 1 浏览器媒体 API 与设备管理 ✅ 已发布 2 第一个 P2P 视频通话 ✅ 已发布 3 信令服务器设计与会话状态机 ✅ 已发布 4 SDP 解剖与媒体协商 ✅ 已发布 5 ICE、STUN、TURN 与 NAT 穿透 ✅ 已发布 6 Data Channel 与 SCTP over DTLS ✅ 已发布 7 DTLS 握手与 SRTP 加密体系 ✅ 已发布 8 RTP/RTCP 媒体传输与 QoS ✅ 已发布 9 音视频编解码与 Simulcast 入门 ✅ 已发布 10 带宽估计与拥塞控制 GCC ✅ 已发布 11 Simulcast、SVC 与选择性订阅 ✅ 已发布 12 SFU/MCU/Mesh 架构与 Pion 实战 ✅ 已发布 13 调试工具链与可观测性 ✅ 已发布 14 TURN 集群部署与多区域扩展 ✅ 已发布 15 Capstone 生产级视频会议系统 ✅ 已发布