16、数据统计与监控:获取Stats API、监控延迟/丢包率/吞吐量、将数据可视化

做WebRTC开发,最怕什么?

我个人最怕的是——用户说“卡”,但你不知道哪里卡。是网络不行?是编码器抽风?还是对端机器扛不住了?

没有数据,你就是在盲猜。所以这一章,咱们来聊聊怎么把WebRTC的“黑盒”打开,把延迟、丢包率、吞吐量这些关键指标抓出来,甚至画成图表。

16.1 核心数据源:RTCPeerConnection的Stats API

WebRTC内置了一套统计接口,叫getStats()。你想想看,浏览器其实一直在默默收集各种网络和媒体数据,只是默认不告诉你。调用这个方法,就能拿到一份“体检报告”。

基本用法很简单:

const pc = new RTCPeerConnection(configuration);

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

这里返回的report对象,类型非常多。我刚开始接触时也被搞晕了。其实你只需要关注几个核心类型:

report.type 含义 关键字段
inbound-rtp 接收端RTP流(远端→本地) packetsReceived, packetsLost, jitter, framesDecoded
outbound-rtp 发送端RTP流(本地→远端) packetsSent, bytesSent, retransmittedPacketsSent
candidate-pair 当前使用的网络连接 currentRoundTripTime, availableOutgoingBitrate, state
transport 传输层信息 bytesSent, bytesReceived, dtlsState
我的习惯: 不要一次性遍历所有report。先按type过滤,只处理你关心的那几种。否则数据量太大,前端渲染会卡。

16.2 三个核心指标:延迟、丢包率、吞吐量

数据拿到了,怎么算?我直接给你公式和代码。

16.2.1 延迟(RTT)

延迟就是数据从A到B再回来的时间。在candidate-pair里,有个字段叫currentRoundTripTime,单位是秒。

function getRTT(statsMap) {
  let rtt = 0;
  statsMap.forEach(report => {
    if (report.type === 'candidate-pair' && report.state === 'succeeded') {
      rtt = report.currentRoundTripTime * 1000; // 转成毫秒
    }
  });
  return rtt;
}

嗯,这里要注意:currentRoundTripTime是平滑后的值,不是瞬时值。如果你想要更灵敏的监控,可以用totalRoundTripTimeresponsesReceived自己算平均值。

16.2.2 丢包率

丢包率是衡量网络质量的关键。在inbound-rtp里,有packetsLostpacketsReceived

function getPacketLoss(statsMap) {
  let lossRate = 0;
  statsMap.forEach(report => {
    if (report.type === 'inbound-rtp' && report.kind === 'video') {
      const total = report.packetsLost + report.packetsReceived;
      if (total > 0) {
        lossRate = (report.packetsLost / total) * 100;
      }
    }
  });
  return lossRate; // 百分比
}
我曾经踩过的坑: 丢包率不是一直累加的!packetsLost是累计值。如果你直接拿它做除法,得到的是“从连接开始到现在的总丢包率”。这其实没什么用。正确的做法是:每次采样时,记录上一次的值,然后计算差值。

改进后的代码:

let prevStats = { packetsLost: 0, packetsReceived: 0 };

function getDeltaPacketLoss(currentStats) {
  const lostDelta = currentStats.packetsLost - prevStats.packetsLost;
  const totalDelta = (currentStats.packetsLost + currentStats.packetsReceived) 
                     - (prevStats.packetsLost + prevStats.packetsReceived);
  prevStats = { 
    packetsLost: currentStats.packetsLost, 
    packetsReceived: currentStats.packetsReceived 
  };
  if (totalDelta === 0) return 0;
  return (lostDelta / totalDelta) * 100;
}

16.2.3 吞吐量(比特率)

吞吐量就是每秒传输了多少数据。这个需要自己算差值。

let lastBytes = 0;
let lastTimestamp = 0;

function getBitrate(currentBytes, currentTimestamp) {
  if (lastTimestamp === 0) {
    lastBytes = currentBytes;
    lastTimestamp = currentTimestamp;
    return 0;
  }
  const deltaBytes = currentBytes - lastBytes;
  const deltaTime = (currentTimestamp - lastTimestamp) / 1000; // 转成秒
  lastBytes = currentBytes;
  lastTimestamp = currentTimestamp;
  return (deltaBytes * 8) / deltaTime; // 转成bps
}

你可以在outbound-rtpbytesSent,或者inbound-rtpbytesReceived里拿到字节数。

16.3 数据可视化:把数字变成图表

光看数字太枯燥了。我个人习惯用Canvas或者SVG画实时折线图。这里我用一个轻量方案——纯SVG绘制。

下面这张图展示了我们监控系统的核心流程:

WebRTC数据统计与监控流程 getStats() 采集 解析Stats报告 计算RTT/丢包率/吞吐量 SVG/Canvas图表渲染 存储历史数据 每2秒循环采集 告警/日志输出

实际画图时,我推荐用<polyline>来画折线。每次采样后,把新数据点push到数组里,然后更新路径的points属性。

// 简化版:用SVG画实时折线图
const svgNS = "http://www.w3.org/2000/svg";
const polyline = document.createElementNS(svgNS, "polyline");
polyline.setAttribute("stroke", "#4A90D9");
polyline.setAttribute("stroke-width", "2");
polyline.setAttribute("fill", "none");
svgElement.appendChild(polyline);

let dataPoints = [];
const MAX_POINTS = 50; // 只保留最近50个点

function updateChart(value) {
  dataPoints.push(value);
  if (dataPoints.length > MAX_POINTS) {
    dataPoints.shift();
  }
  // 将数据映射到SVG坐标
  const points = dataPoints.map((v, i) => {
    const x = i * (800 / MAX_POINTS);
    const y = 200 - (v / 100) * 180; // 假设值范围0-100
    return `${x},${y}`;
  }).join(" ");
  polyline.setAttribute("points", points);
}
小技巧: 不要每次更新都重新创建polyline。复用同一个元素,只更新points属性,性能会好很多。我试过每秒更新20次,CPU占用几乎没变化。

16.4 避坑指南与最佳实践

最后,分享几个我实际项目中总结的经验:

  • 采样频率别太高。 每秒1-2次就够了。你想想看,网络变化没那么快,频繁采样反而增加浏览器负担。
  • 注意跨域问题。 如果你在Worker里调用getStats(),记得把RTCPeerConnection传进去。Worker里不能直接创建PeerConnection。
  • 数据要平滑。 原始数据抖动很大。我习惯用移动平均(Moving Average)做平滑处理,比如取最近3次采样的平均值。
  • 别忘了清理定时器。 页面关闭或连接断开时,记得clearInterval。否则定时器会一直跑,造成内存泄漏。

核心要点回顾:

  • getStats()获取原始数据,按type过滤
  • 丢包率一定要算差值,不要用累计值
  • 吞吐量用字节差除以时间差,再乘以8转成bps
  • 可视化用SVG的polyline,复用元素提升性能
  • 采样频率1-2Hz,数据做平滑处理

数据统计这块,说白了就是“把看不见的网络状况,变成看得见的数字和图表”。你把这些工具用好,以后用户再反馈卡顿,你就能直接甩出一张监控截图:“你看,当时丢包率飙到15%了,是网络问题,不是我的代码问题。”——嗯,这种感觉还是挺爽的。


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