@observertc/observer-js 1.2.0 → 1.3.0-rc.2

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/dist/index.js CHANGED
@@ -42,9 +42,9 @@ __export(src_exports, {
42
42
  ClientMetaTypes: () => ClientMetaTypes,
43
43
  ClientPopulationIssueDetector: () => ClientPopulationIssueDetector,
44
44
  ClientPopulationIssueTypes: () => ClientPopulationIssueTypes,
45
- ClientSampleSink: () => ClientSampleSink,
46
45
  CodecConsistencyValidator: () => CodecConsistencyValidator,
47
46
  Detectors: () => Detectors,
47
+ ExtensionStatsRegistry: () => ExtensionStatsRegistry,
48
48
  GEOHASH_CELL_SIZES: () => GEOHASH_CELL_SIZES,
49
49
  InMemorySink: () => InMemorySink,
50
50
  IssueFanOutDetector: () => IssueFanOutDetector,
@@ -88,6 +88,7 @@ __export(src_exports, {
88
88
  SCORE_HISTOGRAM_NUMBER_OF_BUCKETS: () => SCORE_HISTOGRAM_NUMBER_OF_BUCKETS,
89
89
  SfuCongestionDetector: () => SfuCongestionDetector,
90
90
  SimulcastReceiverValidator: () => SimulcastReceiverValidator,
91
+ SinkRegistry: () => SinkRegistry,
91
92
  SlidingWindow: () => SlidingWindow,
92
93
  TrackDeliveryMismatchDetector: () => TrackDeliveryMismatchDetector,
93
94
  TrackDeliveryMismatchTypes: () => TrackDeliveryMismatchTypes,
@@ -108,7 +109,6 @@ __export(src_exports, {
108
109
  createDefaultMediasoupRemoteTrackResolverFactory: () => createDefaultMediasoupRemoteTrackResolverFactory,
109
110
  createInMemorySink: () => createInMemorySink,
110
111
  createJsonlFileSink: () => createJsonlFileSink,
111
- createJsonlFileSinkFactory: () => createJsonlFileSinkFactory,
112
112
  createLogger: () => createLogger,
113
113
  createP2pRemoteTrackResolverFactory: () => createP2pRemoteTrackResolverFactory,
114
114
  defaultCallSummaryConfig: () => defaultCallSummaryConfig,
@@ -171,18 +171,189 @@ function createLogger(moduleName) {
171
171
  }
172
172
  }();
173
173
  }
174
- function setObserverLogger(logger11) {
175
- mainLogger = logger11;
174
+ function setObserverLogger(logger13) {
175
+ mainLogger = logger13;
176
176
  }
177
177
 
178
178
  // src/ObservedCall.ts
179
- var import_node_events3 = require("events");
179
+ var import_node_events4 = require("events");
180
+
181
+ // src/sinks/SinkRegistry.ts
182
+ var import_uuid = require("uuid");
183
+ var logger = createLogger("SinkRegistry");
184
+ var SinkRegistry = class {
185
+ constructor(parent) {
186
+ this.parent = parent;
187
+ }
188
+ parent;
189
+ // This registry's sinks and its descendants'.
190
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
191
+ _sinks = /* @__PURE__ */ new Map();
192
+ /** Number of open sinks in this registry and its descendants. */
193
+ get size() {
194
+ return this._sinks.size;
195
+ }
196
+ keys() {
197
+ return this._sinks.keys();
198
+ }
199
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
200
+ values() {
201
+ return this._sinks.values();
202
+ }
203
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
204
+ get(id) {
205
+ return this._sinks.get(id);
206
+ }
207
+ /**
208
+ * Construct `new SinkClass(settings)` and register it. The id (`settings.id`, generated when
209
+ * omitted) is checked first, so nothing is constructed — no file is opened — when it is taken:
210
+ * `undefined` is returned and a warning logged.
211
+ */
212
+ create(SinkClass, settings) {
213
+ const lineage = this._lineage();
214
+ const id = settings.id ?? (0, import_uuid.v4)();
215
+ if (lineage[lineage.length - 1]._sinks.has(id)) {
216
+ logger.warn("Sink id %s is already in use; sink ids must be unique per observer. No sink was created", id);
217
+ return void 0;
218
+ }
219
+ const sink = new SinkClass({ ...settings, id });
220
+ const onError = (err) => logger.warn("Sink %s failed: %o", id, err);
221
+ sink.on("error", onError);
222
+ sink.once("close", () => sink.off("error", onError));
223
+ sink.once("ended", () => {
224
+ for (const registry of lineage) {
225
+ if (registry._sinks.get(id) === sink) registry._sinks.delete(id);
226
+ }
227
+ });
228
+ for (const registry of lineage) registry._sinks.set(id, sink);
229
+ return sink;
230
+ }
231
+ /** Write `record` to the sink `id`. `false` when there is no such sink, or on backpressure. */
232
+ write(id, record) {
233
+ const sink = this._sinks.get(id);
234
+ if (!sink) {
235
+ logger.debug("No sink %s; record dropped", id);
236
+ return false;
237
+ }
238
+ return sink.write(record);
239
+ }
240
+ /** End every sink of this registry and of its descendants. */
241
+ clear() {
242
+ for (const sink of [...this._sinks.values()]) {
243
+ try {
244
+ sink.end();
245
+ } catch (err) {
246
+ logger.warn("Sink %s failed to end: %o", sink.id, err);
247
+ }
248
+ }
249
+ }
250
+ /** This registry and its ancestors, up to the root. */
251
+ _lineage() {
252
+ return [this, ...this.parent?._lineage() ?? []];
253
+ }
254
+ };
255
+
256
+ // src/sinks/ExtensionStatsRegistry.ts
257
+ var logger2 = createLogger("ExtensionStats");
258
+ var ExtensionStatsRegistry = class {
259
+ constructor(id, _sinks) {
260
+ this.id = id;
261
+ this._sinks = _sinks;
262
+ }
263
+ id;
264
+ _sinks;
265
+ /** The last data accepted for each extension stats id. */
266
+ values = /* @__PURE__ */ new Map();
267
+ closed = false;
268
+ /**
269
+ * Refuse further `create` / `accept`, and end the extension stats' sinks (the sinks whose id is
270
+ * an extension stats id). Other sinks of the owner, e.g. client sample sinks, are left to the
271
+ * owner's `sinks.clear()`. Called by the owner when it closes.
272
+ */
273
+ close() {
274
+ if (this.closed) return;
275
+ this.closed = true;
276
+ for (const id of this.values.keys()) {
277
+ this._sinks.get(id)?.end();
278
+ }
279
+ }
280
+ create(settings) {
281
+ if (this.closed) {
282
+ logger2.warn("Attempted to create extension stats %s on closed %s", settings.id, this.id);
283
+ return void 0;
284
+ }
285
+ if (this.values.has(settings.id)) {
286
+ logger2.warn("Extension stats with id %s already exists on %s", settings.id, this.id);
287
+ return void 0;
288
+ }
289
+ this.values.set(settings.id, void 0);
290
+ if (!settings.createSink) return void 0;
291
+ const [SinkClass, sinkSettings] = settings.createSink;
292
+ this._sinks.get(settings.id)?.end();
293
+ return this._sinks.create(SinkClass, { ...sinkSettings, id: settings.id });
294
+ }
295
+ accept(id, data) {
296
+ if (this.closed) {
297
+ logger2.debug("Extension stats %s dropped: %s is closed", id, this.id);
298
+ return;
299
+ }
300
+ this.values.set(id, data);
301
+ try {
302
+ this._sinks.get(id)?.write(data);
303
+ } catch (err) {
304
+ logger2.warn("Failed to write extension stats %s to sink: %o", id, err);
305
+ }
306
+ }
307
+ get(id) {
308
+ return this.values.get(id);
309
+ }
310
+ };
311
+
312
+ // src/sinks/JsonlFileSink.ts
313
+ var fs = __toESM(require("fs"));
314
+ var import_node_events = require("events");
315
+ var JsonlFileSink = class extends import_node_events.EventEmitter {
316
+ id;
317
+ /** The file this sink writes to. Read it (e.g. in a `close` handler) to upload/move the file. */
318
+ path;
319
+ _stream;
320
+ _serializeSample;
321
+ _ended = false;
322
+ constructor(options) {
323
+ super();
324
+ this.id = options.id ?? options.path;
325
+ this.path = options.path;
326
+ this._stream = fs.createWriteStream(this.path, { flags: "a" });
327
+ this._stream.on("close", () => this.emit("close"));
328
+ this._stream.on("finish", () => this.emit("finish"));
329
+ this._stream.on("drain", () => this.emit("drain"));
330
+ this._stream.on("error", (err) => this.emit("error", err));
331
+ this._serializeSample = options.serializeSample ?? ((record) => JSON.stringify(record));
332
+ }
333
+ get ended() {
334
+ return this._ended;
335
+ }
336
+ write(record) {
337
+ if (this._ended) return false;
338
+ return this._stream.write(`${this._serializeSample(record)}
339
+ `);
340
+ }
341
+ end() {
342
+ if (this._ended) return;
343
+ this._ended = true;
344
+ this.emit("ended");
345
+ this._stream.end();
346
+ }
347
+ };
348
+ function createJsonlFileSink(options) {
349
+ return new JsonlFileSink(options);
350
+ }
180
351
 
181
352
  // src/ObservedClient.ts
182
- var import_node_events2 = require("events");
353
+ var import_node_events3 = require("events");
183
354
 
184
355
  // src/ObservedPeerConnection.ts
185
- var import_node_events = require("events");
356
+ var import_node_events2 = require("events");
186
357
 
187
358
  // src/utils/stats.ts
188
359
  function percentile(values, p) {
@@ -339,8 +510,9 @@ function percentileOfHistogram(histogram, bucketWidth, p, options = {}) {
339
510
  continue;
340
511
  }
341
512
  if (options.openEnded && index === histogram.length - 1 && options.max !== void 0) return options.max;
342
- const estimate = index * bucketWidth + (rank - cumulative) / count * bucketWidth;
343
- return Math.min(Math.max(estimate, options.min ?? estimate), options.max ?? estimate);
513
+ const lower = Math.max(index * bucketWidth, options.min ?? -Infinity);
514
+ const upper = Math.min((index + 1) * bucketWidth, options.max ?? Infinity);
515
+ return lower + (rank - cumulative) / count * Math.max(0, upper - lower);
344
516
  }
345
517
  return options.max;
346
518
  }
@@ -1874,14 +2046,14 @@ var ObservedOutboundTrack = class {
1874
2046
 
1875
2047
  // src/ObservedPeerConnection.ts
1876
2048
  var ascending2 = (a, b) => a - b;
1877
- var logger = createLogger("ObservedPeerConnection");
2049
+ var logger3 = createLogger("ObservedPeerConnection");
1878
2050
  function publicAddressOfLocalCandidate(candidate) {
1879
2051
  if (!candidate) return void 0;
1880
2052
  if (candidate.candidateType !== "relay" && isPublicIpAddress(candidate.address)) return candidate.address;
1881
2053
  if (isPublicIpAddress(candidate.relatedAddress)) return candidate.relatedAddress;
1882
2054
  return void 0;
1883
2055
  }
1884
- var ObservedPeerConnection = class extends import_node_events.EventEmitter {
2056
+ var ObservedPeerConnection = class extends import_node_events2.EventEmitter {
1885
2057
  constructor(peerConnectionId, client) {
1886
2058
  super();
1887
2059
  this.peerConnectionId = peerConnectionId;
@@ -2463,7 +2635,7 @@ var ObservedPeerConnection = class extends import_node_events.EventEmitter {
2463
2635
  let observedCertificate = this.observedCertificates.get(stats.id);
2464
2636
  if (!observedCertificate) {
2465
2637
  if (!stats.timestamp || !stats.id || !stats.fingerprint) {
2466
- return logger.warn(
2638
+ return logger3.warn(
2467
2639
  `ObservedPeerConnection received an invalid CertificateStats (missing timestamp OR id OR fingerprint field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2468
2640
  stats
2469
2641
  );
@@ -2482,7 +2654,7 @@ var ObservedPeerConnection = class extends import_node_events.EventEmitter {
2482
2654
  let observedCodec = this.observedCodecs.get(stats.id);
2483
2655
  if (!observedCodec) {
2484
2656
  if (!stats.timestamp || !stats.id || !stats.mimeType) {
2485
- return logger.warn(
2657
+ return logger3.warn(
2486
2658
  `ObservedPeerConnection received an invalid CodecStats (missing timestamp OR id OR mimeType field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2487
2659
  stats
2488
2660
  );
@@ -2501,7 +2673,7 @@ var ObservedPeerConnection = class extends import_node_events.EventEmitter {
2501
2673
  let observedDataChannel = this.observedDataChannels.get(stats.id);
2502
2674
  if (!observedDataChannel) {
2503
2675
  if (!stats.timestamp || !stats.id) {
2504
- return logger.warn(
2676
+ return logger3.warn(
2505
2677
  `ObservedPeerConnection received an invalid DataChannelStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2506
2678
  stats
2507
2679
  );
@@ -2520,7 +2692,7 @@ var ObservedPeerConnection = class extends import_node_events.EventEmitter {
2520
2692
  let observedIceCandidate = this.observedIceCandidates.get(stats.id);
2521
2693
  if (!observedIceCandidate) {
2522
2694
  if (!stats.timestamp || !stats.id) {
2523
- return logger.warn(
2695
+ return logger3.warn(
2524
2696
  `ObservedPeerConnection received an invalid IceCandidateStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2525
2697
  stats
2526
2698
  );
@@ -2539,7 +2711,7 @@ var ObservedPeerConnection = class extends import_node_events.EventEmitter {
2539
2711
  let observedIceCandidatePair = this.observedIceCandidatesPair.get(stats.id);
2540
2712
  if (!observedIceCandidatePair) {
2541
2713
  if (!stats.timestamp || !stats.id) {
2542
- return logger.warn(
2714
+ return logger3.warn(
2543
2715
  `ObservedPeerConnection received an invalid IceCandidateStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2544
2716
  stats
2545
2717
  );
@@ -2558,7 +2730,7 @@ var ObservedPeerConnection = class extends import_node_events.EventEmitter {
2558
2730
  let observedIceTransport = this.observedIceTransports.get(stats.id);
2559
2731
  if (!observedIceTransport) {
2560
2732
  if (!stats.timestamp || !stats.id) {
2561
- return logger.warn(
2733
+ return logger3.warn(
2562
2734
  `ObservedPeerConnection received an invalid IceCandidateStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2563
2735
  stats
2564
2736
  );
@@ -2577,7 +2749,7 @@ var ObservedPeerConnection = class extends import_node_events.EventEmitter {
2577
2749
  let observedInboundRtp = this.observedInboundRtps.get(stats.ssrc);
2578
2750
  if (!observedInboundRtp) {
2579
2751
  if (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind || !stats.trackIdentifier) {
2580
- return logger.warn(
2752
+ return logger3.warn(
2581
2753
  `ObservedPeerConnection received an invalid InboundRtpStats (missing timestamp OR id OR ssrc OR kind OR trackIdentifier field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2582
2754
  stats
2583
2755
  );
@@ -2603,7 +2775,7 @@ var ObservedPeerConnection = class extends import_node_events.EventEmitter {
2603
2775
  let observedInboundTrack = this.observedInboundTracks.get(stats.id);
2604
2776
  if (!observedInboundTrack) {
2605
2777
  if (!stats.timestamp || !stats.id || !stats.kind) {
2606
- return logger.warn(
2778
+ return logger3.warn(
2607
2779
  `ObservedPeerConnection received an invalid InboundTrackSample (missing timestamp OR id OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2608
2780
  stats
2609
2781
  );
@@ -2631,7 +2803,7 @@ var ObservedPeerConnection = class extends import_node_events.EventEmitter {
2631
2803
  let observedMediaPlayout = this.observedMediaPlayouts.get(stats.id);
2632
2804
  if (!observedMediaPlayout) {
2633
2805
  if (!stats.timestamp || !stats.id || !stats.kind) {
2634
- return logger.warn(
2806
+ return logger3.warn(
2635
2807
  `ObservedPeerConnection received an invalid InboundRtpStats (missing timestamp OR id OR kind OR trackIdentifier field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2636
2808
  stats
2637
2809
  );
@@ -2655,7 +2827,7 @@ var ObservedPeerConnection = class extends import_node_events.EventEmitter {
2655
2827
  let observedMediaSource = this.observedMediaSources.get(stats.id);
2656
2828
  if (!observedMediaSource) {
2657
2829
  if (!stats.timestamp || !stats.id || !stats.kind) {
2658
- return logger.warn(
2830
+ return logger3.warn(
2659
2831
  `ObservedPeerConnection received an invalid InboundRtpStats (missing timestamp OR id OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2660
2832
  stats
2661
2833
  );
@@ -2679,7 +2851,7 @@ var ObservedPeerConnection = class extends import_node_events.EventEmitter {
2679
2851
  let observedOutboundRtp = this.observedOutboundRtps.get(stats.ssrc);
2680
2852
  if (!observedOutboundRtp) {
2681
2853
  if (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind) {
2682
- return logger.warn(
2854
+ return logger3.warn(
2683
2855
  `ObservedPeerConnection received an invalid OutboundRtpStats (missing timestamp OR id OR ssrc OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2684
2856
  stats
2685
2857
  );
@@ -2704,7 +2876,7 @@ var ObservedPeerConnection = class extends import_node_events.EventEmitter {
2704
2876
  let observedOutboundTrack = this.observedOutboundTracks.get(stats.id);
2705
2877
  if (!observedOutboundTrack) {
2706
2878
  if (!stats.timestamp || !stats.id || !stats.kind) {
2707
- return logger.warn(
2879
+ return logger3.warn(
2708
2880
  `ObservedPeerConnection received an invalid OutboundTrackSample (missing timestamp OR id OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2709
2881
  stats
2710
2882
  );
@@ -2732,7 +2904,7 @@ var ObservedPeerConnection = class extends import_node_events.EventEmitter {
2732
2904
  let observedPeerConnectionTransport = this.observedPeerConnectionTransports.get(stats.id);
2733
2905
  if (!observedPeerConnectionTransport) {
2734
2906
  if (!stats.timestamp || !stats.id) {
2735
- return logger.warn(
2907
+ return logger3.warn(
2736
2908
  `ObservedPeerConnection received an invalid PeerConnectionTransportStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2737
2909
  stats
2738
2910
  );
@@ -2751,7 +2923,7 @@ var ObservedPeerConnection = class extends import_node_events.EventEmitter {
2751
2923
  let observedRemoteInboundRtp = this.observedRemoteInboundRtps.get(stats.ssrc);
2752
2924
  if (!observedRemoteInboundRtp) {
2753
2925
  if (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind) {
2754
- return logger.warn(
2926
+ return logger3.warn(
2755
2927
  `ObservedPeerConnection received an invalid RemoteInboundRtpStats (missing timestamp OR id OR ssrc OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2756
2928
  stats
2757
2929
  );
@@ -2776,7 +2948,7 @@ var ObservedPeerConnection = class extends import_node_events.EventEmitter {
2776
2948
  let observedRemoteOutboundRtp = this.observedRemoteOutboundRtps.get(stats.ssrc);
2777
2949
  if (!observedRemoteOutboundRtp) {
2778
2950
  if (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind) {
2779
- return logger.warn(
2951
+ return logger3.warn(
2780
2952
  `ObservedPeerConnection received an invalid RemoteOutboundRtpStats (missing timestamp OR id OR ssrc OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2781
2953
  stats
2782
2954
  );
@@ -2876,35 +3048,27 @@ function baseIssueType(type) {
2876
3048
  }
2877
3049
 
2878
3050
  // src/ObservedClient.ts
2879
- var logger2 = createLogger("ObservedClient");
3051
+ var logger4 = createLogger("ObservedClient");
2880
3052
  var RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS = 20;
2881
3053
  var RTT_HISTOGRAM_NUMBER_OF_BUCKETS = 50;
2882
3054
  var SCORE_HISTOGRAM_BUCKET_WIDTH = 1;
2883
3055
  var SCORE_HISTOGRAM_NUMBER_OF_BUCKETS = 5;
2884
- var ObservedClient = class extends import_node_events2.EventEmitter {
3056
+ var ObservedClient = class extends import_node_events3.EventEmitter {
2885
3057
  constructor(settings, call, activeIssues) {
2886
3058
  super();
2887
3059
  this.call = call;
2888
3060
  this.setMaxListeners(Infinity);
2889
3061
  this.eventScope = { observer: call.observer, observedCall: call, observedClient: this };
2890
- this.sink = call.observer.config.createClientSink?.({ clientId: settings.clientId, observedCall: call });
2891
3062
  this.clientId = settings.clientId;
2892
3063
  this.appData = settings.appData ?? {};
2893
3064
  this.settings = {
2894
3065
  closeClientIfIdleForMs: settings.closeClientIfIdleForMs
2895
3066
  };
2896
- if (this.sink) {
2897
- const onError = (err) => logger2.warn("ClientSampleSink error for client %s: %o", this.clientId, err);
2898
- this.sink.once("close", () => this.sink?.off("error", onError));
2899
- this.sink.on("error", onError);
2900
- }
2901
3067
  this.activeIssues = activeIssues;
2902
3068
  }
2903
3069
  call;
2904
3070
  clientId;
2905
3071
  observedPeerConnections = /* @__PURE__ */ new Map();
2906
- /** Per-client sink (from `ObserverConfig.createClientSink`), if any. */
2907
- sink;
2908
3072
  /** Ancestry base shared by all Observer-bus events originating at this client. */
2909
3073
  eventScope;
2910
3074
  calculatedScore = {
@@ -3043,6 +3207,13 @@ var ObservedClient = class extends import_node_events2.EventEmitter {
3043
3207
  _closedPeerConnectionIds = /* @__PURE__ */ new Set();
3044
3208
  closeTimer;
3045
3209
  /** The distinct `publicAddress`es currently known by this client's open peer connections. */
3210
+ /**
3211
+ * The sink this client's samples are written to: whatever is registered under its client id in
3212
+ * `call.sinks` (the `createClientJsonlSink` file, or a sink the application added), if any.
3213
+ */
3214
+ get sink() {
3215
+ return this.call.sinks.get(this.clientId);
3216
+ }
3046
3217
  get publicAddresses() {
3047
3218
  const result = /* @__PURE__ */ new Set();
3048
3219
  for (const observedPeerConnection of this.observedPeerConnections.values()) {
@@ -3137,12 +3308,12 @@ var ObservedClient = class extends import_node_events2.EventEmitter {
3137
3308
  try {
3138
3309
  this.sink?.end();
3139
3310
  } catch (err) {
3140
- logger2.warn("ClientSampleSink.end failed for client %s: %o", this.clientId, err);
3311
+ logger4.warn("Sink end failed for client %s: %o", this.clientId, err);
3141
3312
  }
3142
3313
  }
3143
3314
  accept(sample, context) {
3144
3315
  if (this.closed) {
3145
- logger2.warn("Attempted to accept a sample on a closed client %s", this.clientId);
3316
+ logger4.warn("Attempted to accept a sample on a closed client %s", this.clientId);
3146
3317
  return;
3147
3318
  }
3148
3319
  if (this.closeTimer) {
@@ -3332,7 +3503,7 @@ var ObservedClient = class extends import_node_events2.EventEmitter {
3332
3503
  try {
3333
3504
  this.sink?.write(sample);
3334
3505
  } catch (err) {
3335
- logger2.warn("ClientSampleSink.write failed for client %s: %o", this.clientId, err);
3506
+ logger4.warn("Sink write failed for client %s: %o", this.clientId, err);
3336
3507
  }
3337
3508
  }
3338
3509
  injectMetaData(metaData) {
@@ -3515,10 +3686,10 @@ var ObservedClient = class extends import_node_events2.EventEmitter {
3515
3686
  this._notify("client-rejoined", { ...this.eventScope, timestamp: event.timestamp });
3516
3687
  } else {
3517
3688
  this.joinedAt = event.timestamp;
3518
- logger2.warn(`Client ${this.clientId} joinedAt timestamp was updated to ${event.timestamp}. the joined event will not be emitted.`);
3689
+ logger4.warn(`Client ${this.clientId} joinedAt timestamp was updated to ${event.timestamp}. the joined event will not be emitted.`);
3519
3690
  }
3520
3691
  }
3521
- logger2.debug("Client %s joined at %o", this.clientId, event);
3692
+ logger4.debug("Client %s joined at %o", this.clientId, event);
3522
3693
  break;
3523
3694
  }
3524
3695
  case "CLIENT_LEFT" /* CLIENT_LEFT */: {
@@ -3528,10 +3699,10 @@ var ObservedClient = class extends import_node_events2.EventEmitter {
3528
3699
  this.emit("left");
3529
3700
  this._notify("client-left", { ...this.eventScope });
3530
3701
  } else {
3531
- logger2.warn(`Client ${this.clientId} leftAt timestamp was already set`);
3702
+ logger4.warn(`Client ${this.clientId} leftAt timestamp was already set`);
3532
3703
  }
3533
3704
  }
3534
- logger2.debug("Client %s left at %o", this.clientId, event);
3705
+ logger4.debug("Client %s left at %o", this.clientId, event);
3535
3706
  break;
3536
3707
  }
3537
3708
  case "PEER_CONNECTION_OPENED" /* PEER_CONNECTION_OPENED */: {
@@ -3543,7 +3714,7 @@ var ObservedClient = class extends import_node_events2.EventEmitter {
3543
3714
  } else if (postBuffer) {
3544
3715
  postBuffer.push(event);
3545
3716
  } else {
3546
- logger2.warn("Received PEER_CONNECTION_OPENED event without a corresponding observedPeerConnection: %o", event);
3717
+ logger4.warn("Received PEER_CONNECTION_OPENED event without a corresponding observedPeerConnection: %o", event);
3547
3718
  }
3548
3719
  }
3549
3720
  break;
@@ -3560,7 +3731,7 @@ var ObservedClient = class extends import_node_events2.EventEmitter {
3560
3731
  } else if (postBuffer) {
3561
3732
  postBuffer.push(event);
3562
3733
  } else {
3563
- logger2.warn("Received PEER_CONNECTION_CLOSED event without a corresponding observedPeerConnection: %o", event);
3734
+ logger4.warn("Received PEER_CONNECTION_CLOSED event without a corresponding observedPeerConnection: %o", event);
3564
3735
  }
3565
3736
  }
3566
3737
  break;
@@ -3575,7 +3746,7 @@ var ObservedClient = class extends import_node_events2.EventEmitter {
3575
3746
  } else if (postBuffer) {
3576
3747
  postBuffer.push(event);
3577
3748
  } else {
3578
- logger2.warn("Received MEDIA_TRACK_ADDED event without a corresponding observedPeerConnection or observedTrack: %o", event);
3749
+ logger4.warn("Received MEDIA_TRACK_ADDED event without a corresponding observedPeerConnection or observedTrack: %o", event);
3579
3750
  }
3580
3751
  }
3581
3752
  break;
@@ -3590,7 +3761,7 @@ var ObservedClient = class extends import_node_events2.EventEmitter {
3590
3761
  } else if (postBuffer) {
3591
3762
  postBuffer.push(event);
3592
3763
  } else {
3593
- logger2.warn("Received MEDIA_TRACK_REMOVED event without a corresponding observedPeerConnection or observedTrack: %o", event);
3764
+ logger4.warn("Received MEDIA_TRACK_REMOVED event without a corresponding observedPeerConnection or observedTrack: %o", event);
3594
3765
  }
3595
3766
  }
3596
3767
  break;
@@ -3605,7 +3776,7 @@ var ObservedClient = class extends import_node_events2.EventEmitter {
3605
3776
  } else if (postBuffer) {
3606
3777
  postBuffer.push(event);
3607
3778
  } else {
3608
- logger2.warn("Received DATA_CHANNEL_OPENED event without a corresponding observedPeerConnection or observedDataChannel: %o", event);
3779
+ logger4.warn("Received DATA_CHANNEL_OPENED event without a corresponding observedPeerConnection or observedDataChannel: %o", event);
3609
3780
  }
3610
3781
  }
3611
3782
  break;
@@ -3620,7 +3791,7 @@ var ObservedClient = class extends import_node_events2.EventEmitter {
3620
3791
  } else if (postBuffer) {
3621
3792
  postBuffer.push(event);
3622
3793
  } else {
3623
- logger2.warn("Received DATA_CHANNEL_CLOSE event without a corresponding observedPeerConnection or observedDataChannel: %o", event);
3794
+ logger4.warn("Received DATA_CHANNEL_CLOSE event without a corresponding observedPeerConnection or observedDataChannel: %o", event);
3624
3795
  }
3625
3796
  }
3626
3797
  break;
@@ -3640,7 +3811,7 @@ var ObservedClient = class extends import_node_events2.EventEmitter {
3640
3811
  } else if (postBuffer) {
3641
3812
  postBuffer.push(event);
3642
3813
  } else {
3643
- logger2.warn("Received MEDIA_TRACK_MUTED event without a corresponding observedPeerConnection or observedTrack: %o", event);
3814
+ logger4.warn("Received MEDIA_TRACK_MUTED event without a corresponding observedPeerConnection or observedTrack: %o", event);
3644
3815
  }
3645
3816
  }
3646
3817
  break;
@@ -3660,7 +3831,7 @@ var ObservedClient = class extends import_node_events2.EventEmitter {
3660
3831
  } else if (postBuffer) {
3661
3832
  postBuffer.push(event);
3662
3833
  } else {
3663
- logger2.warn("Received MEDIA_TRACK_UNMUTED event without a corresponding observedPeerConnection or observedTrack: %o", event);
3834
+ logger4.warn("Received MEDIA_TRACK_UNMUTED event without a corresponding observedPeerConnection or observedTrack: %o", event);
3664
3835
  }
3665
3836
  }
3666
3837
  break;
@@ -3675,7 +3846,7 @@ var ObservedClient = class extends import_node_events2.EventEmitter {
3675
3846
  } else if (postBuffer) {
3676
3847
  postBuffer.push(event);
3677
3848
  } else {
3678
- logger2.warn("Received ICE_CONNECTION_STATE_CHANGED event without a corresponding observedPeerConnection: %o", event);
3849
+ logger4.warn("Received ICE_CONNECTION_STATE_CHANGED event without a corresponding observedPeerConnection: %o", event);
3679
3850
  }
3680
3851
  }
3681
3852
  break;
@@ -3690,7 +3861,7 @@ var ObservedClient = class extends import_node_events2.EventEmitter {
3690
3861
  } else if (postBuffer) {
3691
3862
  postBuffer.push(event);
3692
3863
  } else {
3693
- logger2.warn("Received ICE_GATHERING_STATE_CHANGED event without a corresponding observedPeerConnection: %o", event);
3864
+ logger4.warn("Received ICE_GATHERING_STATE_CHANGED event without a corresponding observedPeerConnection: %o", event);
3694
3865
  }
3695
3866
  }
3696
3867
  break;
@@ -3705,7 +3876,7 @@ var ObservedClient = class extends import_node_events2.EventEmitter {
3705
3876
  } else if (postBuffer) {
3706
3877
  postBuffer.push(event);
3707
3878
  } else {
3708
- logger2.warn("Received PEER_CONNECTION_STATE_CHANGED event without a corresponding observedPeerConnection: %o", event);
3879
+ logger4.warn("Received PEER_CONNECTION_STATE_CHANGED event without a corresponding observedPeerConnection: %o", event);
3709
3880
  }
3710
3881
  }
3711
3882
  break;
@@ -3724,11 +3895,11 @@ var ObservedClient = class extends import_node_events2.EventEmitter {
3724
3895
  let observedPeerConnection = this.observedPeerConnections.get(sample.peerConnectionId);
3725
3896
  if (!observedPeerConnection) {
3726
3897
  if (this._closedPeerConnectionIds.has(sample.peerConnectionId)) {
3727
- logger2.debug("Ignoring stats of peer connection %s, already closed by client %s", sample.peerConnectionId, this.clientId);
3898
+ logger4.debug("Ignoring stats of peer connection %s, already closed by client %s", sample.peerConnectionId, this.clientId);
3728
3899
  return void 0;
3729
3900
  }
3730
3901
  if (!sample.peerConnectionId) {
3731
- return logger2.warn(
3902
+ return logger4.warn(
3732
3903
  `ObservedClient received an invalid PeerConnectionSample (missing peerConnectionId field). ClientId: ${this.clientId}, CallId: ${this.call.callId}`,
3733
3904
  sample
3734
3905
  ), void 0;
@@ -3788,7 +3959,7 @@ var ObservedClient = class extends import_node_events2.EventEmitter {
3788
3959
  try {
3789
3960
  this.sink?.write(finalSample);
3790
3961
  } catch (err) {
3791
- logger2.warn("ClientSampleSink.write failed during injection flush for client %s: %o", this.clientId, err);
3962
+ logger4.warn("Sink write failed during injection flush for client %s: %o", this.clientId, err);
3792
3963
  }
3793
3964
  }
3794
3965
  /** Emit an Observer-bus event scoped to this client (or a peer connection under it). */
@@ -3816,7 +3987,7 @@ var DefaultCallScoreCalculator = class {
3816
3987
  };
3817
3988
 
3818
3989
  // src/detectors/Detectors.ts
3819
- var logger3 = createLogger("Detectors");
3990
+ var logger5 = createLogger("Detectors");
3820
3991
  var Detectors = class {
3821
3992
  _detectors;
3822
3993
  constructor(...detectors) {
@@ -3897,7 +4068,7 @@ var Detectors = class {
3897
4068
  try {
3898
4069
  detector.update();
3899
4070
  } catch (err) {
3900
- logger3.warn(`Error updating detector ${detector?.name}`, err);
4071
+ logger5.warn(`Error updating detector ${detector?.name}`, err);
3901
4072
  }
3902
4073
  }
3903
4074
  }
@@ -3910,7 +4081,7 @@ var Detectors = class {
3910
4081
  try {
3911
4082
  detector.close?.();
3912
4083
  } catch (err) {
3913
- logger3.warn(`Error closing detector ${detector?.name}`, err);
4084
+ logger5.warn(`Error closing detector ${detector?.name}`, err);
3914
4085
  }
3915
4086
  }
3916
4087
  };
@@ -4877,8 +5048,8 @@ var AudioEchoDetector = class _AudioEchoDetector {
4877
5048
  };
4878
5049
 
4879
5050
  // src/ObservedCall.ts
4880
- var logger4 = createLogger("ObservedCall");
4881
- var ObservedCall = class extends import_node_events3.EventEmitter {
5051
+ var logger6 = createLogger("ObservedCall");
5052
+ var ObservedCall = class extends import_node_events4.EventEmitter {
4882
5053
  constructor(settings, observer, activeIssuesRegistry) {
4883
5054
  super();
4884
5055
  this.observer = observer;
@@ -4886,6 +5057,8 @@ var ObservedCall = class extends import_node_events3.EventEmitter {
4886
5057
  this.setMaxListeners(Infinity);
4887
5058
  this.eventScope = { observer: this.observer, observedCall: this };
4888
5059
  this.callId = settings.callId;
5060
+ this.sinks = new SinkRegistry(this.observer.sinks);
5061
+ this.extensionStats = new ExtensionStatsRegistry(this.callId, this.sinks);
4889
5062
  this.appData = settings.appData ?? {};
4890
5063
  this.scoreCalculator = new DefaultCallScoreCalculator(this);
4891
5064
  this.detectors = new Detectors();
@@ -4942,6 +5115,8 @@ var ObservedCall = class extends import_node_events3.EventEmitter {
4942
5115
  settings;
4943
5116
  /** Ancestry base shared by all Observer-bus events originating at this call. */
4944
5117
  eventScope;
5118
+ sinks;
5119
+ extensionStats;
4945
5120
  closeTimer;
4946
5121
  get numberOfClients() {
4947
5122
  return this.observedClients.size;
@@ -4984,7 +5159,7 @@ var ObservedCall = class extends import_node_events3.EventEmitter {
4984
5159
  break;
4985
5160
  }
4986
5161
  default: {
4987
- logger4.warn("Unknown detector name %s; skipping", name);
5162
+ logger6.warn("Unknown detector name %s; skipping", name);
4988
5163
  return this;
4989
5164
  }
4990
5165
  }
@@ -5054,6 +5229,8 @@ var ObservedCall = class extends import_node_events3.EventEmitter {
5054
5229
  if (client.joinedAt) minSampleTimestamps = Math.min(minSampleTimestamps ?? client.joinedAt, client.joinedAt);
5055
5230
  if (client.leftAt) maxSampleTimestamps = Math.max(maxSampleTimestamps ?? client.leftAt, client.leftAt);
5056
5231
  }
5232
+ this.extensionStats.close();
5233
+ this.sinks.clear();
5057
5234
  if (this.startedAt === void 0) this.startedAt = minSampleTimestamps;
5058
5235
  if (this.endedAt === void 0) this.endedAt = maxSampleTimestamps;
5059
5236
  this.closedAt = Date.now();
@@ -5066,17 +5243,26 @@ var ObservedCall = class extends import_node_events3.EventEmitter {
5066
5243
  this.emit("close");
5067
5244
  this._notify("call-closed", { ...this.eventScope });
5068
5245
  }
5246
+ createExtensionStats(settings) {
5247
+ return this.extensionStats.create(settings);
5248
+ }
5249
+ acceptExtensionStats(id, data) {
5250
+ this.extensionStats.accept(id, data);
5251
+ }
5252
+ getExtensionStats(id) {
5253
+ return this.extensionStats.get(id);
5254
+ }
5069
5255
  getObservedClient(clientId) {
5070
5256
  if (this.closed || !this.observedClients.has(clientId)) return;
5071
5257
  return this.observedClients.get(clientId);
5072
5258
  }
5073
5259
  createObservedClient(settings, acceptCtx) {
5074
5260
  if (this.closed) {
5075
- logger4.warn("Attempted to create a client (clientId: %s) on a closed call %s", settings.clientId, this.callId);
5261
+ logger6.warn("Attempted to create a client (clientId: %s) on a closed call %s", settings.clientId, this.callId);
5076
5262
  return void 0;
5077
5263
  }
5078
5264
  if (this.observedClients.has(settings.clientId)) {
5079
- logger4.warn("Client with id %s already exists in call %s; returning the existing instance", settings.clientId, this.callId);
5265
+ logger6.warn("Client with id %s already exists in call %s; returning the existing instance", settings.clientId, this.callId);
5080
5266
  return this.observedClients.get(settings.clientId);
5081
5267
  }
5082
5268
  const clientSettings = {
@@ -5126,10 +5312,17 @@ var ObservedCall = class extends import_node_events3.EventEmitter {
5126
5312
  clearTimeout(this.closeTimer);
5127
5313
  this.closeTimer = void 0;
5128
5314
  }
5315
+ const jsonlPath = this.observer.config.createClientJsonlSink?.({ observedCall: this, clientId: settings.clientId });
5316
+ let jsonlSink;
5317
+ if (jsonlPath !== void 0 && this.observer.sinks.get(settings.clientId)) {
5318
+ logger6.warn("No sample file for client %s in call %s: sink id %s is already in use", settings.clientId, this.callId, settings.clientId);
5319
+ } else if (jsonlPath !== void 0) {
5320
+ jsonlSink = this.sinks.create(JsonlFileSink, { id: settings.clientId, path: jsonlPath });
5321
+ }
5129
5322
  this.emit("newclient", result);
5130
5323
  this._notify("client-added", { ...this.eventScope, observedClient: result });
5131
- if (result.sink) {
5132
- this._notify("client-sink-created", { ...this.eventScope, observedClient: result, sink: result.sink });
5324
+ if (jsonlSink) {
5325
+ this._notify("client-sink-created", { ...this.eventScope, observedClient: result, sink: jsonlSink });
5133
5326
  }
5134
5327
  if (wasEmpty) {
5135
5328
  this.emit("not-empty");
@@ -5182,10 +5375,10 @@ var ObservedCall = class extends import_node_events3.EventEmitter {
5182
5375
  };
5183
5376
 
5184
5377
  // src/Observer.ts
5185
- var import_node_events6 = require("events");
5378
+ var import_node_events7 = require("events");
5186
5379
 
5187
5380
  // src/ObservedTURN.ts
5188
- var import_node_events4 = require("events");
5381
+ var import_node_events5 = require("events");
5189
5382
 
5190
5383
  // src/ObservedTurnServer.ts
5191
5384
  var ObservedTurnServer = class {
@@ -5245,8 +5438,8 @@ var ObservedTurnServer = class {
5245
5438
  };
5246
5439
 
5247
5440
  // src/ObservedTURN.ts
5248
- var logger5 = createLogger("ObservedTURN");
5249
- var ObservedTURN = class extends import_node_events4.EventEmitter {
5441
+ var logger7 = createLogger("ObservedTURN");
5442
+ var ObservedTURN = class extends import_node_events5.EventEmitter {
5250
5443
  totalBytesSent = 0;
5251
5444
  totalBytesReceived = 0;
5252
5445
  totalPacketsSent = 0;
@@ -5275,12 +5468,12 @@ var ObservedTURN = class extends import_node_events4.EventEmitter {
5275
5468
  addPeerConnection(peerConnection) {
5276
5469
  const turnPairs = peerConnection.selectedIceCandiadtePairForTurn;
5277
5470
  if (turnPairs.length !== 1) {
5278
- return logger5.warn(`Expected exactly one TURN pair, but found for peerconnection ${peerConnection.peerConnectionId}`, turnPairs.length), void 0;
5471
+ return logger7.warn(`Expected exactly one TURN pair, but found for peerconnection ${peerConnection.peerConnectionId}`, turnPairs.length), void 0;
5279
5472
  }
5280
5473
  const candidatePair = turnPairs[0];
5281
5474
  const rawUrl = candidatePair.getLocalCandidate()?.url;
5282
5475
  if (!rawUrl) {
5283
- return logger5.warn(`No local candidate URL found for peerconnection ${peerConnection.peerConnectionId}`), void 0;
5476
+ return logger7.warn(`No local candidate URL found for peerconnection ${peerConnection.peerConnectionId}`), void 0;
5284
5477
  }
5285
5478
  const turnServerUrl = rawUrl.split("?")[0];
5286
5479
  let turnServer = this.servers.get(turnServerUrl);
@@ -5356,14 +5549,16 @@ var MiddlewareProcessor = class {
5356
5549
  };
5357
5550
 
5358
5551
  // src/ObservedMediasoupRouter.ts
5359
- var import_node_events5 = require("events");
5360
- var logger6 = createLogger("ObservedMediasoupRouter");
5361
- var ObservedMediasoupRouter = class extends import_node_events5.EventEmitter {
5552
+ var import_node_events6 = require("events");
5553
+ var logger8 = createLogger("ObservedMediasoupRouter");
5554
+ var ObservedMediasoupRouter = class extends import_node_events6.EventEmitter {
5362
5555
  router;
5363
5556
  appData;
5364
5557
  sample;
5365
5558
  webrtcTransportIds = /* @__PURE__ */ new Set();
5366
5559
  closed = false;
5560
+ sinks;
5561
+ extensionStats;
5367
5562
  // Id -> sample indexes over the arrays in `this.sample`, so an entity can be reached in O(1)
5368
5563
  // instead of scanning (`sample.producers.find(...)`). They hold the *same* objects as the arrays.
5369
5564
  _transportSamples = /* @__PURE__ */ new Map();
@@ -5372,11 +5567,17 @@ var ObservedMediasoupRouter = class extends import_node_events5.EventEmitter {
5372
5567
  _dataProducerSamples = /* @__PURE__ */ new Map();
5373
5568
  _dataConsumerSamples = /* @__PURE__ */ new Map();
5374
5569
  _enrich;
5375
- constructor(settings) {
5570
+ /**
5571
+ * @param parentSinks the parent of `sinks`: `observer.sinks`, passed by
5572
+ * `Observer.createObservedMediasoupRouter`. Internal; a router constructed directly has none.
5573
+ */
5574
+ constructor(settings, parentSinks) {
5376
5575
  super();
5377
5576
  this.router = settings.router;
5378
5577
  this.appData = settings.appData ?? {};
5379
5578
  this._enrich = settings.enrich;
5579
+ this.sinks = new SinkRegistry(parentSinks);
5580
+ this.extensionStats = new ExtensionStatsRegistry(this.router.id, this.sinks);
5380
5581
  this.sample = {
5381
5582
  routerId: this.router.id,
5382
5583
  attachments: settings.attachments ?? {},
@@ -5445,8 +5646,19 @@ var ObservedMediasoupRouter = class extends import_node_events5.EventEmitter {
5445
5646
  if (this.closed) return;
5446
5647
  this.closed = true;
5447
5648
  if (this.sample.closedAt === void 0) this.sample.closedAt = Date.now();
5649
+ this.extensionStats.close();
5650
+ this.sinks.clear();
5448
5651
  this.emit("close");
5449
5652
  }
5653
+ createExtensionStats(settings) {
5654
+ return this.extensionStats.create(settings);
5655
+ }
5656
+ acceptExtensionStats(id, data) {
5657
+ this.extensionStats.accept(id, data);
5658
+ }
5659
+ getExtensionStats(id) {
5660
+ return this.extensionStats.get(id);
5661
+ }
5450
5662
  addTransport = (transport) => {
5451
5663
  let transportType = transport.type;
5452
5664
  if (!transportType) {
@@ -5455,7 +5667,7 @@ var ObservedMediasoupRouter = class extends import_node_events5.EventEmitter {
5455
5667
  else if (transport.constructor.name === "PipeTransport") transportType = "pipe";
5456
5668
  else if (transport.constructor.name === "DirectTransport") transportType = "direct";
5457
5669
  }
5458
- if (!transportType) return logger6.warn(`Unknown transport type for transport ${transport.id}: ${transport.constructor.name}`);
5670
+ if (!transportType) return logger8.warn(`Unknown transport type for transport ${transport.id}: ${transport.constructor.name}`);
5459
5671
  switch (transportType) {
5460
5672
  case "webrtc":
5461
5673
  return this.addWebRtcTransport(transport);
@@ -5466,7 +5678,7 @@ var ObservedMediasoupRouter = class extends import_node_events5.EventEmitter {
5466
5678
  case "direct":
5467
5679
  return this.addDirectTransport(transport);
5468
5680
  default:
5469
- return logger6.warn(`Unsupported transport type for transport ${transport.id}: ${transportType}`);
5681
+ return logger8.warn(`Unsupported transport type for transport ${transport.id}: ${transportType}`);
5470
5682
  }
5471
5683
  };
5472
5684
  addWebRtcTransport(transport) {
@@ -5708,7 +5920,7 @@ var ObservedMediasoupRouter = class extends import_node_events5.EventEmitter {
5708
5920
  const attachments = enrich();
5709
5921
  if (attachments) sample.attachments = { ...sample.attachments, ...attachments };
5710
5922
  } catch (err) {
5711
- logger6.warn("Mediasoup sample enricher threw for router %s: %o", this.id, err);
5923
+ logger8.warn("Mediasoup sample enricher threw for router %s: %o", this.id, err);
5712
5924
  }
5713
5925
  }
5714
5926
  _attachTransportObserverListeners(transport, transportSample, onClose) {
@@ -5733,7 +5945,7 @@ var ObservedMediasoupRouter = class extends import_node_events5.EventEmitter {
5733
5945
  };
5734
5946
 
5735
5947
  // src/issues/ActiveIssuesRegistry.ts
5736
- var logger7 = createLogger("ActiveIssuesRegistry");
5948
+ var logger9 = createLogger("ActiveIssuesRegistry");
5737
5949
  var ActiveIssuesRegistry = class {
5738
5950
  constructor(parent) {
5739
5951
  this.parent = parent;
@@ -5819,7 +6031,7 @@ var ActiveIssuesRegistry = class {
5819
6031
  try {
5820
6032
  apply(target);
5821
6033
  } catch (err) {
5822
- logger7.warn("An issue tracker threw while being notified; continuing with the rest. %o", err);
6034
+ logger9.warn("An issue tracker threw while being notified; continuing with the rest. %o", err);
5823
6035
  }
5824
6036
  }
5825
6037
  };
@@ -5848,7 +6060,7 @@ function createCallSummary(callId, config) {
5848
6060
  }
5849
6061
 
5850
6062
  // src/summaries/CallSummaryCollector.ts
5851
- var logger8 = createLogger("CallSummaryCollector");
6063
+ var logger10 = createLogger("CallSummaryCollector");
5852
6064
  var CallSummaryCollector = class {
5853
6065
  constructor(_observer, _config) {
5854
6066
  this._observer = _observer;
@@ -5923,7 +6135,7 @@ var CallSummaryCollector = class {
5923
6135
  try {
5924
6136
  handler(summary, scratch, ...args);
5925
6137
  } catch (err) {
5926
- logger8.warn("A call-summary handler for %s threw; continuing. %o", event, err);
6138
+ logger10.warn("A call-summary handler for %s threw; continuing. %o", event, err);
5927
6139
  }
5928
6140
  };
5929
6141
  this._observer.on(event, listener);
@@ -6348,7 +6560,7 @@ function geohash(location, precision = 3) {
6348
6560
  }
6349
6561
 
6350
6562
  // src/detectors/ClientPopulationIssueDetector.ts
6351
- var logger9 = createLogger("ClientPopulationIssueDetector");
6563
+ var logger11 = createLogger("ClientPopulationIssueDetector");
6352
6564
  var ClientPopulationIssueTypes = {
6353
6565
  /** One issue type is concentrated on one client population while the rest of the fleet is fine. */
6354
6566
  clientPopulationIssue: "CLIENT_POPULATION_ISSUE"
@@ -6370,7 +6582,7 @@ var ClientPopulationIssueDetector = class _ClientPopulationIssueDetector {
6370
6582
  ...config
6371
6583
  };
6372
6584
  if (this._config.groupBy === "location" && !this._config.resolveClientLocation) {
6373
- logger9.warn(
6585
+ logger11.warn(
6374
6586
  "%s is configured with groupBy: 'location' but no resolveClientLocation, so no client can be placed and nothing will ever be detected",
6375
6587
  this.name
6376
6588
  );
@@ -7204,11 +7416,13 @@ function mediaKindOf(mimeType) {
7204
7416
  }
7205
7417
 
7206
7418
  // src/Observer.ts
7207
- var logger10 = createLogger("Observer");
7208
- var Observer = class extends import_node_events6.EventEmitter {
7419
+ var logger12 = createLogger("Observer");
7420
+ var Observer = class extends import_node_events7.EventEmitter {
7209
7421
  observedTURN = new ObservedTURN();
7210
7422
  observedCalls = /* @__PURE__ */ new Map();
7211
7423
  observedMediasoupRouters = /* @__PURE__ */ new Map();
7424
+ sinks = new SinkRegistry();
7425
+ extensionStats = new ExtensionStatsRegistry("observer", this.sinks);
7212
7426
  /** Ancestry base shared by all Observer-bus events originating at the observer. */
7213
7427
  eventScope = { observer: this };
7214
7428
  config;
@@ -7323,7 +7537,7 @@ var Observer = class extends import_node_events6.EventEmitter {
7323
7537
  break;
7324
7538
  }
7325
7539
  default: {
7326
- logger10.warn("Unknown detector name %s; skipping", name);
7540
+ logger12.warn("Unknown detector name %s; skipping", name);
7327
7541
  return this;
7328
7542
  }
7329
7543
  }
@@ -7408,7 +7622,7 @@ var Observer = class extends import_node_events6.EventEmitter {
7408
7622
  break;
7409
7623
  }
7410
7624
  if (!validator) {
7411
- logger10.warn("Unknown validator name %s; skipping", name);
7625
+ logger12.warn("Unknown validator name %s; skipping", name);
7412
7626
  return this;
7413
7627
  }
7414
7628
  this.validators.add(validator);
@@ -7439,7 +7653,7 @@ var Observer = class extends import_node_events6.EventEmitter {
7439
7653
  try {
7440
7654
  validator.cancel(reason);
7441
7655
  } catch (err) {
7442
- logger10.warn("Error cancelling validator %s: %o", validator.name, err);
7656
+ logger12.warn("Error cancelling validator %s: %o", validator.name, err);
7443
7657
  }
7444
7658
  }
7445
7659
  return matching.length;
@@ -7455,11 +7669,11 @@ var Observer = class extends import_node_events6.EventEmitter {
7455
7669
  */
7456
7670
  createObservedCall(settings, acceptCtx) {
7457
7671
  if (this.closed) {
7458
- logger10.warn("Attempted to create a call (callId: %s) on a closed observer", settings.callId);
7672
+ logger12.warn("Attempted to create a call (callId: %s) on a closed observer", settings.callId);
7459
7673
  return void 0;
7460
7674
  }
7461
7675
  if (this.observedCalls.has(settings.callId)) {
7462
- logger10.warn("Observed Call with id %s already exists; returning the existing instance", settings.callId);
7676
+ logger12.warn("Observed Call with id %s already exists; returning the existing instance", settings.callId);
7463
7677
  return this.observedCalls.get(settings.callId);
7464
7678
  }
7465
7679
  const callSettings = {
@@ -7497,14 +7711,14 @@ var Observer = class extends import_node_events6.EventEmitter {
7497
7711
  }
7498
7712
  createObservedMediasoupRouter(settings) {
7499
7713
  if (this.closed) {
7500
- logger10.warn("Attempted to create mediasoup router (id: %d) on a closed observer", settings.router.id);
7714
+ logger12.warn("Attempted to create mediasoup router (id: %d) on a closed observer", settings.router.id);
7501
7715
  return void 0;
7502
7716
  }
7503
7717
  if (this.observedMediasoupRouters.has(settings.router.id)) {
7504
- logger10.warn("Observed Mediasoup Router (id %s) already exists; returning the existing instance", settings.router.id);
7718
+ logger12.warn("Observed Mediasoup Router (id %s) already exists; returning the existing instance", settings.router.id);
7505
7719
  return this.observedMediasoupRouters.get(settings.router.id);
7506
7720
  }
7507
- const observedMediasoupRouter = new ObservedMediasoupRouter(settings);
7721
+ const observedMediasoupRouter = new ObservedMediasoupRouter(settings, this.sinks);
7508
7722
  const observedMediasoupRouterScope = {
7509
7723
  observedMediasoupRouter,
7510
7724
  observer: this
@@ -7532,7 +7746,7 @@ var Observer = class extends import_node_events6.EventEmitter {
7532
7746
  }
7533
7747
  close() {
7534
7748
  if (this.closed) {
7535
- return logger10.debug("Attempted to close twice");
7749
+ return logger12.debug("Attempted to close twice");
7536
7750
  }
7537
7751
  this.closed = true;
7538
7752
  for (const call of [...this.observedCalls.values()]) call.close();
@@ -7541,8 +7755,19 @@ var Observer = class extends import_node_events6.EventEmitter {
7541
7755
  this.validators.clear();
7542
7756
  this.callSummaryCollector?.close();
7543
7757
  this.activeIssuesRegistry.clear();
7758
+ this.extensionStats.close();
7759
+ this.sinks.clear();
7544
7760
  this._notify("observer-closed", { ...this.eventScope });
7545
7761
  }
7762
+ createExtensionStats(settings) {
7763
+ return this.extensionStats.create(settings);
7764
+ }
7765
+ acceptExtensionStats(id, data) {
7766
+ this.extensionStats.accept(id, data);
7767
+ }
7768
+ getExtensionStats(id) {
7769
+ return this.extensionStats.get(id);
7770
+ }
7546
7771
  accept(sample, context) {
7547
7772
  if (this.closed) {
7548
7773
  return this._notify("sample-rejected", { ...this.eventScope, reason: "observer-closed", sample });
@@ -7550,7 +7775,7 @@ var Observer = class extends import_node_events6.EventEmitter {
7550
7775
  try {
7551
7776
  this.acceptMiddlewares.process({ sample, context });
7552
7777
  } catch (err) {
7553
- logger10.warn("An accept middleware threw; dropping the sample. %o", err);
7778
+ logger12.warn("An accept middleware threw; dropping the sample. %o", err);
7554
7779
  }
7555
7780
  if (!sample.callId) {
7556
7781
  this._notify("sample-rejected", { ...this.eventScope, reason: "missing-callId", sample });
@@ -7590,7 +7815,7 @@ var Observer = class extends import_node_events6.EventEmitter {
7590
7815
  try {
7591
7816
  validator.update();
7592
7817
  } catch (err) {
7593
- logger10.warn("Error running validator %s: %o", validator.name, err);
7818
+ logger12.warn("Error running validator %s: %o", validator.name, err);
7594
7819
  }
7595
7820
  }
7596
7821
  this._notify("observer-updated", { ...this.eventScope });
@@ -7826,67 +8051,34 @@ var TrendTester = class {
7826
8051
  }
7827
8052
  };
7828
8053
 
7829
- // src/sinks/ClientSampleSink.ts
7830
- var import_node_events7 = require("events");
7831
- var ClientSampleSink = class extends import_node_events7.EventEmitter {
7832
- };
7833
-
7834
- // src/sinks/JsonlFileSink.ts
7835
- var fs = __toESM(require("fs"));
7836
- var path = __toESM(require("path"));
7837
- var JsonlFileSink = class extends ClientSampleSink {
7838
- /** The file this sink writes to. Read it (e.g. in a `close` handler) to upload/move the file. */
7839
- path;
7840
- _stream;
7841
- _serializeSample;
7842
- constructor(options) {
7843
- super();
7844
- this.path = options.path;
7845
- this._stream = fs.createWriteStream(this.path, { flags: "a" });
7846
- this._stream.on("close", () => this.emit("close"));
7847
- this._stream.on("finish", () => this.emit("finish"));
7848
- this._stream.on("drain", () => this.emit("drain"));
7849
- this._stream.on("error", (err) => this.emit("error", err));
7850
- this._serializeSample = options.serializeSample ?? ((sample) => JSON.stringify(sample));
7851
- }
7852
- write(sample) {
7853
- return this._stream.write(`${this._serializeSample(sample)}
7854
- `);
7855
- }
7856
- end() {
7857
- this._stream.end();
7858
- }
7859
- };
7860
- function createJsonlFileSink(options) {
7861
- return new JsonlFileSink(options);
7862
- }
7863
- function createJsonlFileSinkFactory(options) {
7864
- return ({ clientId, observedCall }) => {
7865
- const filename = options.getFileName?.({ callId: observedCall.callId, clientId }) ?? `${observedCall.callId}__${clientId}.jsonl`;
7866
- return createJsonlFileSink({
7867
- path: path.join(options.directory, filename),
7868
- serializeSample: options.serializeSample
7869
- });
7870
- };
7871
- }
7872
-
7873
8054
  // src/sinks/InMemorySink.ts
7874
- var InMemorySink = class extends ClientSampleSink {
7875
- constructor(samples = []) {
8055
+ var import_node_events8 = require("events");
8056
+ var InMemorySink = class extends import_node_events8.EventEmitter {
8057
+ id;
8058
+ samples;
8059
+ _ended = false;
8060
+ constructor(settings = {}) {
7876
8061
  super();
7877
- this.samples = samples;
8062
+ this.id = settings.id ?? "in-memory";
8063
+ this.samples = settings.samples ?? [];
8064
+ }
8065
+ get ended() {
8066
+ return this._ended;
7878
8067
  }
7879
- samples;
7880
8068
  write(sample) {
8069
+ if (this._ended) return false;
7881
8070
  this.samples.push(sample);
7882
8071
  return true;
7883
8072
  }
7884
8073
  end() {
8074
+ if (this._ended) return;
8075
+ this._ended = true;
8076
+ this.emit("ended");
7885
8077
  this.emit("close");
7886
8078
  }
7887
8079
  };
7888
8080
  function createInMemorySink(samples) {
7889
- return new InMemorySink(samples);
8081
+ return new InMemorySink({ samples });
7890
8082
  }
7891
8083
 
7892
8084
  // src/resolvers/RemoteTrackResolver.ts
@@ -8075,9 +8267,9 @@ function createP2pRemoteTrackResolverFactory() {
8075
8267
  ClientMetaTypes,
8076
8268
  ClientPopulationIssueDetector,
8077
8269
  ClientPopulationIssueTypes,
8078
- ClientSampleSink,
8079
8270
  CodecConsistencyValidator,
8080
8271
  Detectors,
8272
+ ExtensionStatsRegistry,
8081
8273
  GEOHASH_CELL_SIZES,
8082
8274
  InMemorySink,
8083
8275
  IssueFanOutDetector,
@@ -8121,6 +8313,7 @@ function createP2pRemoteTrackResolverFactory() {
8121
8313
  SCORE_HISTOGRAM_NUMBER_OF_BUCKETS,
8122
8314
  SfuCongestionDetector,
8123
8315
  SimulcastReceiverValidator,
8316
+ SinkRegistry,
8124
8317
  SlidingWindow,
8125
8318
  TrackDeliveryMismatchDetector,
8126
8319
  TrackDeliveryMismatchTypes,
@@ -8141,7 +8334,6 @@ function createP2pRemoteTrackResolverFactory() {
8141
8334
  createDefaultMediasoupRemoteTrackResolverFactory,
8142
8335
  createInMemorySink,
8143
8336
  createJsonlFileSink,
8144
- createJsonlFileSinkFactory,
8145
8337
  createLogger,
8146
8338
  createP2pRemoteTrackResolverFactory,
8147
8339
  defaultCallSummaryConfig,