WebRTC Metrics
Abstract |
WebRTC Metrics |
Category |
Learning note |
Authors |
Walter Fan |
Status |
v1.0 |
Updated |
2026-03-20 |
Overview
WebRTC 提供了 webrtc_stats, 包含了诸多层面的指标, 浏览器提供了 getStats() API, Native SDK 需要调用类似的接口。
我们如果要计算在一段区间内的 metrics , 还需要做些计算。因为大多数的指标都是线性增长的,所以要把上一次取得的数据缓存下来,将这一次取得的指标减去上一次的指标
例如:
packetsSent = currentPacketsSent - previousPacketsSent
frameRateReceived = (currentFramesReceived - previousFramesReceived)/(currentTimeMs - previousTimeMs)/1000
getStats() API 详解
RTCPeerConnection.getStats() 是 WebRTC 中获取实时统计数据的核心 API。它返回一个 Promise,resolve 后得到一个 RTCStatsReport 对象。
基本用法如下:
const pc = new RTCPeerConnection();
// 获取所有统计数据
const stats = await pc.getStats();
// 遍历所有统计报告
stats.forEach(report => {
console.log(`Type: ${report.type}, ID: ${report.id}`);
console.log(`Timestamp: ${report.timestamp}`);
console.log(JSON.stringify(report, null, 2));
});
// 获取特定 sender 或 receiver 的统计数据
const senders = pc.getSenders();
for (const sender of senders) {
const senderStats = await sender.getStats();
senderStats.forEach(report => {
if (report.type === 'outbound-rtp') {
console.log('Outbound RTP stats:', report);
}
});
}
RTCStatsReport 结构
RTCStatsReport 是一个 Map-like 对象,其中每个条目都是一个 RTCStats 字典。每个统计报告都包含以下基础字段:
字段 |
类型 |
说明 |
|---|---|---|
|
string |
统计对象的唯一标识符 |
|
string |
统计类型,如 |
|
DOMHighResTimeStamp |
统计数据采集的时间戳(毫秒) |
常见的统计类型包括:
inbound-rtp: 入站 RTP 流统计outbound-rtp: 出站 RTP 流统计remote-inbound-rtp: 远端入站 RTP 流统计(来自 RTCP Receiver Report)remote-outbound-rtp: 远端出站 RTP 流统计(来自 RTCP Sender Report)candidate-pair: ICE 候选对统计local-candidate: 本地 ICE 候选统计remote-candidate: 远端 ICE 候选统计media-source: 媒体源统计codec: 编解码器信息transport: 传输层统计data-channel: 数据通道统计
这些统计类型之间通过 ID 引用相互关联,形成一个完整的统计数据图。例如,inbound-rtp 报告中的 codecId 字段指向对应的 codec 报告。
RTCStatsReport 关键指标
RTCInboundRtpStreamStats
RTCInboundRtpStreamStats 描述了接收端 RTP 流的统计信息,是评估接收质量的核心指标集。
字段 |
类型 |
说明 |
|---|---|---|
|
unsigned long |
已接收的 RTP 包总数 |
|
unsigned long long |
已接收的 RTP 负载字节总数(不含头部和填充) |
|
long |
累计丢包数,由 RTCP RR 中的 cumulative number of packets lost 得出 |
|
double |
当前到达间隔抖动(秒),按 RFC 3550 算法计算 |
|
unsigned long |
已成功解码的视频帧总数(仅视频) |
|
unsigned long |
被丢弃的视频帧总数(仅视频),通常因为解码过慢或渲染不及时 |
|
double |
所有帧解码耗时总和(秒),可用于计算平均解码时间 |
|
unsigned long |
已解码的关键帧总数(仅视频) |
|
unsigned long |
已接收的完整视频帧总数(仅视频) |
|
double |
当前解码帧率(仅视频) |
|
double |
连续解码帧之间的延迟总和(秒) |
|
double |
连续解码帧之间延迟的平方和,用于计算抖动 |
|
unsigned long |
发送的 NACK 请求总数 |
|
unsigned long |
发送的 FIR(Full Intra Request)请求总数 |
|
unsigned long |
发送的 PLI(Picture Loss Indication)请求总数 |
|
unsigned long long |
已接收的 RTP 头部字节总数 |
|
DOMHighResTimeStamp |
最后一个 RTP 包到达的时间戳 |
|
double |
抖动缓冲区引入的总延迟(秒) |
|
unsigned long long |
从抖动缓冲区输出的样本/帧总数 |
使用示例:
stats.forEach(report => {
if (report.type === 'inbound-rtp' && report.kind === 'video') {
console.log(`Packets received: ${report.packetsReceived}`);
console.log(`Packets lost: ${report.packetsLost}`);
console.log(`Jitter: ${report.jitter} seconds`);
console.log(`Frames decoded: ${report.framesDecoded}`);
console.log(`Frames dropped: ${report.framesDropped}`);
console.log(`Key frames decoded: ${report.keyFramesDecoded}`);
// 计算平均解码时间
if (report.framesDecoded > 0) {
const avgDecodeTime = report.totalDecodeTime / report.framesDecoded;
console.log(`Avg decode time: ${avgDecodeTime * 1000} ms`);
}
// 计算平均抖动缓冲延迟
if (report.jitterBufferEmittedCount > 0) {
const avgJitterBufferDelay =
report.jitterBufferDelay / report.jitterBufferEmittedCount;
console.log(`Avg jitter buffer delay: ${avgJitterBufferDelay * 1000} ms`);
}
}
});
RTCOutboundRtpStreamStats
RTCOutboundRtpStreamStats 描述了发送端 RTP 流的统计信息,用于评估发送质量和编码性能。
字段 |
类型 |
说明 |
|---|---|---|
|
unsigned long |
已发送的 RTP 包总数 |
|
unsigned long long |
已发送的 RTP 负载字节总数 |
|
unsigned long |
已编码的视频帧总数(仅视频) |
|
unsigned long |
已发送的关键帧总数(仅视频) |
|
string |
当前质量受限原因: |
|
object |
各质量受限原因的累计持续时间(秒),键为原因字符串 |
|
unsigned long |
因质量受限而改变分辨率的次数 |
|
unsigned long |
重传的 RTP 包总数 |
|
unsigned long long |
重传的字节总数 |
|
unsigned long long |
已发送的 RTP 头部字节总数 |
|
unsigned long |
收到的 NACK 请求总数 |
|
unsigned long |
收到的 FIR 请求总数 |
|
unsigned long |
收到的 PLI 请求总数 |
|
double |
所有帧编码耗时总和(秒) |
|
double |
所有包从捕获到发送的延迟总和(秒) |
|
unsigned long |
最后编码帧的宽度 |
|
unsigned long |
最后编码帧的高度 |
|
double |
当前编码帧率 |
|
double |
编码器当前目标码率(bps) |
qualityLimitationReason 是一个非常有用的指标,它直接告诉我们当前编码质量受限的原因:
stats.forEach(report => {
if (report.type === 'outbound-rtp' && report.kind === 'video') {
console.log(`Quality limitation reason: ${report.qualityLimitationReason}`);
console.log(`Quality limitation durations:`,
report.qualityLimitationDurations);
console.log(`Resolution changes: ${report.qualityLimitationResolutionChanges}`);
console.log(`Target bitrate: ${report.targetBitrate} bps`);
console.log(`Retransmitted packets: ${report.retransmittedPacketsSent}`);
// 计算平均编码时间
if (report.framesEncoded > 0) {
const avgEncodeTime = report.totalEncodeTime / report.framesEncoded;
console.log(`Avg encode time: ${avgEncodeTime * 1000} ms`);
}
}
});
RTCRemoteInboundRtpStreamStats
RTCRemoteInboundRtpStreamStats 包含远端接收方通过 RTCP Receiver Report 反馈回来的统计信息,是评估端到端传输质量的关键数据源。
字段 |
类型 |
说明 |
|---|---|---|
|
double |
最近一次测量的往返时间(秒),基于 RTCP SR/RR 计算 |
|
double |
所有 RTT 测量值的总和(秒) |
|
unsigned long |
RTT 测量的次数,用于计算平均 RTT |
|
double |
自上次 RR 以来的丢包率(0.0 ~ 1.0) |
|
long |
远端报告的累计丢包数 |
|
double |
远端报告的到达间隔抖动(秒) |
|
string |
关联的本地 outbound-rtp 统计报告 ID |
stats.forEach(report => {
if (report.type === 'remote-inbound-rtp') {
console.log(`RTT: ${report.roundTripTime * 1000} ms`);
console.log(`Fraction lost: ${(report.fractionLost * 100).toFixed(2)}%`);
// 计算平均 RTT
if (report.roundTripTimeMeasurements > 0) {
const avgRtt = report.totalRoundTripTime
/ report.roundTripTimeMeasurements;
console.log(`Average RTT: ${avgRtt * 1000} ms`);
}
}
});
RTCIceCandidatePairStats
RTCIceCandidatePairStats 描述了 ICE 候选对的传输层统计信息,反映了网络连接的整体状况。
字段 |
类型 |
说明 |
|---|---|---|
|
double |
当前 ICE 连接的往返时间(秒),基于 STUN 连通性检查 |
|
double |
估算的可用出站带宽(bps),由拥塞控制算法提供 |
|
double |
估算的可用入站带宽(bps) |
|
unsigned long long |
该候选对上接收的总字节数 |
|
unsigned long long |
该候选对上发送的总字节数 |
|
double |
所有 STUN RTT 测量值的总和(秒) |
|
unsigned long long |
收到的 STUN 连通性检查响应总数 |
|
unsigned long long |
发送的 STUN 连通性检查请求总数 |
|
string |
候选对状态: |
|
boolean |
该候选对是否被提名使用 |
|
unsigned long long |
发送的 consent 请求总数(用于连接保活) |
stats.forEach(report => {
if (report.type === 'candidate-pair' && report.nominated) {
console.log(`Current RTT: ${report.currentRoundTripTime * 1000} ms`);
console.log(`Available outgoing bitrate: ${
(report.availableOutgoingBitrate / 1000000).toFixed(2)} Mbps`);
console.log(`Bytes sent: ${report.bytesSent}`);
console.log(`Bytes received: ${report.bytesReceived}`);
console.log(`State: ${report.state}`);
}
});
RTCMediaSourceStats
RTCMediaSourceStats 描述了媒体源(摄像头、麦克风或屏幕共享)的属性信息。
字段 |
类型 |
说明 |
|---|---|---|
|
unsigned long |
视频源的宽度(像素) |
|
unsigned long |
视频源的高度(像素) |
|
double |
视频源的当前帧率 |
|
unsigned long |
视频源产生的总帧数 |
字段 |
类型 |
说明 |
|---|---|---|
|
double |
音频电平(0.0 ~ 1.0),0 表示静音 |
|
double |
音频能量总和,用于计算平均音量 |
|
double |
音频采样的总持续时间(秒) |
|
double |
回声回损(dB),值越大回声消除效果越好 |
|
double |
回声回损增强(dB) |
常用计算公式
由于 getStats() 返回的大多数指标是累计值,我们需要通过两次采样的差值来计算区间内的实时指标。以下是常用的计算公式和对应的 JavaScript 实现。
丢包率计算
丢包率是衡量网络质量最直接的指标之一:
// 计算区间丢包率
function calculatePacketLossRate(currentStats, previousStats) {
const packetsLostDelta =
currentStats.packetsLost - previousStats.packetsLost;
const packetsReceivedDelta =
currentStats.packetsReceived - previousStats.packetsReceived;
const totalPacketsDelta = packetsReceivedDelta + packetsLostDelta;
if (totalPacketsDelta <= 0) return 0;
const lossRate = packetsLostDelta / totalPacketsDelta;
return Math.max(0, Math.min(1, lossRate)); // 限制在 0~1 之间
}
// 使用示例
const lossRate = calculatePacketLossRate(currentInbound, previousInbound);
console.log(`Packet loss rate: ${(lossRate * 100).toFixed(2)}%`);
注解
packetsLost 可能为负值(当出现重复包时),因此需要做边界检查。
码率计算
码率(Bitrate)反映了媒体流的数据传输速率:
// 计算区间码率 (bps)
function calculateBitrate(currentStats, previousStats) {
const bytesDelta = currentStats.bytesReceived !== undefined
? currentStats.bytesReceived - previousStats.bytesReceived
: currentStats.bytesSent - previousStats.bytesSent;
const timeDelta =
(currentStats.timestamp - previousStats.timestamp) / 1000; // 转为秒
if (timeDelta <= 0) return 0;
return (bytesDelta * 8) / timeDelta; // 转为 bits per second
}
// 使用示例
const bitrate = calculateBitrate(currentInbound, previousInbound);
console.log(`Bitrate: ${(bitrate / 1000).toFixed(0)} kbps`);
帧率计算
帧率反映了视频的流畅程度:
// 计算区间帧率
function calculateFrameRate(currentStats, previousStats) {
const framesDecodedDelta =
currentStats.framesDecoded - previousStats.framesDecoded;
const timeDelta =
(currentStats.timestamp - previousStats.timestamp) / 1000;
if (timeDelta <= 0) return 0;
return framesDecodedDelta / timeDelta;
}
// 发送端帧率
function calculateSendFrameRate(currentStats, previousStats) {
const framesEncodedDelta =
currentStats.framesEncoded - previousStats.framesEncoded;
const timeDelta =
(currentStats.timestamp - previousStats.timestamp) / 1000;
if (timeDelta <= 0) return 0;
return framesEncodedDelta / timeDelta;
}
抖动与 RTT
抖动(Jitter)和往返时间(RTT)可以直接从统计报告中读取,也可以计算平均值:
// 从 inbound-rtp 获取抖动
function getJitter(inboundStats) {
return inboundStats.jitter; // 单位: 秒
}
// 从 remote-inbound-rtp 获取 RTT
function getRoundTripTime(remoteInboundStats) {
return remoteInboundStats.roundTripTime; // 单位: 秒
}
// 计算平均 RTT
function getAverageRTT(remoteInboundStats) {
if (remoteInboundStats.roundTripTimeMeasurements > 0) {
return remoteInboundStats.totalRoundTripTime
/ remoteInboundStats.roundTripTimeMeasurements;
}
return 0;
}
// 从 candidate-pair 获取 ICE 层 RTT
function getIceRTT(candidatePairStats) {
return candidatePairStats.currentRoundTripTime; // 单位: 秒
}
综合采集示例
以下是一个完整的统计数据采集和计算示例:
class WebRTCMetricsCollector {
constructor(peerConnection, intervalMs = 1000) {
this.pc = peerConnection;
this.intervalMs = intervalMs;
this.previousStats = new Map();
this.timer = null;
}
start() {
this.timer = setInterval(() => this.collect(), this.intervalMs);
}
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
async collect() {
const stats = await this.pc.getStats();
const metrics = {};
stats.forEach(report => {
const prev = this.previousStats.get(report.id);
if (report.type === 'inbound-rtp' && prev) {
const timeDelta =
(report.timestamp - prev.timestamp) / 1000;
if (timeDelta > 0) {
const key = `${report.kind}_inbound`;
metrics[key] = {
bitrate: ((report.bytesReceived - prev.bytesReceived)
* 8) / timeDelta,
packetLossRate: this._calcLossRate(report, prev),
jitter: report.jitter * 1000, // ms
};
if (report.kind === 'video') {
metrics[key].frameRate =
(report.framesDecoded - prev.framesDecoded)
/ timeDelta;
metrics[key].framesDropped =
report.framesDropped - prev.framesDropped;
}
}
}
if (report.type === 'outbound-rtp' && prev) {
const timeDelta =
(report.timestamp - prev.timestamp) / 1000;
if (timeDelta > 0) {
const key = `${report.kind}_outbound`;
metrics[key] = {
bitrate: ((report.bytesSent - prev.bytesSent)
* 8) / timeDelta,
qualityLimitationReason:
report.qualityLimitationReason,
retransmittedPackets:
report.retransmittedPacketsSent
- (prev.retransmittedPacketsSent || 0),
};
if (report.kind === 'video') {
metrics[key].frameRate =
(report.framesEncoded - prev.framesEncoded)
/ timeDelta;
}
}
}
if (report.type === 'remote-inbound-rtp') {
metrics[`rtt_${report.kind}`] = {
roundTripTime: report.roundTripTime * 1000, // ms
fractionLost: report.fractionLost,
};
}
if (report.type === 'candidate-pair' && report.nominated) {
metrics.transport = {
currentRtt: report.currentRoundTripTime * 1000,
availableBitrate: report.availableOutgoingBitrate,
};
}
this.previousStats.set(report.id, report);
});
this.onMetrics(metrics);
}
_calcLossRate(current, previous) {
const lostDelta = current.packetsLost - previous.packetsLost;
const recvDelta =
current.packetsReceived - previous.packetsReceived;
const total = recvDelta + lostDelta;
if (total <= 0) return 0;
return Math.max(0, lostDelta / total);
}
onMetrics(metrics) {
// 子类或回调覆盖此方法
console.log('Metrics:', JSON.stringify(metrics, null, 2));
}
}
// 使用
const collector = new WebRTCMetricsCollector(peerConnection, 2000);
collector.onMetrics = (metrics) => {
// 发送到监控后端或更新 UI
sendToMonitoringBackend(metrics);
};
collector.start();
RTCP XR 扩展指标
Considerations for Selecting RTCP Extended Report (XR) Metrics for the WebRTC Statistics API
RTCP XR(Extended Reports)定义在 RFC 3611 中,提供了比标准 RTCP SR/RR 更丰富的质量度量指标。WebRTC 统计 API 中的部分指标来源于 RTCP XR。
RTCP XR 指标可分为三大类:
network impact metrics(网络影响指标)
application impact metrics(应用影响指标)
recovery metrics(恢复指标)
相关 RFC:
RFC 3611 - RTP Control Protocol Extended Reports (RTCP XR)
RFC 6958 - burst/gap loss metric reporting
RFC 7003 - burst/gap discard metric reporting
RFC 6776 - Measurement Identity and Information Reporting Using a SDES Item and an RTCP Extended Report (XR) Block
Burst/Gap Loss 指标
RFC 6958 定义了突发/间隙丢包指标,用于更精确地描述丢包模式:
Burst Duration: 突发丢包持续时间
Burst Density: 突发期间的丢包密度(丢包包数 / 总包数)
Gap Duration: 间隙(正常传输)持续时间
Gap Density: 间隙期间的丢包密度(通常很低)
突发丢包比均匀丢包对音视频质量的影响更大,因为:
突发丢包可能导致连续多帧损坏,FEC 难以恢复
突发丢包期间 NACK 重传可能来不及
视频可能需要请求关键帧,导致明显卡顿
VoIP 质量指标
RFC 3611 Section 4.7 定义了 VoIP Metrics Report Block,包含以下关键指标:
R-Factor(传输评定因子)
R-Factor 是 E-model(ITU-T G.107)的输出,综合考虑了延迟、丢包、编解码器等因素对语音质量的影响:
R = R0 - Is - Id - Ie_eff + A
其中:
R0 = 基础信噪比(通常为 93.2)
Is = 同时发生的损伤因子(如量化噪声)
Id = 延迟损伤因子
Ie_eff = 设备损伤因子(与编解码器和丢包相关)
A = 优势因子(用户对不同通信方式的容忍度补偿)
R-Factor 的取值范围和对应质量等级:
R-Factor |
质量等级 |
用户满意度 |
|---|---|---|
90 ~ 100 |
优秀 |
非常满意 |
80 ~ 90 |
良好 |
满意 |
70 ~ 80 |
一般 |
部分用户不满意 |
60 ~ 70 |
较差 |
多数用户不满意 |
< 60 |
很差 |
几乎所有用户不满意 |
MOS(平均意见分)估算
MOS 可以从 R-Factor 转换得到:
若 R < 0: MOS = 1.0
若 R > 100: MOS = 4.5
否则:
MOS = 1 + 0.035 * R + R * (R - 60) * (100 - R) * 7.0e-6
MOS 的取值范围为 1.0 ~ 4.5:
MOS |
质量等级 |
说明 |
|---|---|---|
4.3 ~ 5.0 |
优秀 |
高清语音质量 |
4.0 ~ 4.3 |
良好 |
传统电话质量 |
3.6 ~ 4.0 |
一般 |
可接受但有明显瑕疵 |
3.1 ~ 3.6 |
较差 |
勉强可用 |
< 3.1 |
很差 |
不可接受 |
延迟指标
RTCP XR 中与延迟相关的指标包括:
End System Delay: 端系统延迟,包括编码、缓冲、解码等处理时间
One-Way Delay: 单向延迟,通过 NTP 时间戳估算
Round Trip Delay: 往返延迟,通过 RTCP SR/RR 时间戳计算
端到端延迟的组成:
E2E Delay = Capture Delay
+ Encode Delay
+ Packetization Delay
+ Network Delay (one-way)
+ Jitter Buffer Delay
+ Decode Delay
+ Render Delay
监控与告警
质量阈值参考
以下是 WebRTC 通话质量监控的常用阈值参考:
指标 |
正常 |
警告 |
严重 |
单位 |
|---|---|---|---|---|
丢包率 (Packet Loss) |
< 2% |
2% ~ 5% |
> 5% |
% |
往返时间 (RTT) |
< 150ms |
150 ~ 300ms |
> 300ms |
ms |
抖动 (Jitter) |
< 30ms |
30 ~ 75ms |
> 75ms |
ms |
可用带宽 |
> 1 Mbps |
500k ~ 1M |
< 500 kbps |
bps |
视频帧率 |
> 24 fps |
15 ~ 24 fps |
< 15 fps |
fps |
视频分辨率 |
>= 720p |
360p ~ 720p |
< 360p |
px |
音频电平 |
> 0.01 |
0.001 ~ 0.01 |
< 0.001 |
level |
NACK 频率 |
< 10/s |
10 ~ 50/s |
> 50/s |
次/秒 |
PLI 频率 |
< 1/min |
1 ~ 5/min |
> 5/min |
次/分 |
解码帧丢弃率 |
< 1% |
1% ~ 5% |
> 5% |
% |
qualityLimitationReason |
none |
bandwidth |
cpu |
— |
质量问题特征识别
不同的质量问题有不同的指标特征组合,可以帮助快速定位根因:
网络拥塞:
丢包率上升
RTT 增大
availableOutgoingBitrate下降qualityLimitationReason为bandwidth分辨率和帧率可能自适应降低
CPU 过载:
qualityLimitationReason为cpu编码帧率下降
totalEncodeTime增长过快(平均编码时间增大)分辨率可能降低
丢包率和 RTT 正常
网络抖动:
jitter 值增大
抖动缓冲区延迟增大
可能伴随少量丢包
RTT 波动较大
单向丢包(上行或下行):
本地
inbound-rtp.packetsLost增大(下行丢包)remote-inbound-rtp.fractionLost增大(上行丢包)需要区分方向以定位问题
音频问题:
audioLevel持续为 0(麦克风静音或未采集)jitterBufferDelay过大(音频延迟高)丢包导致 PLC(Packet Loss Concealment)频繁触发
Dashboard 设计建议
一个有效的 WebRTC 质量监控 Dashboard 应包含以下面板:
实时概览面板: 当前通话数、活跃用户数、整体质量评分
网络质量面板: RTT 分布图、丢包率趋势、抖动趋势
媒体质量面板: 码率趋势、帧率趋势、分辨率变化
编码质量面板: qualityLimitationReason 分布、编码时间趋势
告警面板: 实时告警列表、告警趋势、Top-N 问题用户
用户体验面板: MOS 估算分布、通话质量评分分布
工具与实践
chrome://webrtc-internals
Chrome 浏览器内置了强大的 WebRTC 调试工具,可通过地址栏输入 chrome://webrtc-internals 访问。
主要功能:
实时统计图表: 自动绘制所有 WebRTC 统计指标的时间序列图
事件日志: 记录 PeerConnection 的所有 API 调用和事件
SDP 查看: 显示 Offer/Answer SDP 的完整内容
ICE 候选信息: 展示所有 ICE 候选及其状态
导出功能: 可将所有数据导出为 JSON 文件供离线分析
使用步骤:
打开
chrome://webrtc-internals在另一个标签页中开始 WebRTC 通话
回到 webrtc-internals 页面,可以看到新创建的 PeerConnection
点击展开查看各项统计数据和图表
通话结束后可点击 "Create Dump" 导出数据
注解
Firefox 也提供了类似的工具: about:webrtc。
getStats() 轮询模式
在生产环境中,通常需要定期轮询 getStats() 来持续监控通话质量:
class StatsPoller {
constructor(pc, onStats, intervalMs = 2000) {
this.pc = pc;
this.onStats = onStats;
this.intervalMs = intervalMs;
this.prevReports = new Map();
this.running = false;
}
start() {
this.running = true;
this._poll();
}
stop() {
this.running = false;
}
async _poll() {
if (!this.running) return;
try {
const stats = await this.pc.getStats();
const processed = this._processStats(stats);
this.onStats(processed);
} catch (e) {
console.error('Stats polling error:', e);
}
setTimeout(() => this._poll(), this.intervalMs);
}
_processStats(stats) {
const result = {
timestamp: Date.now(),
audio: { inbound: null, outbound: null },
video: { inbound: null, outbound: null },
transport: null,
};
stats.forEach(report => {
const prev = this.prevReports.get(report.id);
const timeDelta = prev
? (report.timestamp - prev.timestamp) / 1000
: 0;
if (report.type === 'inbound-rtp' && prev && timeDelta > 0) {
result[report.kind].inbound = {
bitrate: ((report.bytesReceived
- prev.bytesReceived) * 8) / timeDelta,
packetsLost: report.packetsLost - prev.packetsLost,
jitter: report.jitter * 1000,
...(report.kind === 'video' && {
frameRate: (report.framesDecoded
- prev.framesDecoded) / timeDelta,
resolution: `${report.frameWidth}x${report.frameHeight}`,
}),
};
}
if (report.type === 'outbound-rtp' && prev && timeDelta > 0) {
result[report.kind].outbound = {
bitrate: ((report.bytesSent
- prev.bytesSent) * 8) / timeDelta,
...(report.kind === 'video' && {
frameRate: (report.framesEncoded
- prev.framesEncoded) / timeDelta,
qualityLimitation: report.qualityLimitationReason,
}),
};
}
if (report.type === 'candidate-pair' && report.nominated) {
result.transport = {
rtt: report.currentRoundTripTime * 1000,
availableBitrate: report.availableOutgoingBitrate,
};
}
this.prevReports.set(report.id, report);
});
return result;
}
}
第三方监控工具
除了自建监控系统,还可以使用以下第三方 WebRTC 监控服务:
callstats.io (现为 Amazon Chime SDK 的一部分): 提供端到端的 WebRTC 质量监控和分析
testRTC: 提供 WebRTC 应用的自动化测试和监控
Location Insights by Location Labs: 网络质量分析
Srlocation: 实时通信质量监控平台
WebRTC-Experiment: 开源的 WebRTC 测试工具集
自建监控系统的基本架构:
┌──────────┐ getStats() ┌──────────────┐
│ Browser │ ──────────────> │ Stats │
│ Client │ │ Collector │
└──────────┘ └──────┬───────┘
│ HTTP/WebSocket
▼
┌──────────────┐
│ Monitoring │
│ Backend │
└──────┬───────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ Time- │ │ Alert │ │ Dashboard │
│ Series DB │ │ Engine │ │ (Grafana) │
└───────────┘ └───────────┘ └───────────┘