OBSERVERTC / Client Monitor
Metrics & missing values
Distinguish browser measurements, adapted stats and calculated values.
Metrics help you explain what a client is sending, receiving, and playing. Start with the overview measurements, then follow the affected track or stream when something looks wrong. A missing measurement means the browser or current interval did not supply enough evidence; it does not mean the value is zero.
When interpreting a value, distinguish its origin: browser stat, adapted/inferred stat, derived measurement, declared context, detector/score result, identity/lifecycle metadata. Application-defined measurements belong to application extensions.
Monitor ownership
The authoritative raw-stat families and exact field sets are in the schema inventory. Their monitor ownership is:
| Monitor | Browser report / content | Wire location under a PeerConnectionSample |
|---|---|---|
InboundRtpMonitor | inbound-rtp: received/lost packets, jitter, received bytes, frame decode/render/freeze counters, jitter buffer, concealment, FEC/RTX, corruption | inboundRtps[] |
OutboundRtpMonitor | outbound-rtp: sent packets/bytes, encoded frames, target bitrate, encode cost, QP, limitation durations, retransmission and feedback | outboundRtps[] |
RemoteInboundRtpMonitor | remote-inbound-rtp: receiver-reported loss/jitter/RTT for local outbound stream | remoteInboundRtps[] |
RemoteOutboundRtpMonitor | remote-outbound-rtp: sender reports for a local inbound stream | remoteOutboundRtps[] |
CodecMonitor | codec identifiers, MIME type, clock rate, channels, FMTP, transport link | codecs[] |
MediaSourceMonitor | media-source capture geometry/fps/frames and audio energy/duration | mediaSources[] |
MediaPlayoutMonitor | media-playout synthesized audio duration/events, playout delay, sample totals | mediaPlayouts[] |
IceTransportMonitor | transport: packets/bytes, ICE/DTLS state/roles, pair/certificate references, negotiated crypto | iceTransports[] |
IceCandidateMonitor | local-candidate/remote-candidate addresses, ports, type, protocol, relay metadata | iceCandidates[] |
IceCandidatePairMonitor | candidate-pair states, nomination, connectivity checks, RTT, throughput/BWE, endpoint refs | iceCandidatePairs[] |
CertificateMonitor | certificate fingerprint/algorithm/base64 and issuer reference | certificates[] |
DataChannelMonitor | data-channel state, label, protocol, identifier, messages/bytes | dataChannels[] |
PeerConnectionTransportMonitor | peer-connection report: dataChannelsOpened/Closed | peerConnectionTransports[] |
InboundTrackMonitor / OutboundTrackMonitor | MediaStreamTrack binding, context, aggregate/derived values; no matching browser track report assumed | inboundTracks[] / outboundTracks[], limited projection |
PeerConnectionMonitor | graph owner and aggregate, not the browser’s peer-connection report | PC envelope |
ClientMonitor | cross-PC aggregates, events, issues, extensions and score | ClientSample root |
ExtensionStatsMonitor | application-defined payload, identity and freshness | extensionStats[] at root |
SelectedIcePath | history/view over selected ICE tuple and its evidence | no dedicated sample record |
Source references
The full field inventory is intentionally not reduced to the above examples.
Core formulas
Let Δx = positiveDelta(current, previous), dtMs = current.timestamp − previous.timestamp, dt = dtMs/1000. positiveDelta returns undefined, not zero, on missing inputs or a backwards counter. Each consumer may handle that differently; some explicitly coalesce to zero. Read the formula appendix for every assignment and its guards.
| Value | Implemented calculation / scope |
|---|---|
| RTP bitrate | max(0, Δbytes × 8 / dt) |
| RTP packet rate | Δpackets / dt |
Outbound payloadBitrate | max(0, (ΔbytesSent − ΔheaderBytesSent − (ΔretransmittedBytesSent ?? 0)) × 8 / dt); this is the implementation even if the field name invites a different byte accounting assumption |
Inbound deltaFractionLost | Δlost/(Δlost+Δreceived) only when both deltas are present and both are positive; otherwise zero in that guarded block. This means all-loss/zero-received is not represented as 1 by this expression |
Remote inbound deltaFractionLost | Δlost/(Δlost+Δreceived) when denominator >0, else 0; no-new-RTCP-report resets interval readings |
BitPerPixel | bitrate/(width × height × framesPerSecond) with truthy geometry/fps/bitrate gates |
Inbound avgFramesPerSec | mean of last at most 10 truthy browser FPS readings |
fpsVolatility | mean absolute deviation of those FPS readings divided by their mean; deprecated |
ewmaFps | existing EWMA ×0.9 + new FPS ×0.1; first/truthy-state handling matters |
interFrameDelayVariation | sqrt(max(0, ΔsquaredGap/N − (Δgap/N)²)) / (Δgap/N), N=ΔframesDecoded, N>1 |
inventedSpeechRatio | max(0, ΔconcealedSamples − (ΔsilentConcealedSamples ?? 0)) / ΔtotalSamplesReceived |
timeStretchRate | ((ΔinsertedSamplesForDeceleration ?? 0)+(ΔremovedSamplesForAcceleration ?? 0))/ΔtotalSamplesReceived; a fraction, not per-second rate |
concealmentEventRate | ΔconcealmentEvents/dt |
discardRate | ΔpacketsDiscarded/(ΔpacketsDiscarded+(ΔpacketsReceived ?? 0)); zero if consumed count is zero |
avgJitterBufferDelayInMs | 1000 × ΔjitterBufferDelay / ΔjitterBufferEmittedCount |
jitterBufferTargetDelayInMs | analogous target-delay delta / emitted count ×1000 |
decodeTimePerFrameInMs | 1000 × ΔtotalDecodeTime / ΔframesDecoded |
avgEncodeTimePerFrameInMs | 1000 × ΔtotalEncodeTime / ΔframesEncoded |
avgQpPerFrame | ΔqpSum divided by decoded/encoded frames, depending on direction |
normalizedQp | inbound average QP divided by codec scale from qpScaleOf, clamped 0..1; unknown codec => undefined |
droppedFrameRatio / renderRatio | ΔframesDropped/ΔframesReceived; ΔframesRendered/ΔframesDecoded |
frozenTimeRatio / pausedTimeRatio | ΔtotalFreezesDuration/dt; ΔtotalPausesDuration/dt |
keyFrameRate, PLI/FIR/NACK rates | corresponding counter delta / dt |
retransmissionRatio | retransmitted byte delta / total byte delta, capped at 1; zero/undefined behavior differs by direction |
avgPacketSendDelayInMs | 1000 × ΔtotalPacketSendDelay/ΔpacketsSent |
qualityLimitationDurationShares | nonnegative deltas of none/cpu/bandwidth/other divided by sum of those deltas; undefined for no progress |
source producedFps | nonnegative Δsource.frames/dt |
source rmsAudioLevel | sqrt(ΔtotalAudioEnergy/ΔtotalSamplesDuration) when duration >0 |
playoutDelayPerSampleInMs | 1000 × ΔtotalPlayoutDelay/ΔtotalSamplesCount |
synthesizedSamplesRatio | ΔsynthesizedSamplesDuration/ΔtotalSamplesDuration, with explicit fallback behavior in MediaPlayoutMonitor |
| ICE mean RTT | ΔtotalRoundTripTime/ΔresponsesReceived |
| RTCP mean RTT | ΔtotalRoundTripTime/ΔroundTripTimeMeasurements |
Source references
- client-monitor-js/src/utils/common.ts · positiveDelta
- client-monitor-js/src/monitors/InboundRtpMonitor.ts · accept
- client-monitor-js/src/monitors/OutboundRtpMonitor.ts · accept
- client-monitor-js/src/monitors/RemoteInboundRtpMonitor.ts · accept
- client-monitor-js/src/monitors/MediaSourceMonitor.ts · accept
- client-monitor-js/src/monitors/MediaPlayoutMonitor.ts · accept
- client-monitor-js/src/monitors/IceCandidatePairMonitor.ts · accept
- client-monitor-js/src/utils/quantizer.ts
Aggregates and transport stability
PC sums distinguish audio/video/data-channel directions. Quality-loss averages include streams that actually carried relevant packets; missing measurement differs from zero loss. RTT paths are kept separate (rtcpRttInSec, iceRttInSec), with current RTT preferring RTCP and falling back to ICE. Root avgRttInSec averages PC values and returns -1 with no PCs. PC legacy loss sums and newer per-stream means are not interchangeable.
Source references
transportStability is the normalized MOS-like value produced by utils/transportStability.ts: effective latency is RTT/2 + 2×jitter +10 ms; a piecewise latency impairment and 2.5×loss-percent reduce R; a cubic maps clamped R to MOS; normalized against this model’s best/worst MOS. It requires all three measurements. It is not a direct measurement of user satisfaction or a selected-pair-switch counter. client-monitor-js/src/utils/transportStability.ts.
Missing values and resets
There is no single universal nullification pass. The current code uses several distinct mechanisms:
- Missing raw inputs:
InboundRtpMonitorexplicitly replaces known fields, including undefined; several other monitors useObject.assignand can retain omitted properties. Derived assignments are conditional, so some previous derived values can remain. Do not promise universal freshness without checking the owner. - Backwards counters:
positiveDeltareturns undefined;MediaPlayoutMonitorexplicitly converts some such deltas to zero. Remote RTCP repeated/stale timestamps clear interval readings. Inbound repeated timestamps replace raw fields but return before recomputing derived values; outbound returns earlier. - Shared windows:
SlicedWindowuses null for unavailable totals, rejects repeated timestamps, resets across excessive gaps and requires full slices. N samples span N−1 intervals. Detection/recovery are different slices; inspect their configured offsets rather than assuming a trailing duration in milliseconds. - Paused/ended/background/muted tracks: individual detectors stand down according to their own guards. There is no blanket rule that every detector handles every gate identically. Missing-input branches can preserve an open issue while publishing
inputsUnavailable, whereas some detectors resolve it. - Zero score: full-scale deductions clamp the affected component to zero. Connectivity issues are deliberately not charged again on the PC; dry-track issues can zero media components. There is no global “any network fault nullifies every score” rule.
- Aggregate absent dimensions: undefined/null dimensions are omitted from client RMSE; no measurable dimensions causes calculator early return, leaving the root’s previous/default
score, which starts at 5. This differs from documentation saying the root necessarily becomes undefined. - Wire absence: undefined properties disappear under JSON serialization; Avro nullable fields and generated TS optional fields are separate representations. On-change ICE omissions must be retained by the receiver, but current observer code does not retain them (known limitations).
Source references
- client-monitor-js/src/utils/SlicedWindow.ts
- client-monitor-js/src/monitors/InboundRtpMonitor.ts
- client-monitor-js/src/monitors/OutboundRtpMonitor.ts
- client-monitor-js/src/monitors/RemoteInboundRtpMonitor.ts
- client-monitor-js/src/monitors/MediaPlayoutMonitor.ts
- client-monitor-js/src/scores/DefaultScoreCalculator.ts