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);
}

这段代码做了三件事:

  1. 创建连接并添加视频轨道
  2. 配置三个 Simulcast 流(高清、标清、低清)
  3. 提供切换函数,接收端可以随时切换到任意一路流

实战技巧:切换流时,我建议不要直接切,而是先订阅新流,等新流的第一帧到达后再取消旧流。这样可以避免切换过程中出现黑屏。代码实现上,可以用 ontrack 事件来判断新流是否就绪。

12.5 知识体系总览

下面这张图总结了 Simulcast 和 SVC 的核心逻辑:

Simulcast vs SVC 核心逻辑对比 发送端 Simulcast 720p 360p 180p SVC 分层编码 单路码流 TL0 (基础层) TL1 (增强层1) TL2 (增强层2) 接收端A 接收端B 选择订阅 720p 或 360p 解码 TL0 + TL1 核心区别 Simulcast:多路独立流,接收端切换流 | SVC:单路分层流,接收端选择层

说白了,Simulcast 是「多路并行,各取所需」,SVC 是「一路多層,按需提取」。两者没有绝对的好坏,关键看你的场景。

我个人更倾向于在大型会议中用 Simulcast,因为 SFU 的压力小,扩展性好。而在点对点通话或小规模会议中,SVC 的带宽优势就体现出来了。

最后提醒一句:不管选哪种方案,一定要做好带宽估计和码率自适应。否则再好的分层策略,遇到网络抖动也是白搭。


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