Monitors & derived metrics
On this page
getStats() returns a flat map of records joined by string ids, containing monotonically
increasing counters. The monitor turns that into a navigable object graph with per-interval
metrics already computed.
The monitor hierarchy
ClientMonitor
├── PeerConnectionMonitor[]
│ ├── InboundRtpMonitor[]
│ ├── OutboundRtpMonitor[]
│ ├── RemoteInboundRtpMonitor[]
│ ├── RemoteOutboundRtpMonitor[]
│ ├── MediaSourceMonitor[]
│ ├── MediaPlayoutMonitor[]
│ ├── CodecMonitor[]
│ ├── IceTransportMonitor[]
│ ├── IceCandidateMonitor[]
│ ├── IceCandidatePairMonitor[]
│ ├── CertificateMonitor[]
│ └── DataChannelMonitor[]
├── InboundTrackMonitor[]
└── OutboundTrackMonitor[]Track monitors sit at the client level rather than under a peer connection, because that is how applications think about them — “Alice’s camera” is one thing, even when it is carried by three simulcast RTP streams.
Navigating
monitor.peerConnections; // PeerConnectionMonitor[]
monitor.tracks; // (InboundTrackMonitor | OutboundTrackMonitor)[]
monitor.getTrackMonitor(track.id); // by MediaStreamTrack id
track.getInboundRtp(); // inbound track → its RTP monitor
track.getOutboundRtps(); // outbound track → all simulcast layers
track.getHighestLayer(); // outbound track → the top layer
outboundRtp.getRemoteInboundRtp(); // what the far end reports about this stream
outboundRtp.getMediaSource(); // the local capture feeding it
pcMonitor.mappedInboundRtpMonitors;
pcMonitor.mappedOutboundRtpMonitors;
pcMonitor.mappedIceCandidatePairMonitors;
pcMonitor.mappedDataChannelMonitors;Client-level metrics
Available directly on ClientMonitor, aggregated across every peer connection:
// Bitrates (bits per second)
monitor.sendingAudioBitrate;
monitor.sendingVideoBitrate;
monitor.receivingAudioBitrate;
monitor.receivingVideoBitrate;
// Network capacity, from the selected candidate pairs
monitor.totalAvailableIncomingBitrate;
monitor.totalAvailableOutgoingBitrate;
// Quality
monitor.avgRttInSec; // average RTT across connections
monitor.score; // 0.0–5.0
monitor.scoreReasons;
// Self-observation
monitor.durationOfCollectingStatsInMs; // how long the last getStats() pass took
durationOfCollectingStatsInMs is a CPU signal
getStats() runs on the main thread. When it starts taking seconds instead of milliseconds, the
tab is saturated — which is exactly why CpuPerformanceDetector watches this number alongside
encoder and decoder signals.
Peer connection metrics
// Bitrates by media type
pc.sendingAudioBitrate;
pc.sendingVideoBitrate;
pc.receivingAudioBitrate;
pc.receivingVideoBitrate;
// Loss
pc.outboundFractionLost;
pc.inboundFractionalLost;
// Per-interval deltas
pc.deltaInboundPacketsLost;
pc.deltaInboundPacketsReceived;
pc.deltaOutboundPacketsSent;
pc.deltaAudioBytesSent;
pc.deltaVideoBytesSent;
pc.deltaDataChannelBytesSent;
// RTT and timing
pc.avgRttInSec;
pc.ewmaRttInSec; // smoothed
pc.connectingStartedAt;
pc.connectedAt;
// Topology
pc.usingTURN; // relayed
pc.usingTCP; // TCP transport rather than UDP
pc.iceState;
// Historical peaks (never reset)
pc.highestSeenSendingBitrate;
pc.highestSeenReceivingBitrate;
pc.highestSeenAvailableIncomingBitrate;
pc.highestSeenAvailableOutgoingBitrate;Why the peaks matter
highestSeenAvailableOutgoingBitrate gives you a per-session ceiling to compare the current
estimate against. “Available bandwidth is 400 kbps” means nothing on its own; “available bandwidth
is 400 kbps having peaked at 3 Mbps ten seconds ago” is a congestion event.
Track metrics
Inbound
track.track; // the underlying MediaStreamTrack
track.kind; // 'audio' | 'video'
track.direction; // 'inbound'
track.bitrate; // receiving bitrate (bps)
track.jitter; // seconds
track.fractionLost; // 0..1
track.dtxMode; // discontinuous transmission
track.score; // 0.0–5.0
track.scoreReasons;
track.detectors; // attached detectors
Outbound
track.track; // the underlying MediaStreamTrack
track.bitrate; // aggregate across simulcast layers
track.sendingPacketRate;
track.remoteReceivedPacketRate; // from RTCP receiver reports
track.jitter; // as reported by the remote peer
track.fractionLost; // as reported by the remote peer
track.score;
track.getHighestLayer();
track.getOutboundRtps();RTP-level metrics
InboundRtpMonitor
// Rates
rtp.bitrate;
rtp.packetRate;
rtp.fractionLost;
rtp.bitPerPixel; // video coding efficiency
// Video
rtp.avgFramesPerSec;
rtp.ewmaFps; // smoothed
rtp.fpsVolatility; // lower is more stable
rtp.isFreezed;
// Audio
rtp.receivingAudioSamples;
rtp.desync;
// Per-interval deltas
rtp.deltaPacketsLost;
rtp.deltaPacketsReceived;
rtp.deltaBytesReceived;
rtp.deltaJitterBufferDelay;
rtp.deltaFramesDecoded;
rtp.deltaFramesReceived;
rtp.deltaFramesRendered;
rtp.deltaCorruptionProbability;
rtp.deltaTime; // ms covered by these deltas
OutboundRtpMonitor
rtp.bitrate; // total, including headers and retransmissions
rtp.payloadBitrate; // payload only
rtp.packetRate;
rtp.bitPerPixel;
rtp.deltaPacketsSent;
rtp.deltaBytesSent;bitrate vs payloadBitrate
The gap between them is your overhead: RTP headers plus retransmissions. A widening gap under
constant payloadBitrate means loss is forcing retransmissions — visible before quality drops.
Remote RTP monitors
remoteInbound.packetRate; // what the far end is receiving
remoteInbound.deltaPacketsLost; // what the far end lost this interval
remoteOutbound.bitrate; // what the far end says it is sending
ICE and transport metrics
iceTransport.sendingBitrate;
iceTransport.receivingBitrate;
iceTransport.deltaPacketsSent;
iceTransport.deltaPacketsReceived;
iceTransport.deltaBytesSent;
iceTransport.deltaBytesReceived;
candidatePair.availableIncomingBitrate; // the browser's bandwidth estimate
candidatePair.availableOutgoingBitrate;
iceTransport.selectedCandidatePair;Data channel and playout metrics
dataChannel.deltaBytesSent;
dataChannel.deltaBytesReceived;
mediaPlayout.deltaSynthesizedSamplesDuration; // audio the browser had to invent
mediaPlayout.deltaSamplesDuration;The ratio of those two is the cleanest available audio-gap indicator: if 8 % of playout duration was synthesized, the user heard artefacts regardless of what the packet-loss number says.
Reading metrics in practice
monitor.on("stats-collected", () => {
// Client-level summary.
const totalSending = monitor.sendingAudioBitrate + monitor.sendingVideoBitrate;
for (const pc of monitor.peerConnections) {
console.log(`PC ${pc.peerConnectionId}`, {
rttMs: (pc.avgRttInSec ?? 0) * 1000,
turn: pc.usingTURN,
headroom: pc.highestSeenAvailableOutgoingBitrate - pc.sendingVideoBitrate,
});
}
// Per-track video quality.
for (const track of monitor.tracks) {
if (track.kind !== "video" || track.direction !== "inbound") continue;
const rtp = track.getInboundRtp();
console.log(track.track.id, {
bitrate: rtp?.bitrate,
fps: rtp?.ewmaFps,
volatility: rtp?.fpsVolatility,
loss: rtp?.fractionLost,
});
}
});attachments and appData on monitors
Every monitor in the hierarchy carries both bags. attachments ships with the sample;
appData never leaves the browser.
// Reaches your backend — this is how a server knows what a track is.
trackMonitor.attachments = {
roomId: "room-456",
participantRole: "presenter",
mediaType: "screen-share",
producerId: producer.id,
};
// Stays local.
trackMonitor.appData = {
videoElementId: "tile-7",
localProcessingFlags: { enableBlur: true },
};See ClientSample for how attachments appear on the wire, and
Remote track resolution for what a server can do with them.