13、Simulcast与SVC:多流与可伸缩编码实战

聊到视频通话的流畅度,很多同学第一反应就是「降低分辨率」。但你想过没有,一个1080p的流,强行降到360p,接收端看到的画面糊成一团,体验极差。更麻烦的是,不同接收端的网络条件千差万别——有人用5G,有人还在3G边缘挣扎。

这时候,Simulcast和SVC就派上用场了。说白了,它们就是让发送端「多准备几手」,接收端「按需取用」。我在做跨国会议系统时,就靠这俩技术解决了「一人卡顿,全员降质」的尴尬局面。

13.1 Simulcast(多流)原理

Simulcast,直译就是「同时广播」。发送端同时编码出多个不同质量的视频流,比如一个720p、一个360p、一个180p。接收端根据自己的带宽和屏幕尺寸,选一个最合适的流来解码。

嗯,这里要注意:Simulcast不是简单的「复制三份」。每个流都是独立编码的,有独立的帧率、分辨率和码率。代价就是——编码器的压力翻倍,带宽占用也翻倍。

核心要点:Simulcast是「空间冗余」——多份完整流,接收端选一份。

我在项目中遇到过一个问题:三个流同时编码,CPU直接飙到90%。后来我们做了动态策略——当检测到CPU过载时,自动降为双流甚至单流。这个坑,我建议你提前留好开关。

13.2 SVC(可伸缩视频编码)原理

SVC的思路完全不同。它只编码一份码流,但这份码流是「分层」的。比如:

  • 基础层:最低分辨率、最低质量,所有接收端都能解码
  • 增强层:在基础层之上叠加,提升分辨率或质量

接收端可以只解码基础层,也可以「基础层+增强层1+增强层2」逐步叠加。说白了,SVC是「时间/空间/质量」三个维度的可伸缩。

个人经验:SVC在理论上很优雅,但实际编码效率比Simulcast低10%-20%。我做过对比测试,同样画质下,SVC的码率比Simulcast高出一截。所以选哪个,得看你的场景。

为什么会这样?因为SVC的层间预测虽然节省了带宽,但引入了额外的编码开销。你想想看,每一层都要依赖下层,编码器得做更多计算。

13.3 Simulcast vs SVC:怎么选?

维度 Simulcast SVC
编码效率 高(独立编码) 中(层间预测有开销)
带宽占用 高(多份完整流) 低(一份流,按需取层)
CPU负载 高(多个编码器) 中(一个编码器,但层数多)
接收端灵活性 高(直接切换流) 高(逐层叠加)
浏览器支持 广泛(Chrome、Firefox) 有限(Chrome支持VP9 SVC)

我个人习惯:如果目标用户主要是桌面端、带宽充足,优先Simulcast。如果移动端用户多、网络波动大,SVC更合适。当然,两者也可以结合——我就做过「Simulcast + 每流内部SVC」的方案,效果不错,但复杂度也上去了。

13.4 在WebRTC中启用Simulcast

启用Simulcast,核心是在创建RTCPeerConnection时,通过编码参数告诉浏览器:「我要发多份流」。

看代码:

const pc = new RTCPeerConnection();

// 获取本地视频流
const stream = await navigator.mediaDevices.getUserMedia({ video: true });

// 添加轨道,并指定Simulcast编码参数
const transceiver = pc.addTransceiver(stream.getVideoTracks()[0], {
  direction: 'sendonly',
  sendEncodings: [
    { rid: 'h', maxBitrate: 900000, scaleResolutionDownBy: 1.0 },  // 720p
    { rid: 'm', maxBitrate: 300000, scaleResolutionDownBy: 2.0 },  // 360p
    { rid: 'l', maxBitrate: 100000, scaleResolutionDownBy: 4.0 }   // 180p
  ]
});

这里的关键参数:

  • rid:流的标识,接收端靠它来区分
  • maxBitrate:每个流的最大码率
  • scaleResolutionDownBy:分辨率缩放因子。1.0是原始分辨率,2.0是缩小一半

避坑指南:我曾经把scaleResolutionDownBy写成了0.5,结果浏览器直接报错。这个值必须≥1.0,表示「缩小多少倍」,不是「保留多少比例」。

发送端搞定后,接收端怎么知道有三个流?答案是:通过RTP头部扩展。每个流在RTP包中携带自己的rid,接收端根据rid来区分。

13.5 接收端选择流

接收端收到多个流后,需要根据自身条件选一个。WebRTC提供了两种方式:

方式一:手动切换

通过设置接收端的编码参数,指定要解码哪个rid:

// 假设接收端有一个transceiver
const receiver = pc.getReceivers()[0];

// 手动选择要解码的流
await receiver.setCodecPreferences([
  { rid: 'm' }  // 只解码中等质量的流
]);

这种方式适合「用户手动选择画质」的场景。比如视频会议中,用户点击「流畅模式」,我们就切到低码率流。

方式二:自适应切换

WebRTC内置了带宽估计机制,可以自动切换流。你只需要在发送端设置好编码参数,接收端会基于REM B(带宽估计)动态选择。

具体做法:

// 发送端设置编码参数时,不指定rid,让浏览器自动选择
const transceiver = pc.addTransceiver(track, {
  sendEncodings: [
    { maxBitrate: 900000, scaleResolutionDownBy: 1.0 },
    { maxBitrate: 300000, scaleResolutionDownBy: 2.0 },
    { maxBitrate: 100000, scaleResolutionDownBy: 4.0 }
  ]
});

浏览器会根据接收端的带宽估计,自动在三个流之间切换。当带宽充足时,接收端收到高质量流;带宽下降时,自动切到低质量流。

我的建议:自适应切换虽然方便,但切换过程可能有短暂的花屏或卡顿。如果你对体验要求极高,可以自己实现「平滑切换」——先缓冲几帧新流,再切换解码器。我在直播项目中就是这么做的,切换几乎无感。

13.6 核心逻辑图

下面这张图展示了Simulcast的完整流程:发送端编码三个流,接收端根据带宽选择其中一个解码。

Simulcast 核心流程 发送端 流 H (720p) 流 M (360p) 流 L (180p) 带宽估计 接收端 选择逻辑 带宽高 → 选流H 带宽中 → 选流M 带宽低 → 选流L 发送端编码三个独立流,接收端基于带宽估计选择最合适的流

13.7 实战中的坑与技巧

  • 流数不要贪多:我见过有人设了5个流,结果编码器直接崩溃。一般3个流足够覆盖大多数场景。
  • 码率设置要合理:三个流的码率不要等差。比如720p给900kbps,360p给300kbps,180p给100kbps。这样切换时码率变化平滑。
  • 注意浏览器差异:Chrome对Simulcast支持最好,Firefox次之,Safari目前只支持SVC。如果你要兼容Safari,建议用VP9 SVC。
  • 监控带宽变化:接收端可以通过pc.getReceivers()[0].getStats()获取当前流的码率,结合带宽估计,决定是否切换。

总结一下:Simulcast是「多流备选」,SVC是「单流分层」。选哪个,取决于你的目标平台和网络环境。我个人更倾向Simulcast,因为浏览器支持好、实现简单。但如果你追求极致的带宽效率,SVC值得深入研究。

嗯,这一章的内容就到这里。记住:多流不是目的,让每个用户都能看到流畅的视频才是。