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-2 秒调用一次
getStats()。 - 数据提取:从统计报告中提取关键指标。
- 数据更新:更新 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 知识体系图
下面这张图展示了本章的核心逻辑:从采集到展示的完整链路。
25.6 避坑指南
我曾经在项目中踩过几个坑,分享出来,大家少走弯路。
- 统计对象可能为空:刚建立连接时,
getStats()返回的 Map 可能没有你想要的类型。建议加个判空。 - 单位不一致:
jitter和roundTripTime的单位是秒,不是毫秒。记得转换。 - 带宽值波动大:
availableOutgoingBitrate会频繁变化。建议做平滑处理,比如取最近 5 秒的平均值。 - 不要在主线程做太多计算:如果监控面板数据量大,考虑用
requestAnimationFrame或 Web Worker 处理。
我的习惯: 我会把采集到的数据存到一个数组里,保留最近 30 秒的数据。这样既能看实时值,也能看趋势。用户反馈卡顿时,翻一翻历史数据,问题一目了然。
好了,这一章的内容就到这里。质量监控是 WebRTC 开发中容易被忽视但极其重要的一环。没有监控,优化就是盲人摸象。希望你能把今天学到的用起来,让你的应用更稳定、更可靠。