19、质量监控:RTCStats API、延迟统计、丢包率分析

做WebRTC开发,最怕什么?

不是功能做不出来,而是用户说「卡了」、「听不清」、「画面不动了」,你却拿不出任何数据来排查。我早期做第一个移动端项目时就吃过这个亏——用户投诉视频通话质量差,我连是延迟高还是丢包严重都说不清楚。后来老老实实把RTCStats API用起来,才算是有了「眼睛」。

这一章,我们就来聊聊怎么给WebRTC装上监控系统。说白了,就是三件事:延迟统计、丢包率分析、以及如何用RTCStats API拿到这些数据。

19.1 RTCStats API 是什么?

RTCStats API是浏览器原生提供的统计接口。你不需要引入任何第三方库,直接调用RTCPeerConnection.getStats()就能拿到一整套网络和媒体质量数据。

我个人习惯在建立连接后,每隔1到2秒轮询一次。为什么是这个频率?太频繁会影响性能,太稀疏又抓不到瞬时的抖动。

// 基础用法
const pc = new RTCPeerConnection(configuration);

// 每隔2秒采集一次
setInterval(async () => {
  const stats = await pc.getStats();
  stats.forEach(report => {
    console.log(report.type, report);
  });
}, 2000);

嗯,这里要注意:getStats()返回的是一个Map结构,每个条目都有一个type字段。常见的type有:

type 含义 关键字段
inbound-rtp 接收端RTP流统计 packetsReceived, bytesReceived, jitter, packetsLost
outbound-rtp 发送端RTP流统计 packetsSent, bytesSent, retransmittedPacketsSent
candidate-pair ICE候选连接对 currentRoundTripTime, availableOutgoingBitrate
remote-inbound-rtp 远端反馈的接收统计 roundTripTime, fractionLost
track 媒体轨道的统计 framesReceived, framesDecoded, framesDropped

你想想看,有了这些数据,用户说卡的时候,你至少能判断是网络问题还是设备解码能力问题。

19.2 延迟统计:不只是RTT

延迟是用户感知最直接的指标。但「延迟」其实分好几种:

  • 端到端延迟:从摄像头采集到远端屏幕显示的总耗时
  • 网络RTT:数据包往返时间
  • 抖动:延迟的变化量,比延迟本身更影响体验

我在项目中遇到过这样的情况:RTT只有30ms,但用户还是觉得画面有延迟。后来发现是抖动太大,缓冲区来不及平滑。所以只看RTT是不够的。

19.2.1 获取RTT

RTT可以从candidate-paircurrentRoundTripTime拿到,单位是秒:

// 获取当前连接的RTT
async function getRTT(pc) {
  const stats = await pc.getStats();
  let rtt = 0;
  stats.forEach(report => {
    if (report.type === 'candidate-pair' && report.nominated) {
      rtt = report.currentRoundTripTime * 1000; // 转为毫秒
    }
  });
  return rtt;
}

另外,remote-inbound-rtp里也有roundTripTime字段,这是基于RTCP反馈计算的,更接近真实的端到端往返时间。

19.2.2 抖动计算

WebRTC已经帮你算好了抖动值。在inbound-rtp里有个jitter字段,单位是秒。不过要注意,这个值是RTP时间戳单位下的抖动,需要根据采样率换算成毫秒。

// 获取音频/视频抖动
function getJitter(report) {
  // 音频通常采样率是8000或48000
  const sampleRate = report.kind === 'audio' ? 8000 : 90000;
  return (report.jitter || 0) / sampleRate * 1000; // 转为毫秒
}
我的经验:音频抖动超过30ms就能感觉到断续,视频抖动超过50ms画面就开始卡顿。建议设置告警阈值:音频抖动>40ms告警,视频抖动>80ms告警。

19.3 丢包率分析

丢包是WebRTC质量的头号杀手。但丢包率怎么算?很多人直接拿packetsLost / packetsReceived,这是错的。

正确的做法是:

  1. inbound-rtp拿到packetsLostpacketsReceived
  2. 总包数 = packetsLost + packetsReceived
  3. 丢包率 = packetsLost / 总包数
// 计算接收端丢包率
function calcLossRate(report) {
  const lost = report.packetsLost || 0;
  const received = report.packetsReceived || 0;
  const total = lost + received;
  if (total === 0) return 0;
  return (lost / total) * 100;
}

我曾经踩过一个坑:只看接收端丢包率,忽略了发送端重传。实际上,WebRTC有NACK和FEC机制,很多丢包在底层就被恢复了。所以更准确的指标是有效丢包率——即经过重传和FEC后仍然丢失的比例。

有效丢包率可以从remote-inbound-rtpfractionLost字段获取,这个值是远端反馈的、经过重传后的丢包比例:

