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 里的信息知道它们的存在,然后根据带宽和屏幕尺寸动态切换。

关键点: Simulcast 的配置信息全部写在 SDP 中。如果你不会看 SDP,那调试 Simulcast 基本就是盲人摸象。

13.2 在 SDP 中定位 Simulcast 配置

我们先看一个典型的 Simulcast SDP 片段。这是发送端的 SDP,注意看 msidssrc 部分:

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 的 chrome://webrtc-internals 抓取 SDP。直接搜索 "simulcast" 关键字,就能快速定位到相关配置。如果搜不到,说明 Simulcast 根本没启用。

13.3 验证多流发送和接收

光看 SDP 还不够。你得确认这三路流真的在网络上传输了。我一般用三步验证法:

  1. 抓包验证:用 Wireshark 抓 RTP 包,过滤条件设为 rtp.ssrc == 12345678 || rtp.ssrc == 23456789 || rtp.ssrc == 34567890。如果三个 SSRC 都有数据包,说明发送端确实在发三路流。
  2. 接收端日志:在接收端打印 RTCRtpReceiver.getStats(),查看 bytesReceivedpacketsReceived。如果三个 SSRC 都有统计信息,说明接收端收到了三路流。
  3. 视频渲染验证:在接收端强制切换 RID,观察画面分辨率是否变化。比如从 RID=96(高清)切换到 RID=98(低清),画面应该明显变模糊。
注意: 我曾经遇到过一个坑——发送端配置了 Simulcast,但接收端只显示了一路流。查了半天发现是接收端的 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:recvrid:... 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
避坑指南: 我曾经在调试一个 Simulcast 问题时,发现发送端明明配置了三路流,但接收端始终只显示两路。查了两天,最后发现是编码器的硬件限制——那个摄像头在 1080p 模式下只能同时输出两路编码流。所以,Simulcast 不是你想发几路就能发几路的,硬件能力是硬约束。

13.6 用 SVG 理解 Simulcast 架构

说了这么多,不如一张图来得直观。下面是我画的 Simulcast 架构图,展示了发送端、接收端和 SDP 协商的关系:

发送端 摄像头 编码器1 (1080p) 编码器2 (720p) 编码器3 (360p) SSRC: 12345678 SSRC: 23456789 SSRC: 34567890 SDP: simulcast:send 96,97,98 网络 RTP流1 RTP流2 RTP流3 接收端 RTCRtpReceiver 流选择器 (RID) 解码器 视频渲染 SDP: simulcast:recv 96,97,98 RTP流1 RTP流2 RTP流3

这张图展示了 Simulcast 的完整链路:发送端摄像头采集画面后,经过三个编码器生成三路不同分辨率的流,每路流有独立的 SSRC。SDP 中通过 simulcast:sendrid 字段描述这些流。接收端收到 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 流。

个人经验: 我建议你在调试 Simulcast 时,先在发送端用 RTCRtpSender.getParameters() 打印出 encodings 数组,确认三路流的配置是否正确。然后在接收端用上面的代码打印接收参数。两边一对比,问题基本就定位了。

Simulcast 调试的核心就是 SDP。你只要学会看 SDP 里的 simulcast 字段、rid 字段和 ssrc 字段,就能掌握整个多流发送和接收的全貌。嗯,说白了就是「SDP 里写什么,网络里就传什么」。下次遇到 Simulcast 问题,先抓 SDP,别急着改代码。


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