18、网络质量监控:RTCStatsReport解析、丢包率与延迟统计、自适应码率调整
做视频会议,最怕什么?
画面卡成PPT,声音断断续续,对方问你“在吗”你却听不见。嗯,这些我都经历过。而且说实话,早期我踩过的坑,比你们想象的多得多。
网络质量监控,说白了就是给WebRTC装上一双眼睛。让它能感知网络好坏,然后自动调整。这节课,我就带你把这双眼睛做出来。
18.1 RTCStatsReport:WebRTC的体检报告
WebRTC内部其实一直在默默统计各种数据。它把这些数据打包成一个叫RTCStatsReport的东西。你可以把它理解成一份体检报告。
怎么拿到这份报告?很简单,调用peerConnection.getStats()就行。
// 获取统计报告
peerConnection.getStats().then(report => {
report.forEach(stats => {
console.log(stats.type, stats);
});
});
这份报告里有很多“类型”。我挑几个最常用的说:
- inbound-rtp:接收端数据。包括收到的包数、丢包数、抖动等。
- outbound-rtp:发送端数据。包括发送的包数、编码器信息等。
- candidate-pair:连接信息。包括RTT(往返时延)、可用带宽等。
- track:音视频轨道的统计。比如帧率、分辨率。
getStats(),然后把这些数据存到一个全局对象里。这样既能实时监控,又不会太耗性能。
18.2 丢包率统计:别让数据“消失”
丢包率,是衡量网络质量最直接的指标。WebRTC里怎么算?
看inbound-rtp里的两个字段:packetsLost和packetsReceived。
// 计算丢包率
function calculatePacketLoss(report) {
let inboundStats = null;
report.forEach(stats => {
if (stats.type === 'inbound-rtp' && stats.kind === 'video') {
inboundStats = stats;
}
});
if (!inboundStats) return 0;
const total = inboundStats.packetsLost + inboundStats.packetsReceived;
if (total === 0) return 0;
return (inboundStats.packetsLost / total) * 100;
}
丢包率超过5%,画面就开始有可见的卡顿了。超过10%,基本就没法看了。
outbound-rtp的数据。记住,丢包率要看接收端的统计,也就是inbound-rtp。
18.3 延迟统计:你的声音迟到了几秒?
延迟,也叫RTT(Round-Trip Time)。它表示数据从A到B再回到A的时间。
在candidate-pair里,有个字段叫currentRoundTripTime,单位是秒。
// 获取RTT
function getRTT(report) {
let rtt = 0;
report.forEach(stats => {
if (stats.type === 'candidate-pair' && stats.nominated) {
rtt = stats.currentRoundTripTime * 1000; // 转成毫秒
}
});
return rtt;
}
RTT在100ms以内,体验很好。200ms以上,对话就开始有“对不上嘴”的感觉了。超过500ms,基本就是“你说完,我三秒后才回应”。
除了RTT,还有个指标叫抖动(jitter)。它表示延迟的变化幅度。抖动大,说明网络不稳定。
// 获取抖动
function getJitter(report) {
let jitter = 0;
report.forEach(stats => {
if (stats.type === 'inbound-rtp' && stats.kind === 'video') {
jitter = stats.jitter * 1000; // 转成毫秒
}
});
return jitter;
}
18.4 自适应码率调整:让视频“随网而变”
拿到网络数据后,我们要做什么?当然是调整码率。
WebRTC本身有内置的拥塞控制算法,但很多时候不够“聪明”。我习惯自己写一套调整逻辑。
核心思路很简单:
- 网络好 → 提高码率,提升画质
- 网络差 → 降低码率,保证流畅
具体怎么做?通过RTCRtpSender的setParameters方法。
// 自适应码率调整
function adaptiveBitrate(peerConnection, currentBitrate) {
// 获取统计报告
peerConnection.getStats().then(report => {
const packetLoss = calculatePacketLoss(report);
const rtt = getRTT(report);
let newBitrate = currentBitrate;
// 网络差,降低码率
if (packetLoss > 5 || rtt > 200) {
newBitrate = Math.max(100, currentBitrate - 200); // 每次降200kbps,最低100kbps
}
// 网络好,提高码率
else if (packetLoss < 1 && rtt < 100) {
newBitrate = Math.min(2000, currentBitrate + 100); // 每次加100kbps,最高2000kbps
}
// 应用新码率
const sender = peerConnection.getSenders().find(s => s.track?.kind === 'video');
if (sender) {
const params = sender.getParameters();
if (!params.encodings) params.encodings = [{}];
params.encodings[0].maxBitrate = newBitrate * 1000; // 转成bps
sender.setParameters(params).catch(e => console.error('设置码率失败', e));
}
});
}
18.5 核心逻辑流程图
下面这张图,概括了整个网络质量监控与自适应调整的流程:
18.6 完整监控类实现
把上面这些零散的东西,封装成一个类。这样用起来就方便多了。
class NetworkMonitor {
constructor(peerConnection) {
this.pc = peerConnection;
this.stats = {
packetLoss: 0,
rtt: 0,
jitter: 0,
bitrate: 500 // 初始码率500kbps
};
this.timer = null;
}
start() {
this.timer = setInterval(() => {
this.updateStats();
}, 2000);
}
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
updateStats() {
this.pc.getStats().then(report => {
// 更新丢包率
this.stats.packetLoss = this.calculatePacketLoss(report);
// 更新RTT
this.stats.rtt = this.getRTT(report);
// 更新抖动
this.stats.jitter = this.getJitter(report);
// 自适应调整
this.adaptiveBitrate();
});
}
calculatePacketLoss(report) {
let inboundStats = null;
report.forEach(stats => {
if (stats.type === 'inbound-rtp' && stats.kind === 'video') {
inboundStats = stats;
}
});
if (!inboundStats) return 0;
const total = inboundStats.packetsLost + inboundStats.packetsReceived;
if (total === 0) return 0;
return (inboundStats.packetsLost / total) * 100;
}
getRTT(report) {
let rtt = 0;
report.forEach(stats => {
if (stats.type === 'candidate-pair' && stats.nominated) {
rtt = stats.currentRoundTripTime * 1000;
}
});
return rtt;
}
getJitter(report) {
let jitter = 0;
report.forEach(stats => {
if (stats.type === 'inbound-rtp' && stats.kind === 'video') {
jitter = stats.jitter * 1000;
}
});
return jitter;
}
adaptiveBitrate() {
const { packetLoss, rtt, bitrate } = this.stats;
let newBitrate = bitrate;
if (packetLoss > 5 || rtt > 200) {
newBitrate = Math.max(100, bitrate - 200);
} else if (packetLoss < 1 && rtt < 100) {
newBitrate = Math.min(2000, bitrate + 100);
}
if (newBitrate !== bitrate) {
this.stats.bitrate = newBitrate;
this.applyBitrate(newBitrate);
}
}
applyBitrate(bitrate) {
const sender = this.pc.getSenders().find(s => s.track?.kind === 'video');
if (sender) {
const params = sender.getParameters();
if (!params.encodings) params.encodings = [{}];
params.encodings[0].maxBitrate = bitrate * 1000;
sender.setParameters(params).catch(e => console.error('设置码率失败', e));
}
}
getStats() {
return this.stats;
}
}
// 使用示例
const monitor = new NetworkMonitor(peerConnection);
monitor.start();
// 在UI上显示
setInterval(() => {
const stats = monitor.getStats();
console.log(`丢包率: ${stats.packetLoss.toFixed(1)}%, RTT: ${stats.rtt.toFixed(0)}ms, 码率: ${stats.bitrate}kbps`);
}, 2000);
核心要点总结:
- 用
getStats()获取网络数据,每2秒一次 - 丢包率看
inbound-rtp的packetsLost和packetsReceived - 延迟看
candidate-pair的currentRoundTripTime - 码率调整通过
setParameters修改maxBitrate - 调整幅度要小,频率要低,避免画面忽好忽坏
网络质量监控这块,说白了就是“感知-决策-执行”的循环。你感知得越准,决策得越合理,用户的体验就越好。我在好几个项目里都用这套逻辑,效果还不错。你拿过去,稍微调调参数,应该也能直接用。
公众号:蓝海资料掘金营,微信deep3321