11、媒体流统计与监控:getStats API详解、获取音频/视频的收发统计、丢包率、延迟、抖动计算、实时监控面板实现
做WebRTC开发,最怕什么?
我最怕用户说「你那个视频通话卡成PPT了」,但我自己这边一切正常。这时候你拿什么去跟用户对线?凭感觉?不行。
所以,getStats API 就是我们手里最趁手的「黑匣子」。它能告诉你每一帧、每一个包到底经历了什么。今天我就带你把它彻底吃透。
11.1 getStats API 基础用法
先看最核心的东西。getStats 是 RTCPeerConnection 上的一个方法,调用它会返回一个 RTCStatsReport 对象。这个对象里装满了各种统计信息。
// 基础调用
const stats = await peerConnection.getStats();
stats.forEach(report => {
console.log(report.type, report);
});
嗯,就这么简单。但实际项目中,你不可能把所有的报告都打印出来。你需要知道怎么筛选。
我个人习惯先判断 report.type,然后只处理我关心的类型。常见的类型有:
| type 值 | 含义 | 关键字段 |
|---|---|---|
inbound-rtp |
接收端 RTP 统计 | packetsReceived, bytesReceived, jitter, packetsLost |
outbound-rtp |
发送端 RTP 统计 | packetsSent, bytesSent, retransmittedPacketsSent |
remote-inbound-rtp |
远端接收反馈 | roundTripTime, fractionLost |
track |
媒体轨道统计 | framesReceived, framesDecoded, framesDropped |
candidate-pair |
ICE 候选对统计 | availableOutgoingBitrate, currentRoundTripTime |
11.2 音频收发统计
音频统计,说白了就是看「有没有声音断掉」或者「声音是不是忽大忽小」。
我一般这样获取音频的接收统计:
async function getAudioStats(pc) {
const stats = await pc.getStats();
let audioStats = {};
stats.forEach(report => {
if (report.type === 'inbound-rtp' && report.kind === 'audio') {
audioStats = {
packetsReceived: report.packetsReceived,
packetsLost: report.packetsLost,
jitter: report.jitter,
totalAudioEnergy: report.totalAudioEnergy,
totalSamplesDuration: report.totalSamplesDuration
};
}
});
return audioStats;
}
这里有个关键点——totalAudioEnergy 和 totalSamplesDuration 这两个字段,可以用来计算平均音量。公式很简单:
const averageVolume = totalAudioEnergy / totalSamplesDuration;
这个值越大,说明对方说话声音越大。我在做会议系统时,就用这个来判断用户是不是「闭麦了但没完全闭麦」——音量一直为零,但麦克风状态显示开启,那八成是硬件问题。
11.3 视频收发统计
视频统计比音频复杂一些。因为视频有帧率、分辨率、编码质量这些维度。
async function getVideoStats(pc) {
const stats = await pc.getStats();
let videoStats = {};
stats.forEach(report => {
if (report.type === 'inbound-rtp' && report.kind === 'video') {
videoStats = {
framesReceived: report.framesReceived,
framesDecoded: report.framesDecoded,
framesDropped: report.framesDropped,
frameWidth: report.frameWidth,
frameHeight: report.frameHeight,
framesPerSecond: report.framesPerSecond,
jitter: report.jitter,
packetsLost: report.packetsLost
};
}
if (report.type === 'outbound-rtp' && report.kind === 'video') {
videoStats.bitrate = report.bitrateMean || report.targetBitrate;
videoStats.qualityLimitationReason = report.qualityLimitationReason;
}
});
return videoStats;
}
注意 qualityLimitationReason 这个字段。它告诉你为什么视频质量下降了。常见值有:
bandwidth—— 带宽不够,自动降质cpu—— 编码器跑不动了none—— 一切正常
11.4 核心指标计算:丢包率、延迟、抖动
这三个指标是衡量通话质量的「三驾马车」。我一个个说。
11.4.1 丢包率
丢包率 = 丢失的包数 / 总包数。但要注意,这个值要取「远端反馈」的才准。
function calculatePacketLoss(stats) {
let lossRate = 0;
stats.forEach(report => {
if (report.type === 'remote-inbound-rtp') {
const total = report.packetsReceived + report.packetsLost;
if (total > 0) {
lossRate = (report.packetsLost / total) * 100;
}
}
});
return lossRate.toFixed(2); // 返回百分比
}
丢包率超过 5%,用户就能感知到卡顿。超过 10%,基本没法正常通话了。
11.4.2 延迟(RTT)
RTT 就是数据从你这里到对方再回来的时间。这个值可以从 remote-inbound-rtp 或 candidate-pair 里拿。
function calculateRTT(stats) {
let rtt = 0;
stats.forEach(report => {
if (report.type === 'candidate-pair' && report.nominated) {
rtt = report.currentRoundTripTime * 1000; // 转为毫秒
}
});
return rtt.toFixed(0);
}
RTT 在 100ms 以内算优秀,200ms 以内可接受,超过 300ms 就会有明显的延迟感。
11.4.3 抖动(Jitter)
抖动是包到达时间的不稳定性。WebRTC 内部已经帮你算好了,直接从 inbound-rtp 的 jitter 字段拿就行。
function calculateJitter(stats) {
let jitter = 0;
stats.forEach(report => {
if (report.type === 'inbound-rtp' && report.kind === 'audio') {
jitter = report.jitter * 1000; // 转为毫秒
}
});
return jitter.toFixed(1);
}
音频抖动超过 50ms,声音就会「断断续续」。视频抖动容忍度高一些,但超过 100ms 也会出现画面撕裂。
- 丢包率 = (packetsLost / (packetsReceived + packetsLost)) × 100%
- RTT = currentRoundTripTime × 1000 (ms)
- 抖动 = jitter × 1000 (ms)
11.5 实时监控面板实现
光有数据不行,你得把它展示出来。我习惯做一个「三栏式」的监控面板。
先看整体结构:
实现代码其实不复杂。核心就是一个定时器,每秒调用一次 getStats,然后更新 UI。
class StatsMonitor {
constructor(pc, containerId) {
this.pc = pc;
this.container = document.getElementById(containerId);
this.timer = null;
}
start() {
this.timer = setInterval(async () => {
const stats = await this.pc.getStats();
const data = this.processStats(stats);
this.render(data);
}, 1000);
}
processStats(stats) {
let result = {
audio: {},
video: {},
network: {}
};
stats.forEach(report => {
// 音频接收
if (report.type === 'inbound-rtp' && report.kind === 'audio') {
result.audio.packetsLost = report.packetsLost;
result.audio.jitter = (report.jitter * 1000).toFixed(1);
result.audio.packetsReceived = report.packetsReceived;
}
// 视频接收
if (report.type === 'inbound-rtp' && report.kind === 'video') {
result.video.framesPerSecond = report.framesPerSecond;
result.video.frameWidth = report.frameWidth;
result.video.frameHeight = report.frameHeight;
result.video.framesDropped = report.framesDropped;
}
// 网络 RTT
if (report.type === 'candidate-pair' && report.nominated) {
result.network.rtt = (report.currentRoundTripTime * 1000).toFixed(0);
result.network.availableBitrate = report.availableOutgoingBitrate;
}
});
// 计算丢包率
const audioTotal = (result.audio.packetsReceived || 0) + (result.audio.packetsLost || 0);
result.audio.lossRate = audioTotal > 0
? ((result.audio.packetsLost / audioTotal) * 100).toFixed(2)
: '0.00';
return result;
}
render(data) {
this.container.innerHTML = `
<div class="stats-panel">
<div class="stats-section">
<h4>🎵 音频统计</h4>
<p>丢包率: ${data.audio.lossRate}%</p>
<p>抖动: ${data.audio.jitter}ms</p>
<p>接收包数: ${data.audio.packetsReceived}</p>
</div>
<div class="stats-section">
<h4>🎬 视频统计</h4>
<p>帧率: ${data.video.framesPerSecond}fps</p>
<p>分辨率: ${data.video.frameWidth}x${data.video.frameHeight}</p>
<p>丢帧数: ${data.video.framesDropped}</p>
</div>
<div class="stats-section">
<h4>🌐 网络统计</h4>
<p>RTT: ${data.network.rtt}ms</p>
<p>可用带宽: ${(data.network.availableBitrate / 1000).toFixed(0)}kbps</p>
</div>
</div>
`;
}
stop() {
clearInterval(this.timer);
}
}
// 使用方式
const monitor = new StatsMonitor(peerConnection, 'stats-container');
monitor.start();
11.6 监控面板的视觉设计
数据有了,怎么展示才直观?我总结了几条原则:
- 颜色预警: 丢包率 < 1% 绿色,1%-5% 黄色,> 5% 红色
- 数值变化: 用动画或闪烁提示用户「数据在变化」
- 关键指标置顶: RTT、丢包率、抖动这三个放在最显眼的位置
嗯,其实你想想看,用户最关心的就是「我现在的通话质量好不好」。所以面板上最好有一个「综合评分」,把三个指标加权算出一个分数。我在项目中就是这么做的:
function calculateQualityScore(stats) {
let score = 100;
// 丢包扣分
if (stats.lossRate > 5) score -= 30;
else if (stats.lossRate > 2) score -= 15;
else if (stats.lossRate > 1) score -= 5;
// RTT 扣分
if (stats.rtt > 300) score -= 25;
else if (stats.rtt > 200) score -= 10;
else if (stats.rtt > 100) score -= 5;
// 抖动扣分
if (stats.jitter > 100) score -= 20;
else if (stats.jitter > 50) score -= 10;
return Math.max(0, score);
}
这个评分虽然简单,但很实用。用户一看就知道「现在是绿色,没问题」还是「红色,该检查网络了」。
好了,getStats 这块的内容差不多就这些。从 API 调用到指标计算,再到面板实现,一条线串下来。你只要照着做,就能给你的 WebRTC 应用加上一双「眼睛」。
公众号:蓝海资料掘金营,微信deep3321