12、Simulcast与SVC:Simulcast原理、SVC分层编码、多流切换实战
这一章我们来聊聊视频会议里最核心的痛点——带宽适配。
你想想看,一个会议室里,有人用5G手机,有人还在用3G热点,还有人连着公司WiFi但正在下载大文件。如果我只发一路视频流,那要么高清用户觉得糊,要么低带宽用户卡成PPT。怎么办?
业界有两个主流方案:Simulcast 和 SVC。我当年刚接触时也傻傻分不清,后来在几个项目中反复踩坑,才算真正吃透。今天我把这些经验掰开揉碎讲给你。
12.1 Simulcast:简单粗暴的多流并行
Simulcast 的思路很直接——发送端同时编码多路不同分辨率的视频流,接收端根据自身带宽选择最合适的一路。
举个例子:
- 高清流:1280x720,2Mbps
- 标清流:640x360,500Kbps
- 低清流:320x180,150Kbps
接收端A带宽充足,订阅高清流;接收端B在移动网络,订阅低清流。各取所需,互不影响。
核心要点:Simulcast 是发送端编码多路独立流,接收端选择性订阅。每一路都是完整的独立码流,互不依赖。
我在项目中遇到过一个问题:客户端同时编码三路流,CPU直接飙到80%。后来发现是编码器配置没优化——其实可以用硬件编码器分担一路,软件编码器分担另外两路,或者干脆只编两路(720p + 360p),180p由360p下采样得到。
12.1.1 Simulcast 的 SDP 协商
Simulcast 在 WebRTC 中通过 SDP 的 simulcast 属性来协商。看个例子:
// 发送端 SDP 片段
a=simulcast:send rid=h;q;l
a=rid:h send pt=96;max-width=1280;max-height=720
a=rid:q send pt=97;max-width=640;max-height=360
a=rid:l send pt=98;max-width=320;max-height=180
这里 rid 是流的标识符,h 代表高清,q 代表标清,l 代表低清。接收端在 SDP 中声明自己要接收哪些 rid:
// 接收端 SDP 片段
a=simulcast:recv rid=h;q;l
嗯,这里要注意:Simulcast 的 SDP 协商在不同浏览器实现上有细微差异。Chrome 和 Firefox 的 rid 命名规则就不完全一样。我建议你在做跨浏览器测试时,用 chrome://webrtc-internals 抓一下 SDP 看看。
12.2 SVC:优雅的分层编码
SVC(Scalable Video Coding)的思路更优雅——只编码一路流,但这路流包含多个层次。接收端可以根据带宽,只解码部分层次。
SVC 通常分三个维度:
- 时间可伸缩(Temporal):控制帧率。比如基础层15fps,增强层30fps
- 空间可伸缩(Spatial):控制分辨率。比如基础层360p,增强层720p
- 质量可伸缩(Quality):控制量化参数。比如基础层QP=30,增强层QP=20
个人经验:实际项目中,时间可伸缩用得最多。因为帧率降低对用户体验的影响最小,而且实现起来最简单。空间可伸缩在移动端场景很实用,但编码器支持度不如时间可伸缩好。
12.2.1 SVC 的依赖关系
SVC 的核心是层间依赖。看这个典型的三层时间可伸缩结构:
TL0: I P P P ... (基础层,15fps)
TL1: B B B B ... (增强层1,30fps)
TL2: b b b b ... (增强层2,60fps)
TL0 是独立可解码的。TL1 依赖 TL0。TL2 依赖 TL1 和 TL0。如果带宽只够传 TL0,那就只解码 TL0,得到15fps的视频。带宽够了,再加 TL1 和 TL2。
避坑指南:我曾经在项目中遇到一个问题——SVC 流在丢包环境下,增强层丢失导致基础层也跟着花屏。排查后发现是编码器配置了错误的依赖关系。记住:增强层绝对不能反向依赖基础层之外的其他增强层,否则丢包会引发级联故障。
12.3 Simulcast vs SVC:怎么选?
这个问题我经常被问到。直接上对比表:
| 维度 | Simulcast | SVC |
|---|---|---|
| 编码复杂度 | 高(多路独立编码) | 低(单路分层编码) |
| 带宽利用率 | 低(冗余传输) | 高(按需传输) |
| 切换延迟 | 低(直接切换流) | 中(需等待关键帧) |
| 编码器支持 | 所有编码器都支持 | 仅 H.264 SVC、VP9、AV1 |
| SFU 复杂度 | 低(简单转发) | 中(需解析层信息) |
| 适用场景 | 大会议、多接收端 | 点对点、小规模会议 |
我的建议是:
- 如果你的 SFU 性能足够,且接收端数量多(>10),选 Simulcast。因为 SFU 不需要做任何转码,直接转发即可。
- 如果带宽是瓶颈,且接收端数量少(<5),选 SVC。因为 SVC 的带宽利用率更高。
- 如果两者都支持,那就都开启,让客户端根据实际情况选择。
12.4 多流切换实战
理论讲完了,我们来写点代码。下面是一个基于 Simulcast 的多流切换示例:
// 创建 RTCPeerConnection
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
// 添加视频轨道,开启 Simulcast
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
const [videoTrack] = stream.getVideoTracks();
// 使用 RTCRtpSender 的 setParameters 来切换流
const sender = pc.addTrack(videoTrack, stream);
// 获取当前参数
const params = sender.getParameters();
// 配置 Simulcast 编码参数
params.encodings = [
{ rid: 'h', maxBitrate: 2_000_000, scaleResolutionDownBy: 1.0 },
{ rid: 'm', maxBitrate: 500_000, scaleResolutionDownBy: 2.0 },
{ rid: 'l', maxBitrate: 150_000, scaleResolutionDownBy: 4.0 }
];
// 应用参数
await sender.setParameters(params);
// 接收端切换流
function switchToStream(rid) {
const receiver = pc.getReceivers()[0];
const params = receiver.getParameters();
// 只保留目标 rid 的编码
params.encodings = params.encodings.filter(e => e.rid === rid);
receiver.setParameters(params);
}
这段代码做了三件事:
- 创建连接并添加视频轨道
- 配置三个 Simulcast 流(高清、标清、低清)
- 提供切换函数,接收端可以随时切换到任意一路流
实战技巧:切换流时,我建议不要直接切,而是先订阅新流,等新流的第一帧到达后再取消旧流。这样可以避免切换过程中出现黑屏。代码实现上,可以用 ontrack 事件来判断新流是否就绪。
12.5 知识体系总览
下面这张图总结了 Simulcast 和 SVC 的核心逻辑:
说白了,Simulcast 是「多路并行,各取所需」,SVC 是「一路多層,按需提取」。两者没有绝对的好坏,关键看你的场景。
我个人更倾向于在大型会议中用 Simulcast,因为 SFU 的压力小,扩展性好。而在点对点通话或小规模会议中,SVC 的带宽优势就体现出来了。
最后提醒一句:不管选哪种方案,一定要做好带宽估计和码率自适应。否则再好的分层策略,遇到网络抖动也是白搭。
公众号:蓝海资料掘金营,微信deep3321