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 知识体系总览

下面这张图总结了本章的核心逻辑:

媒体协商优化知识体系 媒体协商 (SDP交换) 重协商场景 Transceiver机制 Simulcast / SVC 触发场景 添加/移除轨道 · 改变编解码器 调整带宽 · ICE重启 direction属性 sendrecv · sendonly recvonly · inactive 编码策略 Simulcast: 多路独立编码 SVC: 单路分层编码 优化目标:低延迟 · 高画质 · 自适应网络

从图中可以看到,重协商、Transceiver、Simulcast/SVC三者是相互关联的。Transceiver是基础容器,重协商是触发动作,Simulcast/SVC是编码策略。理解了这个三角关系,你就能灵活应对各种媒体协商场景。

最后说一句:媒体协商优化没有银弹。每个项目都有自己的特点,需要你根据实际场景去权衡。我建议你从最简单的Simulcast开始,等遇到性能瓶颈了再考虑SVC。别一开始就追求完美,先跑起来再说。


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