5、媒体流调试:使用 getStats() API 获取音频/视频的发送接收统计,分析丢包、抖动、码率

各位好,我是老周。今天我们来聊聊 WebRTC 调试里最硬核的一环——getStats()。说实话,很多同学做 WebRTC 开发,能跑通音视频通话就觉得完事了。但一旦出现卡顿、花屏、声音断断续续,就抓瞎了。这时候,getStats() 就是你手里的听诊器。

我个人习惯,在每次通话建立后,先拉一次基础统计,看看有没有“先天不足”。你想想看,如果一开始就丢包 5%,那后面再怎么优化也白搭。所以,这一节我们就把 getStats() 吃透。

5.1 getStats() 是什么?

getStats() 是 RTCPeerConnection 上的一个方法。它返回一个 RTCStatsReport 对象,里面包含了当前连接的各种统计信息。说白了,就是浏览器帮你记了一本“通话流水账”。

调用方式很简单:

const stats = await peerConnection.getStats();
stats.forEach(report => {
  console.log(report.type, report);
});

这里要注意,getStats() 返回的是一个 Map 结构,每个 key 对应一个统计对象。对象的 type 字段告诉我们这是什么类型的统计。常见的 type 有:

  • inbound-rtp:接收端的 RTP 统计(音频/视频)
  • outbound-rtp:发送端的 RTP 统计
  • candidate-pair:ICE 连接质量
  • local-candidate / remote-candidate:本地/远程 ICE 候选信息
  • track:媒体轨道的统计
  • codec:编解码器信息

嗯,这里要提醒一下:不同浏览器返回的字段名可能略有差异。比如 Chrome 和 Firefox 在某些字段上命名不同。我建议你在调试时先打印全部字段看看。

5.2 核心指标:丢包、抖动、码率

我们做调试,最关心的就是这三个指标。下面我一个个讲。

5.2.1 丢包率(Packet Loss)

丢包是音视频质量的头号杀手。在 getStats() 中,丢包相关的字段主要在 inbound-rtpoutbound-rtp 里。

对于接收端(inbound-rtp),关键字段是:

  • packetsLost:累计丢失的包数
  • packetsReceived:累计收到的包数

丢包率计算公式:

let lossRate = packetsLost / (packetsLost + packetsReceived);

对于发送端(outbound-rtp),也有 packetsSentretransmittedPacketsSent。但注意,发送端丢包通常是通过 RTCP 反馈来计算的,所以更可靠的是看接收端的统计。

实战经验: 我在一个跨国会议项目中遇到过,丢包率在 2% 以下时用户基本无感,超过 5% 就开始出现卡顿和花屏。超过 10% 基本没法用了。所以,我一般把 3% 作为告警阈值。

5.2.2 抖动(Jitter)

抖动是指数据包到达时间的变化程度。如果抖动很大,即使不丢包,音视频也会出现忽快忽慢的感觉。

在 getStats() 中,抖动字段是 jitter(单位:秒)。注意,Chrome 返回的 jitter 是秒,Firefox 返回的是毫秒?不,其实都是秒,但 Firefox 的 jitter 值可能看起来很小。我建议统一乘以 1000 转成毫秒来看。

let jitterMs = report.jitter * 1000;

抖动一般跟网络拥塞有关。我个人习惯,如果抖动超过 30ms,就要考虑加 jitter buffer 或者调整码率了。

小技巧: 你可以用 jitterBufferDelay 字段(如果有的话)来查看 jitter buffer 实际造成的延迟。这个字段在 Chrome 的 inbound-rtp 里可以找到。

5.2.3 码率(Bitrate)

码率决定了音视频的清晰度。在 getStats() 中,码率不是直接给的,需要自己算。

对于发送端(outbound-rtp),关键字段是:

  • bytesSent:累计发送的字节数
  • timestamp:当前时间戳

码率计算公式(单位:kbps):

