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:音视频轨道的统计。比如帧率、分辨率。
我的习惯:我一般每2秒调用一次getStats(),然后把这些数据存到一个全局对象里。这样既能实时监控,又不会太耗性能。

18.2 丢包率统计:别让数据“消失”

丢包率,是衡量网络质量最直接的指标。WebRTC里怎么算?

inbound-rtp里的两个字段:packetsLostpacketsReceived

// 计算丢包率
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%,基本就没法看了。

我曾经踩过的坑:有一次我发现丢包率一直显示0,但画面明明很卡。后来才发现,我拿的是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本身有内置的拥塞控制算法,但很多时候不够“聪明”。我习惯自己写一套调整逻辑。

核心思路很简单:

  • 网络好 → 提高码率,提升画质
  • 网络差 → 降低码率,保证流畅

具体怎么做?通过RTCRtpSendersetParameters方法。

// 自适应码率调整
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));
        }
    });
}
我建议:不要频繁调整码率。我一般每5秒判断一次。调整幅度也别太大,每次增减100-200kbps比较稳妥。否则画面会忽好忽坏,体验反而更差。

18.5 核心逻辑流程图

下面这张图,概括了整个网络质量监控与自适应调整的流程:

网络质量监控与自适应码率调整流程 定时器(每2秒) 调用 getStats() 解析 RTCStatsReport 计算丢包率、RTT、抖动 网络好坏? 提高码率(+100kbps) 降低码率(-200kbps) 循环

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-rtppacketsLostpacketsReceived
  • 延迟看candidate-paircurrentRoundTripTime
  • 码率调整通过setParameters修改maxBitrate
  • 调整幅度要小,频率要低,避免画面忽好忽坏

网络质量监控这块,说白了就是“感知-决策-执行”的循环。你感知得越准,决策得越合理,用户的体验就越好。我在好几个项目里都用这套逻辑,效果还不错。你拿过去,稍微调调参数,应该也能直接用。


公众号:蓝海资料掘金营,微信deep3321