2. WebRTC基础回顾

做WebRTC推流,第一件事不是调编码参数,也不是改拥塞算法。

而是先搞清楚你的网络到底什么德行。

我见过太多团队,花了两周优化编码器,结果卡顿是因为上行带宽只有500kbps,码率却设了2Mbps。说白了,方向错了。

这一章,我们就来给网络做个全面体检。三个核心指标:带宽、丢包与RTT、NAT类型。摸清了底牌,后面的优化才有依据。

核心观点:网络环境摸底是推流优化的前提。跳过这一步,所有优化都是盲人摸象。

2.1 带宽探测:你到底能跑多快?

带宽不是一成不变的。Wi-Fi信号波动、小区共享带宽、运营商限速,都会让你的实际可用带宽忽高忽低。

WebRTC内部有带宽估计模块,但它的初始值往往偏保守。我个人习惯在建立连接前,先主动做一次带宽探测。

2.1.1 探测方法

  • 基于RTT的探测:通过发送递增大小的数据包,观察RTT的突变点。当RTT突然增大,说明网络开始排队,接近带宽上限。
  • 基于吞吐量的探测:直接发送大块数据,测量单位时间内的接收量。简单粗暴,但容易对现有网络造成冲击。
  • WebRTC内置的GCC(Google Congestion Control):基于延迟和丢包的混合估计。我建议用它做实时调整,但不要依赖它做初始探测。

我的经验:在项目初期,我会写一个简单的带宽探测工具,用UDP发送递增数据包,持续3-5秒。这样能快速摸清网络的“天花板”。

2.1.2 带宽探测代码示例

下面是一个简化的带宽探测逻辑,使用Node.js实现。实际生产环境需要更完善的超时和重试机制。

const dgram = require('dgram');
const client = dgram.createSocket('udp4');

const SERVER_PORT = 5000;
const SERVER_HOST = '192.168.1.100';
const PACKET_SIZE = 1400; // 接近MTU
const INTERVAL = 10; // 毫秒

let sentBytes = 0;
let startTime = Date.now();

function sendProbe() {
  const buf = Buffer.alloc(PACKET_SIZE);
  buf.writeUInt32BE(sentBytes, 0); // 序列号
  client.send(buf, SERVER_PORT, SERVER_HOST, (err) => {
    if (!err) {
      sentBytes += PACKET_SIZE;
      if (Date.now() - startTime < 5000) { // 持续5秒
        setTimeout(sendProbe, INTERVAL);
      } else {
        const elapsed = (Date.now() - startTime) / 1000;
        const bandwidth = (sentBytes * 8) / elapsed / 1000; // kbps
        console.log(`Estimated bandwidth: ${bandwidth.toFixed(2)} kbps`);
        client.close();
      }
    }
  });
}

sendProbe();

2.1.3 带宽探测结果解读

探测结果 可能原因 建议操作
带宽稳定 > 2Mbps 网络良好 可尝试720p推流
带宽在 500kbps - 2Mbps 中等网络 建议使用自适应码率
带宽 < 500kbps 网络较差 考虑降分辨率或使用SVC
带宽波动剧烈 无线干扰或共享带宽 启用带宽估计平滑算法

注意:带宽探测本身会占用网络资源。不要在推流过程中频繁探测,建议在连接建立前或推流空闲时进行。

2.2 丢包率与RTT测量:网络质量的晴雨表

带宽够用不代表不卡顿。丢包和延迟才是卡顿的元凶。

丢包率直接决定视频能否连续播放。RTT则影响交互的实时性。这两个指标,必须实时监控。

2.2.1 测量方法

WebRTC的RTCP Receiver Report(RR)和Sender Report(SR)已经包含了丢包率和RTT信息。我们可以直接从中提取。

但如果你需要更精细的控制,可以自己实现探测包机制。

