25、质量监控:统计信息获取、RTCStats API、QoS指标、监控面板实现

做实时通信,最怕什么?

画面卡成幻灯片,声音断断续续,用户骂娘,你还找不到原因。

我早期做WebRTC项目时,就吃过这个亏。上线第一天,用户反馈视频通话质量差,我愣是抓瞎了半天,不知道是网络问题还是编码问题。后来才意识到——没有监控,就没有优化

这一章,我们就来聊聊WebRTC的质量监控。说白了,就是怎么拿到数据、怎么看懂数据、怎么把数据展示出来。

25.1 统计信息获取:getStats() 基础用法

WebRTC 提供了 RTCPeerConnection.getStats() 方法,用来获取底层统计信息。这是监控的入口。

核心方法:

const pc = new RTCPeerConnection(configuration);
const stats = await pc.getStats();

返回的是一个 Map 对象,key 是统计对象的 id,value 是 RTCStats 类型的对象。每个对象都有 type 字段,标识它是什么类型的统计。

我习惯这样遍历:

pc.getStats().then(stats => {
  stats.forEach(report => {
    console.log(report.type, report);
  });
});

嗯,这里要注意:getStats() 是异步的,记得用 await.then()

25.2 RTCStats API 详解

RTCStats API 包含多种统计类型。我挑几个最常用的说说。

类型 说明 常见字段
inbound-rtp 接收端 RTP 统计 packetsReceived, bytesReceived, jitter, packetsLost
outbound-rtp 发送端 RTP 统计 packetsSent, bytesSent, retransmittedPacketsSent
candidate-pair ICE 候选对统计 availableOutgoingBitrate, roundTripTime, state
local-candidate 本地 ICE 候选 ip, port, protocol, candidateType
remote-candidate 远端 ICE 候选 ip, port, protocol, candidateType
track 轨道统计 trackIdentifier, kind, framesReceived, framesDecoded
transport 传输层统计 bytesSent, bytesReceived, dtlsState

举个例子,获取接收端的抖动和丢包:

pc.getStats().then(stats => {
  stats.forEach(report => {
    if (report.type === 'inbound-rtp' && report.kind === 'video') {
      console.log('抖动:', report.jitter, '秒');
      console.log('丢包:', report.packetsLost);
      console.log('总包数:', report.packetsReceived);
    }
  });
});

为什么关注抖动和丢包?因为这两个指标直接决定了用户体验。抖动大,画面就会忽快忽慢;丢包多,画面就会花屏、卡顿。

25.3 QoS 指标:衡量通话质量的关键

QoS(Quality of Service)指标,说白了就是衡量通话质量好不好。我总结了几项核心指标:

  • RTT(往返时延):从发送到收到确认的时间。低于 100ms 体验很好,超过 300ms 就能感觉到延迟。
  • 抖动(Jitter):数据包到达时间的波动。低于 30ms 正常,超过 50ms 就会卡顿。
  • 丢包率(Packet Loss Rate):丢失包占总包数的比例。低于 1% 几乎无感,超过 5% 明显影响。
  • 可用带宽(Available Bitrate):当前网络能支持的最大码率。这个值会动态变化。
  • 帧率(FPS):每秒渲染的帧数。视频通话一般 15-30fps。
  • 分辨率:视频的宽高。常见 640x480、1280x720。

我的经验: 不要只看单一指标。比如丢包率低但抖动大,体验依然很差。建议综合评估。

计算丢包率的公式:

// 从 inbound-rtp 中获取
const lossRate = report.packetsLost / (report.packetsReceived + report.packetsLost);

计算 RTT 可以从 candidate-pair 中拿:

pc.getStats().then(stats => {
  stats.forEach(report => {
    if (report.type === 'candidate-pair' && report.state === 'succeeded') {
      console.log('RTT:', report.roundTripTime, '秒');
      console.log('可用带宽:', report.availableOutgoingBitrate, 'bps');
    }
  });
});

25.4 监控面板实现:从数据到可视化

