19、质量监控:RTCStats API、延迟统计、丢包率分析
做WebRTC开发,最怕什么?
不是功能做不出来,而是用户说「卡了」、「听不清」、「画面不动了」,你却拿不出任何数据来排查。我早期做第一个移动端项目时就吃过这个亏——用户投诉视频通话质量差,我连是延迟高还是丢包严重都说不清楚。后来老老实实把RTCStats API用起来,才算是有了「眼睛」。
这一章,我们就来聊聊怎么给WebRTC装上监控系统。说白了,就是三件事:延迟统计、丢包率分析、以及如何用RTCStats API拿到这些数据。
19.1 RTCStats API 是什么?
RTCStats API是浏览器原生提供的统计接口。你不需要引入任何第三方库,直接调用RTCPeerConnection.getStats()就能拿到一整套网络和媒体质量数据。
我个人习惯在建立连接后,每隔1到2秒轮询一次。为什么是这个频率?太频繁会影响性能,太稀疏又抓不到瞬时的抖动。
// 基础用法
const pc = new RTCPeerConnection(configuration);
// 每隔2秒采集一次
setInterval(async () => {
const stats = await pc.getStats();
stats.forEach(report => {
console.log(report.type, report);
});
}, 2000);
嗯,这里要注意:getStats()返回的是一个Map结构,每个条目都有一个type字段。常见的type有:
| type | 含义 | 关键字段 |
|---|---|---|
inbound-rtp |
接收端RTP流统计 | packetsReceived, bytesReceived, jitter, packetsLost |
outbound-rtp |
发送端RTP流统计 | packetsSent, bytesSent, retransmittedPacketsSent |
candidate-pair |
ICE候选连接对 | currentRoundTripTime, availableOutgoingBitrate |
remote-inbound-rtp |
远端反馈的接收统计 | roundTripTime, fractionLost |
track |
媒体轨道的统计 | framesReceived, framesDecoded, framesDropped |
你想想看,有了这些数据,用户说卡的时候,你至少能判断是网络问题还是设备解码能力问题。
19.2 延迟统计:不只是RTT
延迟是用户感知最直接的指标。但「延迟」其实分好几种:
- 端到端延迟:从摄像头采集到远端屏幕显示的总耗时
- 网络RTT:数据包往返时间
- 抖动:延迟的变化量,比延迟本身更影响体验
我在项目中遇到过这样的情况:RTT只有30ms,但用户还是觉得画面有延迟。后来发现是抖动太大,缓冲区来不及平滑。所以只看RTT是不够的。
19.2.1 获取RTT
RTT可以从candidate-pair的currentRoundTripTime拿到,单位是秒:
// 获取当前连接的RTT
async function getRTT(pc) {
const stats = await pc.getStats();
let rtt = 0;
stats.forEach(report => {
if (report.type === 'candidate-pair' && report.nominated) {
rtt = report.currentRoundTripTime * 1000; // 转为毫秒
}
});
return rtt;
}
另外,remote-inbound-rtp里也有roundTripTime字段,这是基于RTCP反馈计算的,更接近真实的端到端往返时间。
19.2.2 抖动计算
WebRTC已经帮你算好了抖动值。在inbound-rtp里有个jitter字段,单位是秒。不过要注意,这个值是RTP时间戳单位下的抖动,需要根据采样率换算成毫秒。
// 获取音频/视频抖动
function getJitter(report) {
// 音频通常采样率是8000或48000
const sampleRate = report.kind === 'audio' ? 8000 : 90000;
return (report.jitter || 0) / sampleRate * 1000; // 转为毫秒
}
19.3 丢包率分析
丢包是WebRTC质量的头号杀手。但丢包率怎么算?很多人直接拿packetsLost / packetsReceived,这是错的。
正确的做法是:
- 从
inbound-rtp拿到packetsLost和packetsReceived - 总包数 = packetsLost + packetsReceived
- 丢包率 = packetsLost / 总包数
// 计算接收端丢包率
function calcLossRate(report) {
const lost = report.packetsLost || 0;
const received = report.packetsReceived || 0;
const total = lost + received;
if (total === 0) return 0;
return (lost / total) * 100;
}
我曾经踩过一个坑:只看接收端丢包率,忽略了发送端重传。实际上,WebRTC有NACK和FEC机制,很多丢包在底层就被恢复了。所以更准确的指标是有效丢包率——即经过重传和FEC后仍然丢失的比例。
有效丢包率可以从remote-inbound-rtp的fractionLost字段获取,这个值是远端反馈的、经过重传后的丢包比例:
// 获取有效丢包率(远端反馈)
function getEffectiveLoss(stats) {
let loss = 0;
stats.forEach(report => {
if (report.type === 'remote-inbound-rtp') {
loss = report.fractionLost / 256 * 100; // fractionLost是8位整数,范围0-255
}
});
return loss;
}
fractionLost不是所有浏览器都支持。Chrome支持较好,Safari和部分移动端浏览器可能缺失。建议同时采集两端数据做交叉验证。
19.4 知识体系与核心逻辑
下面这张图梳理了质量监控的整体架构。我建议你把它当作一个「监控仪表盘」的设计蓝图:
这张图的核心逻辑是:先采集原始数据,再分类提取关键指标,然后根据阈值做判断,最后输出到可视化界面或上报到服务端。我自己的项目里,还会把数据存到本地数据库,方便事后回溯分析。
19.5 实战:搭建一个简易监控面板
光说不练假把式。下面是一个完整的监控代码片段,你可以直接集成到项目中:
class QualityMonitor {
constructor(pc) {
this.pc = pc;
this.metrics = {
rtt: 0,
jitter: 0,
lossRate: 0,
bitrate: 0,
frameRate: 0
};
this.interval = null;
}
start(intervalMs = 2000) {
this.interval = setInterval(() => this.collect(), intervalMs);
}
stop() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}
async collect() {
const stats = await this.pc.getStats();
let prevBytes = 0;
let prevTs = 0;
stats.forEach(report => {
// 网络指标
if (report.type === 'candidate-pair' && report.nominated) {
this.metrics.rtt = report.currentRoundTripTime * 1000;
}
// 接收端媒体指标
if (report.type === 'inbound-rtp' && report.kind === 'video') {
const total = (report.packetsLost || 0) + (report.packetsReceived || 0);
this.metrics.lossRate = total > 0 ? (report.packetsLost / total) * 100 : 0;
this.metrics.jitter = (report.jitter || 0) / 90000 * 1000;
// 码率计算(需要两次采样差值)
if (prevBytes > 0) {
const deltaBytes = report.bytesReceived - prevBytes;
const deltaTime = (report.timestamp - prevTs) / 1000;
this.metrics.bitrate = (deltaBytes * 8) / deltaTime / 1000; // kbps
}
prevBytes = report.bytesReceived;
prevTs = report.timestamp;
// 帧率
if (report.framesPerSecond) {
this.metrics.frameRate = report.framesPerSecond;
}
}
});
// 输出到控制台或UI
console.table(this.metrics);
this.updateUI();
}
updateUI() {
// 这里可以更新DOM或发送到后端
// 例如:document.getElementById('rtt').textContent = this.metrics.rtt + 'ms';
}
}
// 使用示例
const monitor = new QualityMonitor(peerConnection);
monitor.start(2000);
- 码率计算必须用两次采样的差值,单次采样没有意义
- 丢包率要区分「原始丢包率」和「有效丢包率」
- 抖动值要根据媒体类型换算(音频8kHz,视频90kHz)
- 建议同时监控发送端和接收端,对比数据可以发现更多问题
19.6 避坑指南
最后分享几个我踩过的坑:
- 我曾经以为getStats()是同步的——实际上它是异步的,一定要用await或then。早期代码里忘了await,拿到的全是undefined,排查了半天。
- 不同浏览器的字段名不一样——比如Safari的
candidate-pair里没有currentRoundTripTime,得用roundTripTime。建议做一层兼容封装。 - 移动端频繁调用getStats()会耗电——我建议在弱网或用户反馈问题时才提高采集频率,平时1-2秒一次就够了。
- 丢包率突然飙升不一定是网络问题——有一次我发现丢包率从1%跳到30%,排查半天发现是服务端转码压力大,导致丢包。所以监控要结合服务端日志一起看。
嗯,质量监控这件事,说白了就是给WebRTC装上「黑匣子」。平时可能用不上,但一旦出问题,它就是救命稻草。我建议你在项目初期就把这套机制加进去,别等到用户投诉了再补。
公众号:蓝海资料掘金营,微信deep3321