// 伪代码:从RTCP中提取丢包率和RTT
function parseRTCP(packet) {
  // 假设packet是RTCP RR报文
  const fractionLost = packet.readUInt8(8); // 丢包率,单位256分之一
  const cumulativeLost = packet.readUInt24(9); // 累计丢包数
  const lastSR = packet.readUInt32(16); // 上次SR的NTP时间戳
  const delaySinceLastSR = packet.readUInt32(20); // 距上次SR的延迟

  const lossRate = fractionLost / 256.0;
  const rtt = delaySinceLastSR - lastSR; // 简化计算

  return { lossRate, rtt };
}

2.2.2 指标阈值参考

指标 优秀 良好 较差 不可用
丢包率 < 0.5% 0.5% - 2% 2% - 5% > 5%
RTT < 50ms 50ms - 150ms 150ms - 300ms > 300ms

避坑指南:我曾经遇到一个案例,丢包率只有1%,但视频依然卡顿。后来发现是RTT高达400ms,导致NACK重传包迟迟不到。所以,丢包率和RTT要一起看,不能只看一个。

2.3 NAT类型检测:你的节点能直连吗?

WebRTC的P2P连接依赖ICE(Interactive Connectivity Establishment)协议。ICE能否成功,很大程度上取决于NAT类型。

如果双方都是对称NAT,那STUN和TURN都救不了,只能走TURN中继。中继意味着额外的延迟和带宽成本。

2.3.1 常见NAT类型

  • 完全锥形NAT:最友好,任何外部主机都能通过映射地址访问内部主机。
  • 限制锥形NAT:只有内部主机曾经通信过的外部主机才能访问。
  • 端口限制锥形NAT:在限制锥形基础上,还限制了端口。
  • 对称NAT:最严格,每次通信都使用不同的映射地址和端口。P2P几乎不可能。

2.3.2 检测方法

经典的RFC 3489方法,需要三个STUN服务器。但实际项目中,我更喜欢用Google的libjingle中的NAT检测逻辑,它更稳定。

检测流程如下:

  1. 向STUN服务器发送请求,获取公网IP和端口。
  2. 向另一个IP的STUN服务器发送请求,看公网IP和端口是否变化。
  3. 让STUN服务器从不同IP和端口回包,看是否能收到。

根据结果,可以判断NAT类型。

关键点:对称NAT是P2P的天敌。如果检测到对称NAT,建议直接启用TURN中继,不要浪费时间做P2P尝试。

2.4 本章知识体系

下面这张图,把网络环境摸底的三个维度串起来了。你可以把它当作排查卡顿的起点。

网络环境摸底知识体系 网络环境摸底核心流程 网络环境摸底 带宽探测 丢包率与RTT测量 NAT类型检测 基于RTT探测 基于吞吐量探测 RTCP RR/SR提取 自定义探测包 RFC 3489方法 libjingle检测 输出:可用带宽 / 丢包率 / RTT / NAT类型 💡 实战建议 1. 带宽探测在连接建立前做一次,推流中依赖GCC实时调整。 2. 丢包率和RTT要持续监控,超过阈值立即触发降级策略。 3. NAT类型检测结果决定ICE策略:对称NAT直接走TURN。

2.5 实战:整合三个指标

光测出来没用,得会用。我通常的做法是,在推流SDK初始化时,依次执行带宽探测、丢包率基线测量和NAT类型检测。然后根据结果,动态调整推流参数。

举个例子:

  • 如果带宽 > 2Mbps,丢包率 < 1%,NAT为锥形,直接走P2P推720p。
  • 如果带宽 < 1Mbps,丢包率 > 3%,NAT为对称,走TURN中继,降分辨率到360p,开启FEC。
  • 如果带宽波动大,启用自适应码率,让GCC接管。

注意:网络环境是动态变化的。推流过程中,要定期(比如每30秒)重新评估带宽和丢包率,但不能频繁做NAT检测。NAT类型在会话期间基本不变。

嗯,到这里,网络环境摸底就讲完了。你想想看,有了这些数据,你还会盲目地调编码参数吗?