// 获取有效丢包率(远端反馈)
function getEffectiveLoss(stats) {
  let loss = 0;
  stats.forEach(report => {
    if (report.type === 'remote-inbound-rtp') {
      loss = report.fractionLost / 256 * 100; // fractionLost是8位整数,范围0-255
    }
  });
  return loss;
}
注意:fractionLost不是所有浏览器都支持。Chrome支持较好,Safari和部分移动端浏览器可能缺失。建议同时采集两端数据做交叉验证。

19.4 知识体系与核心逻辑

下面这张图梳理了质量监控的整体架构。我建议你把它当作一个「监控仪表盘」的设计蓝图:

WebRTC 质量监控核心逻辑 数据采集层 RTCPeerConnection.getStats() 轮询(间隔1-2秒) 数据分类与提取 网络质量 RTT / 抖动 / 丢包率 媒体质量 帧率 / 分辨率 / 码率 连接状态 ICE状态 / 候选对质量 分析与告警层 阈值判断 → 触发告警 / 自适应调整(码率、分辨率) 可视化仪表盘 / 日志上报

这张图的核心逻辑是:先采集原始数据,再分类提取关键指标,然后根据阈值做判断,最后输出到可视化界面或上报到服务端。我自己的项目里,还会把数据存到本地数据库,方便事后回溯分析。

19.5 实战:搭建一个简易监控面板

光说不练假把式。下面是一个完整的监控代码片段,你可以直接集成到项目中:

class QualityMonitor {
  constructor(pc) {
    this.pc = pc;
    this.metrics = {
      rtt: 0,
      jitter: 0,
      lossRate: 0,
      bitrate: 0,
      frameRate: 0
    };
    this.interval = null;
  }

  start(intervalMs = 2000) {
    this.interval = setInterval(() => this.collect(), intervalMs);
  }

  stop() {
    if (this.interval) {
      clearInterval(this.interval);
      this.interval = null;
    }
  }

  async collect() {
    const stats = await this.pc.getStats();
    let prevBytes = 0;
    let prevTs = 0;

    stats.forEach(report => {
      // 网络指标
      if (report.type === 'candidate-pair' && report.nominated) {
        this.metrics.rtt = report.currentRoundTripTime * 1000;
      }

      // 接收端媒体指标
      if (report.type === 'inbound-rtp' && report.kind === 'video') {
        const total = (report.packetsLost || 0) + (report.packetsReceived || 0);
        this.metrics.lossRate = total > 0 ? (report.packetsLost / total) * 100 : 0;
        this.metrics.jitter = (report.jitter || 0) / 90000 * 1000;

        // 码率计算(需要两次采样差值)
        if (prevBytes > 0) {
          const deltaBytes = report.bytesReceived - prevBytes;
          const deltaTime = (report.timestamp - prevTs) / 1000;
          this.metrics.bitrate = (deltaBytes * 8) / deltaTime / 1000; // kbps
        }
        prevBytes = report.bytesReceived;
        prevTs = report.timestamp;

        // 帧率
        if (report.framesPerSecond) {
          this.metrics.frameRate = report.framesPerSecond;
        }
      }
    });

    // 输出到控制台或UI
    console.table(this.metrics);
    this.updateUI();
  }

  updateUI() {
    // 这里可以更新DOM或发送到后端
    // 例如:document.getElementById('rtt').textContent = this.metrics.rtt + 'ms';
  }
}

// 使用示例
const monitor = new QualityMonitor(peerConnection);
monitor.start(2000);
核心要点:
  • 码率计算必须用两次采样的差值,单次采样没有意义
  • 丢包率要区分「原始丢包率」和「有效丢包率」
  • 抖动值要根据媒体类型换算(音频8kHz,视频90kHz)
  • 建议同时监控发送端和接收端,对比数据可以发现更多问题

19.6 避坑指南

最后分享几个我踩过的坑:

  • 我曾经以为getStats()是同步的——实际上它是异步的,一定要用await或then。早期代码里忘了await,拿到的全是undefined,排查了半天。
  • 不同浏览器的字段名不一样——比如Safari的candidate-pair里没有currentRoundTripTime,得用roundTripTime。建议做一层兼容封装。
  • 移动端频繁调用getStats()会耗电——我建议在弱网或用户反馈问题时才提高采集频率,平时1-2秒一次就够了。
  • 丢包率突然飙升不一定是网络问题——有一次我发现丢包率从1%跳到30%,排查半天发现是服务端转码压力大,导致丢包。所以监控要结合服务端日志一起看。

嗯,质量监控这件事,说白了就是给WebRTC装上「黑匣子」。平时可能用不上,但一旦出问题,它就是救命稻草。我建议你在项目初期就把这套机制加进去,别等到用户投诉了再补。


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