Architecture

   BROWSER                                    SERVER                            YOUR SYSTEM
┌───────────────────────┐              ┌────────────────────────┐          ┌──────────────────┐
│  client-monitor-js    │              │      observer-js       │          │                  │
│                       │              │                        │          │  Metrics         │
│  • poll getStats()    │ ClientSample │  • live call model     │  events  │  Alerting        │
│  • derive metrics     │─────────────►│  • cross-client        │─────────►│  Dashboards      │
│  • run detectors      │  (JSON or    │    detectors           │          │  Storage         │
│  • score quality      │   protobuf)  │  • publisher↔subscriber│          │  Post-call       │
│  • emit samples       │              │    correlation         │          │    reports       │
└───────────────────────┘              └───────────┬────────────┘          └──────────────────┘
                                          ┌────────▼────────┐
                                          │   mediasoup     │
                                          │   Router        │  ← optional: the SFU's own
                                          │   observation   │    ground truth
                                          └─────────────────┘

The organising principle

If a condition is detectable on the client, the client’s verdict is the source of truth

This one rule explains most of ObserveRTC’s design.

The browser has information the server never gets: whether an ICE disconnected persisted or healed in 200 ms, whether concealedSamples rose during speech or during silence, whether the jitter buffer grew and NetEQ is time-stretching or grew and succeeded. A server re-deriving those from raw counters sees less and guesses more.

So client-monitor-js decides what is wrong with this endpoint, and observer-js never repeats that work. It answers the orthogonal question: who else is in this state, what do they share, and where does the fault begin?

Client side

client-monitor-js polls getStats() on a configurable period and does four things with the result:

StepOutput
AdaptBrowser-specific differences normalised away
DeriveCounters differenced into bitrates, rates, deltas, smoothed and volatility metrics
DetectDetectors raise and resolve stateful issues with hysteresis
Score0–5 quality per track, per connection and per client, with reasons

On the sampling period it snapshots all of it — plus events, issues, metadata and your own extension stats — into a ClientSample.

Nothing here requires a server. A monitor with no sample-created handler is a perfectly good in-browser diagnostics tool.

client-monitor-js →

Transport

ClientSample is plain JSON, so any transport works: fetch, sendBeacon, a WebSocket, a message queue. For volume, two delta codecs send only what changed since the previous sample — which is where nearly all of the saving is, because a sample mostly repeats itself from one interval to the next:

CodecTake it when
samples-protobuf-codecBytes on the wire are the binding constraint
samples-json-codecThe transport already compresses, and you would rather have zero dependencies (~2 KB) and a payload you can read in a log

Both are stateful: one encoder per client, one decoder per client stream, fed in order, over an ordered lossless transport.

The library does not ship a transport, and does not want to — the right choice depends on your existing telemetry pipeline.

Server side

observer-js accepts samples through one method and maintains a live in-memory tree:

Observer → ObservedCall → ObservedClient → ObservedPeerConnection → sub-stats

Entities are created lazily by id and garbage-collected when they stop appearing. Everything is reachable from one typed event bus, where each payload carries its full ancestry.

On top of that model sit two kinds of analysis:

  • Detectors run every tick and answer “is something wrong right now?” — across the participants of a call, or across the calls of a fleet.
  • Validators run once and answer “is this deployment built correctly?” — is the resolver wired, does the SFU adapt layers per receiver, is everyone on the codec you configured.

observer-js →

What the server adds that a browser cannot

QuestionMechanism
Is this the room or this person?Concurrent-issue detection across participants of one call
Is this our infrastructure?The same symptom across independent calls — they share only the servers
Is this the publisher or the receiver?Publisher ↔ subscriber track links from a RemoteTrackResolver
Is the SFU forwarding, or did the camera stop?Publisher’s outbound RTP versus every subscriber’s dry-track verdict
Is this one kind of client?Grouping by browser / engine / platform / OS, gated on relative risk
Is a TURN server down?Population collapse plus a healthy control group elsewhere
Is anyone even subscribed to this track?The resolver’s silence — an empty subscriber set

Optional: the SFU’s own view

When you run mediasoup, observer-js can attach to a live Router and passively record the server’s ground truth — transports, producers, consumers, their exact lifetimes and state transitions — into a MediasoupRouterSample, completely independent of the client pipeline.

Peer connections and mediasoup WebRTC transports share ids, so the observer can tell you when a client’s peer connection corresponds to one of the router’s transports. It emits that as an event and steps back: how you associate the two is application-specific.

SFU integration →

Where your system takes over

ObserveRTC deliberately stops at the event boundary. It does not ship a database, a dashboard, an alert router or a retention policy, because those are exactly the parts every organisation already has opinions about.

What it gives you instead:

  • Events to feed your alerting and metrics pipeline
  • A live model to query for real-time views
  • Sinks that persist the exact samples that were accepted — which means past incidents can be replayed through a fresh observer with different detector settings, offline, in seconds

Deployment shapes