// 需要保存上一次的 bytesSent 和 timestamp
let bitrate = (bytesSent - lastBytesSent) * 8 / (timestamp - lastTimestamp) / 1000;

对于接收端(inbound-rtp),用 bytesReceived 同理。

这里有个坑:第一次计算时没有上一次值,所以码率为 0。我一般会等 1 秒后再算。

注意: 码率波动是正常的。但如果码率突然掉到 0,说明网络可能断开了。我曾经遇到过一个 bug,用户切到后台后,浏览器为了省电暂停了发送,码率直接归零。这时候需要监听 iceconnectionstatechange 来区分是网络问题还是应用层问题。

5.3 实战:写一个调试面板

光说不练假把式。下面我写一个简单的调试面板,每秒拉一次 getStats(),把关键指标显示出来。

const pc = new RTCPeerConnection();
// ... 添加音视频轨道 ...

let lastStats = {};

async function pollStats() {
  const stats = await pc.getStats();
  stats.forEach(report => {
    if (report.type === 'inbound-rtp' && report.kind === 'video') {
      // 视频接收端统计
      const packetsLost = report.packetsLost;
      const packetsReceived = report.packetsReceived;
      const lossRate = packetsLost / (packetsLost + packetsReceived);
      const jitter = report.jitter * 1000; // 转毫秒

      // 计算码率
      const now = report.timestamp;
      const bytes = report.bytesReceived;
      let bitrate = 0;
      if (lastStats[report.id]) {
        const deltaBytes = bytes - lastStats[report.id].bytes;
        const deltaTime = (now - lastStats[report.id].timestamp) / 1000; // 转秒
        bitrate = (deltaBytes * 8) / deltaTime / 1000; // kbps
      }
      lastStats[report.id] = { bytes, timestamp: now };

      console.log(`丢包率: ${(lossRate * 100).toFixed(2)}%`);
      console.log(`抖动: ${jitter.toFixed(2)} ms`);
      console.log(`码率: ${bitrate.toFixed(0)} kbps`);
    }
  });
  setTimeout(pollStats, 1000);
}

pollStats();

这段代码每秒打印一次视频接收端的三个核心指标。你可以把它扩展成 UI 显示,或者发送到远程日志服务器。

5.4 知识体系图

下面我用一张 SVG 图来总结 getStats() 的核心逻辑。这张图展示了从调用 getStats() 到分析丢包、抖动、码率的完整流程。

getStats() 调试核心流程 调用 getStats() 遍历 RTCStatsReport 对象 丢包率 packetsLost / (packetsLost + packetsReceived) 抖动 jitter * 1000 (ms) 码率 (bytesDelta * 8) / timeDelta (kbps) 根据阈值判断网络质量,决定是否降码率或切换线路

5.5 避坑指南

最后,我分享几个实际项目中踩过的坑。

  • 统计不是实时的:getStats() 返回的是累计值,不是瞬时值。所以计算码率时一定要做差值,而且时间间隔不能太短(至少 500ms)。
  • 浏览器差异:Chrome 的 inbound-rtp 里没有 framesDropped?不,其实有,但字段名可能叫 framesDroppedframesDecoded 的差值。我建议你打印所有字段看看。
  • 多流场景:如果同时有多个音视频轨道,每个轨道都有自己的 inbound-rtp 和 outbound-rtp。记得用 kind 字段区分 audio 和 video。
  • getStats() 调用频率:不要每 100ms 调一次,浏览器会受不了。我一般用 1 秒间隔,既能看到趋势,又不影响性能。

我的习惯: 在调试阶段,我会把 getStats() 的结果同时打印到控制台和发送到远程日志服务器。这样线上出问题时,可以回溯当时的网络状态。我曾经靠这个方法定位到一个“用户 WiFi 信号差导致丢包 20%”的 case。

好了,关于 getStats() 的核心用法就讲到这里。记住,丢包、抖动、码率这三个指标是 WebRTC 调试的基石。下次你的用户说“卡”,别急着怀疑代码,先拉一下统计看看。

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