@observertc/observer-js 1.0.0-beta.8 → 1.0.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,6 +3,9 @@
3
3
  [![NPM version](https://img.shields.io/npm/v/@observertc/observer-js.svg)](https://www.npmjs.com/package/@observertc/observer-js)
4
4
  [![License](https://img.shields.io/npm/l/@observertc/observer-js.svg)](https://github.com/observertc/observer-js/blob/main/LICENSE)
5
5
 
6
+ > **In one line:** feed it WebRTC `getStats()` snapshots, and get back a live, queryable model of
7
+ > every call plus a single typed event stream to react to.
8
+
6
9
  `observer-js` is a **server-side Node.js library for monitoring WebRTC sessions**. A WebRTC
7
10
  application (typically an SFU or a signaling/stats backend) feeds it `ClientSample` objects —
8
11
  periodic snapshots of each participant's `RTCPeerConnection.getStats()` output plus
@@ -10,6 +13,24 @@ application events — and `observer-js` maintains a live, in-memory model of ev
10
13
  participant, peer connection, and media stream, derives per-interval and cumulative metrics,
11
14
  and emits a single, unified stream of typed events the application can react to.
12
15
 
16
+ **What you can do with it:**
17
+
18
+ - **Monitor calls live** — a queryable in-memory tree of every call, client, peer connection,
19
+ track, codec, ICE candidate and data channel, each holding current **and** cumulative metrics.
20
+ - **React on one event bus** — subscribe once on the `Observer`; every payload carries its full
21
+ ancestry (`call → client → peer connection → stat`), so you never walk the tree to subscribe.
22
+ - **Get derived metrics for free** — counter-reset-safe per-tick deltas, bitrates, jitter, RTT,
23
+ fraction-lost, remote-RTP (RTCP) correlation, and TURN/TCP usage from the selected candidate pair.
24
+ - **Correlate across an SFU** — link a publisher's outbound track to every subscriber's inbound
25
+ track (`RemoteTrackResolver`), and observe mediasoup routers/transports/producers/consumers
26
+ on the server side.
27
+ - **Detect server-only problems** — cross-client `Detector`s raise `call-issue`s for conditions no
28
+ single client can see (e.g. everyone in a call degrading at once).
29
+ - **Persist every sample** — per-client sinks (JSONL file, in-memory, or your own) for archival,
30
+ streaming, and offline replay.
31
+ - **Drop it in safely** — warn-don't-throw, a pluggable logger, dual **ESM + CommonJS**, and **no**
32
+ media-stack dependency in the core.
33
+
13
34
  > **Status:** `1.0.0-beta`. The API described here is current and intended to be implemented
14
35
  > against directly. This document is written to be self-sufficient: an engineer (or an AI
15
36
  > agent) should be able to integrate the library, or develop it further, from this file alone.
@@ -24,23 +45,21 @@ and emits a single, unified stream of typed events the application can react to.
24
45
  ## Table of contents
25
46
 
26
47
  1. [Installation](#installation)
27
- 2. [Mental model](#mental-model)
28
- 3. [Quick start](#quick-start)
29
- 4. [Data flow](#data-flow)
30
- 5. [Entity hierarchy](#entity-hierarchy)
31
- 6. [Ingestion: `accept()`, context & lifecycle](#ingestion-accept-context--lifecycle)
32
- 7. [Update policies](#update-policies)
33
- 8. [The event bus](#the-event-bus) ← the core of the API
34
- 9. [API reference](#api-reference)
35
- 10. [Schema types (`ClientSample`)](#schema-types-clientsample)
36
- 11. [Detectors (server-side extension point)](#detectors-server-side-extension-point)
37
- 12. [Remote track resolution (mediasoup / SFU)](#remote-track-resolution-mediasoup--sfu)
48
+ 2. [Quick start](#quick-start)
49
+ 3. [Data flow](#data-flow)
50
+ 4. [Entity hierarchy](#entity-hierarchy)
51
+ 5. [Ingestion: `accept()`, context & lifecycle](#ingestion-accept-context--lifecycle)
52
+ 6. [Update policies](#update-policies)
53
+ 7. [The event bus](#the-event-bus) ← the core of the API
54
+ 8. [API reference](#api-reference)
55
+ 9. [Schema types (`ClientSample`)](#schema-types-clientsample)
56
+ 10. [Detectors (server-side extension point)](#detectors-server-side-extension-point)
57
+ 11. [Remote track resolution (mediasoup / SFU)](#remote-track-resolution-mediasoup--sfu)
58
+ 12. [Mediasoup router observation](#mediasoup-router-observation)
38
59
  13. [Sinks (per-client sample persistence)](#sinks-per-client-sample-persistence)
39
60
  14. [Logging](#logging)
40
61
  15. [Error-handling philosophy](#error-handling-philosophy)
41
- 16. [Public exports](#public-exports)
42
- 17. [Development & extension guide](#development--extension-guide)
43
- 18. [Not yet implemented / roadmap](#not-yet-implemented--roadmap)
62
+ 16. [Development & extension guide](#development--extension-guide)
44
63
 
45
64
  ---
46
65
 
@@ -72,31 +91,6 @@ produced on the client (e.g. by `@observertc/client-monitor-js`) conform to the
72
91
 
73
92
  ---
74
93
 
75
- ## Mental model
76
-
77
- Five ideas are enough to use the whole library:
78
-
79
- 1. **One ingestion method.** `observer.accept(sample, context?)` is how data gets in.
80
- Calls, clients, and peer connections are created automatically the first time their id
81
- appears in a sample.
82
-
83
- 2. **A live entity tree.** `Observer → ObservedCall → ObservedClient → ObservedPeerConnection
84
- → {inbound/outbound RTP, tracks, data channels, ICE, codecs, …}`. Every node holds current
85
- and cumulative metrics and is reachable by id through `Map`s on its parent.
86
-
87
- 3. **One event bus.** Everything worth subscribing to is emitted on the **`Observer`** itself
88
- (it is an `EventEmitter`). Each event payload is an **object carrying the full ancestry**
89
- of the entity it came from. You never have to walk the tree to subscribe.
90
-
91
- 4. **Pull or react.** You can read fields off the entities at any time (pull), and/or react to
92
- events (push). The `*-updated` events fire on each processing tick.
93
-
94
- 5. **Warn, don't throw.** Operational problems (bad config, duplicate ids, closed entities,
95
- malformed samples) never throw; they warn through the pluggable logger and degrade
96
- gracefully (returning `undefined` or emitting `sample-rejected`).
97
-
98
- ---
99
-
100
94
  ## Quick start
101
95
 
102
96
  ```ts
@@ -364,6 +358,16 @@ additional field(s) on top of that scope.
364
358
  | `observer-closed` | — | `observer.close()` |
365
359
  | `sample-rejected` | `{ reason: 'observer-closed' \| 'missing-callId' \| 'missing-clientId', sample: ClientSample }` | a sample was dropped by `accept()` |
366
360
 
361
+ #### Mediasoup level — scope `{ observer, observedMediasoupRouter }`
362
+
363
+ | Event | Extra | Fires when |
364
+ |-------|-------|-----------|
365
+ | `mediasoup-router-added` | — | `observer.createObservedMediasoupRouter(...)` registered a router |
366
+ | `mediasoup-router-matched-with-call` | `{ observedCall }` | the router was matched to a call — explicitly via `callId`, or by WebRTC-transport ↔ peer-connection correlation. Emitted **once per distinct call**; the observer stores **nothing** — your handler decides what to do |
367
+ | `mediasoup-router-removed` | — | the underlying mediasoup router closed (its `router.observer` `close` fired) |
368
+
369
+ See [Mediasoup router observation](#mediasoup-router-observation) for the full design and examples.
370
+
367
371
  #### Call level — scope `{ observer, observedCall }`
368
372
 
369
373
  | Event | Extra | Fires when |
@@ -620,6 +624,74 @@ mediasoup set `PRODUCER_*` / `CONSUMER_*` / `DATA_PRODUCER_*` / `DATA_CONSUMER_*
620
624
  `MEDIA_DEVICES_SUPPORTED_CONSTRAINTS`, `USER_MEDIA_ERROR`, `LOCAL_SDP`, `OPERATION_SYSTEM`,
621
625
  `ENGINE`, `PLATFORM`, `BROWSER`.
622
626
 
627
+ ### Worked example: a real `ClientSample`
628
+
629
+ Two consecutive samples from one participant ("Guest" in room `qq0iwfnd`) of an
630
+ edumeet/mediasoup call show what actually flows through `accept()`: a rich **join snapshot**,
631
+ then lean **steady-state ticks**.
632
+
633
+ **Sample 1 — the join snapshot.** Carries the one-off lifecycle `clientEvents` and device
634
+ `clientMetaItems` alongside the first stats. (Abbreviated; ids and times are from the real log.)
635
+
636
+ ```jsonc
637
+ {
638
+ "timestamp": 1780572332518,
639
+ "callId": "d3dbf2f5-79be-4cb8-9d43-fb404f07ef27",
640
+ "clientId": "c926983c-4468-4046-ae8c-a9cabe1a1868",
641
+ "score": 0, // no quality measured yet on the join tick
642
+ "attachments": { "displayName": "Guest", "roomId": "qq0iwfnd", "actualSessionId": "d3dbf2f5-…" },
643
+
644
+ "clientEvents": [ // chronological lifecycle (12 in the real sample)
645
+ { "type": "CLIENT_JOINED", "timestamp": 1780572324515 },
646
+ { "type": "PEER_CONNECTION_OPENED", "timestamp": 1780572326790 }, // pc=b81c8d9d (media)
647
+ { "type": "ICE_GATHERING_STATE_CHANGED", "timestamp": 1780572326811 }, // → gathering
648
+ { "type": "PEER_CONNECTION_STATE_CHANGED", "timestamp": 1780572326812 }, // → connecting
649
+ { "type": "PRODUCER_ADDED", "timestamp": 1780572326821 }, // producer=1abdaf82 (audio)
650
+ { "type": "MEDIA_TRACK_ADDED", "timestamp": 1780572326821 }, // track=36ae42df (audio)
651
+ { "type": "PEER_CONNECTION_STATE_CHANGED", "timestamp": 1780572326827 }, // → connected
652
+ { "type": "PRODUCER_ADDED", "timestamp": 1780572326837 }, // producer=ba06a35b (video)
653
+ { "type": "DATA_PRODUCER_CREATED", "timestamp": 1780572326853 }
654
+ ],
655
+
656
+ "clientMetaItems": [ // environment & devices, one-off (10 in the real sample)
657
+ { "type": "USER_AGENT_DATA", "payload": "{…Chrome 148 / macOS…}" },
658
+ { "type": "MEDIA_DEVICE", "payload": "{…\"BRIO 4K Stream Edition\"…}" }
659
+ // …mic / camera / speaker devices…
660
+ ],
661
+
662
+ "peerConnections": [
663
+ {
664
+ "peerConnectionId": "b81c8d9d-…", // the media PC — Guest publishes to the SFU
665
+ "outboundRtps": [ /* audio + video */ ],
666
+ "outboundTracks": [ /* mic + camera: label, settings, capabilities */ ],
667
+ "remoteInboundRtps": [ /* RTCP feedback from the SFU */ ],
668
+ "codecs": [ /* … */ ], "iceTransports": [ /* … */ ],
669
+ "iceCandidatePairs": [ /* … */ ], "dataChannels": [ /* … */ ]
670
+ },
671
+ { "peerConnectionId": "8635acb7-…", "peerConnectionTransports": [ /* … */ ] } // signaling-only PC
672
+ ]
673
+ }
674
+ ```
675
+
676
+ What `accept()` does with it, in order — each step emits on the bus with full ancestry:
677
+
678
+ 1. lazily creates the `ObservedCall` → **`call-added`**;
679
+ 2. creates the `ObservedClient` → **`client-added`**, then **`client-joined`** (from `CLIENT_JOINED`);
680
+ 3. creates an `ObservedPeerConnection` per entry → **`peer-connection-added`** (×2 here);
681
+ 4. creates an `ObservedOutboundTrack` per track → **`outbound-track-added`**, plus the matching
682
+ **`outbound-rtp-added`**;
683
+ 5. replays the device list as **`client-metadata`** events and the lifecycle items as
684
+ **`client-event`**; and finally **`client-updated`** for the whole tick.
685
+
686
+ `attachments.roomId` lands on `observedClient.attachments` (read it on `client-updated`, **not** at
687
+ creation — see [Ingestion](#ingestion-accept-context--lifecycle)).
688
+
689
+ **Sample 2 — a steady-state tick** (~8 s later): same `callId` / `clientId`, **no** new
690
+ `clientEvents` or `clientMetaItems`, just refreshed `peerConnections` stats. Each PC now scores `5`
691
+ and the aggregate client `score` is `4.74` — a healthy call. This is the shape of nearly every
692
+ sample: each tick refreshes metrics and fires the `*-updated` events, while the heavy join
693
+ snapshot happens only once.
694
+
623
695
  ---
624
696
 
625
697
  ## Detectors (server-side extension point)
@@ -716,6 +788,135 @@ For the mediasoup factory, the application puts `producerId` / `consumerId` (and
716
788
 
717
789
  ---
718
790
 
791
+ ## Mediasoup router observation
792
+
793
+ Everything above is built from the **client-reported** `ClientSample`. When you run a
794
+ [mediasoup](https://mediasoup.org) SFU you also have the **server's own** ground truth — its
795
+ routers, transports, producers, consumers and data channels, with exact lifetimes and state
796
+ transitions. `ObservedMediasoupRouter` captures that server-side view into a
797
+ **`MediasoupRouterSample`**, completely independent of the client sample pipeline.
798
+
799
+ ### The concept
800
+
801
+ You hand the observer a live mediasoup `Router`; it attaches to mediasoup's own `observer` API and,
802
+ from then on, **passively records** the router's topology and lifecycle — with no polling and no
803
+ changes to your media code:
804
+
805
+ - new transports (`webrtc` / `plain` / `pipe` / `direct`), their selected `tuple`, and ICE state
806
+ transitions;
807
+ - producers (codec, SSRCs/RIDs, `pause`/`resume`) and consumers (`pause`/`resume`,
808
+ `producerPaused`/`producerResumed`);
809
+ - data producers and data consumers;
810
+ - `createdAt` / `closedAt` for every entity above.
811
+
812
+ All of it accumulates on `observedMediasoupRouter.sample` (a `MediasoupRouterSample` — see
813
+ [`src/schema/MediasoupRouter.ts`](./src/schema/MediasoupRouter.ts)). This is a *Sample*, not a
814
+ *Report*: it mirrors the naming of `ClientSample` and is yours to snapshot, persist, or correlate.
815
+
816
+ ### Matching a router to a call — by **event**, not by storage
817
+
818
+ A router belongs to one or more calls, but **the observer does not store the router (or its sample)
819
+ on the `ObservedCall`.** Instead, when a match is found it emits
820
+ **`mediasoup-router-matched-with-call`** and steps back — *your application* decides what the
821
+ pairing means. Stamp the `callId` into the router's `appData`, build your own index, attach the
822
+ sample to the call in your database — whatever fits your system. The library stays unopinionated and
823
+ loosely coupled.
824
+
825
+ There are two ways a match is discovered, controlled by the settings you pass to
826
+ `createObservedMediasoupRouter`:
827
+
828
+ - **Explicit (`callId`)** — you already know the call. If that call currently exists,
829
+ `mediasoup-router-matched-with-call` fires immediately.
830
+ - **Implicit (`bindCallByWebRtcTransportId: true`)** — let the observer discover it. As peer
831
+ connections are observed (`peer-connection-added`), the observer checks whether the peer
832
+ connection's id is one of the router's WebRTC transport ids. On a hit, it's a match. It emits
833
+ **once per distinct call**, and keeps watching so additional calls sharing the router can still
834
+ match later. The internal listener is removed automatically when the router closes or the observer
835
+ closes.
836
+
837
+ When the underlying mediasoup router closes, its `close` propagates to `ObservedMediasoupRouter`,
838
+ which emits **`mediasoup-router-removed`**. That is your cue to do whatever cleanup or persistence
839
+ you want with the now-final `sample` — again, the observer itself keeps nothing.
840
+
841
+ ### Options — `observer.createObservedMediasoupRouter(settings)`
842
+
843
+ | Field | Type | Required | Meaning |
844
+ |-------|------|----------|---------|
845
+ | `router` | `mediasoup.types.Router` | yes | the live router to observe; the observer attaches to `router.observer` |
846
+ | `routerId` | `string` | yes | your id for the router (the sample also carries `router.id`) |
847
+ | `appData` | `Record<string, unknown>` | no | application-owned bag on the `ObservedMediasoupRouter` (e.g. where you record the matched `callId`) |
848
+ | `attachments` | `Record<string, unknown>` | no | free-form data copied onto `sample.attachments` |
849
+ | `callId` | `string` | no | **explicit match**: emit `mediasoup-router-matched-with-call` now if this call exists |
850
+ | `bindCallByWebRtcTransportId` | `boolean` | no | **implicit match**: discover the call(s) by correlating WebRTC transport ids with peer-connection ids |
851
+
852
+ Returns the `ObservedMediasoupRouter`, or `undefined` if the observer is closed (a router with the
853
+ same id returns the existing instance — both warn).
854
+
855
+ Useful members on the returned object: `.sample` (the `MediasoupRouterSample`), `.appData`,
856
+ `.attachments` (getter over `sample.attachments`), `.webrtcTransportIds: Set<string>`, `.id`,
857
+ `.close()`.
858
+
859
+ ### Example
860
+
861
+ ```ts
862
+ import { Observer } from '@observertc/observer-js';
863
+ import type { ObservedMediasoupRouterScope, ObservedCallScope } from '@observertc/observer-js';
864
+
865
+ const observer = new Observer();
866
+
867
+ // 1) Feed client samples as usual so the observer knows about calls & peer connections.
868
+ // (e.g. transport-layer: observer.accept(clientSample, context))
869
+
870
+ // 2) Observe the SFU side. Let the observer discover which call this router serves.
871
+ const router = /* your mediasoup router */ undefined as any;
872
+ const observedRouter = observer.createObservedMediasoupRouter({
873
+ router,
874
+ routerId: router.id,
875
+ bindCallByWebRtcTransportId: true, // discover the call by peer-connection correlation
876
+ appData: {}, // we'll record the matched callId here
877
+ });
878
+
879
+ // 3) The observer found a call for the router — WE decide what to do with the pairing.
880
+ observer.on('mediasoup-router-matched-with-call',
881
+ ({ observedMediasoupRouter, observedCall }: ObservedMediasoupRouterScope & ObservedCallScope) => {
882
+ // e.g. remember the association on the router's appData…
883
+ observedMediasoupRouter.appData.callId = observedCall.callId;
884
+ // …or attach the live server sample to the call in your own store:
885
+ myStore.linkRouterToCall(observedCall.callId, observedMediasoupRouter.sample);
886
+ },
887
+ );
888
+
889
+ // 4) The router closed — WE decide what to persist/forward with the final sample.
890
+ observer.on('mediasoup-router-removed', ({ observedMediasoupRouter }: ObservedMediasoupRouterScope) => {
891
+ const callId = observedMediasoupRouter.appData.callId as string | undefined;
892
+ myStore.saveRouterSample(callId, observedMediasoupRouter.sample);
893
+ });
894
+
895
+ // (optional) react to the router being registered at all:
896
+ observer.on('mediasoup-router-added', ({ observedMediasoupRouter }) => {
897
+ console.log('observing router', observedMediasoupRouter.id);
898
+ });
899
+ ```
900
+
901
+ If you already know the call, skip discovery and match explicitly:
902
+
903
+ ```ts
904
+ observer.createObservedMediasoupRouter({ router, routerId: router.id, callId });
905
+ // → `mediasoup-router-matched-with-call` fires immediately if `callId` is a known call
906
+ ```
907
+
908
+ ### Why event-driven instead of storing on the call
909
+
910
+ - **Loose coupling.** The call model stays about client telemetry; the SFU view lives on its own
911
+ object and is associated only if and how *you* choose.
912
+ - **You own the association.** One router may serve multiple calls, a call may be served by multiple
913
+ routers, and the right place to keep that mapping is application-specific — so the observer hands
914
+ you the match and the final sample and gets out of the way.
915
+ - **No silent accumulation.** Nothing is appended to `ObservedCall`, so there is no hidden growth or
916
+ lifetime you have to reason about; the router sample lives exactly as long as you keep a reference.
917
+
918
+ ---
919
+
719
920
  ## Sinks (per-client sample persistence)
720
921
 
721
922
  A **sink** receives the samples a client accepts — for archival, streaming, or later offline
@@ -878,48 +1079,6 @@ are caught by `accept()` and surfaced as a warning.
878
1079
 
879
1080
  ---
880
1081
 
881
- ## Public exports
882
-
883
- ```ts
884
- // Entry: src/index.ts
885
- export { Observer } from './Observer';
886
- export type { ObserverEvents, SampleRejectedReason, AcceptContext, CallAppDataFactory, ClientAppDataFactory } from './Observer';
887
- export type { ObserverEventBase, ObservedCallScope, ObservedClientScope, ObservedPeerConnectionScope } from './ObserverEvents';
888
-
889
- export { ObservedCall, ObservedClient, ObservedPeerConnection } from './…';
890
- export { ObservedInboundTrack, ObservedOutboundTrack } from './…';
891
- export { ObservedInboundRtp, ObservedOutboundRtp, ObservedRemoteInboundRtp, ObservedRemoteOutboundRtp } from './…';
892
- export { ObservedMediaSource, ObservedMediaPlayout, ObservedCodec, ObservedCertificate, ObservedDataChannel } from './…';
893
- export { ObservedIceCandidate, ObservedIceCandidatePair, ObservedIceTransport, ObservedPeerConnectionTransport } from './…';
894
-
895
- export { ClientSample, ClientIssue, ClientEvent, ClientMetaData } from './schema/ClientSample';
896
- export { ClientEventTypes } from './schema/ClientEventTypes';
897
- export { ClientMetaTypes } from './schema/ClientMetaTypes';
898
-
899
- export { ScoreCalculator } from './scores/ScoreCalculator';
900
- export { Detectors } from './detectors/Detectors';
901
- export type { Detector } from './detectors/Detector';
902
-
903
- export { createLogger, setObserverLogger } from './common/logger';
904
- export type { Logger, ObserverLogger } from './common/logger';
905
-
906
- // sinks: base class (subclass it for a custom destination) + built-ins
907
- export { ClientSampleSink } from './sinks/ClientSampleSink';
908
- export type { ClientSampleSinkEvents, ClientSampleSinkFactory } from './sinks/ClientSampleSink';
909
- export { JsonlFileSink, createJsonlFileSink, createJsonlFileSinkFactory } from './sinks/JsonlFileSink';
910
- export type { JsonlFileSinkOptions, JsonlFileSinkFactoryOptions } from './sinks/JsonlFileSink';
911
- export { InMemorySink, createInMemorySink } from './sinks/InMemorySink';
912
-
913
- export { Middleware } from './common/Middleware';
914
-
915
- // remote track correlation
916
- export { RemoteTrackResolver } from './utils/RemoteTrackResolver';
917
- export type { RemoteTrackResolvers, RemoteTrackResolverFactory } from './utils/RemoteTrackResolver';
918
- export { createDefaultMediasoupRemoteTrackResolverFactory, createP2pRemoteTrackResolverFactory } from './utils/RemoteTrackResolverFactories';
919
- ```
920
-
921
- ---
922
-
923
1082
  ## Development & extension guide
924
1083
 
925
1084
  ```bash
@@ -969,24 +1128,6 @@ event map + scope types), `detectors/` (`Detector`, `Detectors`), `scores/`, `up
969
1128
 
970
1129
  ---
971
1130
 
972
- ## Not yet implemented / roadmap
973
-
974
- For an agent continuing the work, these are explicitly **not** present yet:
975
-
976
- - **Tests.** There is currently only a placeholder spec. The two `accept()` methods
977
- (`ObservedClient`, `ObservedPeerConnection`) are the priority for characterization tests. (A
978
- CI gate running lint + typecheck + test is already in place — see `.github/workflows/ci.yml`.)
979
- - **Built-in detectors / quality classifier.** The registry exists; concrete server-side
980
- detectors (e.g. producer→consumer delivery mismatch, quality outlier, asymmetric media) are
981
- to be designed.
982
- - **Per-tick snapshot API.** A serializable snapshot per `*-updated` tick to replace consuming
983
- the fine-grained `*-updated` events (under consideration).
984
- - **Monotonic-timestamp / clock-skew handling** for client clock jumps.
985
- - **Batch `processSamples()`** entry point for offline analysis of recorded sample streams.
986
- - **Expanded derived metrics** (jitter-buffer delay, concealment, freeze fraction, encode/decode
987
- CPU, quality-limitation breakdown, etc.) and first-class producer/consumer & PC-direction
988
- fields beyond `attachments`.
989
-
990
1131
  ## License
991
1132
 
992
1133
  Apache-2.0. Part of the [ObserverTC](https://github.com/observertc) ecosystem.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { EventEmitter } from 'events';
2
+ import { types } from 'mediasoup';
2
3
 
3
4
  /**
4
5
  * The WebRTC app provided custom stats payload
@@ -2082,6 +2083,179 @@ type ClientSampleSinkFactory = (params: {
2082
2083
  observedCall: ObservedCall;
2083
2084
  }) => ClientSampleSink | undefined;
2084
2085
 
2086
+ /** mediasoup `TransportTuple` (the selected local/remote address pair of a transport). */
2087
+ type TransportTuple = {
2088
+ localAddress: string;
2089
+ localPort: number;
2090
+ remoteIp?: string;
2091
+ remotePort?: number;
2092
+ protocol: 'udp' | 'tcp';
2093
+ };
2094
+ /** mediasoup `RtpCodecParameters` (the negotiated codec of a producer/consumer). */
2095
+ type RtpCodecParameters = {
2096
+ mimeType: string;
2097
+ payloadType: number;
2098
+ clockRate: number;
2099
+ channels?: number;
2100
+ parameters?: Record<string, unknown>;
2101
+ rtcpFeedback?: {
2102
+ type: string;
2103
+ parameter?: string;
2104
+ }[];
2105
+ };
2106
+ type SampleHistoryItem<T extends string> = Record<string, unknown> & {
2107
+ type: T;
2108
+ timestamp: number;
2109
+ };
2110
+ type MediasoupRouterSample = Record<string, unknown> & {
2111
+ routerId: string;
2112
+ attachments: Record<string, unknown>;
2113
+ createdAt: number;
2114
+ closedAt?: number;
2115
+ producers: MediasoupProducerSample[];
2116
+ consumers: MediasoupConsumerSample[];
2117
+ dataProducers: MediasoupDataProducerSample[];
2118
+ dataConsumers: MediasoupDataConsumerSample[];
2119
+ transports: MediasoupTransportSample[];
2120
+ };
2121
+ type MediasoupWebRtcTransportSampleEventTypes = 'icestate-changed-to-new' | 'icestate-changed-to-connected' | 'icestate-changed-to-completed' | 'icestate-changed-to-disconnected' | 'icestate-changed-to-closed' | 'dtlsstate-changed-to-new' | 'dtlsstate-changed-to-connecting' | 'dtlsstate-changed-to-connected' | 'dtlsstate-changed-to-failed' | 'dtlsstate-changed-to-closed' | 'sctpstate-changed-to-new' | 'sctpstate-changed-to-connecting' | 'sctpstate-changed-to-connected' | 'sctpstate-changed-to-failed' | 'sctpstate-changed-to-closed' | 'iceselectedtuple-changed';
2122
+ type MediasoupWebRtcTransportSampleEventMap = {
2123
+ [K in MediasoupWebRtcTransportSampleEventTypes]: SampleHistoryItem<K>;
2124
+ }[MediasoupWebRtcTransportSampleEventTypes];
2125
+ type MediasoupWebRtcTransportSample = {
2126
+ type: 'webrtc';
2127
+ history: MediasoupWebRtcTransportSampleEventMap[];
2128
+ };
2129
+ type MediasoupPlainTransportSampleEventTypes = 'sctpstate-changed-to-new' | 'sctpstate-changed-to-connecting' | 'sctpstate-changed-to-connected' | 'sctpstate-changed-to-failed' | 'sctpstate-changed-to-closed' | 'tuple-changed' | 'rtcptuple-changed';
2130
+ type MediasoupPlainTransportSampleEventMap = {
2131
+ [K in MediasoupPlainTransportSampleEventTypes]: SampleHistoryItem<K>;
2132
+ }[MediasoupPlainTransportSampleEventTypes];
2133
+ type MediasoupPlainTransportSample = {
2134
+ type: 'plain';
2135
+ rtcpTuple?: TransportTuple;
2136
+ history: MediasoupPlainTransportSampleEventMap[];
2137
+ };
2138
+ type MediasoupPipeTransportSampleEventTypes = 'sctpstate-changed-to-new' | 'sctpstate-changed-to-connecting' | 'sctpstate-changed-to-connected' | 'sctpstate-changed-to-failed' | 'sctpstate-changed-to-closed';
2139
+ type MediasoupPipeTransportSampleEventMap = {
2140
+ [K in MediasoupPipeTransportSampleEventTypes]: SampleHistoryItem<K>;
2141
+ }[MediasoupPipeTransportSampleEventTypes];
2142
+ type MediasoupPipeTransportSample = {
2143
+ type: 'pipe';
2144
+ history: MediasoupPipeTransportSampleEventMap[];
2145
+ };
2146
+ type MediasoupDirectTransportSampleEventTypes = never;
2147
+ type MediasoupDirectTransportSampleEventMap = {
2148
+ [K in MediasoupDirectTransportSampleEventTypes]: SampleHistoryItem<K>;
2149
+ }[MediasoupDirectTransportSampleEventTypes];
2150
+ type MediasoupDirectTransportSample = {
2151
+ type: 'direct';
2152
+ history: MediasoupDirectTransportSampleEventMap[];
2153
+ };
2154
+ type MediasoupTransportSample = Record<string, unknown> & {
2155
+ id: string;
2156
+ createdAt: number;
2157
+ connectedAt?: number;
2158
+ closedAt?: number;
2159
+ tuple?: TransportTuple;
2160
+ } & (MediasoupWebRtcTransportSample | MediasoupPlainTransportSample | MediasoupPipeTransportSample | MediasoupDirectTransportSample);
2161
+ type MediasoupProducerSampleEventMap = {
2162
+ 'pause': undefined;
2163
+ 'resume': undefined;
2164
+ 'degraded': undefined;
2165
+ 'restored': undefined;
2166
+ };
2167
+ type MediasoupProducerSampleEvent = {
2168
+ [K in keyof MediasoupProducerSampleEventMap]: SampleHistoryItem<K>;
2169
+ }[keyof MediasoupProducerSampleEventMap];
2170
+ type MediasoupProducerSample = Record<string, unknown> & {
2171
+ id: string;
2172
+ transportId: string;
2173
+ createdAt: number;
2174
+ closedAt?: number;
2175
+ codecInfo: RtpCodecParameters;
2176
+ kind: 'audio' | 'video';
2177
+ ssrcs?: number[];
2178
+ rids?: string[];
2179
+ history: MediasoupProducerSampleEvent[];
2180
+ };
2181
+ type MediasoupConsumerSampleEventMap = {
2182
+ 'pause': undefined;
2183
+ 'resume': undefined;
2184
+ 'producerPaused': undefined;
2185
+ 'producerResumed': undefined;
2186
+ 'stopped': undefined;
2187
+ 'started': undefined;
2188
+ 'degraded': undefined;
2189
+ 'restored': undefined;
2190
+ };
2191
+ type MediasoupConsumerSampleEvent = {
2192
+ [K in keyof MediasoupConsumerSampleEventMap]: SampleHistoryItem<K>;
2193
+ }[keyof MediasoupConsumerSampleEventMap];
2194
+ type MediasoupConsumerSample = Record<string, unknown> & {
2195
+ id: string;
2196
+ producerId: string;
2197
+ transportId: string;
2198
+ createdAt: number;
2199
+ closedAt?: number;
2200
+ kind: 'audio' | 'video';
2201
+ history: MediasoupConsumerSampleEvent[];
2202
+ };
2203
+ type MediasoupDataProducerSample = Record<string, unknown> & {
2204
+ id: string;
2205
+ transportId: string;
2206
+ createdAt: number;
2207
+ closedAt?: number;
2208
+ label: string;
2209
+ protocol: string;
2210
+ };
2211
+ type MediasoupDataConsumerSample = Record<string, unknown> & {
2212
+ id: string;
2213
+ dataProducerId: string;
2214
+ transportId: string;
2215
+ createdAt: number;
2216
+ closedAt?: number;
2217
+ label: string;
2218
+ protocol: string;
2219
+ };
2220
+
2221
+ type ObservedMediasoupRouterSettings<AppData extends Record<string, unknown> = Record<string, unknown>> = {
2222
+ routerId: string;
2223
+ router: types.Router;
2224
+ appData?: AppData;
2225
+ attachments?: Record<string, unknown>;
2226
+ };
2227
+ type ObservedMediasoupRouterEvents = {
2228
+ close: [];
2229
+ };
2230
+ declare interface ObservedMediasoupRouter {
2231
+ on<U extends keyof ObservedMediasoupRouterEvents>(event: U, listener: (...args: ObservedMediasoupRouterEvents[U]) => void): this;
2232
+ off<U extends keyof ObservedMediasoupRouterEvents>(event: U, listener: (...args: ObservedMediasoupRouterEvents[U]) => void): this;
2233
+ once<U extends keyof ObservedMediasoupRouterEvents>(event: U, listener: (...args: ObservedMediasoupRouterEvents[U]) => void): this;
2234
+ emit<U extends keyof ObservedMediasoupRouterEvents>(event: U, ...args: ObservedMediasoupRouterEvents[U]): boolean;
2235
+ }
2236
+ declare class ObservedMediasoupRouter<AppData extends Record<string, unknown> = Record<string, unknown>> extends EventEmitter {
2237
+ readonly router: types.Router;
2238
+ readonly sample: MediasoupRouterSample;
2239
+ appData: AppData;
2240
+ readonly webrtcTransportIds: Set<string>;
2241
+ get attachments(): Record<string, unknown>;
2242
+ closed: boolean;
2243
+ constructor(settings: ObservedMediasoupRouterSettings<AppData>);
2244
+ get id(): string;
2245
+ addTransport: (transport: types.Transport) => void;
2246
+ addWebRtcTransport(transport: types.WebRtcTransport): void;
2247
+ addPlainTransport(transport: types.PlainTransport): void;
2248
+ addPipeTransport(transport: types.PipeTransport): void;
2249
+ addDirectTransport(transport: types.DirectTransport): void;
2250
+ addProducer(transport: types.Transport, producer: types.Producer): void;
2251
+ addConsumer(transport: types.Transport, consumer: types.Consumer): void;
2252
+ addDataProducer(transport: types.Transport, dataProducer: types.DataProducer): void;
2253
+ addDataConsumer(transport: types.Transport, dataConsumer: types.DataConsumer): void;
2254
+ close(): void;
2255
+ private attachRouterListeners;
2256
+ private _attachTransportObserverListeners;
2257
+ }
2258
+
2085
2259
  /**
2086
2260
  * The Observer is the single event bus for the whole hierarchy. Every event
2087
2261
  * worth subscribing to is emitted on the Observer with a payload object that
@@ -2109,6 +2283,9 @@ type ObservedClientScope = ObservedCallScope & {
2109
2283
  type ObservedPeerConnectionScope = ObservedClientScope & {
2110
2284
  observedPeerConnection: ObservedPeerConnection;
2111
2285
  };
2286
+ type ObservedMediasoupRouterScope = Omit<ObserverEventBase, 'context'> & {
2287
+ observedMediasoupRouter: ObservedMediasoupRouter;
2288
+ };
2112
2289
  type ObserverEvents = {
2113
2290
  'observer-updated': [ObserverEventBase];
2114
2291
  'observer-closed': [ObserverEventBase];
@@ -2116,6 +2293,9 @@ type ObserverEvents = {
2116
2293
  reason: SampleRejectedReason;
2117
2294
  sample: ClientSample;
2118
2295
  }];
2296
+ 'mediasoup-router-added': [ObservedMediasoupRouterScope];
2297
+ 'mediasoup-router-removed': [ObservedMediasoupRouterScope];
2298
+ 'mediasoup-router-matched-with-call': [ObservedMediasoupRouterScope & ObservedCallScope];
2119
2299
  'call-added': [ObservedCallScope];
2120
2300
  'call-updated': [ObservedCallScope];
2121
2301
  'call-closed': [ObservedCallScope];
@@ -2636,6 +2816,7 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
2636
2816
  readonly config: ObserverConfig<AppData>;
2637
2817
  readonly observedTURN: ObservedTURN;
2638
2818
  readonly observedCalls: Map<string, ObservedCall<Record<string, unknown>>>;
2819
+ readonly observedMediasoupRouters: Map<string, ObservedMediasoupRouter<Record<string, unknown>>>;
2639
2820
  updater?: Updater;
2640
2821
  /** Ancestry base shared by all Observer-bus events originating at the observer. */
2641
2822
  readonly eventScope: ObserverEventBase;
@@ -2656,6 +2837,10 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
2656
2837
  getObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(callId: string): ObservedCall<T> | undefined;
2657
2838
  createObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedCallSettings<T>): ObservedCall<T> | undefined;
2658
2839
  getOrCreateObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedCallSettings<T>): ObservedCall<T> | undefined;
2840
+ createObservedMediasoupRouter<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedMediasoupRouterSettings<T> & {
2841
+ callId?: string;
2842
+ bindCallByWebRtcTransportId?: boolean;
2843
+ }): ObservedMediasoupRouter<Record<string, unknown>> | undefined;
2659
2844
  close(): void;
2660
2845
  accept(sample: ClientSample, context?: AcceptContext): void;
2661
2846
  update(): void;
@@ -2768,4 +2953,4 @@ declare function createInMemorySink(samples?: ClientSample[]): InMemorySink;
2768
2953
  declare function createDefaultMediasoupRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
2769
2954
  declare function createP2pRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
2770
2955
 
2771
- export { type AcceptContext, type AcceptMiddleware, type AcceptMiddlewarePayload, type CallAppDataFactory, type ClientAppDataFactory, type ClientEvent, ClientEventTypes, type ClientIssue, type ClientMetaData, ClientMetaTypes, type ClientSample, ClientSampleSink, type ClientSampleSinkEvents, type ClientSampleSinkFactory, type Detector, Detectors, InMemorySink, JsonlFileSink, type JsonlFileSinkFactoryOptions, type JsonlFileSinkOptions, type Logger, type Middleware, ObservedCall, type ObservedCallScope, ObservedCertificate, ObservedClient, type ObservedClientScope, ObservedCodec, ObservedDataChannel, ObservedIceCandidate, ObservedIceCandidatePair, ObservedIceTransport, ObservedInboundRtp, ObservedInboundTrack, ObservedMediaPlayout, ObservedMediaSource, ObservedOutboundRtp, ObservedOutboundTrack, ObservedPeerConnection, type ObservedPeerConnectionScope, ObservedPeerConnectionTransport, ObservedRemoteInboundRtp, ObservedRemoteOutboundRtp, Observer, type ObserverEventBase, type ObserverEvents, type ObserverLogger, RemoteTrackResolver, type RemoteTrackResolverFactory, type RemoteTrackResolvers, type SampleRejectedReason, type ScoreCalculator, createDefaultMediasoupRemoteTrackResolverFactory, createInMemorySink, createJsonlFileSink, createJsonlFileSinkFactory, createLogger, createP2pRemoteTrackResolverFactory, setObserverLogger };
2956
+ export { type AcceptContext, type AcceptMiddleware, type AcceptMiddlewarePayload, type CallAppDataFactory, type ClientAppDataFactory, type ClientEvent, ClientEventTypes, type ClientIssue, type ClientMetaData, ClientMetaTypes, type ClientSample, ClientSampleSink, type ClientSampleSinkEvents, type ClientSampleSinkFactory, type Detector, Detectors, InMemorySink, JsonlFileSink, type JsonlFileSinkFactoryOptions, type JsonlFileSinkOptions, type Logger, type MediasoupConsumerSample, type MediasoupConsumerSampleEvent, type MediasoupDataConsumerSample, type MediasoupDataProducerSample, type MediasoupDirectTransportSample, type MediasoupDirectTransportSampleEventMap, type MediasoupPipeTransportSample, type MediasoupPipeTransportSampleEventMap, type MediasoupPlainTransportSample, type MediasoupPlainTransportSampleEventMap, type MediasoupProducerSample, type MediasoupProducerSampleEvent, type MediasoupRouterSample, type MediasoupTransportSample, type MediasoupWebRtcTransportSample, type MediasoupWebRtcTransportSampleEventMap, type Middleware, ObservedCall, type ObservedCallScope, ObservedCertificate, ObservedClient, type ObservedClientScope, ObservedCodec, ObservedDataChannel, ObservedIceCandidate, ObservedIceCandidatePair, ObservedIceTransport, ObservedInboundRtp, ObservedInboundTrack, ObservedMediaPlayout, ObservedMediaSource, ObservedMediasoupRouter, type ObservedMediasoupRouterEvents, type ObservedMediasoupRouterScope, type ObservedMediasoupRouterSettings, ObservedOutboundRtp, ObservedOutboundTrack, ObservedPeerConnection, type ObservedPeerConnectionScope, ObservedPeerConnectionTransport, ObservedRemoteInboundRtp, ObservedRemoteOutboundRtp, Observer, type ObserverEventBase, type ObserverEvents, type ObserverLogger, RemoteTrackResolver, type RemoteTrackResolverFactory, type RemoteTrackResolvers, type SampleRejectedReason, type ScoreCalculator, createDefaultMediasoupRemoteTrackResolverFactory, createInMemorySink, createJsonlFileSink, createJsonlFileSinkFactory, createLogger, createP2pRemoteTrackResolverFactory, setObserverLogger };