@observertc/observer-js 1.0.0-beta.7 → 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,12 +13,30 @@ 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.
16
37
  > A companion doc, [`docs/logging.md`](./docs/logging.md), covers logging integration in depth.
17
38
 
18
- > **Packaging:** server-side, **Node.js ≥ 20**, shipped as a **dual ESM + CommonJS** build — so it
39
+ > **Packaging:** server-side, **Node.js ≥ 22**, shipped as a **dual ESM + CommonJS** build — so it
19
40
  > works whether your project uses `import` (ESM) or `require()` (CommonJS). Everything — including
20
41
  > the built-in file sink — is exported from the single `@observertc/observer-js` entry.
21
42
 
@@ -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
 
@@ -52,7 +71,7 @@ npm install @observertc/observer-js
52
71
  yarn add @observertc/observer-js
53
72
  ```
54
73
 
55
- **Server-side, Node.js ≥ 20, dual ESM + CommonJS.** The package ships both module formats, so it
74
+ **Server-side, Node.js ≥ 22, dual ESM + CommonJS.** The package ships both module formats, so it
56
75
  works the same whether your project is ESM or CommonJS — your import line is unchanged either way:
57
76
 
58
77
  ```ts
@@ -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
@@ -297,7 +291,8 @@ These return `undefined` (and warn) when the parent is closed; `createObservedCa
297
291
  "Update" means *recompute aggregated metrics and emit the `*-updated` event* at that level.
298
292
  Both the observer and each call have a configurable trigger. Updates are **event-driven** — there
299
293
  is no built-in timer. An app that wants a fixed cadence can call `observer.update()` /
300
- `call.update()` from its own `setInterval`.
294
+ `call.update()` from its own `setInterval`. With `'none'`, **nothing auto-updates** — the level
295
+ updates only when the application calls the public `update()` itself.
301
296
 
302
297
  **Observer-level** (`ObserverConfig.updatePolicy`, default `update-when-all-call-updated`):
303
298
 
@@ -305,6 +300,7 @@ is no built-in timer. An app that wants a fixed cadence can call `observer.updat
305
300
  |--------|-------------------------------------|
306
301
  | `update-on-any-call-updated` | any call updates |
307
302
  | `update-when-all-call-updated` | every call has updated since the last observer update |
303
+ | `none` | never automatically — only when the app calls `observer.update()` |
308
304
 
309
305
  **Call-level** (`ObservedCallSettings.updatePolicy`, defaulted from
310
306
  `ObserverConfig.defaultCallUpdatePolicy`):
@@ -313,6 +309,7 @@ is no built-in timer. An app that wants a fixed cadence can call `observer.updat
313
309
  |--------|--------------------------------|
314
310
  | `update-on-any-client-updated` | any client in the call updates |
315
311
  | `update-when-all-client-updated` | every client has updated since the last call update |
312
+ | `none` | never automatically — only when the app calls `call.update()` |
316
313
 
317
314
  ---
318
315
 
@@ -361,6 +358,16 @@ additional field(s) on top of that scope.
361
358
  | `observer-closed` | — | `observer.close()` |
362
359
  | `sample-rejected` | `{ reason: 'observer-closed' \| 'missing-callId' \| 'missing-clientId', sample: ClientSample }` | a sample was dropped by `accept()` |
363
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
+
364
371
  #### Call level — scope `{ observer, observedCall }`
365
372
 
366
373
  | Event | Extra | Fires when |
@@ -437,7 +444,7 @@ listen to them, but prefer the bus equivalents above for application logic.
437
444
  new Observer<AppData>(config?: ObserverConfig<AppData>)
438
445
 
439
446
  type ObserverConfig<AppData = Record<string, unknown>> = {
440
- updatePolicy?: 'update-on-any-call-updated' | 'update-when-all-call-updated';
447
+ updatePolicy?: 'update-on-any-call-updated' | 'update-when-all-call-updated' | 'none';
441
448
  defaultCallUpdatePolicy?: ObservedCallSettings['updatePolicy'];
442
449
  appData?: AppData;
443
450
  closeClientIfIdleForMs?: number;
@@ -488,7 +495,7 @@ Key members:
488
495
 
489
496
  ```ts
