一、Simulcast 到底是什么?

Simulcast,中文叫「联播」。说白了,就是一个发送端同时发出多路不同分辨率的视频流。

我记得第一次接触这个概念是在做视频会议项目的时候。当时有个客户反馈:「你们这个会议,有人用 4K 显示器,有人用手机,画质怎么调都不对。」嗯,这就是典型的单流问题——你只能发一种分辨率,要么高清的让手机端卡死,要么低清的让大屏用户骂娘。

Simulcast 的解决方案很简单:发送端同时编码三路流,比如 1080p、720p、360p。接收端根据自己的屏幕和带宽,选最合适的那一路。

核心思想: 一次编码,多路输出,按需订阅。

二、Simulcast 的工作原理

2.1 编码端:三路并行编码

发送端拿到摄像头采集的原始帧后,不是只编一路。而是同时送进三个编码器实例,分别输出不同分辨率。

// 伪代码示意
const encodings = [
  { rid: 'h', scaleResolutionDownBy: 1.0, maxBitrate: 2_500_000 },  // 1080p
  { rid: 'm', scaleResolutionDownBy: 2.0, maxBitrate: 1_000_000 },  // 540p
  { rid: 'l', scaleResolutionDownBy: 4.0, maxBitrate: 300_000 }     // 270p
];

const sender = pc.addTrack(track);
sender.setParameters({ encodings });

这里有个坑,我踩过。Chrome 的 Simulcast 要求 rid 必须是 'h'、'm'、'l' 这三个值,大小写敏感。你写个 'high'、'medium'、'low',浏览器直接忽略,只发一路。

2.2 传输层:RTP 流标识

每一路流在 RTP 层面用不同的 SSRC 标识。接收端通过 SSRC 来区分哪一路是高清,哪一路是标清。

实际抓包时你会看到类似这样的结构:

SSRC 0x12345678 → 高清流 (rid=h)
SSRC 0x87654321 → 标清流 (rid=m)
SSRC 0x11223344 → 低清流 (rid=l)

2.3 接收端:动态切换

接收端不是三路全收。它只订阅最适合自己的那一路。网络变差了?从高清切到标清。屏幕变大了?从标清切回高清。

这个切换过程是无感知的,因为 SFU 在中间做了转发控制。

我的经验: 切换时建议加一个 200ms 的缓冲。不要因为网络瞬间抖动就频繁切换,否则用户会看到画面忽大忽小,体验反而更差。

三、Simulcast vs SVC:到底选哪个?

很多新手会问:Simulcast 和 SVC 不都是做分层吗?有什么区别?

嗯,这个问题我当年也纠结过。直接上对比表:

对比维度 Simulcast SVC
编码方式 多路独立编码 单路分层编码
码率开销 较高(约 1.5x - 2x) 较低(约 1.1x - 1.3x)
解码复杂度 低(标准解码器即可) 高(需要 SVC 解码器)
浏览器支持 Chrome、Firefox 原生支持 仅 Safari 部分支持
灵活性 每路独立码率、帧率 依赖层间依赖关系
适用场景 多人会议、直播 点播、存储优化

我个人习惯:实时通信场景无脑选 Simulcast。为什么?因为浏览器兼容性摆在那里。你想想看,SVC 在 Chrome 上至今不支持,你总不能要求所有用户都换 Safari 吧?

避坑指南: 我曾经在一个项目中尝试用 SVC 做分层,结果发现 Firefox 根本不认。最后不得不回退到 Simulcast,白白浪费了两周时间。所以,除非你确定所有客户端都支持 SVC,否则别碰。

四、Simulcast 的配置与优化

4.1 码率配置:别照搬默认值

WebRTC 的默认码率配置其实挺保守的。1080p 默认只给 2500kbps,这在静态场景下够用,但如果是屏幕共享或者运动画面,画质会明显下降。

我建议的配置方案:

const encodings = [
  { rid: 'h', scaleResolutionDownBy: 1.0, maxBitrate: 4_000_000 },  // 1080p: 4Mbps
  { rid: 'm', scaleResolutionDownBy: 2.0, maxBitrate: 1_500_000 },  // 540p: 1.5Mbps
  { rid: 'l', scaleResolutionDownBy: 4.0, maxBitrate: 500_000 }     // 270p: 500Kbps
];

注意:maxBitrate 是上限,不是固定值。实际码率会根据网络状况动态调整。

4.2 分辨率选择:别盲目追求 4K

很多人觉得 Simulcast 就是分辨率越高越好。其实不然。你想想看,如果发送端是 4K 摄像头,你强行编三路 4K、2K、1080p,CPU 直接爆炸。

我的经验:最高路不要超过 1080p。原因很简单:

  • 1080p 在 95% 的屏幕上已经够用
  • 再高的分辨率对 CPU 和带宽都是浪费
  • 移动端根本用不到 4K

4.3 帧率控制:别让低清流也跑 30fps

低清流的主要作用是保底。网络差的时候,用户能看清画面就行,不需要流畅。

我一般这样配:

const encodings = [
  { rid: 'h', scaleResolutionDownBy: 1.0, maxBitrate: 4_000_000, maxFramerate: 30 },
  { rid: 'm', scaleResolutionDownBy: 2.0, maxBitrate: 1_500_000, maxFramerate: 20 },
  { rid: 'l', scaleResolutionDownBy: 4.0, maxBitrate: 500_000, maxFramerate: 10 }
];

低清流 10fps 就够了。这样能省下不少带宽给高清流。

4.4 动态调整:别写死

Simulcast 的配置不是一成不变的。用户网络在变,屏幕在变,你的配置也得跟着变。

我常用的策略:

  • 上行带宽充足:开启三路流,高清流给满码率
  • 上行带宽不足:关闭高清流,只发中低两路
  • 上行带宽极差:只发低清流,甚至降帧率到 5fps

这个逻辑可以通过 RTCRtpSender.setParameters() 动态调整,不需要重新协商。

小技巧: 我习惯在发送端加一个带宽探测模块。每 5 秒检测一次上行带宽,然后动态调整 Simulcast 的路数。这样既能保证画质,又不会把网络打满。

五、Simulcast 的架构图

下面这张图展示了 Simulcast 的完整工作流程:

发送端 摄像头采集 编码器 (1080p) 编码器 (720p) 编码器 (360p) SFU 接收三路流 按需转发 动态切换 接收端 A 订阅 1080p 接收端 B 订阅 720p 接收端 C 订阅 360p 终端节点 编码器 SFU 转发 图:Simulcast 三路流从发送端到接收端的完整流程

六、总结

Simulcast 是 WebRTC 中解决多端适配问题的核心方案。它的核心逻辑就一句话:多路编码,按需订阅

配置时记住三点:

  • 码率别照搬默认值,根据场景调高
  • 最高路分辨率控制在 1080p 以内
  • 低清流帧率可以降到 10fps 省带宽

至于 SVC,除非你确定所有客户端都支持,否则别碰。我在项目里吃过这个亏,不想你再踩一遍。

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