The event bus

This is the primary API. The Observer is the single emitter for the entire hierarchy. ObservedCall, ObservedClient and ObservedPeerConnection are EventEmitters too, but those local events are reserved for internal teardown wiring — application code belongs on the bus.

Payload shape

Every event delivers exactly one argument: an object containing the ancestry from the observer down to the entity that raised it, plus any event-specific subject.

type ObserverEventBase           = { observer: Observer; context?: AcceptContext };
type ObservedCallScope           = ObserverEventBase   & { observedCall: ObservedCall };
type ObservedClientScope         = ObservedCallScope   & { observedClient: ObservedClient };
type ObservedPeerConnectionScope = ObservedClientScope & { observedPeerConnection: ObservedPeerConnection };

So a peer-connection-level event hands you everything above it as well:

observer.on("inbound-rtp-added", ({
    observer, observedCall, observedClient, observedPeerConnection, observedInboundRtp,
}) => {
    // all five present and typed
});

on / off / once / emit are fully typed against the event map — the handler argument is inferred from the event name.

Observer level

Scope { observer }.

EventExtra payloadFires when
observer-updatedobserver.update() ran
observer-closedobserver.close()
sample-rejected{ reason: 'observer-closed' | 'missing-callId' | 'missing-clientId', sample }a sample was dropped by accept()
observer-issue{ issue: ObserverIssue }observer.addIssue(...) — a cross-call / SFU-wide finding
validation-ready{ validator: string, report: ValidationReport }a validator settled — once per check, not per tick

Call level

Scope { observer, observedCall }.

EventExtraFires when
call-addeda call is created
call-updated{ context? }call.update() ran
call-closedthe call closed
call-emptythe last client left
call-not-emptythe first client joined a previously-empty call
call-issue{ issue: ObserverIssue }call.addIssue(...) — a server-side detector finding

Client level

Scope { observer, observedCall, observedClient }.

EventExtraFires when
client-addeda client is created
client-sink-created{ sink: ClientSampleSink }a per-client sink was created; fires right after client-added
client-updated{ sample, elapsedTimeInMs, context? }the client processed a sample
client-closedthe client closed
client-joinedfirst CLIENT_JOINED event seen
client-leftCLIENT_LEFT seen, or inferred on close
client-rejoined{ timestamp }a later CLIENT_JOINED after an earlier join
client-issue{ issue: ClientIssue }a client-reported issue arrived, or client.addIssue(...)
client-issue-resolved{ resolvedIssue: ResolvedActiveClientIssue }a stateful issue ended — carries durationInMs and resolvedBy
client-metadata{ metaData: ClientMetaData }a meta item arrived
client-extension-stats{ extensionStats: ExtensionStat }an app-defined extension stat arrived
client-event{ event: ClientEvent }any client event was processed

client-issue-resolved is the one to build on

client-issue tells you a symptom started. client-issue-resolved tells you the whole episode — what it was, how long it lasted, and who closed it ('client', 'timeout' or 'client-closed'). Analytics and post-call reporting should be driven from the resolution, not the raise.

Peer-connection level

Scope { observer, observedCall, observedClient, observedPeerConnection }.

EventExtraNotes
peer-connection-added / -closedlifecycle of the PC
peer-connection-updated{ context? }the PC processed a sample
ice-connection-state-changed{ state }driven by client events
ice-gathering-state-changed{ state }
connection-state-changed{ state }
inbound-track-added / -updated / -removed / -muted / -unmuted{ observedInboundTrack }
outbound-track-added / -updated / -removed / -muted / -unmuted{ observedOutboundTrack }
inbound-rtp-added / -updated / -removed{ observedInboundRtp }-updated fires every tick
outbound-rtp-added / -updated / -removed{ observedOutboundRtp }-updated fires every tick
remote-inbound-rtp-added / -updated / -removed{ observedRemoteInboundRtp }
remote-outbound-rtp-added / -updated / -removed{ observedRemoteOutboundRtp }
data-channel-added / -updated / -removed{ observedDataChannel }
ice-candidate-added / -updated / -removed{ observedIceCandidate }
ice-candidate-pair-added / -updated / -removed{ observedIceCandidatePair }
ice-transport-added / -updated / -removed{ observedIceTransport }
codec-added / -updated / -removed{ observedCodec }
media-source-added / -updated / -removed{ observedMediaSource }
media-playout-added / -updated / -removed{ observedMediaPlayout }
peer-connection-transport-added / -updated / -removed{ observedPeerConnectionTransport }
certificate-added / -updated / -removed{ observedCertificate }

Volume

The sub-stat *-updated events fire on every peer-connection accept(), per stream. At a thousand participants that is tens of thousands of handler invocations per sampling period.

For high-throughput servers, subscribe only to what you need, or read fields off the entities on client-updated / call-updated instead — one event per client per tick, with the whole tree reachable from it.

mediasoup level

Scope { observer, observedMediasoupRouter }. See SFU integration.

EventExtraFires when
mediasoup-router-addedobserver.createObservedMediasoupRouter(...) registered a router
mediasoup-router-matched-with-peer-connection{ observedCall, observedClient, observedPeerConnection }a peer connection’s id matched one of the router’s WebRTC transport ids. Opt-in via matchPeerConnectionByWebRtcTransportId: true
mediasoup-router-removedthe underlying mediasoup router closed

Local lifecycle events

These stay on the individual entities, for teardown and coordination. You may listen to them, but prefer the bus for application logic.

EntityLocal events
ObservedCallupdate, newclient, empty, not-empty, close
ObservedClientupdate (sample, elapsedTimeInMs), close, joined, left
ObservedPeerConnectionremoved-inbound-track, removed-outbound-track, close

Choosing a subscription strategy