@observertc/observer-js 1.0.0-beta.5 → 1.0.0-beta.6

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
@@ -104,9 +104,8 @@ import { Observer, ClientSample } from '@observertc/observer-js';
104
104
 
105
105
  // 1. Create an observer.
106
106
  const observer = new Observer({
107
- // how often the observer aggregates call/client metrics:
108
- updatePolicy: 'update-on-interval',
109
- updateIntervalInMs: 5000,
107
+ // when the observer aggregates call/client metrics:
108
+ updatePolicy: 'update-when-all-call-updated',
110
109
  // default policy applied to calls created automatically by accept():
111
110
  defaultCallUpdatePolicy: 'update-on-any-client-updated',
112
111
  // optional auto-teardown:
@@ -177,7 +176,7 @@ client getStats() ──► ClientSample ──► observer.accept(sample, c
177
176
  |-------|-----------|------------------------|-------|
178
177
  | `Observer` | `new Observer(config?)` | — (root) | `observedCalls: Map<string, ObservedCall>`, global counters, the event bus |
179
178
  | `ObservedCall` | `observer.createObservedCall(settings)` / lazily by `accept` | `observedCalls` | `observedClients: Map<string, ObservedClient>`, call-wide metrics, `detectors`, `scoreCalculator` |
180
- | `ObservedClient` | `call.createObservedClient(settings)` / lazily | `observedClients` | `observedPeerConnections: Map<string, ObservedPeerConnection>`, per-client metrics, `report` |
179
+ | `ObservedClient` | `call.createObservedClient(settings)` / lazily | `observedClients` | `observedPeerConnections: Map<string, ObservedPeerConnection>`, per-client metrics |
181
180
  | `ObservedPeerConnection` | lazily, from `sample.peerConnections[]` | `observedPeerConnections` | the 15 sub-stat maps below, transport/RTT/bitrate metrics |
182
181
  | Sub-stats | lazily, from the `PeerConnectionSample` | maps on the PC | individual WebRTC stat objects |
183
182
 
@@ -206,12 +205,49 @@ fields from the schema plus derived fields (deltas, bitrates).
206
205
 
207
206
  The single entry point. It:
208
207
 
209
- 1. drops + emits `sample-rejected` if the observer is closed, or `callId`/`clientId` is missing;
210
- 2. gets or lazily creates the `ObservedCall` and `ObservedClient`;
211
- 3. shallow-merges `context` into the created/looked-up entity `appData`;
212
- 4. delegates to `client.accept(sample, context)`, which fans out to each
208
+ 1. drops + emits `sample-rejected` if the observer is closed;
209
+ 2. runs the sample through the **global accept-middleware chain** (see below);
210
+ 3. (chain terminal) drops + emits `sample-rejected` if `callId`/`clientId` is missing;
211
+ 4. gets or lazily creates the `ObservedCall` and `ObservedClient` (their `appData` comes from the
212
+ configured factories, never from `context`);
213
+ 5. delegates to `client.accept(sample, context)`, which fans out to each
213
214
  `ObservedPeerConnection.accept(pcSample, context)`.
214
215
 
216
+ ### Accept middlewares (global pre-dispatch hook)
217
+
218
+ `observer.addAcceptMiddleware(...)` registers middlewares run on **every** sample inside
219
+ `accept()`, in order, **before** the sample is dispatched to any call or client. Each middleware
220
+ gets a `{ sample, context }` payload; it can inspect or mutate the sample (set/normalize
221
+ `callId`/`clientId`, enrich, redact) or the context, then call `next(payload)` to continue.
222
+ **Not calling `next` drops the sample** — nothing is created and no event fires. A throwing
223
+ middleware is caught and warns (the sample is dropped), never crashing `accept()`.
224
+
225
+ ```ts
226
+ import { Observer, AcceptMiddleware } from '@observertc/observer-js';
227
+
228
+ const observer = new Observer();
229
+
230
+ // derive callId/clientId from the app's own attachment, before dispatch
231
+ const route: AcceptMiddleware = ({ sample }, next) => {
232
+ sample.callId ??= sample.attachments?.roomId as string;
233
+ sample.clientId ??= sample.attachments?.peerId as string;
234
+ next({ sample });
235
+ };
236
+
237
+ // drop samples from a blocklisted client (never dispatched)
238
+ const filter: AcceptMiddleware = (payload, next) => {
239
+ if (blocked.has(payload.sample.clientId)) return; // no next() => dropped
240
+ next(payload);
241
+ };
242
+
243
+ observer.addAcceptMiddleware(route, filter);
244
+ // observer.removeAcceptMiddleware(route);
245
+ ```
246
+
247
+ This is a lightweight global injection point, distinct from the larger (not-yet-built)
248
+ `ClientSampleProcessor` pipeline in the roadmap. When no middleware is registered, `accept()`
249
+ dispatches directly with no overhead.
250
+
215
251
  ### `context` (the `AcceptContext`)
216
252
 
217
253
  ```ts
@@ -259,9 +295,9 @@ These return `undefined` (and warn) when the parent is closed; `createObservedCa
259
295
  ## Update policies
260
296
 
261
297
  "Update" means *recompute aggregated metrics and emit the `*-updated` event* at that level.
262
- Both the observer and each call have a configurable trigger. The `update-on-interval` policy
263
- **requires** an interval and this is enforced at compile time (a discriminated union); at
264
- runtime a missing interval warns and falls back rather than throwing.
298
+ Both the observer and each call have a configurable trigger. Updates are **event-driven** — there
299
+ is no built-in timer. An app that wants a fixed cadence can call `observer.update()` /
300
+ `call.update()` from its own `setInterval`.
265
301
 
266
302
  **Observer-level** (`ObserverConfig.updatePolicy`, default `update-when-all-call-updated`):
267
303
 
@@ -269,7 +305,6 @@ runtime a missing interval warns and falls back rather than throwing.
269
305
  |--------|-------------------------------------|
270
306
  | `update-on-any-call-updated` | any call updates |
271
307
  | `update-when-all-call-updated` | every call has updated since the last observer update |
272
- | `update-on-interval` | a timer fires (`updateIntervalInMs` required) |
273
308
 
274
309
  **Call-level** (`ObservedCallSettings.updatePolicy`, defaulted from
275
310
  `ObserverConfig.defaultCallUpdatePolicy`):
@@ -278,7 +313,6 @@ runtime a missing interval warns and falls back rather than throwing.
278
313
  |--------|--------------------------------|
279
314
  | `update-on-any-client-updated` | any client in the call updates |
280
315
  | `update-when-all-client-updated` | every client has updated since the last call update |
281
- | `update-on-interval` | a timer fires (`updateIntervalInMs` required) |
282
316
 
283
317
  ---
284
318
 
@@ -353,7 +387,6 @@ additional field(s) on top of that scope.
353
387
  | `client-metadata` | `{ metaData: ClientMetaData }` | a client meta item arrived |
354
388
  | `client-extension-stats` | `{ extensionStats: ExtensionStat }` | an app-defined extension stat arrived |
355
389
  | `client-event` | `{ event: ClientEvent }` | any client event was processed |
356
- | `client-track-report` | `{ report: TrackReport }` | a track was removed; final per-track report |
357
390
 
358
391
  #### Peer-connection level — scope `{ observer, observedCall, observedClient, observedPeerConnection }`
359
392
 
@@ -403,12 +436,9 @@ listen to them, but prefer the bus equivalents above for application logic.
403
436
  ```ts
404
437
  new Observer<AppData>(config?: ObserverConfig<AppData>)
405
438
 
406
- type ObserverConfig<AppData = Record<string, unknown>> =
407
- ( { updatePolicy: 'update-on-interval'; updateIntervalInMs: number }
408
- | { updatePolicy?: 'update-on-any-call-updated' | 'update-when-all-call-updated'; updateIntervalInMs?: number }
409
- ) & {
439
+ type ObserverConfig<AppData = Record<string, unknown>> = {
440
+ updatePolicy?: 'update-on-any-call-updated' | 'update-when-all-call-updated';
410
441
  defaultCallUpdatePolicy?: ObservedCallSettings['updatePolicy'];
411
- defaultCallUpdateIntervalInMs?: number;
412
442
  appData?: AppData;
413
443
  closeClientIfIdleForMs?: number;
414
444
  closeCallIfEmptyForMs?: number;
@@ -418,6 +448,8 @@ type ObserverConfig<AppData = Record<string, unknown>> =
418
448
  createClientAppData?: (p: { clientId: string; observedCall: ObservedCall }) => Record<string, unknown>;
419
449
  // sink factory — produces a per-client sink that receives every accepted sample (see Sinks).
420
450
  createClientSink?: (p: { clientId: string; observedCall: ObservedCall }) => ClientSampleSink | undefined;
451
+ // track-resolver factory — produces a call's RemoteTrackResolver (see Remote track resolution).
452
+ createTrackResolver?: (observedCall: ObservedCall) => RemoteTrackResolver | undefined;
421
453
  };
422
454
  ```
423
455
 
@@ -438,6 +470,7 @@ const observer = new Observer({
438
470
  Key members:
439
471
 
440
472
  - `accept(sample: ClientSample, context?: AcceptContext): void`
473
+ - `addAcceptMiddleware(...mw: AcceptMiddleware[]): this` / `removeAcceptMiddleware(...mw): this` — global pre-dispatch sample hooks (see [Accept middlewares](#accept-middlewares-global-pre-dispatch-hook))
441
474
  - `getObservedCall<T>(callId): ObservedCall<T> | undefined`
442
475
  - `createObservedCall<T>(settings): ObservedCall<T> | undefined`
443
476
  - `getOrCreateObservedCall<T>(settings): ObservedCall<T> | undefined`
@@ -454,13 +487,10 @@ Key members:
454
487
  ### `ObservedCall`
455
488
 
456
489
  ```ts
457
- type ObservedCallSettings<AppData = Record<string, unknown>> =
458
- ( { updatePolicy: 'update-on-interval'; updateIntervalInMs: number }
459
- | { updatePolicy?: 'update-on-any-client-updated' | 'update-when-all-client-updated'; updateIntervalInMs?: number }
460
- ) & {
490
+ type ObservedCallSettings<AppData = Record<string, unknown>> = {
491
+ updatePolicy?: 'update-on-any-client-updated' | 'update-when-all-client-updated';
461
492
  callId: string;
462
493
  appData?: AppData;
463
- remoteTrackResolvePolicy?: 'p2p' | 'mediasoup-sfu' | 'none';
464
494
  closeCallIfEmptyForMs?: number;
465
495
  };
466
496
  ```
@@ -473,7 +503,7 @@ Key members:
473
503
  - `addIssue(issue: ClientIssue): void` — raise a **call-level** issue → emits `call-issue`
474
504
  - `readonly detectors: Detectors` — server-side detector registry (empty by default; see [Detectors](#detectors-server-side-extension-point))
475
505
  - `scoreCalculator: ScoreCalculator`, `get score()`, `readonly calculatedScore`
476
- - `remoteTrackResolver?: RemoteTrackResolver`
506
+ - `remoteTrackResolver?: RemoteTrackResolver` — set from `ObserverConfig.createTrackResolver` at call creation (see [Remote track resolution](#remote-track-resolution-mediasoup--sfu))
477
507
  - aggregates: `numberOfIssues`, `numberOfPeerConnections`, `numberOfInboundRtpStreams`,
478
508
  `numberOfOutboundRtpStreams`, `numberOfDataChannels`, `maxNumberOfClients`,
479
509
  `clientsUsedTurn: Set<string>`, `startedAt?`, `endedAt?`, `closedAt?`, `closed`
@@ -507,7 +537,6 @@ Key members:
507
537
  - Per-tick deltas: `deltaReceivedAudioBytes`, `deltaSentAudioBytes`, … (see source for the full set)
508
538
  - Lifecycle: `joinedAt?`, `leftAt?`, `closedAt?`, `closed`, `get score()`
509
539
  - Metadata: `browser?`, `engine?`, `platform?`, `operationSystem?`, `mediaDevices`, `mediaConstraints`
510
- - `readonly report: ClientReport` — cumulative report (byte/packet totals + RTT & score distributions)
511
540
  - `accept(sample, context?)`, `close()`
512
541
 
513
542
  ### `ObservedPeerConnection`
@@ -588,9 +617,6 @@ mediasoup set `PRODUCER_*` / `CONSUMER_*` / `DATA_PRODUCER_*` / `DATA_CONSUMER_*
588
617
  `MEDIA_DEVICES_SUPPORTED_CONSTRAINTS`, `USER_MEDIA_ERROR`, `LOCAL_SDP`, `OPERATION_SYSTEM`,
589
618
  `ENGINE`, `PLATFORM`, `BROWSER`.
590
619
 
591
- `Reports` exported: `ClientReport` (cumulative per-client totals + RTT/score distributions) and
592
- `TrackReport` (per-track final report, delivered on `client-track-report`).
593
-
594
620
  ---
595
621
 
596
622
  ## Detectors (server-side extension point)
@@ -643,29 +669,47 @@ class Detectors {
643
669
  ## Remote track resolution (mediasoup / SFU)
644
670
 
645
671
  In an SFU, one participant's **outbound** track is delivered to other participants as **inbound**
646
- tracks. To correlate them server-side, set `remoteTrackResolvePolicy: 'mediasoup-sfu'` on the
647
- call settings. The built-in `MediasoupRemoteTrackResolver` subscribes to the bus (filtered to
648
- its call) and maps producer↔consumer using track `attachments` (`producerId`, `consumerId`):
672
+ tracks (one **publisher** → many **subscribers**). Correlation is **opt-in** per observer: set
673
+ `ObserverConfig.createTrackResolver`, a factory invoked when each call is created that returns the
674
+ call's `RemoteTrackResolver` (or `undefined` for none).
675
+
676
+ `RemoteTrackResolver` is a generic, strategy-driven class. It subscribes to the bus (filtered to
677
+ its call) and links tracks by **publisher id** — the link key — maintaining the links directly on
678
+ the tracks: `inboundTrack.remoteOutboundTrack` and `outboundTrack.remoteInboundTracks: Set`.
649
679
 
650
680
  ```ts
651
- const call = observer.createObservedCall({ callId, remoteTrackResolvePolicy: 'mediasoup-sfu' });
652
- // later, given an inbound track:
653
- const source = inboundTrack.getRemoteOutboundTrack(); // the producing ObservedOutboundTrack
654
- const consumers = outboundTrack.getRemoteInboundTracks(); // the consuming ObservedInboundTrack[]
681
+ import { Observer, createDefaultMediasoupRemoteTrackResolverFactory } from '@observertc/observer-js';
682
+
683
+ const observer = new Observer({
684
+ createTrackResolver: createDefaultMediasoupRemoteTrackResolverFactory(),
685
+ });
686
+
687
+ // later, given tracks (links are kept up to date as tracks come and go):
688
+ const source = inboundTrack.remoteOutboundTrack; // the publishing ObservedOutboundTrack
689
+ const receivers = [ ...outboundTrack.remoteInboundTracks ]; // the subscribing ObservedInboundTrack[]
655
690
  ```
656
691
 
657
- Custom topologies can implement the `RemoteTrackResolver` interface and assign
658
- `call.remoteTrackResolver`:
692
+ Two built-in factories ship: `createDefaultMediasoupRemoteTrackResolverFactory()` (publisher =
693
+ `attachments.producerId`, subscriber = `attachments.consumerId`) and
694
+ `createP2pRemoteTrackResolverFactory()` (matches by RTP **SSRC**, preserved end-to-end in p2p).
695
+
696
+ For any other topology, build a `RemoteTrackResolver` with your own key resolvers — the publisher
697
+ id is just whatever links a subscribed track to the published one:
659
698
 
660
699
  ```ts
661
- interface RemoteTrackResolver {
662
- resolveRemoteOutboundTrack(inboundTrack: ObservedInboundTrack): ObservedOutboundTrack | undefined;
663
- resolveRemoteInboundTracks(outboundTrack: ObservedOutboundTrack): ObservedInboundTrack[] | undefined;
664
- }
700
+ import { Observer, RemoteTrackResolver } from '@observertc/observer-js';
701
+
702
+ const observer = new Observer({
703
+ createTrackResolver: (observedCall) => new RemoteTrackResolver(observedCall, {
704
+ resolveOutboundTrackPublisherId: (out) => out.attachments?.mediaId as string | undefined,
705
+ resolveInboundTrackPublisherId: (inb) => inb.attachments?.mediaId as string | undefined,
706
+ resolveInboundTrackSubscriberId: (inb) => inb.attachments?.subId as string | undefined, // optional
707
+ }),
708
+ });
665
709
  ```
666
710
 
667
- The application is expected to put `direction` (`'send'`/`'recv'`), `producerId`, `consumerId`,
668
- and `label` into `PeerConnectionSample.attachments` / track `attachments`.
711
+ For the mediasoup factory, the application puts `producerId` / `consumerId` (and optionally
712
+ `direction`, `label`) into the track `attachments`.
669
713
 
670
714
  ---
671
715
 
@@ -819,14 +863,15 @@ filtering, per-module routing, and full silencing.
819
863
 
820
864
  The library **warns and degrades; it does not throw** on operational problems:
821
865
 
822
- - Bad config (`update-on-interval` without an interval) → warn + safe fallback policy.
823
866
  - `createObservedCall` / `createObservedClient` on a closed parent → warn + return `undefined`.
824
867
  - Duplicate id → warn + return the **existing** instance.
825
868
  - `accept()` on a closed client → warn + no-op.
826
869
  - Sample missing `callId`/`clientId`, or observer closed → `sample-rejected` event.
870
+ - A throwing accept-middleware → warn + drop that sample (never crashes `accept()`).
827
871
 
828
- Therefore `create*` and `getOrCreate*` return `T | undefined`; **guard the result.** The only
829
- remaining `throw`s are internal invariants in the unused `Middleware` utility.
872
+ Therefore `create*` and `getOrCreate*` return `T | undefined`; **guard the result.** The
873
+ `Middleware` utility's internal invariants (e.g. calling `next()` twice) throw, but those throws
874
+ are caught by `accept()` and surfaced as a warning.
830
875
 
831
876
  ---
832
877
 
@@ -863,7 +908,11 @@ export type { JsonlFileSinkOptions, JsonlFileSinkFactoryOptions } from './sinks/
863
908
  export { InMemorySink, createInMemorySink } from './sinks/InMemorySink';
864
909
 
865
910
  export { Middleware } from './common/Middleware';
866
- export type { TrackReport, ClientReport } from './Reports';
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';
867
916
  ```
868
917
 
869
918
  ---
package/dist/index.d.mts CHANGED
@@ -1340,6 +1340,25 @@ type CalculatedScore = {
1340
1340
 
1341
1341
  type MediaKind = 'audio' | 'video';
1342
1342
 
1343
+ declare class ObservedMediaPlayout implements MediaPlayoutStats {
1344
+ timestamp: number;
1345
+ id: string;
1346
+ kind: MediaKind;
1347
+ private readonly _peerConnection;
1348
+ private _visited;
1349
+ appData?: Record<string, unknown>;
1350
+ synthesizedSamplesDuration?: number | undefined;
1351
+ synthesizedSamplesEvents?: number | undefined;
1352
+ totalSamplesDuration?: number | undefined;
1353
+ totalPlayoutDelay?: number | undefined;
1354
+ totalSamplesCount?: number | undefined;
1355
+ attachments?: Record<string, unknown> | undefined;
1356
+ constructor(timestamp: number, id: string, kind: MediaKind, _peerConnection: ObservedPeerConnection);
1357
+ get visited(): boolean;
1358
+ getPeerConnection(): ObservedPeerConnection;
1359
+ update(stats: MediaPlayoutStats): void;
1360
+ }
1361
+
1343
1362
  declare class ObservedMediaSource implements MediaSourceStats {
1344
1363
  timestamp: number;
1345
1364
  id: string;
@@ -1579,91 +1598,6 @@ declare class ObservedOutboundRtp implements OutboundRtpStats {
1579
1598
  update(stats: OutboundRtpStats): void;
1580
1599
  }
1581
1600
 
1582
- type InboundAudioTrackReport = {
1583
- trackId: string;
1584
- fractionLostDistribution: {
1585
- lt001: number;
1586
- lt005: number;
1587
- lt010: number;
1588
- lt020: number;
1589
- lt050: number;
1590
- gtOrEq050: number;
1591
- count: number;
1592
- sum: number;
1593
- };
1594
- };
1595
- type InboundVideoTrackReport = {
1596
- trackId: string;
1597
- fractionLostDistribution: {
1598
- lt001: number;
1599
- lt005: number;
1600
- lt010: number;
1601
- lt020: number;
1602
- lt050: number;
1603
- gtOrEq050: number;
1604
- count: number;
1605
- sum: number;
1606
- };
1607
- };
1608
- type OutboundAudioTrackReport = {
1609
- trackId: string;
1610
- };
1611
- type OutboundVideoTrackReport = {
1612
- trackId: string;
1613
- };
1614
- type InboundTrackReport = ({
1615
- kind: 'audio';
1616
- } & InboundAudioTrackReport) | ({
1617
- kind: 'video';
1618
- } & InboundVideoTrackReport);
1619
- type OutboundTrackReport = ({
1620
- kind: 'audio';
1621
- } & OutboundAudioTrackReport) | ({
1622
- kind: 'video';
1623
- } & OutboundVideoTrackReport);
1624
- type TrackReport = ({
1625
- direction: 'inbound';
1626
- } & InboundTrackReport) | ({
1627
- direction: 'outbound';
1628
- } & OutboundTrackReport);
1629
- type ClientReport = {
1630
- callId: string;
1631
- clientId: string;
1632
- totalDataChannelBytesReceived: number;
1633
- totalDataChannelBytesSent: number;
1634
- totalDataChannelMessagesReceived: number;
1635
- totalDataChannelMessagesSent: number;
1636
- totalInboundRtpPacketsReceived: number;
1637
- totalInboundRtpPacketsLost: number;
1638
- totalInboundRtpBytesReceived: number;
1639
- totalOutboundRtpPacketsSent: number;
1640
- totalOutboundRtpBytesSent: number;
1641
- totalAudioBytesReceived: number;
1642
- totalVideoBytesReceived: number;
1643
- totalAudioBytesSent: number;
1644
- totalVideoBytesSent: number;
1645
- totalNumberOfIssues: number;
1646
- issues: Record<string, number>;
1647
- rttDistribution: {
1648
- lt50ms: number;
1649
- lt150ms: number;
1650
- lt300ms: number;
1651
- gtOrEq300ms: number;
1652
- count: number;
1653
- sum: number;
1654
- };
1655
- scoreDistribution: {
1656
- '0': number;
1657
- '1': number;
1658
- '2': number;
1659
- '3': number;
1660
- '4': number;
1661
- '5': number;
1662
- count: number;
1663
- sum: number;
1664
- };
1665
- };
1666
-
1667
1601
  declare class ObservedOutboundTrack implements OutboundTrackSample {
1668
1602
  timestamp: number;
1669
1603
  readonly id: string;
@@ -1673,7 +1607,7 @@ declare class ObservedOutboundTrack implements OutboundTrackSample {
1673
1607
  private readonly _mediaSource?;
1674
1608
  private _visited;
1675
1609
  appData?: Record<string, unknown>;
1676
- readonly report: OutboundTrackReport;
1610
+ readonly remoteInboundTracks: Set<ObservedInboundTrack>;
1677
1611
  readonly calculatedScore: CalculatedScore;
1678
1612
  addedAt?: number | undefined;
1679
1613
  removedAt?: number | undefined;
@@ -1685,29 +1619,9 @@ declare class ObservedOutboundTrack implements OutboundTrackSample {
1685
1619
  getPeerConnection(): ObservedPeerConnection;
1686
1620
  getOutboundRtps(): ObservedOutboundRtp[] | undefined;
1687
1621
  getMediaSource(): ObservedMediaSource | undefined;
1688
- getRemoteInboundTracks(): ObservedInboundTrack[] | undefined;
1689
1622
  update(stats: OutboundTrackSample): void;
1690
1623
  }
1691
1624
 
1692
- declare class ObservedMediaPlayout implements MediaPlayoutStats {
1693
- timestamp: number;
1694
- id: string;
1695
- kind: MediaKind;
1696
- private readonly _peerConnection;
1697
- private _visited;
1698
- appData?: Record<string, unknown>;
1699
- synthesizedSamplesDuration?: number | undefined;
1700
- synthesizedSamplesEvents?: number | undefined;
1701
- totalSamplesDuration?: number | undefined;
1702
- totalPlayoutDelay?: number | undefined;
1703
- totalSamplesCount?: number | undefined;
1704
- attachments?: Record<string, unknown> | undefined;
1705
- constructor(timestamp: number, id: string, kind: MediaKind, _peerConnection: ObservedPeerConnection);
1706
- get visited(): boolean;
1707
- getPeerConnection(): ObservedPeerConnection;
1708
- update(stats: MediaPlayoutStats): void;
1709
- }
1710
-
1711
1625
  declare class ObservedInboundTrack implements InboundTrackSample {
1712
1626
  timestamp: number;
1713
1627
  readonly id: string;
@@ -1717,7 +1631,7 @@ declare class ObservedInboundTrack implements InboundTrackSample {
1717
1631
  private readonly _mediaPlayout?;
1718
1632
  readonly calculatedScore: CalculatedScore;
1719
1633
  appData?: Record<string, unknown>;
1720
- report: InboundTrackReport;
1634
+ remoteOutboundTrack?: ObservedOutboundTrack | undefined;
1721
1635
  private _visited;
1722
1636
  addedAt?: number | undefined;
1723
1637
  removedAt?: number | undefined;
@@ -1729,7 +1643,6 @@ declare class ObservedInboundTrack implements InboundTrackSample {
1729
1643
  getPeerConnection(): ObservedPeerConnection;
1730
1644
  getInboundRtp(): ObservedInboundRtp | undefined;
1731
1645
  getMediaPlayout(): ObservedMediaPlayout | undefined;
1732
- getRemoteOutboundTrack(): ObservedOutboundTrack | undefined;
1733
1646
  update(stats: InboundTrackSample): void;
1734
1647
  }
1735
1648
 
@@ -2237,9 +2150,6 @@ type ObserverEvents = {
2237
2150
  'client-event': [ObservedClientScope & {
2238
2151
  event: ClientEvent;
2239
2152
  }];
2240
- 'client-track-report': [ObservedClientScope & {
2241
- report: TrackReport;
2242
- }];
2243
2153
  'peer-connection-added': [ObservedPeerConnectionScope];
2244
2154
  'peer-connection-updated': [ObservedPeerConnectionScope];
2245
2155
  'peer-connection-closed': [ObservedPeerConnectionScope];
@@ -2479,7 +2389,6 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
2479
2389
  deltaNumberOfIssues: number;
2480
2390
  totalScoreSum: number;
2481
2391
  numberOfScoreMeasurements: number;
2482
- readonly report: ClientReport;
2483
2392
  readonly mediaDevices: MediaDeviceInfo[];
2484
2393
  issues: ClientIssue[];
2485
2394
  private _injections;
@@ -2508,9 +2417,49 @@ interface ScoreCalculator {
2508
2417
  update(): void;
2509
2418
  }
2510
2419
 
2511
- interface RemoteTrackResolver {
2420
+ type RemoteTrackResolverFactory = (observedCall: ObservedCall) => RemoteTrackResolver;
2421
+ /**
2422
+ * Strategy functions that map a track to its publish/subscribe identity. Two tracks are linked
2423
+ * when a subscribed (inbound) track's **publisher id** equals a published (outbound) track's
2424
+ * **publisher id** — one publisher → many subscribers. The **subscriber id** is optional and is
2425
+ * only used to look an inbound track up via `getInboundTrackBySubscriberId`.
2426
+ *
2427
+ * The "publisher id" is whatever a strategy uses as the link key: mediasoup passes its producerId,
2428
+ * a p2p strategy the SSRC, a generic strategy a shared attachment value.
2429
+ */
2430
+ type RemoteTrackResolvers = {
2431
+ /** The publisher id a subscribed (inbound) track receives. Required — this is the link key. */
2432
+ resolveInboundTrackPublisherId: (inboundTrack: ObservedInboundTrack) => string | undefined;
2433
+ /** The publisher id a published (outbound) track represents. Required — this is the link key. */
2434
+ resolveOutboundTrackPublisherId: (outboundTrack: ObservedOutboundTrack) => string | undefined;
2435
+ /** Optional: the inbound track's own subscription id (enables `getInboundTrackBySubscriberId`). */
2436
+ resolveInboundTrackSubscriberId?: (inboundTrack: ObservedInboundTrack) => string | undefined;
2437
+ };
2438
+ /**
2439
+ * Generic, strategy-driven base resolver. It subscribes to the Observer bus (filtered to one call),
2440
+ * links subscribed↔published tracks by publisher id, and maintains the links directly on the tracks
2441
+ * (`inboundTrack.remoteOutboundTrack`, `outboundTrack.remoteInboundTracks`).
2442
+ *
2443
+ * Concrete strategies (mediasoup, p2p-by-SSRC, generic attachment) are just different
2444
+ * {@link RemoteTrackResolvers} passed to this class. Ids are re-resolved on demand rather than
2445
+ * cached, so the only state kept is the two lookup indexes that back the public getters.
2446
+ */
2447
+ declare class RemoteTrackResolver {
2448
+ readonly observedCall: ObservedCall;
2449
+ private readonly resolvers;
2450
+ private readonly _publisherIdToOutboundTrack;
2451
+ private readonly _subscriberIdToInboundTrack;
2452
+ constructor(observedCall: ObservedCall, resolvers: RemoteTrackResolvers);
2453
+ /** The published (outbound) track for a publisher id, if any. */
2454
+ getOutboundTrackByPublisherId(publisherId: string): ObservedOutboundTrack | undefined;
2455
+ /** The subscribed (inbound) track for a subscriber id, if the strategy resolves subscriber ids. */
2456
+ getInboundTrackBySubscriberId(subscriberId: string): ObservedInboundTrack | undefined;
2512
2457
  resolveRemoteOutboundTrack(inboundTrack: ObservedInboundTrack): ObservedOutboundTrack | undefined;
2513
2458
  resolveRemoteInboundTracks(outboundTrack: ObservedOutboundTrack): ObservedInboundTrack[] | undefined;
2459
+ private _addInboundTrack;
2460
+ private _removeInboundTrack;
2461
+ private _addOutboundTrack;
2462
+ private _removeOutboundTrack;
2514
2463
  }
2515
2464
 
2516
2465
  interface Updater {
@@ -2536,16 +2485,11 @@ declare class Detectors {
2536
2485
  }
2537
2486
 
2538
2487
  type ObservedCallUpdateConfig = {
2539
- updatePolicy: 'update-on-interval';
2540
- updateIntervalInMs: number;
2541
- } | {
2542
2488
  updatePolicy?: 'update-on-any-client-updated' | 'update-when-all-client-updated';
2543
- updateIntervalInMs?: number;
2544
2489
  };
2545
2490
  type ObservedCallSettings<AppData extends Record<string, unknown> = Record<string, unknown>> = ObservedCallUpdateConfig & {
2546
2491
  callId: string;
2547
2492
  appData?: AppData;
2548
- remoteTrackResolvePolicy?: 'p2p' | 'mediasoup-sfu' | 'none';
2549
2493
  closeCallIfEmptyForMs?: number;
2550
2494
  };
2551
2495
  type ObservedCallEvents = {
@@ -2606,6 +2550,22 @@ declare class ObservedCall<AppData extends Record<string, unknown> = Record<stri
2606
2550
  private _notify;
2607
2551
  }
2608
2552
 
2553
+ type Middleware<T> = (input: T, next: (nextInput: T) => void) => void;
2554
+ interface Processor<T> {
2555
+ finalCallback?: Callback<T>;
2556
+ process(value: T): void;
2557
+ addMiddleware(...middlewares: Middleware<T>[]): Processor<T>;
2558
+ removeMiddleware(...middlewares: Middleware<T>[]): Processor<T>;
2559
+ }
2560
+ type Callback<T> = (input: T) => void;
2561
+ declare class MiddlewareProcessor<T> implements Processor<T> {
2562
+ private stack;
2563
+ finalCallback?: Callback<T>;
2564
+ addMiddleware(...middlewares: Middleware<T>[]): Processor<T>;
2565
+ removeMiddleware(...middlewares: Middleware<T>[]): Processor<T>;
2566
+ process(value: T): void;
2567
+ }
2568
+
2609
2569
  type SampleRejectedReason = 'observer-closed' | 'missing-callId' | 'missing-clientId';
2610
2570
 
2611
2571
  /**
@@ -2614,12 +2574,20 @@ type SampleRejectedReason = 'observer-closed' | 'missing-callId' | 'missing-clie
2614
2574
  * merged into the `appData` of entities created during the accept pass.
2615
2575
  */
2616
2576
  type AcceptContext = Record<string, unknown>;
2577
+ /** The payload threaded through `accept()` middlewares: the sample and its optional context. */
2578
+ type AcceptMiddlewarePayload = {
2579
+ sample: ClientSample;
2580
+ context?: AcceptContext;
2581
+ };
2582
+ /**
2583
+ * A global middleware run on every sample passed to `observer.accept()`, in registration order,
2584
+ * **before** the sample is dispatched to any call/client. It may inspect or mutate the sample
2585
+ * (e.g. set/normalize `callId`/`clientId`, enrich, redact) or the context, then call
2586
+ * `next(payload)` to continue the chain. Not calling `next` **drops** the sample.
2587
+ */
2588
+ type AcceptMiddleware = Middleware<AcceptMiddlewarePayload>;
2617
2589
  type ObserverUpdateConfig = {
2618
- updatePolicy: 'update-on-interval';
2619
- updateIntervalInMs: number;
2620
- } | {
2621
2590
  updatePolicy?: 'update-on-any-call-updated' | 'update-when-all-call-updated';
2622
- updateIntervalInMs?: number;
2623
2591
  };
2624
2592
  /** Produces the initial `appData` for a call created without an explicit `appData`. */
2625
2593
  type CallAppDataFactory = (params: {
@@ -2633,7 +2601,6 @@ type ClientAppDataFactory = (params: {
2633
2601
  }) => Record<string, unknown>;
2634
2602
  type ObserverConfig<AppData extends Record<string, unknown> = Record<string, unknown>> = ObserverUpdateConfig & {
2635
2603
  defaultCallUpdatePolicy?: ObservedCallSettings['updatePolicy'];
2636
- defaultCallUpdateIntervalInMs?: number;
2637
2604
  appData?: AppData;
2638
2605
  closeClientIfIdleForMs?: number;
2639
2606
  closeCallIfEmptyForMs?: number;
@@ -2651,6 +2618,13 @@ type ObserverConfig<AppData extends Record<string, unknown> = Record<string, unk
2651
2618
  * can be derived from `callId` / `clientId`.
2652
2619
  */
2653
2620
  createClientSink?: ClientSampleSinkFactory;
2621
+ /**
2622
+ * Optional factory invoked when a call is created, producing the call's `RemoteTrackResolver`
2623
+ * (or `undefined` for none). Use the built-ins
2624
+ * (`createDefaultMediasoupRemoteTrackResolverFactory()` / `createP2pRemoteTrackResolverFactory()`)
2625
+ * or build a `RemoteTrackResolver` with custom publisher/subscriber id resolvers.
2626
+ */
2627
+ createTrackResolver?: RemoteTrackResolverFactory;
2654
2628
  };
2655
2629
  declare interface Observer {
2656
2630
  on<U extends keyof ObserverEvents>(event: U, listener: (...args: ObserverEvents[U]) => void): this;
@@ -2674,7 +2648,8 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
2674
2648
  numberOfOutboundRtpStreams: number;
2675
2649
  numberOfDataChannels: number;
2676
2650
  numberOfPeerConnections: number;
2677
- private _timer?;
2651
+ /** Global, pre-dispatch middleware chain run on every accepted sample. */
2652
+ readonly acceptMiddlewares: MiddlewareProcessor<AcceptMiddlewarePayload>;
2678
2653
  constructor(config?: ObserverConfig<AppData>);
2679
2654
  get numberOfCalls(): number;
2680
2655
  get appData(): AppData | undefined;
@@ -2790,6 +2765,7 @@ declare class InMemorySink extends ClientSampleSink {
2790
2765
  }
2791
2766
  declare function createInMemorySink(samples?: ClientSample[]): InMemorySink;
2792
2767
 
2793
- type Middleware<T> = (input: T, next: (nextInput: T) => void) => void;
2768
+ declare function createDefaultMediasoupRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
2769
+ declare function createP2pRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
2794
2770
 
2795
- export { type AcceptContext, type CallAppDataFactory, type ClientAppDataFactory, type ClientEvent, ClientEventTypes, type ClientIssue, type ClientMetaData, ClientMetaTypes, type ClientReport, 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, type SampleRejectedReason, type ScoreCalculator, type TrackReport, createInMemorySink, createJsonlFileSink, createJsonlFileSinkFactory, createLogger, setObserverLogger };
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 };