@observertc/observer-js 1.0.0-beta.10 → 1.0.0-beta.11

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
@@ -63,9 +63,10 @@ and emits a single, unified stream of typed events the application can react to.
63
63
  11. [Remote track resolution (mediasoup / SFU)](#remote-track-resolution-mediasoup--sfu)
64
64
  12. [Mediasoup router observation](#mediasoup-router-observation)
65
65
  13. [Sinks (per-client sample persistence)](#sinks-per-client-sample-persistence)
66
- 14. [Logging](#logging)
67
- 15. [Error-handling philosophy](#error-handling-philosophy)
68
- 16. [Development & extension guide](#development--extension-guide)
66
+ 14. [Injecting data into a client](#injecting-data-into-a-client)
67
+ 15. [Logging](#logging)
68
+ 16. [Error-handling philosophy](#error-handling-philosophy)
69
+ 17. [Development & extension guide](#development--extension-guide)
69
70
 
70
71
  ---
71
72
 
@@ -369,7 +370,7 @@ additional field(s) on top of that scope.
369
370
  | Event | Extra | Fires when |
370
371
  |-------|-------|-----------|
371
372
  | `mediasoup-router-added` | — | `observer.createObservedMediasoupRouter(...)` registered a router |
372
- | `mediasoup-router-matched-with-peer-connection` | `{ observedCall, observedClient, observedPeerConnection }` | a newly added peer connection's id matched one of the router's WebRTC transport ids. |
373
+ | `mediasoup-router-matched-with-peer-connection` | `{ observedCall, observedClient, observedPeerConnection }` | a newly added peer connection's id matched one of the router's WebRTC transport ids. **Opt-in** via `matchPeerConnectionByWebRtcTransportId: true`. |
373
374
  | `mediasoup-router-removed` | — | the underlying mediasoup router closed (its `router.observer` `close` fired) |
374
375
 
375
376
  See [Mediasoup router observation](#mediasoup-router-observation) for the full design and examples.
@@ -539,7 +540,7 @@ Key members:
539
540
  - `readonly sink?: ClientSampleSink` — the per-client sink (see [Sinks](#sinks-per-client-sample-persistence)), if `createClientSink` is configured; listen on it for `close`/`error`
540
541
  - **Injection API** (queue app data to be merged into the next sample processing):
541
542
  `injectEvent(ClientEvent)`, `injectIssue(ClientIssue)`, `injectMetaData(ClientMetaData)`,
542
- `injectExtensionStat(ExtensionStat)`, `injectAttachment(key, value)`
543
+ `injectExtensionStat(ExtensionStat)`, `injectAttachment(attachments: Record<string, unknown>)`
543
544
  - **Direct add API** (process immediately): `addIssue(ClientIssue)`, `addMetadata(ClientMetaData)`,
544
545
  `addExtensionStats(ExtensionStat)`
545
546
  - Metrics (current/derived): `currentAvgRttInMs?`, `currentMinRttInMs?`, `currentMaxRttInMs?`,
@@ -832,11 +833,13 @@ ancestry, so you get the router **and** the matched `observedPeerConnection`, `o
832
833
  `observedCall` in one place. Stamp the `routerId` into the peer connection's / client's `appData`,
833
834
  build your own index, attach the server sample to the call in your database — whatever fits.
834
835
 
835
- How it works: as peer connections are observed (`peer-connection-added`), the observer checks whether
836
- the peer connection's id is one of the router's WebRTC transport ids. On a hit it emits — **once per
837
- matching peer connection** (de-duplicated by peer-connection id) — and keeps watching, so a router
838
- that serves many participants emits one match per participant's transport. The internal listener is
839
- removed automatically when the router closes or the observer closes.
836
+ This matching is **opt-in**: pass `matchPeerConnectionByWebRtcTransportId: true` to
837
+ `createObservedMediasoupRouter`. When enabled, as peer connections are observed
838
+ (`peer-connection-added`) the observer checks whether the peer connection's id is one of the router's
839
+ WebRTC transport ids; on a hit it emits — once per matching peer connection — and keeps watching, so a
840
+ router serving many participants emits one match per participant's transport. When the flag is omitted
841
+ or `false`, no matching is performed and the event never fires. The internal listener is removed
842
+ automatically when the router closes or the observer closes.
840
843
 
841
844
  When the underlying mediasoup router closes, its `close` propagates to `ObservedMediasoupRouter`,
842
845
  which emits **`mediasoup-router-removed`**. That is your cue to do whatever cleanup or persistence
@@ -850,10 +853,11 @@ you want with the now-final `sample` — again, the observer itself keeps nothin
850
853
  | `routerId` | `string` | yes | your id for the router (the sample also carries `router.id`) |
851
854
  | `appData` | `Record<string, unknown>` | no | application-owned bag on the `ObservedMediasoupRouter` |
852
855
  | `attachments` | `Record<string, unknown>` | no | free-form data copied onto `sample.attachments` |
856
+ | `matchPeerConnectionByWebRtcTransportId` | `boolean` | no | opt in to peer-connection matching: emit `mediasoup-router-matched-with-peer-connection` for each peer connection whose id matches one of the router's WebRTC transport ids. Omitted / `false` → no matching, the event never fires |
853
857
 
854
- Peer-connection matching is automatic — there is nothing to opt into. Returns the
855
- `ObservedMediasoupRouter`, or `undefined` if the observer is closed (a router with the same id
856
- returns the existing instance — both warn).
858
+ Peer-connection matching is **off by default**; enable it with
859
+ `matchPeerConnectionByWebRtcTransportId: true`. Returns the `ObservedMediasoupRouter`, or `undefined`
860
+ if the observer is closed (a router with the same id returns the existing instance — both warn).
857
861
 
858
862
  Useful members on the returned object: `.sample` (the `MediasoupRouterSample`), `.appData`,
859
863
  `.attachments` (getter over `sample.attachments`), `.webrtcTransportIds: Set<string>`, `.id`,
@@ -870,9 +874,13 @@ const observer = new Observer();
870
874
  // 1) Feed client samples as usual so the observer knows about calls, clients & peer connections.
871
875
  // (e.g. transport-layer: observer.accept(clientSample, context))
872
876
 
873
- // 2) Observe the SFU side. The observer auto-matches peer connections to this router's transports.
877
+ // 2) Observe the SFU side, opting in to peer-connection matching for this router's transports.
874
878
  const router = /* your mediasoup router */ undefined as any;
875
- const observedRouter = observer.createObservedMediasoupRouter({ router, routerId: router.id });
879
+ const observedRouter = observer.createObservedMediasoupRouter({
880
+ router,
881
+ routerId: router.id,
882
+ matchPeerConnectionByWebRtcTransportId: true,
883
+ });
876
884
 
877
885
  // 3) Every peer connection whose id matches one of the router's WebRTC transport ids fires this —
878
886
  // WE decide what to do with each pairing. The payload carries the full ancestry.
@@ -1031,6 +1039,60 @@ const observer = new Observer({ createClientSink });
1031
1039
  the bus with full ancestry. `ClientSampleSinkFactory` is
1032
1040
  `(p: { clientId: string; observedCall: ObservedCall }) => ClientSampleSink | undefined`.
1033
1041
 
1042
+ ---
1043
+
1044
+ ## Injecting data into a client
1045
+
1046
+ Sometimes the application holds data that belongs on a client's record but isn't part of the
1047
+ client-reported `ClientSample` — a room id or display name, an application-level event
1048
+ (*"recording started"*), a server-detected issue, an extension stat, or a device/meta item.
1049
+ `ObservedClient` exposes **injection** methods that merge such data into the client's sample stream,
1050
+ so it updates the live model **and** is persisted to the client's
1051
+ [sink](#sinks-per-client-sample-persistence) exactly like sampled data.
1052
+
1053
+ | Method | Adds to the sample's | Surfaces as |
1054
+ |--------|----------------------|-------------|
1055
+ | `injectAttachment(attachments)` | `attachments` (merged via `Object.assign`) | `observedClient.attachments` |
1056
+ | `injectEvent(event: ClientEvent)` | `clientEvents` | `client-event` (plus any state the event drives) |
1057
+ | `injectIssue(issue: ClientIssue)` | `clientIssues` | `client-issue` |
1058
+ | `injectMetaData(meta: ClientMetaData)` | `clientMetaItems` | `client-metadata` |
1059
+ | `injectExtensionStat(stat: ExtensionStat)` | `extensionStats` | `client-extension-stats` |
1060
+
1061
+ ### When the injected data lands
1062
+
1063
+ Injection is timing-aware so nothing is dropped, regardless of *when* you call it:
1064
+
1065
+ - **During a sample's processing** — e.g. from inside a `client-updated` / `client-event` handler,
1066
+ which run within `accept()` — the data is applied to the **current** sample immediately: reflected
1067
+ in entity state and written to the sink as part of that sample.
1068
+ - **Between samples** — the data is buffered and merged into the **next** `accept()`'s sample.
1069
+ - **On `close()` with pending injections and no further sample** — the buffer is flushed as a final
1070
+ synthetic sample (applied to state and written to the sink) before the sink is ended, so a
1071
+ last-moment injection is never lost.
1072
+
1073
+ In every case the injected data both updates the live `ObservedClient` and reaches the per-client
1074
+ sink — the sink always receives the final, **injection-merged** sample (the sink write happens at the
1075
+ end of `accept()`, after the merge).
1076
+
1077
+ ### Example
1078
+
1079
+ ```ts
1080
+ // Enrich at creation from your app's knowledge of the participant. Injecting in `client-added`
1081
+ // (which runs just before the first accept) lands on the first sample.
1082
+ observer.on('client-added', ({ observedClient }) => {
1083
+ observedClient.injectAttachment({ roomId: lookupRoomId(observedClient.clientId) });
1084
+ });
1085
+
1086
+ // Application-level signals at any time:
1087
+ const client = observer.getObservedCall(callId)?.getObservedClient(clientId);
1088
+ client?.injectEvent({ type: 'RECORDING_STARTED', timestamp: Date.now() });
1089
+ client?.injectIssue({ type: 'app-kicked-participant', timestamp: Date.now() });
1090
+ ```
1091
+
1092
+ `attachments` are latest-wins (like sampled `attachments`): injecting a key overwrites its previous
1093
+ value. `appData` is unaffected — injections flow into the sample/telemetry, not the app-owned
1094
+ `appData` bag (see [Ingestion](#ingestion-accept-context--lifecycle)).
1095
+
1034
1096
  ## Logging
1035
1097
 
1036
1098
  `observer-js` logs through a single, swappable sink. Out of the box it writes `debug` and
package/dist/index.d.mts CHANGED
@@ -2570,7 +2570,8 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
2570
2570
  numberOfScoreMeasurements: number;
2571
2571
  readonly mediaDevices: MediaDeviceInfo[];
2572
2572
  issues: ClientIssue[];
2573
- private _injections;
2573
+ private _pendingInjections;
2574
+ private _activeSample?;
2574
2575
  private closeTimer?;
2575
2576
  constructor(settings: ObservedClientSettings<AppData>, call: ObservedCall);
2576
2577
  get numberOfPeerConnections(): number;
@@ -2581,13 +2582,14 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
2581
2582
  injectEvent(event: ClientEvent): void;
2582
2583
  injectIssue(issue: ClientIssue): void;
2583
2584
  injectExtensionStat(stat: ExtensionStat): void;
2584
- injectAttachment(key: string, value: unknown): void;
2585
+ injectAttachment(attachments: Record<string, unknown>): void;
2585
2586
  addMetadata(metadata: ClientMetaData): void;
2586
2587
  addIssue(issue: ClientIssue): void;
2587
2588
  addExtensionStats(stats: ExtensionStat): void;
2588
2589
  private _processClientEvent;
2589
2590
  private _updatePeerConnection;
2590
- private _mergeInjections;
2591
+ private _mergePendingInjections;
2592
+ private _flushPendingInjections;
2591
2593
  /** Emit an Observer-bus event scoped to this client (or a peer connection under it). */
2592
2594
  private _notify;
2593
2595
  }
@@ -2836,7 +2838,9 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
2836
2838
  getObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(callId: string): ObservedCall<T> | undefined;
2837
2839
  createObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedCallSettings<T>): ObservedCall<T> | undefined;
2838
2840
  getOrCreateObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedCallSettings<T>): ObservedCall<T> | undefined;
2839
- createObservedMediasoupRouter<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedMediasoupRouterSettings<T>): ObservedMediasoupRouter<Record<string, unknown>> | undefined;
2841
+ createObservedMediasoupRouter<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedMediasoupRouterSettings<T> & {
2842
+ matchPeerConnectionByWebRtcTransportId?: boolean;
2843
+ }): ObservedMediasoupRouter<Record<string, unknown>> | undefined;
2840
2844
  close(): void;
2841
2845
  accept(sample: ClientSample, context?: AcceptContext): void;
2842
2846
  update(): void;
package/dist/index.d.ts CHANGED
@@ -2570,7 +2570,8 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
2570
2570
  numberOfScoreMeasurements: number;
2571
2571
  readonly mediaDevices: MediaDeviceInfo[];
2572
2572
  issues: ClientIssue[];
2573
- private _injections;
2573
+ private _pendingInjections;
2574
+ private _activeSample?;
2574
2575
  private closeTimer?;
2575
2576
  constructor(settings: ObservedClientSettings<AppData>, call: ObservedCall);
2576
2577
  get numberOfPeerConnections(): number;
@@ -2581,13 +2582,14 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
2581
2582
  injectEvent(event: ClientEvent): void;
2582
2583
  injectIssue(issue: ClientIssue): void;
2583
2584
  injectExtensionStat(stat: ExtensionStat): void;
2584
- injectAttachment(key: string, value: unknown): void;
2585
+ injectAttachment(attachments: Record<string, unknown>): void;
2585
2586
  addMetadata(metadata: ClientMetaData): void;
2586
2587
  addIssue(issue: ClientIssue): void;
2587
2588
  addExtensionStats(stats: ExtensionStat): void;
2588
2589
  private _processClientEvent;
2589
2590
  private _updatePeerConnection;
2590
- private _mergeInjections;
2591
+ private _mergePendingInjections;
2592
+ private _flushPendingInjections;
2591
2593
  /** Emit an Observer-bus event scoped to this client (or a peer connection under it). */
2592
2594
  private _notify;
2593
2595
  }
@@ -2836,7 +2838,9 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
2836
2838
  getObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(callId: string): ObservedCall<T> | undefined;
2837
2839
  createObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedCallSettings<T>): ObservedCall<T> | undefined;
2838
2840
  getOrCreateObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedCallSettings<T>): ObservedCall<T> | undefined;
2839
- createObservedMediasoupRouter<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedMediasoupRouterSettings<T>): ObservedMediasoupRouter<Record<string, unknown>> | undefined;
2841
+ createObservedMediasoupRouter<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedMediasoupRouterSettings<T> & {
2842
+ matchPeerConnectionByWebRtcTransportId?: boolean;
2843
+ }): ObservedMediasoupRouter<Record<string, unknown>> | undefined;
2840
2844
  close(): void;
2841
2845
  accept(sample: ClientSample, context?: AcceptContext): void;
2842
2846
  update(): void;
package/dist/index.js CHANGED
@@ -2250,7 +2250,8 @@ var ObservedClient = class extends import_events2.EventEmitter {
2250
2250
  // public totalNumberOfIssues = 0;
2251
2251
  mediaDevices = [];
2252
2252
  issues = [];
2253
- _injections = {};
2253
+ _pendingInjections = {};
2254
+ _activeSample;
2254
2255
  closeTimer;
2255
2256
  get numberOfPeerConnections() {
2256
2257
  return this.observedPeerConnections.size;
@@ -2260,11 +2261,8 @@ var ObservedClient = class extends import_events2.EventEmitter {
2260
2261
  }
2261
2262
  close() {
2262
2263
  if (this.closed) return;
2264
+ this._flushPendingInjections();
2263
2265
  this.closed = true;
2264
- this._injections.clientEvents?.forEach((clientEvent) => this._processClientEvent(clientEvent));
2265
- this._injections.clientIssues?.forEach((clientIssue) => this.addIssue(clientIssue));
2266
- this._injections.extensionStats?.forEach((extensionStat) => this.addExtensionStats(extensionStat));
2267
- this._injections.clientMetaItems?.forEach((clientMetaItem) => this.addMetadata(clientMetaItem));
2268
2266
  Array.from(this.observedPeerConnections.values()).forEach((peerConnection) => peerConnection.close());
2269
2267
  if (!this.leftAt) {
2270
2268
  this.leftAt = this.lastSampleTimestamp;
@@ -2287,7 +2285,6 @@ var ObservedClient = class extends import_events2.EventEmitter {
2287
2285
  logger2.warn("Attempted to accept a sample on a closed client %s", this.clientId);
2288
2286
  return;
2289
2287
  }
2290
- this.sink?.write(sample);
2291
2288
  if (this.closeTimer) {
2292
2289
  clearTimeout(this.closeTimer);
2293
2290
  this.closeTimer = void 0;
@@ -2327,7 +2324,8 @@ var ObservedClient = class extends import_events2.EventEmitter {
2327
2324
  this.usingTCP = false;
2328
2325
  this.currentMinRttInMs = void 0;
2329
2326
  this.currentMaxRttInMs = void 0;
2330
- this._mergeInjections(sample);
2327
+ this._mergePendingInjections(sample);
2328
+ this._activeSample = sample;
2331
2329
  const clientEventsPostBuffer = [];
2332
2330
  for (const clientEvent of sample.clientEvents ?? []) {
2333
2331
  this._processClientEvent(clientEvent, clientEventsPostBuffer);
@@ -2415,31 +2413,68 @@ var ObservedClient = class extends import_events2.EventEmitter {
2415
2413
  this.close();
2416
2414
  }, this.settings.closeClientIfIdleForMs);
2417
2415
  }
2416
+ this._activeSample = void 0;
2417
+ try {
2418
+ this.sink?.write(sample);
2419
+ } catch (err) {
2420
+ logger2.warn("ClientSampleSink.write failed for client %s: %o", this.clientId, err);
2421
+ }
2418
2422
  }
2419
2423
  injectMetaData(metaData) {
2420
2424
  if (this.closed) return;
2421
- if (!this._injections.clientMetaItems) this._injections.clientMetaItems = [];
2422
- this._injections.clientMetaItems.push(metaData);
2425
+ if (this._activeSample) {
2426
+ if (!this._activeSample.clientMetaItems) this._activeSample.clientMetaItems = [];
2427
+ this._activeSample.clientMetaItems.push(metaData);
2428
+ this.addMetadata(metaData);
2429
+ return;
2430
+ }
2431
+ if (!this._pendingInjections.clientMetaItems) this._pendingInjections.clientMetaItems = [];
2432
+ this._pendingInjections.clientMetaItems.push(metaData);
2423
2433
  }
2424
2434
  injectEvent(event) {
2425
2435
  if (this.closed) return;
2426
- if (!this._injections.clientEvents) this._injections.clientEvents = [];
2427
- this._injections.clientEvents.push(event);
2436
+ if (this._activeSample) {
2437
+ if (!this._activeSample.clientEvents) this._activeSample.clientEvents = [];
2438
+ this._activeSample.clientEvents.push(event);
2439
+ this._processClientEvent(event);
2440
+ return;
2441
+ }
2442
+ if (!this._pendingInjections.clientEvents) this._pendingInjections.clientEvents = [];
2443
+ this._pendingInjections.clientEvents.push(event);
2428
2444
  }
2429
2445
  injectIssue(issue) {
2430
2446
  if (this.closed) return;
2431
- if (!this._injections.clientIssues) this._injections.clientIssues = [];
2432
- this._injections.clientIssues.push(issue);
2447
+ if (this._activeSample) {
2448
+ if (!this._activeSample.clientIssues) this._activeSample.clientIssues = [];
2449
+ this._activeSample.clientIssues.push(issue);
2450
+ this.addIssue(issue);
2451
+ ++this.deltaNumberOfIssues;
2452
+ return;
2453
+ }
2454
+ if (!this._pendingInjections.clientIssues) this._pendingInjections.clientIssues = [];
2455
+ this._pendingInjections.clientIssues.push(issue);
2433
2456
  }
2434
2457
  injectExtensionStat(stat) {
2435
2458
  if (this.closed) return;
2436
- if (!this._injections.extensionStats) this._injections.extensionStats = [];
2437
- this._injections.extensionStats.push(stat);
2459
+ if (this._activeSample) {
2460
+ if (!this._activeSample.extensionStats) this._activeSample.extensionStats = [];
2461
+ this._activeSample.extensionStats.push(stat);
2462
+ this.addExtensionStats(stat);
2463
+ return;
2464
+ }
2465
+ if (!this._pendingInjections.extensionStats) this._pendingInjections.extensionStats = [];
2466
+ this._pendingInjections.extensionStats.push(stat);
2438
2467
  }
2439
- injectAttachment(key, value) {
2468
+ injectAttachment(attachments) {
2440
2469
  if (this.closed) return;
2441
- if (!this._injections.attachments) this._injections.attachments = {};
2442
- this._injections.attachments[key] = value;
2470
+ if (this._activeSample) {
2471
+ if (!this._activeSample.attachments) this._activeSample.attachments = {};
2472
+ Object.assign(this._activeSample.attachments, attachments);
2473
+ this.attachments = this._activeSample.attachments;
2474
+ return;
2475
+ }
2476
+ if (!this._pendingInjections.attachments) this._pendingInjections.attachments = {};
2477
+ Object.assign(this._pendingInjections.attachments, attachments);
2443
2478
  }
2444
2479
  addMetadata(metadata) {
2445
2480
  if (this.closed) return;
@@ -2701,35 +2736,53 @@ var ObservedClient = class extends import_events2.EventEmitter {
2701
2736
  observedPeerConnection.accept(sample, context);
2702
2737
  return observedPeerConnection;
2703
2738
  }
2704
- _mergeInjections(sample) {
2739
+ _mergePendingInjections(sample) {
2705
2740
  if (this.closed) return sample;
2706
- if (this._injections.clientEvents) {
2741
+ if (this._pendingInjections.clientEvents) {
2707
2742
  if (!sample.clientEvents) sample.clientEvents = [];
2708
- sample.clientEvents.push(...this._injections.clientEvents);
2709
- this._injections.clientEvents = void 0;
2743
+ sample.clientEvents.push(...this._pendingInjections.clientEvents);
2744
+ this._pendingInjections.clientEvents = void 0;
2710
2745
  }
2711
- if (this._injections.clientIssues) {
2746
+ if (this._pendingInjections.clientIssues) {
2712
2747
  if (!sample.clientIssues) sample.clientIssues = [];
2713
- sample.clientIssues.push(...this._injections.clientIssues);
2714
- this._injections.clientIssues = void 0;
2748
+ sample.clientIssues.push(...this._pendingInjections.clientIssues);
2749
+ this._pendingInjections.clientIssues = void 0;
2715
2750
  }
2716
- if (this._injections.extensionStats) {
2751
+ if (this._pendingInjections.extensionStats) {
2717
2752
  if (!sample.extensionStats) sample.extensionStats = [];
2718
- sample.extensionStats.push(...this._injections.extensionStats);
2719
- this._injections.extensionStats = void 0;
2753
+ sample.extensionStats.push(...this._pendingInjections.extensionStats);
2754
+ this._pendingInjections.extensionStats = void 0;
2720
2755
  }
2721
- if (this._injections.attachments) {
2756
+ if (this._pendingInjections.attachments) {
2722
2757
  if (!sample.attachments) sample.attachments = {};
2723
- Object.assign(sample.attachments, this._injections.attachments);
2724
- this._injections.attachments = void 0;
2758
+ Object.assign(sample.attachments, this._pendingInjections.attachments);
2759
+ this._pendingInjections.attachments = void 0;
2725
2760
  }
2726
- if (this._injections.clientMetaItems) {
2761
+ if (this._pendingInjections.clientMetaItems) {
2727
2762
  if (!sample.clientMetaItems) sample.clientMetaItems = [];
2728
- sample.clientMetaItems.push(...this._injections.clientMetaItems);
2729
- this._injections.clientMetaItems = void 0;
2763
+ sample.clientMetaItems.push(...this._pendingInjections.clientMetaItems);
2764
+ this._pendingInjections.clientMetaItems = void 0;
2730
2765
  }
2731
2766
  return sample;
2732
2767
  }
2768
+ _flushPendingInjections() {
2769
+ const hasPending = Boolean(
2770
+ this._pendingInjections.clientEvents || this._pendingInjections.clientIssues || this._pendingInjections.extensionStats || this._pendingInjections.clientMetaItems || this._pendingInjections.attachments
2771
+ );
2772
+ if (!hasPending) return;
2773
+ const finalSample = { timestamp: Date.now() };
2774
+ this._mergePendingInjections(finalSample);
2775
+ for (const clientEvent of finalSample.clientEvents ?? []) this._processClientEvent(clientEvent);
2776
+ for (const metaData of finalSample.clientMetaItems ?? []) this.addMetadata(metaData);
2777
+ for (const issue of finalSample.clientIssues ?? []) this.addIssue(issue);
2778
+ for (const extensionStat of finalSample.extensionStats ?? []) this.addExtensionStats(extensionStat);
2779
+ if (finalSample.attachments) this.attachments = { ...this.attachments, ...finalSample.attachments };
2780
+ try {
2781
+ this.sink?.write(finalSample);
2782
+ } catch (err) {
2783
+ logger2.warn("ClientSampleSink.write failed during injection flush for client %s: %o", this.clientId, err);
2784
+ }
2785
+ }
2733
2786
  /** Emit an Observer-bus event scoped to this client (or a peer connection under it). */
2734
2787
  _notify(type, ...args) {
2735
2788
  this.call.observer.emit(type, ...args);
@@ -3729,17 +3782,19 @@ var Observer = class extends import_events6.EventEmitter {
3729
3782
  observedMediasoupRouter,
3730
3783
  observer: this
3731
3784
  };
3732
- const onPeerConnectionAdded = (observedPeerConnectionScope) => {
3733
- if (!observedMediasoupRouter.webrtcTransportIds.has(observedPeerConnectionScope.observedPeerConnection.peerConnectionId)) return;
3734
- this.emit("mediasoup-router-matched-with-peer-connection", {
3735
- ...observedMediasoupRouterScope,
3736
- ...observedPeerConnectionScope
3737
- });
3738
- };
3739
- const stopMatching = () => this.off("peer-connection-added", onPeerConnectionAdded);
3740
- this.on("peer-connection-added", onPeerConnectionAdded);
3741
- this.once("observer-closed", stopMatching);
3742
- observedMediasoupRouter.once("close", stopMatching);
3785
+ if (settings.matchPeerConnectionByWebRtcTransportId) {
3786
+ const onPeerConnectionAdded = (observedPeerConnectionScope) => {
3787
+ if (!observedMediasoupRouter.webrtcTransportIds.has(observedPeerConnectionScope.observedPeerConnection.peerConnectionId)) return;
3788
+ this.emit("mediasoup-router-matched-with-peer-connection", {
3789
+ ...observedMediasoupRouterScope,
3790
+ ...observedPeerConnectionScope
3791
+ });
3792
+ };
3793
+ const stopMatching = () => this.off("peer-connection-added", onPeerConnectionAdded);
3794
+ this.on("peer-connection-added", onPeerConnectionAdded);
3795
+ this.once("observer-closed", stopMatching);
3796
+ observedMediasoupRouter.once("close", stopMatching);
3797
+ }
3743
3798
  observedMediasoupRouter.once("close", () => {
3744
3799
  this.observedMediasoupRouters.delete(observedMediasoupRouter.id);
3745
3800
  this.emit("mediasoup-router-removed", observedMediasoupRouterScope);