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-rtp 和 outbound-rtp 里。
对于接收端(inbound-rtp),关键字段是:
packetsLost:累计丢失的包数packetsReceived:累计收到的包数
丢包率计算公式:
let lossRate = packetsLost / (packetsLost + packetsReceived);
对于发送端(outbound-rtp),也有 packetsSent 和 retransmittedPacketsSent。但注意,发送端丢包通常是通过 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() 到分析丢包、抖动、码率的完整流程。
5.5 避坑指南
最后,我分享几个实际项目中踩过的坑。
- 统计不是实时的:getStats() 返回的是累计值,不是瞬时值。所以计算码率时一定要做差值,而且时间间隔不能太短(至少 500ms)。
- 浏览器差异:Chrome 的 inbound-rtp 里没有
framesDropped?不,其实有,但字段名可能叫framesDropped或framesDecoded的差值。我建议你打印所有字段看看。 - 多流场景:如果同时有多个音视频轨道,每个轨道都有自己的 inbound-rtp 和 outbound-rtp。记得用
kind字段区分 audio 和 video。 - getStats() 调用频率:不要每 100ms 调一次,浏览器会受不了。我一般用 1 秒间隔,既能看到趋势,又不影响性能。
我的习惯: 在调试阶段,我会把 getStats() 的结果同时打印到控制台和发送到远程日志服务器。这样线上出问题时,可以回溯当时的网络状态。我曾经靠这个方法定位到一个“用户 WiFi 信号差导致丢包 20%”的 case。
好了,关于 getStats() 的核心用法就讲到这里。记住,丢包、抖动、码率这三个指标是 WebRTC 调试的基石。下次你的用户说“卡”,别急着怀疑代码,先拉一下统计看看。