13、Simulcast 调试:在 SDP 中查看 Simulcast 配置,验证多流发送和接收
Simulcast 这词儿,说白了就是「同时广播」。在 WebRTC 里,它指的是发送端同时发出多路不同分辨率的视频流。嗯,你可能会问:为什么需要同时发多路?直接发一路高清的不就完了?
我刚开始接触这个功能时也有同样的疑惑。后来在一次跨国视频会议项目中,我遇到了一个典型场景:发送端在东京,接收端有北京的桌面用户(千兆光纤)、也有印度尼西亚的手机用户(3G 网络)。如果只发一路 1080p,手机端根本扛不住;如果只发 360p,桌面端又嫌糊。Simulcast 就是来解决这个矛盾的——发送端同时发出 1080p、720p、360p 三路流,接收端根据自身网络状况选择最合适的一路。
13.1 Simulcast 的核心原理
Simulcast 不是靠服务器转码实现的。它是在发送端编码器里直接生成多路流。每一路流都有独立的 SSRC(同步源标识符),在 RTP 层面就是完全独立的流。
我个人习惯把 Simulcast 理解为「一个摄像头,三个编码器」。编码器 1 输出 1080p,编码器 2 输出 720p,编码器 3 输出 360p。这三路流通过同一个 PeerConnection 发送出去,接收端通过 SDP 里的信息知道它们的存在,然后根据带宽和屏幕尺寸动态切换。
13.2 在 SDP 中定位 Simulcast 配置
我们先看一个典型的 Simulcast SDP 片段。这是发送端的 SDP,注意看 msid 和 ssrc 部分:
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101
a=msid:stream1 video1
a=ssrc:12345678 cname:user1
a=ssrc:12345678 msid:stream1 video1
a=ssrc:23456789 cname:user1
a=ssrc:23456789 msid:stream1 video1
a=ssrc:34567890 cname:user1
a=ssrc:34567890 msid:stream1 video1
a=simulcast:send 96,97,98
a=rid:96 send
a=rid:97 send
a=rid:98 send
这里有几个关键字段,我一个个说:
- ssrc 数量:三个不同的 SSRC,代表三路独立的 RTP 流。每一路都有自己的序列号和时间戳空间。
- msid:三路流共享同一个 MediaStream ID(stream1),但 Track ID 不同(video1)。这说明它们来自同一个摄像头,但编码参数不同。
- simulcast:send:明确告诉接收端,发送端支持 Simulcast,并且发送了 96、97、98 三个 RID(RTP Stream ID)。
- rid:每一路流的标识符。接收端通过 RID 来区分不同分辨率的流。
chrome://webrtc-internals 抓取 SDP。直接搜索 "simulcast" 关键字,就能快速定位到相关配置。如果搜不到,说明 Simulcast 根本没启用。
13.3 验证多流发送和接收
光看 SDP 还不够。你得确认这三路流真的在网络上传输了。我一般用三步验证法:
- 抓包验证:用 Wireshark 抓 RTP 包,过滤条件设为
rtp.ssrc == 12345678 || rtp.ssrc == 23456789 || rtp.ssrc == 34567890。如果三个 SSRC 都有数据包,说明发送端确实在发三路流。 - 接收端日志:在接收端打印
RTCRtpReceiver.getStats(),查看bytesReceived和packetsReceived。如果三个 SSRC 都有统计信息,说明接收端收到了三路流。 - 视频渲染验证:在接收端强制切换 RID,观察画面分辨率是否变化。比如从 RID=96(高清)切换到 RID=98(低清),画面应该明显变模糊。
RTCRtpReceiver 没有设置 rid 参数。接收端必须明确告诉浏览器「我要接收哪一路流」,否则浏览器默认只收第一路。
13.4 Simulcast 的 SDP 协商过程
Simulcast 的协商不是单方面决定的。发送端说「我要发三路」,接收端得说「我支持收三路」,这个协商过程也体现在 SDP 里。
我们来看一个完整的协商流程:
// 发送端 Offer
a=simulcast:send 96,97,98
a=rid:96 send
a=rid:97 send
a=rid:98 send
// 接收端 Answer
a=simulcast:recv 96,97,98
a=rid:96 recv
a=rid:97 recv
a=rid:98 recv
注意看,接收端的 Answer 里 simulcast:recv 和 rid:... recv 表示它愿意接收这三路流。如果接收端只写了 simulcast:recv 96,那就只收一路。
嗯,这里有个细节:接收端可以只订阅部分流。比如手机端网络不好,它可以在 Answer 里只写 simulcast:recv 98,只收最低分辨率的那路。这个机制叫「选择性订阅」,是 Simulcast 的核心优势之一。
13.5 常见问题与避坑指南
我在实际项目中踩过不少 Simulcast 的坑,挑几个典型的说说:
| 问题 | 现象 | 原因 | 解决方案 |
|---|---|---|---|
| Simulcast 未启用 | SDP 中没有 simulcast 字段 | 编码器不支持或未配置 | 检查 RTCRtpSender.setParameters() 中的 encodings 数组 |
| 接收端只收到一路流 | getStats() 只显示一个 SSRC | 接收端未设置 rid 参数 | 在 RTCRtpReceiver 中指定 rid |
| 流切换卡顿 | 切换 RID 时画面冻结 1-2 秒 | 关键帧间隔过长 | 设置 keyFrameInterval 为 2000ms |
| 带宽浪费 | 三路流总带宽超过预期 | 编码器未做码率限制 | 为每路流设置 maxBitrate |
13.6 用 SVG 理解 Simulcast 架构
说了这么多,不如一张图来得直观。下面是我画的 Simulcast 架构图,展示了发送端、接收端和 SDP 协商的关系:
这张图展示了 Simulcast 的完整链路:发送端摄像头采集画面后,经过三个编码器生成三路不同分辨率的流,每路流有独立的 SSRC。SDP 中通过 simulcast:send 和 rid 字段描述这些流。接收端收到 SDP 后,通过 RTCRtpReceiver 接收流,再根据网络状况选择最合适的 RID 进行解码和渲染。
13.7 实战:用代码验证 Simulcast
最后,我分享一段我在项目中用过的调试代码。这段代码可以在接收端打印出所有收到的 Simulcast 流信息:
// 接收端调试代码
const pc = new RTCPeerConnection();
pc.ontrack = (event) => {
const receiver = event.receiver;
const params = receiver.getParameters();
console.log('收到新轨道:', event.track.kind);
console.log('接收器参数:', params);
// 遍历所有编码
if (params.encodings) {
params.encodings.forEach((enc, index) => {
console.log(`编码 ${index}: rid=${enc.rid}, active=${enc.active}`);
});
}
// 获取统计信息
setInterval(async () => {
const stats = await receiver.getStats();
stats.forEach(stat => {
if (stat.type === 'inbound-rtp' && stat.kind === 'video') {
console.log(`SSRC: ${stat.ssrc}, 码率: ${stat.bytesReceived / 1024} KB/s`);
}
});
}, 5000);
};
这段代码会打印出每个轨道的编码信息,包括 RID 和活跃状态。如果某个编码的 active 为 false,说明接收端没有订阅那路流。我在调试时经常用这个方法来确认接收端是否正确订阅了所有 Simulcast 流。
RTCRtpSender.getParameters() 打印出 encodings 数组,确认三路流的配置是否正确。然后在接收端用上面的代码打印接收参数。两边一对比,问题基本就定位了。
Simulcast 调试的核心就是 SDP。你只要学会看 SDP 里的 simulcast 字段、rid 字段和 ssrc 字段,就能掌握整个多流发送和接收的全貌。嗯,说白了就是「SDP 里写什么,网络里就传什么」。下次遇到 Simulcast 问题,先抓 SDP,别急着改代码。
公众号:蓝海资料掘金营,微信deep3321