490
497
  type ObservedCallSettings<AppData = Record<string, unknown>> = {
491
- updatePolicy?: 'update-on-any-client-updated' | 'update-when-all-client-updated';
498
+ updatePolicy?: 'update-on-any-client-updated' | 'update-when-all-client-updated' | 'none';
492
499
  callId: string;
493
500
  appData?: AppData;
494
501
  closeCallIfEmptyForMs?: number;
@@ -617,6 +624,74 @@ mediasoup set `PRODUCER_*` / `CONSUMER_*` / `DATA_PRODUCER_*` / `DATA_CONSUMER_*
617
624
  `MEDIA_DEVICES_SUPPORTED_CONSTRAINTS`, `USER_MEDIA_ERROR`, `LOCAL_SDP`, `OPERATION_SYSTEM`,
618
625
  `ENGINE`, `PLATFORM`, `BROWSER`.
619
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
+
620
695
  ---
621
696
 
622
697
  ## Detectors (server-side extension point)
@@ -713,6 +788,135 @@ For the mediasoup factory, the application puts `producerId` / `consumerId` (and
713
788
 
714
789
  ---
715
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
+
716
920
  ## Sinks (per-client sample persistence)
717
921
 
718
922
  A **sink** receives the samples a client accepts — for archival, streaming, or later offline
@@ -875,48 +1079,6 @@ are caught by `accept()` and surfaced as a warning.
875
1079
 
876
1080
  ---
877
1081
 
878
- ## Public exports
879
-
880
- ```ts
881
- // Entry: src/index.ts
882
- export { Observer } from './Observer';
883
- export type { ObserverEvents, SampleRejectedReason, AcceptContext, CallAppDataFactory, ClientAppDataFactory } from './Observer';
884
- export type { ObserverEventBase, ObservedCallScope, ObservedClientScope, ObservedPeerConnectionScope } from './ObserverEvents';
885
-
886
- export { ObservedCall, ObservedClient, ObservedPeerConnection } from './…';
887
- export { ObservedInboundTrack, ObservedOutboundTrack } from './…';
888
- export { ObservedInboundRtp, ObservedOutboundRtp, ObservedRemoteInboundRtp, ObservedRemoteOutboundRtp } from './…';
889
- export { ObservedMediaSource, ObservedMediaPlayout, ObservedCodec, ObservedCertificate, ObservedDataChannel } from './…';
890
- export { ObservedIceCandidate, ObservedIceCandidatePair, ObservedIceTransport, ObservedPeerConnectionTransport } from './…';
891
-
892
- export { ClientSample, ClientIssue, ClientEvent, ClientMetaData } from './schema/ClientSample';
893
- export { ClientEventTypes } from './schema/ClientEventTypes';
894
- export { ClientMetaTypes } from './schema/ClientMetaTypes';
895
-
896
- export { ScoreCalculator } from './scores/ScoreCalculator';
897
- export { Detectors } from './detectors/Detectors';
898
- export type { Detector } from './detectors/Detector';
899
-
900
- export { createLogger, setObserverLogger } from './common/logger';
901
- export type { Logger, ObserverLogger } from './common/logger';
902
-
903
- // sinks: base class (subclass it for a custom destination) + built-ins
904
- export { ClientSampleSink } from './sinks/ClientSampleSink';
905
- export type { ClientSampleSinkEvents, ClientSampleSinkFactory } from './sinks/ClientSampleSink';
906
- export { JsonlFileSink, createJsonlFileSink, createJsonlFileSinkFactory } from './sinks/JsonlFileSink';
907
- export type { JsonlFileSinkOptions, JsonlFileSinkFactoryOptions } from './sinks/JsonlFileSink';
908
- export { InMemorySink, createInMemorySink } from './sinks/InMemorySink';
909
-
910
- export { Middleware } from './common/Middleware';
911
-
912
- // remote track correlation
913
- export { RemoteTrackResolver } from './utils/RemoteTrackResolver';
914
- export type { RemoteTrackResolvers, RemoteTrackResolverFactory } from './utils/RemoteTrackResolver';
915
- export { createDefaultMediasoupRemoteTrackResolverFactory, createP2pRemoteTrackResolverFactory } from './utils/RemoteTrackResolverFactories';
916
- ```
917
-
918
- ---
919
-
920
1082
  ## Development & extension guide
921
1083
 
922
1084
  ```bash
@@ -966,24 +1128,6 @@ event map + scope types), `detectors/` (`Detector`, `Detectors`), `scores/`, `up
966
1128
 
967
1129
  ---
968
1130
 
969
- ## Not yet implemented / roadmap
970
-
971
- For an agent continuing the work, these are explicitly **not** present yet:
972
-
973
- - **Tests.** There is currently only a placeholder spec. The two `accept()` methods
974
- (`ObservedClient`, `ObservedPeerConnection`) are the priority for characterization tests. (A
975
- CI gate running lint + typecheck + test is already in place — see `.github/workflows/ci.yml`.)
976
- - **Built-in detectors / quality classifier.** The registry exists; concrete server-side
977
- detectors (e.g. producer→consumer delivery mismatch, quality outlier, asymmetric media) are
978
- to be designed.
979
- - **Per-tick snapshot API.** A serializable snapshot per `*-updated` tick to replace consuming
980
- the fine-grained `*-updated` events (under consideration).
981
- - **Monotonic-timestamp / clock-skew handling** for client clock jumps.
982
- - **Batch `processSamples()`** entry point for offline analysis of recorded sample streams.
983
- - **Expanded derived metrics** (jitter-buffer delay, concealment, freeze fraction, encode/decode
984
- CPU, quality-limitation breakdown, etc.) and first-class producer/consumer & PC-direction
985
- fields beyond `attachments`.
986
-
987
1131
  ## License
988
1132
 
989
1133
  Apache-2.0. Part of the [ObserverTC](https://github.com/observertc) ecosystem.