14、媒体协商优化:重协商场景、Transceiver机制、Simulcast与SVC
媒体协商,说白了就是两个浏览器坐下来商量:「咱俩用啥格式传视频?」。我刚开始做WebRTC时,觉得这步就是交换个SDP嘛,能有多复杂?结果项目一上线,各种重协商、编解码器不匹配、带宽浪费的问题全冒出来了。
今天咱们就聊聊媒体协商的进阶话题。我会把重协商场景、Transceiver机制、还有Simulcast和SVC这几个硬骨头啃下来。嗯,这些都是我在实际项目中踩过的坑,希望能帮你少走弯路。
核心要点:媒体协商不是一次性的握手,而是贯穿整个通话生命周期的动态过程。理解Transceiver、Simulcast和SVC,是优化实时通信体验的关键。
14.1 重协商场景:什么时候需要再来一次?
你想想看,两个人正在视频通话,突然一方想开启屏幕共享。这时候就需要重新协商——因为原来的SDP里没有屏幕共享的媒体描述。
常见的重协商场景有这些:
- 添加/移除媒体轨道:比如从纯音频切换到音视频通话
- 改变编解码器:比如从VP8切换到H.264
- 调整带宽限制:网络变差时降低分辨率或帧率
- Simulcast/SVC参数变更:动态调整分层策略
- ICE重启:网络切换导致需要重新建立连接
我在项目中遇到过最典型的场景:用户从WiFi切到4G,网络带宽骤降。如果不触发重协商,视频就会卡成PPT。后来我加了个带宽监测逻辑,一旦检测到下行带宽低于阈值,就主动触发重协商,把分辨率从720p降到360p。
个人习惯:我一般会在createOffer()时传入特定的offerToReceive选项,而不是等对端来触发。这样能减少一次往返延迟。
14.2 Transceiver机制:媒体协商的基石
RTCRtpTransceiver,这名字听着挺唬人。其实它就是「发送器+接收器」的组合体。每个Transceiver对应一个媒体流方向。
为什么需要Transceiver?因为传统的PeerConnection里,添加一个轨道就要创建一个新的SDP媒体行。而Transceiver允许你更精细地控制每个媒体流的收发行为。
来看个实际例子:
// 创建一个Transceiver,指定方向为"sendrecv"
const transceiver = pc.addTransceiver('video', {
direction: 'sendrecv',
streams: [localStream],
sendEncodings: [
{ rid: 'h', maxBitrate: 900000 },
{ rid: 'm', maxBitrate: 450000 },
{ rid: 'l', maxBitrate: 150000 }
]
});
// 动态改变方向
transceiver.direction = 'sendonly'; // 只发不收
transceiver.direction = 'inactive'; // 暂停收发
我个人习惯把Transceiver的direction属性当作「开关」来用。比如在屏幕共享时,把视频Transceiver设为sendonly,音频Transceiver保持sendrecv。这样既省带宽,又不会丢失语音。
| direction值 | 含义 | 典型场景 |
|---|---|---|
| sendrecv | 既能发也能收 | 默认双向通话 |
| sendonly | 只发不收 | 屏幕共享、直播推流 |
| recvonly | 只收不发 | 观众端、监控查看 |
| inactive | 既不收也不发 | 临时静音、暂停视频 |
注意:修改direction后,一定要触发重协商(createOffer + setLocalDescription + setRemoteDescription),否则对端不会感知到变化。
14.3 Simulcast:同一视频,多路发送
Simulcast,中文叫「联播」或「同时广播」。它的核心思想是:发送端同时编码出多个不同分辨率的视频流,接收端根据自身网络条件选择最合适的一路。
为什么要这么做?因为网络环境千差万别。有人用5G,有人用3G,还有人用公共WiFi。如果只发一路720p,低带宽用户就卡死了。如果只发一路360p,高带宽用户又嫌画质差。
Simulcast的配置方式:
const encodings = [
{ rid: 'high', scaleResolutionDownBy: 1.0, maxBitrate: 900000 },
{ rid: 'medium', scaleResolutionDownBy: 2.0, maxBitrate: 450000 },
{ rid: 'low', scaleResolutionDownBy: 4.0, maxBitrate: 150000 }
];
const transceiver = pc.addTransceiver(track, {
direction: 'sendrecv',
sendEncodings: encodings
});
这里有个关键点:rid是流的标识符,scaleResolutionDownBy控制分辨率缩放比例。1.0表示原始分辨率,2.0表示缩小到一半。
我曾经在一个视频会议项目里用过Simulcast。效果确实好,但代价也大——发送端CPU占用飙升,因为要同时编码三路视频。后来我们做了个动态策略:检测到CPU超过80%时,自动降为两路甚至一路。
避坑指南:Simulcast不是所有浏览器都支持。Chrome支持,Safari部分支持,Firefox...嗯,我建议你查一下最新的兼容性表。我曾经在Firefox上踩过坑,Simulcast配置了但没生效,最后发现是浏览器版本问题。
14.4 SVC:可伸缩视频编码
SVC(Scalable Video Coding)是另一种思路。它不发送多路独立流,而是编码成一个「分层」的流。底层是基础质量,上层是增强层。接收端可以根据带宽决定接收多少层。
打个比方:Simulcast像是三个独立的蛋糕(小、中、大),SVC像是一个可以切片的蛋糕——你可以只吃底层(基础),也可以加上奶油层(增强)。
SVC的优势很明显:
- 带宽利用率高:只传输需要的层,不浪费
- 编码效率高:比Simulcast节省约30%的码率
- 动态调整灵活:可以在不重协商的情况下增减层数
但SVC也有缺点:编码器复杂度高,硬件支持不普遍。目前主流浏览器对VP9的SVC支持较好,H.264的SVC(即SVC扩展)支持有限。
来看个SVC的配置示例:
// 使用VP9的SVC模式
const encodings = [
{
rid: 'l',
scalabilityMode: 'L1T2', // 1层空间,2层时间
maxBitrate: 200000
},
{
rid: 'h',
scalabilityMode: 'L2T2', // 2层空间,2层时间
maxBitrate: 800000
}
];
const transceiver = pc.addTransceiver(track, {
direction: 'sendrecv',
sendEncodings: encodings
});
scalabilityMode的格式是L<空间层数>T<时间层数>。L1T2表示1层空间分辨率 + 2层帧率。接收端可以只收L1(低分辨率),也可以收L1+L2(高分辨率)。
我的建议:如果目标用户主要是Chrome,优先用VP9的SVC。如果兼容性要求高,用Simulcast更稳妥。我一般会在初始化时检测浏览器能力,动态选择方案。
14.5 Simulcast vs SVC:怎么选?
这个问题我经常被问到。直接上对比表:
| 维度 | Simulcast | SVC |
|---|---|---|
| 编码方式 | 多路独立编码 | 单路分层编码 |
| CPU消耗 | 高(多路编码) | 中(单路编码) |
| 带宽效率 | 低(冗余多) | 高(按需传输) |
| 浏览器支持 | 较好(Chrome/Safari) | 有限(VP9较好) |
| 动态切换 | 需重协商 | 无需重协商 |
| 实现复杂度 | 低 | 高 |
我个人习惯是:小规模会议(2-4人)用Simulcast,因为实现简单,调试方便。大规模直播(10人以上)用SVC,因为带宽利用率高,服务器压力小。
14.6 知识体系总览
下面这张图总结了本章的核心逻辑:
从图中可以看到,重协商、Transceiver、Simulcast/SVC三者是相互关联的。Transceiver是基础容器,重协商是触发动作,Simulcast/SVC是编码策略。理解了这个三角关系,你就能灵活应对各种媒体协商场景。
最后说一句:媒体协商优化没有银弹。每个项目都有自己的特点,需要你根据实际场景去权衡。我建议你从最简单的Simulcast开始,等遇到性能瓶颈了再考虑SVC。别一开始就追求完美,先跑起来再说。
公众号:蓝海资料掘金营,微信deep3321