跳到主要内容

getStats全字段速查

RTCPeerConnection.getStats() 是 WebRTC 唯一的官方观测面。线上排障、A/B 实验、SLA 上报全靠它。但 W3C 标准里有 20+ 种 stats type、200+ 字段,没人能背全。本文做两件事:

  1. 全字段表:按 type 分组,每个字段标注含义和"排障用什么"。
  2. 场景对照:卡顿、模糊、回声、断连,分别看哪几个字段。

字段来源 W3C webrtc-stats(Editor's Draft 2026-06)。Chrome 实现与标准基本对齐,少数 goog* 字段已废弃,本文不列。

1. getStats 心智模型

调用方式

const report = await pc.getStats();
for (const stat of report.values()) {
console.log(stat.type, stat.id, stat);
}

采样频率:1~2 秒一次足够。100ms 高频会显著拉 CPU,且很多字段就是累计型,差分计算没意义。

2. 全字段表

2.1 inbound-rtp:本端接收

字段类型/单位含义排障用途
kind"audio"/"video"媒体类型区分音视频
ssrcuint接收的 SSRC关联具体流
midstring对应 m-line找对应 transceiver
ridstringSimulcast 层知道收的是哪一档
codecIdstring引用 codec stat反查编码器
packetsReceiveduint累计收到 RTP 包数流是否在动
packetsLostint累计丢包数(可负,表示重复包)丢包率分子
packetsDiscardeduint抖动缓冲扔掉的包抖动是否太大
packetsRepaireduint通过 FEC 恢复的包FEC 是否有用
bytesReceiveduint累计字节下行码率 = Δbytes/Δt
headerBytesReceiveduintRTP/扩展头字节overhead 分析
jitters抖动估计(RFC 3550)< 30ms 健康
jitterBufferDelays抖动缓冲累计延迟平均缓冲 = jitterBufferDelay / jitterBufferEmittedCount
jitterBufferEmittedCountuint抖动缓冲送出的样本/帧数分母
jitterBufferTargetDelays目标缓冲深度自适应抖动
jitterBufferMinimumDelays最小缓冲深度应用层设置
nackCountuint接收端发出的 NACK 数丢包重传压力
pliCountuint接收端发出的 PLI 数(视频)关键帧请求频率
firCountuint接收端发出的 FIR 数同上,FIR 罕用
framesReceiveduint收到的帧数视频
framesDecodeduint解码成功帧数解码器健康
framesDroppeduint渲染前丢的帧渲染压力
framesPerSeconddouble当前解码帧率< 15 体感差
keyFramesDecodeduint解码的关键帧数关键帧间隔
frameWidth / frameHeightuint最近帧的分辨率实际接收的清晰度
qpSumuintQP 累加和(视频)平均 QP = qpSum/framesDecoded
decodeTimeSecondss累计解码耗时解码瓶颈
totalDecodeTimes同上兼容字段
totalProcessingDelays接收到送出累计延迟端到端延迟一部分
totalAssemblyTimes包组装成帧的耗时高表示丢包重传多
framesAssembledFromMultiplePacketsuint多包组成的帧数关键帧/大帧
freezeCountuint卡顿次数(视频)体验直接指标
totalFreezesDurations累计卡顿时长体验
pauseCountuint主动暂停次数用户操作
totalPausesDurations累计暂停时长与卡顿区分
concealedSamplesuintPLC 补帧样本数(音频)音频丢包恢复量
silentConcealedSamplesuint用静音补的样本数PLC 失败兜底
concealmentEventsuint补帧事件数离散度
insertedSamplesForDecelerationuint抖动缓冲加样本应对网络变快
removedSamplesForAccelerationuint抖动缓冲减样本应对网络变慢
audioLevel0~1当前音量麦克风是否在采
totalAudioEnergyfloat累计能量RMS = √(energy/duration)
totalSamplesDurations总样本时长配合 audioEnergy
fecPacketsReceiveduint收到 FEC 包数FEC 是否启用
fecPacketsDiscardeduintFEC 包但没用上的数越多说明丢包少
lastPacketReceivedTimestampDOMHighResTimeStamp最后收包时间流是否停了
estimatedPlayoutTimestampNTP预计播放时间戳与发送端时序对齐

2.2 outbound-rtp:本端发送

字段类型/单位含义排障用途
kind"audio"/"video"
ssrcuint发出的 SSRC
midstring
ridstringSimulcast 层名哪一档
codecIdstring编码器 stat 引用
mediaSourceIdstring引用 media-source同源关联
packetsSentuint累计发包数上行流是否在动
bytesSentuint累计发送字节上行码率
headerBytesSentuintRTP 头字节overhead
retransmittedPacketsSentuintRTX 重传包数NACK 应答数
retransmittedBytesSentuintRTX 字节重传占带宽
nackCountuint收到的 NACK 数重传压力
pliCountuint收到的 PLI 数关键帧请求压力
firCountuint收到的 FIR 数
framesSentuint发出的帧数
framesEncodeduint编码完成的帧数编码器吞吐
keyFramesEncodeduint编码的关键帧数IDR 频率
framesPerSeconddouble编码帧率编码侧限制
frameWidth / frameHeightuint当前分辨率
qpSumuintQP 累加平均 QP = qpSum/framesEncoded
targetBitrateuint bps编码器目标码率GCC 给的指令
totalEncodeTimes累计编码耗时编码 CPU 瓶颈
totalPacketSendDelays入 Pacer 到出网累计延迟Pacer 压力
encoderImplementationstring"OpenH264" / "ExternalEncoder" 等硬编/软编
powerEfficientEncoderbool是否使用低功耗硬编移动端电池
qualityLimitationReason"none"/"cpu"/"bandwidth"/"other"受限原因关键诊断字段
qualityLimitationDurationsdict<reason, s>各原因累计时长拍 SLA
qualityLimitationResolutionChangesuint因受限切分辨率次数稳定性
scalabilityModestring"L1T3"/"L3T3_KEY"SVC 是否生效
fecPacketsSentuintFEC 包数
activebool该 encoding 是否激活Simulcast 层切
rtxSsrcuint对应 RTX 流 SSRC

2.3 remote-inbound-rtp:对端报告的接收情况(你的发送视角)

字段含义用途
packetsReceived对端收到的包数
packetsLost对端丢包数端到端丢包率
fractionLost0~1对端瞬时丢包率(RTCP RR 报告)
jitters对端测得的抖动
roundTripTimes该流的 RTT 估计
totalRoundTripTimes累计 RTT
roundTripTimeMeasurementsuintRTT 样本数
reportsReceiveduint收到的 RR 数

2.4 remote-outbound-rtp:对端报告的发送情况

字段含义
packetsSent对端发出包数
bytesSent对端发出字节
remoteTimestamp远端时间戳
reportsSent发出的 SR 数
totalRoundTripTimeRTT 累计

2.5 media-source:采集源(仅发送侧)

字段含义用途
kindaudio/video
trackIdentifierstringtrack id
width / height原始采集分辨率采集是否变化
framesPerSecond原始帧率摄像头能力
frames累计采集帧数
audioLevel0~1麦克风电平
totalAudioEnergyfloat
echoReturnLossdBAEC 效果
echoReturnLossEnhancementdBAEC 残留压制
droppedSamplesDurations采集丢样时长
droppedSamplesEventsuint采集丢样事件数

2.6 media-playout:渲染侧(接收侧)

字段含义
kind"audio"
synthesizedSamplesDurationPLC 合成时长
synthesizedSamplesEventsPLC 事件数
totalSamplesDuration总播放时长
totalPlayoutDelay累计播放延迟
totalSamplesCount总样本数

2.7 candidate-pair:当前选用的 ICE 路径

字段含义用途
state"succeeded" 等是否在用
nominatedbool是否被提名
localCandidateId / remoteCandidateIdstring关联候选
priorityuintICE 优先级
bytesSent / bytesReceiveduint路径字节数
packetsSent / packetsReceiveduint路径包数
currentRoundTripTimes当前 RTT
availableOutgoingBitratebpsGCC 估的可用上行
availableIncomingBitratebps下行估计(不一定准)
requestsSent / requestsReceiveduintSTUN binding 请求
consentRequestsSentuintICE consent 请求
lastPacketSentTimestampts最后发包时间
totalRoundTripTimesRTT 累计

2.8 local-candidate / remote-candidate

字段含义
candidateType"host"/"srflx"/"prflx"/"relay"
protocol"udp"/"tcp"
addressIP(隐私模式可能 mDNS)
portuint
priorityuint
url触发该候选的 STUN/TURN URL
relayProtocolTURN 中继协议
networkType"wifi"/"cellular"/"ethernet"(部分浏览器)

2.9 transport

字段含义
bytesSent / bytesReceiveduint
packetsSent / packetsReceiveduint
dtlsState"new"/"connecting"/"connected"/"closed"/"failed"
iceStateRTCIceTransportState
selectedCandidatePairIdstring
localCertificateId / remoteCertificateIdstring
tlsVersionhex
dtlsCipherstring
srtpCipherstring
iceRole"controlling"/"controlled"
iceLocalUsernameFragmentstring

2.10 codec

字段含义
payloadTypeRTP PT
mimeType"video/VP8" 等
clockRate90000/48000
channels音频
sdpFmtpLinefmtp 内容

2.11 certificate

字段含义
fingerprintSHA-256 等
fingerprintAlgorithmstring
base64Certificatestring
issuerCertificateIdstring

2.12 data-channel

字段含义
labelstring
protocolstring
state"open" 等
messagesSent / messagesReceiveduint
bytesSent / bytesReceiveduint
dataChannelIdentifieruint

2.13 peer-connection

字段含义
dataChannelsOpeneduint
dataChannelsCloseduint

3. 排障对照表

按"用户反馈的现象 → 看哪些字段"。

3.1 视频卡顿

字段解读
inbound-rtp.freezeCount直接卡顿次数
inbound-rtp.totalFreezesDuration累计冻结秒数
inbound-rtp.framesPerSecond < 15帧率掉
inbound-rtp.packetsLost 增长快接收侧丢包
inbound-rtp.jitterBufferDelay/Emitted 飙升抖动太大
outbound-rtp.qualityLimitationReason=bandwidth上行受限
outbound-rtp.framesPerSecond 与设定值差距大编码器跟不上
outbound-rtp.qualityLimitationReason=cpuCPU 瓶颈
链路candidate-pair.currentRoundTripTime > 0.3s

3.2 画面模糊

字段解读
outbound-rtp.frameWidth/Heightmedia-source.width/height编码侧降分辨率
outbound-rtp.qualityLimitationReason=bandwidth带宽逼降
outbound-rtp.qpSum / framesEncoded 高(> 35)QP 高 → 模糊
inbound-rtp.frameWidth/Height接收侧实际分辨率
Simulcast: outbound-rtp[rid=f].active=false高层关了

3.3 回声

字段解读
media-source.echoReturnLoss < 10dBAEC 几乎没用
media-source.echoReturnLossEnhancementAEC NLP 不够
inbound-rtp.audioLevel 周期性变化远端可能听到自己
media-source.totalAudioEnergy 高 + 自己说话停止设备拾到了播放声

3.4 声音断续

字段解读
inbound-rtp.concealedSamples 增长快PLC 频繁
inbound-rtp.concealmentEvents离散事件
inbound-rtp.jitter > 30ms抖动
media-playout.synthesizedSamplesDuration渲染侧合成多
inbound-rtp.packetsLost丢包

3.5 RTT 高 / 互动延迟大

字段解读
candidate-pair.currentRoundTripTime当前 RTT
candidate-pair.localCandidateId → 看 candidateType是否走了 relay
remote-inbound-rtp.roundTripTime媒体流 RTT
inbound-rtp.jitterBufferDelay/Emitted抖动缓冲贡献了多少延迟

3.6 频繁断流

字段解读
transport.dtlsStateDTLS 状态
candidate-pair.stateICE 路径状态
candidate-pair.requestsSent 是否在涨ICE keepalive
candidate-pair.consentRequestsSentconsent freshness

4. freezeCount 是怎么算的

W3C 定义:视频帧间隔超过 max(3 × avgFrameInterval, avgFrameInterval + 150ms) 计一次 freeze。

function detectFreeze(framesReceivedTimes) {
const intervals = [];
for (let i = 1; i < framesReceivedTimes.length; i++) {
intervals.push(framesReceivedTimes[i] - framesReceivedTimes[i-1]);
}
const avg = intervals.reduce((a,b) => a+b, 0) / intervals.length;
const threshold = Math.max(3 * avg, avg + 0.15);
let count = 0;
let totalDuration = 0;
for (const iv of intervals) {
if (iv > threshold) {
count++;
totalDuration += iv;
}
}
return { freezeCount: count, totalFreezesDuration: totalDuration };
}

实务上直接读 inbound-rtp.freezeCounttotalFreezesDuration,浏览器算好的更准。

告警阈值:单分钟 freezeCount > 1 即标记为体验差。

5. 推荐上报模型

5.1 采集

class StatsReporter {
constructor(pc, opts = {}) {
this.pc = pc;
this.intervalMs = opts.intervalMs ?? 2000;
this.prev = new Map();
this.timer = null;
this.onSample = opts.onSample ?? (() => {});
}

start() {
this.timer = setInterval(() => this.collect(), this.intervalMs);
}

stop() {
clearInterval(this.timer);
}

async collect() {
const report = await this.pc.getStats();
const sample = { ts: Date.now() };

for (const r of report.values()) {
switch (r.type) {
case 'inbound-rtp':
this.fillInbound(sample, r);
break;
case 'outbound-rtp':
this.fillOutbound(sample, r);
break;
case 'remote-inbound-rtp':
this.fillRemoteInbound(sample, r);
break;
case 'candidate-pair':
if (r.nominated) this.fillCandidatePair(sample, r);
break;
case 'media-source':
this.fillMediaSource(sample, r);
break;
}
}
this.onSample(this.diff(sample));
}

fillInbound(s, r) {
const k = `${r.kind}_in`;
s[`${k}_packetsLost`] = r.packetsLost;
s[`${k}_packetsReceived`] = r.packetsReceived;
s[`${k}_bytesReceived`] = r.bytesReceived;
s[`${k}_jitter`] = r.jitter;
s[`${k}_freezeCount`] = r.freezeCount;
s[`${k}_freezeDur`] = r.totalFreezesDuration;
s[`${k}_framesDecoded`] = r.framesDecoded;
s[`${k}_fps`] = r.framesPerSecond;
s[`${k}_width`] = r.frameWidth;
s[`${k}_height`] = r.frameHeight;
s[`${k}_concealed`] = r.concealedSamples;
}

fillOutbound(s, r) {
const k = `${r.kind}_out_${r.rid ?? '0'}`;
s[`${k}_bytesSent`] = r.bytesSent;
s[`${k}_packetsSent`] = r.packetsSent;
s[`${k}_retxBytes`] = r.retransmittedBytesSent;
s[`${k}_framesEncoded`] = r.framesEncoded;
s[`${k}_fps`] = r.framesPerSecond;
s[`${k}_qpSum`] = r.qpSum;
s[`${k}_targetBitrate`] = r.targetBitrate;
s[`${k}_limit`] = r.qualityLimitationReason;
s[`${k}_active`] = r.active;
s[`${k}_width`] = r.frameWidth;
s[`${k}_height`] = r.frameHeight;
}

fillRemoteInbound(s, r) {
const k = `${r.kind}_remoteIn`;
s[`${k}_fractionLost`] = r.fractionLost;
s[`${k}_rtt`] = r.roundTripTime;
}

fillCandidatePair(s, r) {
s.pair_rtt = r.currentRoundTripTime;
s.pair_availOut = r.availableOutgoingBitrate;
s.pair_availIn = r.availableIncomingBitrate;
s.pair_bytesSent = r.bytesSent;
s.pair_bytesReceived = r.bytesReceived;
}

fillMediaSource(s, r) {
if (r.kind === 'audio') {
s.mic_audioLevel = r.audioLevel;
s.mic_echoReturnLoss = r.echoReturnLoss;
} else {
s.cam_width = r.width;
s.cam_height = r.height;
s.cam_fps = r.framesPerSecond;
}
}

diff(curr) {
// 把累计型字段转成"过去 intervalMs 内的增量"
const out = { ts: curr.ts, intervalMs: this.intervalMs };
const accumulative = [
'video_in_packetsLost', 'video_in_packetsReceived', 'video_in_bytesReceived',
'video_in_freezeCount',
'audio_in_packetsLost', 'audio_in_packetsReceived', 'audio_in_concealed',
'pair_bytesSent', 'pair_bytesReceived',
];
for (const key of accumulative) {
const prev = this.prev.get(key) ?? curr[key];
out[key + '_delta'] = curr[key] - prev;
this.prev.set(key, curr[key]);
}
// 其它字段直接拷
for (const k of Object.keys(curr)) {
if (!accumulative.includes(k)) out[k] = curr[k];
}
return out;
}
}

const reporter = new StatsReporter(pc, {
intervalMs: 5000,
onSample: (s) => {
navigator.sendBeacon('/api/webrtc-stats', JSON.stringify(s));
},
});
reporter.start();

5.2 上报维度建议

维度用途
user_id / room_id / publisher_id关联用户/房间
device / OS / browser version设备分群
network_type (wifi/4g/5g)网络分群
sfu_node服务端归因
client_version版本回滚

5.3 关键告警指标

指标计算阈值
视频丢包率ΔpacketsLost / (ΔpacketsLost + Δpacketsreceived)> 5% 红色
卡顿率Δfreezeduration / intervalMs> 5% 红色
RTTpair_rtt> 0.3s 黄 / > 0.5s 红
平均 QPqpSum / framesEncoded(按差分)> 35 黄
上行受限时长比qualityLimitationDurations.bandwidth / total> 30% 黄

6. 完整诊断函数

async function diagnose(pc) {
const report = await pc.getStats();
const issues = [];
let inboundVideo, outboundVideo, pair, remoteInbound;

for (const r of report.values()) {
if (r.type === 'inbound-rtp' && r.kind === 'video') inboundVideo = r;
if (r.type === 'outbound-rtp' && r.kind === 'video') outboundVideo = r;
if (r.type === 'remote-inbound-rtp' && r.kind === 'video') remoteInbound = r;
if (r.type === 'candidate-pair' && r.nominated) pair = r;
}

if (pair?.currentRoundTripTime > 0.3) {
issues.push(`RTT 高: ${(pair.currentRoundTripTime*1000).toFixed(0)}ms`);
}
if (inboundVideo?.framesPerSecond < 15) {
issues.push(`接收帧率低: ${inboundVideo.framesPerSecond}`);
}
if (inboundVideo?.freezeCount > 0) {
issues.push(`卡顿次数: ${inboundVideo.freezeCount}`);
}
if (outboundVideo?.qualityLimitationReason && outboundVideo.qualityLimitationReason !== 'none') {
issues.push(`编码受限: ${outboundVideo.qualityLimitationReason}`);
}
if (remoteInbound?.fractionLost > 0.05) {
issues.push(`远端丢包率: ${(remoteInbound.fractionLost*100).toFixed(1)}%`);
}
if (pair?.availableOutgoingBitrate < (outboundVideo?.targetBitrate ?? 0) * 1.1) {
issues.push(`上行带宽紧张: ${pair.availableOutgoingBitrate}`);
}

return issues;
}

setInterval(async () => {
const issues = await diagnose(pc);
if (issues.length) console.warn('[WebRTC]', issues.join('; '));
}, 5000);

7. chrome://webrtc-internals 速通

区块看什么
Create Dump拷整页 JSON,用于离线分析
RTCPeerConnection选具体连接
Stats Graphs自动绘制时间序列,重点看 bytesReceivedframesPerSecondpacketsLostavailableOutgoingBitrate
GetUserMedia Requests媒体约束历史
ICE candidate grid看选用哪一对 candidate

Firefox 对应 about:webrtc,字段命名略不同但语义一致。

8. 反模式

反模式后果正确做法
100ms 频率 getStats主线程压力大,触发自身卡顿1~5s 一次
上报全部字段流量爆,存储成本高只取告警相关 20 个
packetsLost / packetsReceived 算丢包率公式错(应是 lost / (lost + received)注意分母
goog-* 字段已废弃,未来移除用标准字段
不做差分直接上报累计值后端拉差分压力大客户端差分
availableIncomingBitrate 当下行带宽真值该字段在多数浏览器上不准用接收侧 bytesReceived 差分
connectionState 替代 iceState含义不同关联看,不要互换
忽略 qualityLimitationReason不知道为何降清晰度必上报
不区分 Simulcast 各 rid 的 outbound-rtp数据混在一起按 rid 分维度
没考虑跨连接重启 stats 重置差分变成负数连接 id 变化时重置 prev

9. 权威资料