拿到数据还不够,得让用户看得懂。我建议做一个实时监控面板,把关键指标展示出来。

下面是一个简单的实现思路:

  1. 定时采集:每隔 1-2 秒调用一次 getStats()
  2. 数据提取:从统计报告中提取关键指标。
  3. 数据更新:更新 DOM 或图表。

代码示例:

class MonitorPanel {
  constructor(pc) {
    this.pc = pc;
    this.statsInterval = null;
    this.metrics = {
      rtt: 0,
      jitter: 0,
      lossRate: 0,
      bitrate: 0,
      fps: 0,
      resolution: '0x0'
    };
  }

  start() {
    this.statsInterval = setInterval(() => this.collect(), 1000);
  }

  stop() {
    clearInterval(this.statsInterval);
  }

  async collect() {
    const stats = await this.pc.getStats();
    stats.forEach(report => {
      if (report.type === 'candidate-pair' && report.state === 'succeeded') {
        this.metrics.rtt = report.roundTripTime * 1000; // 转为毫秒
        this.metrics.bitrate = report.availableOutgoingBitrate;
      }
      if (report.type === 'inbound-rtp' && report.kind === 'video') {
        this.metrics.jitter = report.jitter * 1000; // 转为毫秒
        const total = report.packetsReceived + report.packetsLost;
        this.metrics.lossRate = total > 0 ? (report.packetsLost / total) * 100 : 0;
      }
      if (report.type === 'track' && report.kind === 'video') {
        this.metrics.fps = report.framesPerSecond || 0;
        this.metrics.resolution = `${report.frameWidth}x${report.frameHeight}`;
      }
    });
    this.render();
  }

  render() {
    // 更新 UI,比如设置 innerHTML
    document.getElementById('rtt').textContent = this.metrics.rtt.toFixed(0) + ' ms';
    document.getElementById('jitter').textContent = this.metrics.jitter.toFixed(1) + ' ms';
    document.getElementById('loss').textContent = this.metrics.lossRate.toFixed(2) + ' %';
    document.getElementById('bitrate').textContent = (this.metrics.bitrate / 1000).toFixed(0) + ' kbps';
    document.getElementById('fps').textContent = this.metrics.fps.toFixed(0);
    document.getElementById('resolution').textContent = this.metrics.resolution;
  }
}

// 使用
const monitor = new MonitorPanel(pc);
monitor.start();

注意: 频繁调用 getStats() 会有一定性能开销。我建议采集间隔不要低于 1 秒。如果页面卡顿,可以延长到 2-3 秒。

25.5 知识体系图

下面这张图展示了本章的核心逻辑:从采集到展示的完整链路。

WebRTC 质量监控知识体系 RTCPeerConnection getStats() 采集 inbound-rtp 接收端统计 candidate-pair ICE 候选对 track 轨道统计 抖动 / 丢包率 Jitter / Loss RTT / 带宽 时延 / Bitrate 帧率 / 分辨率 FPS / Resolution 综合评估 QoS 评分 监控面板展示(实时更新)

25.6 避坑指南

我曾经在项目中踩过几个坑,分享出来,大家少走弯路。

  • 统计对象可能为空:刚建立连接时,getStats() 返回的 Map 可能没有你想要的类型。建议加个判空。
  • 单位不一致jitterroundTripTime 的单位是秒,不是毫秒。记得转换。
  • 带宽值波动大availableOutgoingBitrate 会频繁变化。建议做平滑处理,比如取最近 5 秒的平均值。
  • 不要在主线程做太多计算:如果监控面板数据量大,考虑用 requestAnimationFrame 或 Web Worker 处理。

我的习惯: 我会把采集到的数据存到一个数组里,保留最近 30 秒的数据。这样既能看实时值,也能看趋势。用户反馈卡顿时,翻一翻历史数据,问题一目了然。

好了,这一章的内容就到这里。质量监控是 WebRTC 开发中容易被忽视但极其重要的一环。没有监控,优化就是盲人摸象。希望你能把今天学到的用起来,让你的应用更稳定、更可靠。


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