@observertc/observer-js 1.1.0 → 1.2.0-rc.1

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.d.ts CHANGED
@@ -1,6 +1,128 @@
1
- import { EventEmitter } from 'events';
1
+ import { EventEmitter } from 'node:events';
2
2
  import { types } from 'mediasoup';
3
3
 
4
+ /** The lifecycle events a sink emits. */
5
+ type SinkEvents = {
6
+ /**
7
+ * `end()` was called: the sink accepts no more writes. **Required**: a sink emits it
8
+ * synchronously, once, from `end()` — it is how its registry removes it and frees its id.
9
+ */
10
+ ended: [];
11
+ /** The destination is fully written and closed (e.g. a file flushed and its fd closed). */
12
+ close: [];
13
+ /** The destination failed. */
14
+ error: [Error];
15
+ /** All queued data was flushed (before `close`). */
16
+ finish: [];
17
+ /** The buffer drained after backpressure; safe to write more. */
18
+ drain: [];
19
+ };
20
+ /** What a registry passes to a sink's constructor. Implementations extend it with their own settings. */
21
+ type SinkSettings = {
22
+ /** Unique per observer. The registry generates one (uuid v4) when it is omitted. */
23
+ id: string;
24
+ };
25
+ /**
26
+ * A destination for records. It is **object-mode** and agnostic about what passes through it:
27
+ * `write` receives the record itself, and the implementation decides how to serialize it (JSON
28
+ * line, protobuf, a remote payload, …). `JsonlFileSink` and `InMemorySink` are the built-in
29
+ * implementations; `ClientSampleSink` is `Sink<ClientSample>`.
30
+ *
31
+ * Create sinks with a registry's `create(SinkClass, settings)`, which constructs
32
+ * `new SinkClass({ ...settings, id })` and registers it on the object that owns its lifecycle.
33
+ * `end()` closes a sink and — through the `ended` event — removes it from its registry; the owner
34
+ * ends whatever is left when it closes.
35
+ *
36
+ * An implementation typically `extends EventEmitter implements Sink<T>`, and must:
37
+ * - emit `ended` synchronously, once, from `end()` (and make a second `end()` a no-op);
38
+ * - refuse writes after `end()` (return `false`);
39
+ * - emit `close` when the destination is ready, and `error` when it fails.
40
+ */
41
+ interface Sink<T = unknown> {
42
+ readonly id: string;
43
+ /** Accept one record. `false` once ended (nothing written), or on backpressure. */
44
+ write(record: T): boolean;
45
+ /** No more writes; emit `ended`, then flush and emit `close`. Idempotent. */
46
+ end(): void;
47
+ on(event: keyof SinkEvents, listener: (...args: any[]) => void): this;
48
+ once(event: keyof SinkEvents, listener: (...args: any[]) => void): this;
49
+ off(event: keyof SinkEvents, listener: (...args: any[]) => void): this;
50
+ }
51
+
52
+ /** A sink class `SinkRegistry.create` can construct: its constructor takes one settings object (with `id`). */
53
+ type SinkConstructor = new (settings: any) => Sink<any>;
54
+ /** The settings `create` takes for `SinkClass`: its constructor's, with `id` optional. */
55
+ type SinkCreateSettings<C extends SinkConstructor> = Omit<ConstructorParameters<C>[0], 'id'> & {
56
+ id?: string;
57
+ };
58
+ /**
59
+ * The sinks of one object, ended by that object's lifecycle. `create(SinkClass, settings)` builds a
60
+ * sink and registers it; `sink.end()` closes it and removes it; the owner calls `clear()` when it
61
+ * closes, ending whatever is left.
62
+ *
63
+ * Registries form a tree like the issue registries: `observer.sinks` is the parent of every
64
+ * `observedCall.sinks` and `observedMediasoupRouter.sinks`. **A sink id is unique per observer**:
65
+ * `create` refuses an id that any registry of the tree holds. Each registry sees its own sinks and
66
+ * its descendants', so `get`, `write` and `clear` on a parent reach a child's sinks too.
67
+ *
68
+ * A client's sample sink is the one under its client id in its call's registry.
69
+ */
70
+ declare class SinkRegistry {
71
+ readonly parent?: SinkRegistry | undefined;
72
+ private readonly _sinks;
73
+ constructor(parent?: SinkRegistry | undefined);
74
+ /** Number of open sinks in this registry and its descendants. */
75
+ get size(): number;
76
+ keys(): IterableIterator<string>;
77
+ values(): IterableIterator<Sink<any>>;
78
+ get<T extends Sink<any> = Sink>(id: string): T | undefined;
79
+ /**
80
+ * Construct `new SinkClass(settings)` and register it. The id (`settings.id`, generated when
81
+ * omitted) is checked first, so nothing is constructed — no file is opened — when it is taken:
82
+ * `undefined` is returned and a warning logged.
83
+ */
84
+ create<C extends SinkConstructor>(SinkClass: C, settings: SinkCreateSettings<C>): InstanceType<C> | undefined;
85
+ /** Write `record` to the sink `id`. `false` when there is no such sink, or on backpressure. */
86
+ write(id: string, record: unknown): boolean;
87
+ /** End every sink of this registry and of its descendants. */
88
+ clear(): void;
89
+ /** This registry and its ancestors, up to the root. */
90
+ private _lineage;
91
+ }
92
+
93
+ /** The settings of `createExtensionStats` on an observer, a call or a mediasoup router. */
94
+ type CreateExtensionStatsSettings<C extends SinkConstructor> = {
95
+ /** Unique per observer: it is also the id of the sink that receives the stats. */
96
+ id: string;
97
+ /** What `sinks.create` takes; the sink is created under `id` (an `id` in its settings is ignored). */
98
+ createSink?: [SinkClass: C, settings: Omit<SinkCreateSettings<C>, 'id'>];
99
+ };
100
+ /**
101
+ * The extension stats of one object — `observer.extensionStats`, `observedCall.extensionStats`,
102
+ * `observedMediasoupRouter.extensionStats`: the last data per id (`values`), written to the
103
+ * object's sink with the same id. The owner closes it when it closes.
104
+ */
105
+ declare class ExtensionStatsRegistry {
106
+ /** The owner's id (call id, router id, or `observer`), used in log messages. */
107
+ readonly id: string;
108
+ private readonly _sinks;
109
+ /** The last data accepted for each extension stats id. */
110
+ readonly values: Map<string, unknown>;
111
+ closed: boolean;
112
+ constructor(
113
+ /** The owner's id (call id, router id, or `observer`), used in log messages. */
114
+ id: string, _sinks: SinkRegistry);
115
+ /**
116
+ * Refuse further `create` / `accept`, and end the extension stats' sinks (the sinks whose id is
117
+ * an extension stats id). Other sinks of the owner, e.g. client sample sinks, are left to the
118
+ * owner's `sinks.clear()`. Called by the owner when it closes.
119
+ */
120
+ close(): void;
121
+ create<C extends SinkConstructor>(settings: CreateExtensionStatsSettings<C>): InstanceType<C> | undefined;
122
+ accept(id: string, data: unknown): void;
123
+ get<T>(id: string): T | undefined;
124
+ }
125
+
4
126
  declare const schemaVersion = "3.7.0";
5
127
  /**
6
128
  * The WebRTC app provided custom stats payload
@@ -1479,6 +1601,16 @@ declare class ObservedMediaSource implements MediaSourceStats {
1479
1601
  frames?: number | undefined;
1480
1602
  framesPerSecond?: number | undefined;
1481
1603
  attachments?: Record<string, unknown> | undefined;
1604
+ /** Increase of `totalAudioEnergy` in this tick (audio only). */
1605
+ deltaTotalAudioEnergy: number;
1606
+ /** Increase of `totalSamplesDuration` in this tick, in seconds (audio only). */
1607
+ deltaTotalSamplesDuration: number;
1608
+ /**
1609
+ * RMS level (0..1) of what the capture device delivered over the tick, after the browser's own
1610
+ * processing (AEC, NS, AGC). Chrome and Safari report the inputs; Firefox does not, and there the
1611
+ * publisher's level has to come from its receivers — see `ObservedOutboundTrack.rmsAudioLevel`.
1612
+ */
1613
+ rmsAudioLevel?: number;
1482
1614
  constructor(timestamp: number, id: string, kind: MediaKind, _peerConnection: ObservedPeerConnection);
1483
1615
  get visited(): boolean;
1484
1616
  getPeerConnection(): ObservedPeerConnection;
@@ -1932,6 +2064,67 @@ declare function mannKendall(values: number[], alpha?: number): MannKendallResul
1932
2064
  * recomputing every pair from scratch) doesn't have to reimplement the normal approximation.
1933
2065
  */
1934
2066
  declare function mannKendallVerdict(s: number, variance: number, alpha?: number): Pick<MannKendallResult, 'z' | 'pValue' | 'trend'>;
2067
+ /**
2068
+ * Estimates the p-th percentile (0..1) of the values counted in `histogram`, whose bucket `i`
2069
+ * holds the values in `[i * bucketWidth, (i + 1) * bucketWidth)`. Interpolates linearly inside
2070
+ * the bucket the rank falls in, so the error is at most one bucket width. With `openEnded`, the
2071
+ * last bucket holds every value above its lower bound, and a percentile falling in it is reported
2072
+ * as `max`. With `min`/`max` (the observed extremes), the bucket holding them is narrowed to the
2073
+ * observed range before interpolating, so the estimate never leaves `[min, max]`. `undefined` for an
2074
+ * empty histogram.
2075
+ */
2076
+ declare function percentileOfHistogram(histogram: readonly number[], bucketWidth: number, p: number, options?: {
2077
+ min?: number;
2078
+ max?: number;
2079
+ openEnded?: boolean;
2080
+ }): number | undefined;
2081
+
2082
+ declare enum ClientMetaTypes {
2083
+ MEDIA_CONSTRAINT = "MEDIA_CONSTRAINT",
2084
+ MEDIA_DEVICE = "MEDIA_DEVICE",
2085
+ MEDIA_DEVICES_SUPPORTED_CONSTRAINTS = "MEDIA_DEVICES_SUPPORTED_CONSTRAINTS",
2086
+ USER_MEDIA_ERROR = "USER_MEDIA_ERROR",
2087
+ LOCAL_SDP = "LOCAL_SDP",
2088
+ AUDIO_INPUT_DEVICE = "AUDIO_INPUT_DEVICE",
2089
+ OPERATION_SYSTEM = "OPERATION_SYSTEM",
2090
+ ENGINE = "ENGINE",
2091
+ PLATFORM = "PLATFORM",
2092
+ BROWSER = "BROWSER"
2093
+ }
2094
+ type MediaDeviceInfo = {
2095
+ deviceId: string;
2096
+ label: string;
2097
+ kind: string;
2098
+ groupId: string;
2099
+ };
2100
+ /**
2101
+ * Payload of `AUDIO_INPUT_DEVICE`: which device an outbound audio track captures from. Sent by the
2102
+ * client once per track, when it is first monitored.
2103
+ */
2104
+ type AudioInputDevice = {
2105
+ peerConnectionId: string;
2106
+ trackId: string;
2107
+ /** `MediaStreamTrack.label`; empty when capture permission was not yet granted. */
2108
+ label: string;
2109
+ deviceId?: string;
2110
+ groupId?: string;
2111
+ };
2112
+ type Browser = {
2113
+ name: string;
2114
+ version: string;
2115
+ };
2116
+ type Platform = {
2117
+ name: string;
2118
+ version: string;
2119
+ };
2120
+ type Engine = {
2121
+ name: string;
2122
+ version: string;
2123
+ };
2124
+ type OperationSystem = {
2125
+ name: string;
2126
+ version: string;
2127
+ };
1935
2128
 
1936
2129
  /** The per-receiver view the aggregator builds for one subscribed track. */
1937
2130
  type PublishedTrackReceivingDistributionEntry = {
@@ -1983,6 +2176,20 @@ declare class ObservedOutboundTrack implements OutboundTrackSample {
1983
2176
  remoteFractionLost?: number | undefined;
1984
2177
  remoteRttInMs?: number | undefined;
1985
2178
  qualityLimitationReason?: string | undefined;
2179
+ /**
2180
+ * This audio track's RMS level (0..1) over the latest tick — how loud the publisher was sending.
2181
+ * Taken from the publisher's own `media-source` when it reports one (Chrome, Safari), otherwise
2182
+ * the median of what its receivers heard ({@link receivedRmsAudioLevel}), which is what makes it
2183
+ * available for Firefox publishers too. `undefined` for video, or when neither is known.
2184
+ * {@link rmsAudioLevelSource} says which one it is.
2185
+ */
2186
+ rmsAudioLevel?: number;
2187
+ rmsAudioLevelSource?: 'media-source' | 'receivers';
2188
+ /**
2189
+ * Median RMS level (0..1) of this track across the receivers that played it out, each over its own
2190
+ * latest tick. Needs a `RemoteTrackResolver`: without links there are no receivers to ask.
2191
+ */
2192
+ receivedRmsAudioLevel?: number;
1986
2193
  constructor(timestamp: number, id: string, kind: MediaKind, _peerConnection: ObservedPeerConnection, _outboundRtps?: ObservedOutboundRtp[] | undefined, _mediaSource?: ObservedMediaSource | undefined);
1987
2194
  get score(): number | undefined;
1988
2195
  get visited(): boolean;
@@ -1990,7 +2197,10 @@ declare class ObservedOutboundTrack implements OutboundTrackSample {
1990
2197
  getPeerConnection(): ObservedPeerConnection;
1991
2198
  getOutboundRtps(): ObservedOutboundRtp[] | undefined;
1992
2199
  getMediaSource(): ObservedMediaSource | undefined;
2200
+ /** The capture device behind this track, when the client sent `AUDIO_INPUT_DEVICE` metadata for it. */
2201
+ get audioInputDevice(): AudioInputDevice | undefined;
1993
2202
  update(stats: OutboundTrackSample): void;
2203
+ private _updateAudioLevel;
1994
2204
  private createReceivingDistribution;
1995
2205
  }
1996
2206
 
@@ -2146,6 +2356,15 @@ declare class ObservedInboundRtp implements InboundRtpStats {
2146
2356
  deltaFecPacketsReceived: number;
2147
2357
  deltaFecPacketsDiscarded: number;
2148
2358
  deltaPausesDuration: number;
2359
+ deltaTotalAudioEnergy: number;
2360
+ deltaTotalSamplesDuration: number;
2361
+ /**
2362
+ * RMS level (0..1) of the audio played out from this stream over the tick, from the two deltas
2363
+ * above. This is the far end's outgoing level **as this receiver heard it**, which is the only
2364
+ * send-side level available for publishers on browsers that do not report `media-source` audio
2365
+ * levels (Firefox). `undefined` for video and for a tick with no audio accounted for.
2366
+ */
2367
+ rmsAudioLevel?: number;
2149
2368
  /**
2150
2369
  * Mean jitter-buffer delay for the frames/samples emitted in this tick (seconds), derived from
2151
2370
  * the cumulative `jitterBufferDelay` / `jitterBufferEmittedCount` pair — the only correct way to
@@ -2447,40 +2666,6 @@ declare class ObservedPeerConnection extends EventEmitter {
2447
2666
  private _notify;
2448
2667
  }
2449
2668
 
2450
- declare enum ClientMetaTypes {
2451
- MEDIA_CONSTRAINT = "MEDIA_CONSTRAINT",
2452
- MEDIA_DEVICE = "MEDIA_DEVICE",
2453
- MEDIA_DEVICES_SUPPORTED_CONSTRAINTS = "MEDIA_DEVICES_SUPPORTED_CONSTRAINTS",
2454
- USER_MEDIA_ERROR = "USER_MEDIA_ERROR",
2455
- LOCAL_SDP = "LOCAL_SDP",
2456
- OPERATION_SYSTEM = "OPERATION_SYSTEM",
2457
- ENGINE = "ENGINE",
2458
- PLATFORM = "PLATFORM",
2459
- BROWSER = "BROWSER"
2460
- }
2461
- type MediaDeviceInfo = {
2462
- deviceId: string;
2463
- label: string;
2464
- kind: string;
2465
- groupId: string;
2466
- };
2467
- type Browser = {
2468
- name: string;
2469
- version: string;
2470
- };
2471
- type Platform = {
2472
- name: string;
2473
- version: string;
2474
- };
2475
- type Engine = {
2476
- name: string;
2477
- version: string;
2478
- };
2479
- type OperationSystem = {
2480
- name: string;
2481
- version: string;
2482
- };
2483
-
2484
2669
  /**
2485
2670
  * Turning a correlation into a **conclusion**.
2486
2671
  *
@@ -2962,52 +3147,38 @@ declare function isClientIssueResolutionEntry(issue: ClientIssue): boolean;
2962
3147
  /** Strip the `-resolved` suffix, so both entries of a lifecycle share one logical type. */
2963
3148
  declare function baseIssueType(type: string): string;
2964
3149
 
2965
- /** The lifecycle events a sink may emit (a subset of Node's writable-stream events). */
2966
- type ClientSampleSinkEvents = {
2967
- /** The destination is fully written and closed (e.g. a file flushed and its fd closed). */
2968
- close: [];
2969
- /** The destination failed. */
2970
- error: [Error];
2971
- /** `end()` was processed and all queued data was flushed (before `close`). */
2972
- finish: [];
2973
- /** The buffer drained after backpressure; safe to write more. */
2974
- drain: [];
3150
+ type JsonlFileSinkOptions<T = ClientSample> = Partial<SinkSettings> & {
3151
+ /** Absolute or relative file path to append JSONL lines to. */
3152
+ path: string;
3153
+ /** Optional custom serializer; defaults to `JSON.stringify`. */
3154
+ serializeSample?: (record: T) => string;
2975
3155
  };
2976
- declare interface ClientSampleSink {
2977
- on<E extends keyof ClientSampleSinkEvents>(event: E, listener: (...args: ClientSampleSinkEvents[E]) => void): this;
2978
- once<E extends keyof ClientSampleSinkEvents>(event: E, listener: (...args: ClientSampleSinkEvents[E]) => void): this;
2979
- off<E extends keyof ClientSampleSinkEvents>(event: E, listener: (...args: ClientSampleSinkEvents[E]) => void): this;
2980
- emit<E extends keyof ClientSampleSinkEvents>(event: E, ...args: ClientSampleSinkEvents[E]): boolean;
2981
- }
2982
3156
  /**
2983
- * A per-client destination for accepted samples — a typed `EventEmitter` base class. One sink is
2984
- * created per `ObservedClient` (via `ObserverConfig.createClientSink`). It is **object-mode**:
2985
- * `write` receives the `ClientSample` itself, so each sink decides how to serialize it (JSON
2986
- * line, protobuf, msgpack, a remote payload, …).
2987
- *
2988
- * The library calls `write(sample)` synchronously per accepted sample (not awaited), `end()`s the
2989
- * sink when the client closes, and attaches an `error` listener so a failing sink can't crash the
2990
- * process. The application — which created the sink — listens for `close` (destination ready,
2991
- * e.g. a file flushed and its fd closed) and `error`.
2992
- *
2993
- * Subclass this and implement `write`/`end` (and `emit` your lifecycle events). Built-in sinks
2994
- * (`JsonlFileSink`, `InMemorySink`) and their factories are exported from the package root.
3157
+ * A sink that serializes each record to a JSON line and appends it to a file. Defaults to client
3158
+ * samples, but takes any record type: `sinks.create(JsonlFileSink<PipeTransportStats[]>, { id, path })`. It wraps an
3159
+ * `fs.WriteStream` and re-emits its lifecycle events, so `close` fires once the file is flushed
3160
+ * and its descriptor is closed (file ready) and `error` surfaces file errors.
2995
3161
  */
2996
- declare abstract class ClientSampleSink extends EventEmitter {
2997
- /** Accept one sample; the sink serializes it however it likes. `false` signals backpressure. */
2998
- abstract write(sample: ClientSample): boolean;
2999
- /** Finish writing; the sink flushes and (if applicable) emits `close`. */
3000
- abstract end(): void;
3162
+ declare interface JsonlFileSink {
3163
+ on<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
3164
+ once<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
3165
+ off<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
3166
+ emit<E extends keyof SinkEvents>(event: E, ...args: SinkEvents[E]): boolean;
3001
3167
  }
3002
- /**
3003
- * Produces a sink for a newly-created client, or `undefined` for no sink. Receives the
3004
- * client id and its (already-created) parent call, so the destination can be derived
3005
- * from `callId` / `clientId`.
3006
- */
3007
- type ClientSampleSinkFactory = (params: {
3008
- clientId: string;
3009
- observedCall: ObservedCall;
3010
- }) => ClientSampleSink | undefined;
3168
+ declare class JsonlFileSink<T = ClientSample> extends EventEmitter implements Sink<T> {
3169
+ readonly id: string;
3170
+ /** The file this sink writes to. Read it (e.g. in a `close` handler) to upload/move the file. */
3171
+ readonly path: string;
3172
+ private readonly _stream;
3173
+ private readonly _serializeSample;
3174
+ private _ended;
3175
+ constructor(options: JsonlFileSinkOptions<T>);
3176
+ get ended(): boolean;
3177
+ write(record: T): boolean;
3178
+ end(): void;
3179
+ }
3180
+ /** Build a JSONL file sink for a single path. */
3181
+ declare function createJsonlFileSink<T = ClientSample>(options: JsonlFileSinkOptions<T>): JsonlFileSink<T>;
3011
3182
 
3012
3183
  /** mediasoup `TransportTuple` (the selected local/remote address pair of a transport). */
3013
3184
  type TransportTuple = {
@@ -3259,13 +3430,19 @@ declare class ObservedMediasoupRouter<AppData extends Record<string, unknown> =
3259
3430
  readonly sample: MediasoupRouterSample;
3260
3431
  readonly webrtcTransportIds: Set<string>;
3261
3432
  closed: boolean;
3433
+ readonly sinks: SinkRegistry;
3434
+ readonly extensionStats: ExtensionStatsRegistry;
3262
3435
  private readonly _transportSamples;
3263
3436
  private readonly _producerSamples;
3264
3437
  private readonly _consumerSamples;
3265
3438
  private readonly _dataProducerSamples;
3266
3439
  private readonly _dataConsumerSamples;
3267
3440
  private readonly _enrich?;
3268
- constructor(settings: ObservedMediasoupRouterSettings<AppData>);
3441
+ /**
3442
+ * @param parentSinks the parent of `sinks`: `observer.sinks`, passed by
3443
+ * `Observer.createObservedMediasoupRouter`. Internal; a router constructed directly has none.
3444
+ */
3445
+ constructor(settings: ObservedMediasoupRouterSettings<AppData>, parentSinks?: SinkRegistry);
3269
3446
  get id(): string;
3270
3447
  get attachments(): Record<string, unknown>;
3271
3448
  getTransportSample(id: string): MediasoupTransportSample | undefined;
@@ -3290,6 +3467,9 @@ declare class ObservedMediasoupRouter<AppData extends Record<string, unknown> =
3290
3467
  */
3291
3468
  snapshot(): MediasoupRouterSample;
3292
3469
  close(): void;
3470
+ createExtensionStats<C extends SinkConstructor>(settings: CreateExtensionStatsSettings<C>): InstanceType<C> | undefined;
3471
+ acceptExtensionStats(id: string, data: unknown): void;
3472
+ getExtensionStats<T>(id: string): T | undefined;
3293
3473
  addTransport: (transport: types.Transport) => void;
3294
3474
  addWebRtcTransport(transport: types.WebRtcTransport): void;
3295
3475
  addPlainTransport(transport: types.PlainTransport): void;
@@ -3385,7 +3565,7 @@ type ObserverEvents = {
3385
3565
  }];
3386
3566
  'client-added': [ObservedClientScope];
3387
3567
  'client-sink-created': [ObservedClientScope & {
3388
- sink: ClientSampleSink;
3568
+ sink: JsonlFileSink;
3389
3569
  }];
3390
3570
  'client-updated': [ObservedClientScope & {
3391
3571
  sample: ClientSample;
@@ -3648,6 +3828,31 @@ declare class ObservedClientIssueRegistry {
3648
3828
  clear(): void;
3649
3829
  }
3650
3830
 
3831
+ /** Width of one `rttHistogram` bucket. */
3832
+ declare const RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS = 20;
3833
+ /** Number of `rttHistogram` buckets: 0–960 ms in 20 ms steps, plus one bucket for 980 ms and above. */
3834
+ declare const RTT_HISTOGRAM_NUMBER_OF_BUCKETS = 50;
3835
+ /** Width of one `scoreHistogram` bucket. Scores are 0..5. */
3836
+ declare const SCORE_HISTOGRAM_BUCKET_WIDTH = 1;
3837
+ /** Number of `scoreHistogram` buckets: [0, 1), [1, 2), [2, 3), [3, 4) and [4, 5]. */
3838
+ declare const SCORE_HISTOGRAM_NUMBER_OF_BUCKETS = 5;
3839
+ /** Session RTT distribution of a client, in milliseconds, estimated from `rttHistogram`. */
3840
+ type RttSummary = {
3841
+ p50: number | undefined;
3842
+ p75: number | undefined;
3843
+ p95: number | undefined;
3844
+ p99: number | undefined;
3845
+ avg: number | undefined;
3846
+ };
3847
+ /** Session score distribution of a client, estimated from `scoreHistogram`. */
3848
+ type ScoreSummary = {
3849
+ p5: number | undefined;
3850
+ p25: number | undefined;
3851
+ p50: number | undefined;
3852
+ p75: number | undefined;
3853
+ p95: number | undefined;
3854
+ avg: number | undefined;
3855
+ };
3651
3856
  type ObservedClientSettings<AppData extends Record<string, unknown> = Record<string, unknown>> = {
3652
3857
  clientId: string;
3653
3858
  appData?: AppData;
@@ -3669,8 +3874,6 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
3669
3874
  readonly call: ObservedCall;
3670
3875
  readonly clientId: string;
3671
3876
  readonly observedPeerConnections: Map<string, ObservedPeerConnection>;
3672
- /** Per-client sink (from `ObserverConfig.createClientSink`), if any. */
3673
- readonly sink?: ClientSampleSink;
3674
3877
  /** Ancestry base shared by all Observer-bus events originating at this client. */
3675
3878
  readonly eventScope: ObservedClientScope;
3676
3879
  readonly calculatedScore: CalculatedScore;
@@ -3707,10 +3910,6 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
3707
3910
  deltaOutboundPacketsSent: number;
3708
3911
  deltaTransportSentBytes: number;
3709
3912
  deltaTransportReceivedBytes: number;
3710
- deltaRttLt50Measurements: number;
3711
- deltaRttLt150Measurements: number;
3712
- deltaRttLt300Measurements: number;
3713
- deltaRttGtOrEq300Measurements: number;
3714
3913
  currentMaxRttInMs?: number;
3715
3914
  currentMinRttInMs?: number;
3716
3915
  currentAvgRttInMs?: number;
@@ -3726,7 +3925,67 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
3726
3925
  deltaNumberOfIssues: number;
3727
3926
  totalScoreSum: number;
3728
3927
  numberOfScoreMeasurements: number;
3928
+ minScore?: number;
3929
+ maxScore?: number;
3930
+ /**
3931
+ * Score measurements (one per accepted sample with a score) counted into
3932
+ * `SCORE_HISTOGRAM_NUMBER_OF_BUCKETS` buckets of `SCORE_HISTOGRAM_BUCKET_WIDTH`. Histograms of
3933
+ * several clients can be added bucket by bucket to get the distribution of a call.
3934
+ */
3935
+ readonly scoreHistogram: number[];
3936
+ /** Issues raised over the session, counted as in `issueHistogram`. */
3937
+ totalNumberOfIssues: number;
3938
+ /**
3939
+ * Number of times each issue type was raised over the session, keyed by the base type (without
3940
+ * the `-resolved` suffix): `client.issueHistogram['loopback-audio-input']`. A resolution entry is
3941
+ * not counted, and neither is a re-raise of a keyed issue that is still open, so a stateful issue
3942
+ * reported on every tick counts once per open interval. A type never raised is `undefined`.
3943
+ */
3944
+ readonly issueHistogram: Record<string, number>;
3945
+ totalRttSumInMs: number;
3946
+ numberOfRttMeasurements: number;
3947
+ maxRttInMs?: number;
3948
+ minRttInMs?: number;
3949
+ /**
3950
+ * RTT measurements counted into fixed buckets of `RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS`; the last bucket
3951
+ * holds every measurement of 980 ms and above. Constant memory, so percentiles can be estimated
3952
+ * over the whole session (see `rttSummary`) without keeping every measurement. Histograms
3953
+ * of several clients can be added bucket by bucket to get the distribution of a call.
3954
+ */
3955
+ readonly rttHistogram: number[];
3956
+ private _rttSummary?;
3957
+ private _scoreSummary?;
3958
+ totalInboundPacketsLost: number;
3959
+ totalInboundPacketsReceived: number;
3960
+ totalOutboundPacketsSent: number;
3961
+ totalReceivedAudioBytes: number;
3962
+ totalReceivedVideoBytes: number;
3963
+ totalSentAudioBytes: number;
3964
+ totalSentVideoBytes: number;
3965
+ totalTransportReceivedBytes: number;
3966
+ totalTransportSentBytes: number;
3967
+ totalDataChannelBytesReceived: number;
3968
+ totalDataChannelBytesSent: number;
3969
+ totalDataChannelMessagesReceived: number;
3970
+ totalDataChannelMessagesSent: number;
3971
+ /** Highest `sendingAudioBitrate + sendingVideoBitrate` of any sample, in bits per second. */
3972
+ peakSendingBitrate: number;
3973
+ /** Highest `receivingAudioBitrate + receivingVideoBitrate` of any sample, in bits per second. */
3974
+ peakReceivingBitrate: number;
3975
+ numberOfSamplesOverUdp: number;
3976
+ numberOfSamplesOverTcp: number;
3977
+ numberOfSamplesOverTurnUdp: number;
3978
+ numberOfSamplesOverTurnTcp: number;
3979
+ numberOfSamplesWithUnknownPath: number;
3980
+ numberOfPublicAddressChanges: number;
3729
3981
  readonly mediaDevices: MediaDeviceInfo[];
3982
+ /**
3983
+ * The capture device behind each outbound audio track, keyed by track id, from the client's
3984
+ * `AUDIO_INPUT_DEVICE` metadata. Kept for the client's lifetime (a device switch is a new track
3985
+ * id), so a finding raised after a track ended can still name its device. Read it per track via
3986
+ * `ObservedOutboundTrack.audioInputDevice`.
3987
+ */
3988
+ readonly audioInputDevices: Map<string, AudioInputDevice>;
3730
3989
  /**
3731
3990
  * The client's currently **open** stateful issues, keyed by `ClientIssue.key` — the server-side
3732
3991
  * mirror of the client monitor's own active-issue map (client-monitor-js >= 4.6.0).
@@ -3739,16 +3998,31 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
3739
3998
  readonly activeIssues: ObservedClientIssueRegistry;
3740
3999
  private _pendingInjections;
3741
4000
  private _activeSample?;
3742
- /** Peer connections the client reported closed (`PEER_CONNECTION_CLOSED`), closed once the current sample is applied. */
3743
4001
  private readonly _peerConnectionsToClose;
3744
- /** Ids of peer connections closed by the client; late stats for them are ignored rather than resurrecting them. */
3745
4002
  private readonly _closedPeerConnectionIds;
3746
4003
  private closeTimer?;
3747
4004
  constructor(settings: ObservedClientSettings<AppData>, call: ObservedCall, activeIssues: ObservedClientIssueRegistry);
3748
4005
  /** The distinct `publicAddress`es currently known by this client's open peer connections. */
4006
+ /**
4007
+ * The sink this client's samples are written to: whatever is registered under its client id in
4008
+ * `call.sinks` (the `createClientJsonlSink` file, or a sink the application added), if any.
4009
+ */
4010
+ get sink(): Sink<ClientSample> | undefined;
3749
4011
  get publicAddresses(): string[];
3750
4012
  get numberOfPeerConnections(): number;
3751
4013
  get score(): number | undefined;
4014
+ /**
4015
+ * Percentiles (error at most one 20 ms bucket; a percentile in the overflow bucket is reported as
4016
+ * `maxRttInMs`) and mean of every RTT measurement of the session. Cached until the next measurement.
4017
+ */
4018
+ get rttSummary(): Readonly<RttSummary>;
4019
+ /**
4020
+ * Percentiles (error at most one score point) and mean of every score of the session. Cached until
4021
+ * the next score.
4022
+ */
4023
+ get scoreSummary(): Readonly<ScoreSummary>;
4024
+ private _addScoreMeasurement;
4025
+ private _addRttMeasurement;
3752
4026
  close(): void;
3753
4027
  accept(sample: ClientSample, context?: AcceptContext): void;
3754
4028
  injectMetaData(metaData: ClientMetaData): void;
@@ -5219,6 +5493,105 @@ declare class UnconsumedTrackDetector implements Detector {
5219
5493
  close(): void;
5220
5494
  }
5221
5495
 
5496
+ declare const AudioEchoTypes: {
5497
+ /**
5498
+ * One participant's outgoing audio follows another participant's voice: it rises when they speak,
5499
+ * in proportion to how loud they are. The echoing participant is sending the far end's playout
5500
+ * back into the call — speakers into an open mic, or a loopback capture device.
5501
+ */
5502
+ readonly audioEchoSuspected: "AUDIO_ECHO_SUSPECTED";
5503
+ };
5504
+ type AudioEchoDetectorConfig = {
5505
+ /**
5506
+ * Width of one comparison bucket in ms, on the observer clock. Default `5_000`, the usual
5507
+ * client-monitor sampling period. Each track contributes the level of the last tick seen in the
5508
+ * bucket, so a bucket narrower than the clients' sampling period is mostly empty.
5509
+ */
5510
+ bucketMs: number;
5511
+ /** Buckets kept per track, and so the stretch the verdict is taken over. Default `24` (two minutes at 5 s). */
5512
+ windowBuckets: number;
5513
+ /**
5514
+ * RMS level (0..1) above which a bucket counts as someone speaking. Default `0.01` (-40 dBFS):
5515
+ * speech after AGC sits around 0.03–0.1, a quiet room's noise floor well below 0.003.
5516
+ */
5517
+ activeRmsLevel: number;
5518
+ /**
5519
+ * Buckets in which the loudest *other* participant spoke, required before a track is judged.
5520
+ * Default `8`. With fewer, a couple of overlapping sentences look like an echo.
5521
+ */
5522
+ minSourceActiveBuckets: number;
5523
+ /**
5524
+ * Share of those buckets in which the suspect track was active too. Default `0.8`. On its own this
5525
+ * is not evidence — two people talking over each other in the same 5 s score high — which is why
5526
+ * {@link minCorrelation} has to hold as well.
5527
+ */
5528
+ minFollowRatio: number;
5529
+ /**
5530
+ * Pearson correlation, over those same buckets, between the suspect's level and the loudest other
5531
+ * participant's level, both in dB. Default `0.6`. This is the discriminating signal: an echo is
5532
+ * louder when the source is louder, two independent speakers are not.
5533
+ */
5534
+ minCorrelation: number;
5535
+ /** Re-arm time per suspect track in ms. Default `120_000`. */
5536
+ cooldownMs: number;
5537
+ /**
5538
+ * The client issue type the loopback-device check raises, `'loopback-audio-input'` from
5539
+ * client-monitor-js. Only read, never required: when it is open on the suspect track the finding
5540
+ * says so, because it names the cause.
5541
+ */
5542
+ loopbackAudioInputIssueType: string;
5543
+ };
5544
+ /**
5545
+ * Answers **"is someone sending the others' voices back into the call?"** — the echo one participant
5546
+ * causes and everyone else hears, which no endpoint can see about itself.
5547
+ *
5548
+ * Every outbound audio track's level is taken once per tick: `ObservedOutboundTrack.rmsAudioLevel`,
5549
+ * the publisher's own media-source level where the browser reports it, otherwise the median of what
5550
+ * its receivers heard. Levels are bucketed on the observer clock, and each track is compared with
5551
+ * the **loudest other participant** in the same bucket rather than with every participant in turn,
5552
+ * which keeps a tick linear in the number of publishers.
5553
+ *
5554
+ * A track is suspected when, over the buckets in which someone else was speaking:
5555
+ *
5556
+ * - it was active in at least `minFollowRatio` of them, **and**
5557
+ * - its level in dB correlates with theirs at `minCorrelation` or above.
5558
+ *
5559
+ * Ordinary turn-taking fails the first test (the listener is quiet while the other talks); cross-talk
5560
+ * passes the first and fails the second (two independent voices do not rise and fall together). The
5561
+ * echoing participant's own speech does not hurt: it fills buckets in which the others are quiet,
5562
+ * and those are not scored.
5563
+ *
5564
+ * An echo and its source correlate in both directions, so when the source rarely has the floor to
5565
+ * themselves both tracks pass. Only one is reported: the one with the client's `loopback-audio-input`
5566
+ * issue open, else the one that follows the other more reliably, else the quieter one — flagged
5567
+ * `ambiguousDirection` in that last case.
5568
+ *
5569
+ * The source is reported as the participant most often loudest in those buckets. When the suspect
5570
+ * track also has the client's `loopback-audio-input` issue open, the finding says so: that names
5571
+ * the cause, where the correlation alone only shows the effect.
5572
+ *
5573
+ * **Limits.** The comparison is at the clients' sampling granularity (5 s by default), so it catches
5574
+ * sustained echo — a loopback device, speakers into a laptop mic without AEC — and misses faint or
5575
+ * intermittent echo. Clients sample on their own schedules, so the buckets of two participants are
5576
+ * offset by up to one sampling period; `minSourceActiveBuckets` is what absorbs that. For Firefox
5577
+ * publishers the level comes from receivers, which needs a `RemoteTrackResolver`; without one those
5578
+ * tracks carry no level and are never judged.
5579
+ */
5580
+ declare class AudioEchoDetector implements Detector {
5581
+ private readonly call;
5582
+ static readonly NAME: "audio-echo-detector";
5583
+ readonly name: "audio-echo-detector";
5584
+ private readonly _config;
5585
+ private readonly _series;
5586
+ private readonly _lastRaisedAt;
5587
+ private _lastEvaluatedBucket?;
5588
+ constructor(call: ObservedCall, config?: Partial<AudioEchoDetectorConfig>);
5589
+ close(): void;
5590
+ update(): void;
5591
+ private _collect;
5592
+ private _evaluate;
5593
+ }
5594
+
5222
5595
  /**
5223
5596
  * Detectors that reason **across calls**, created once on the observer.
5224
5597
  *
@@ -5247,6 +5620,7 @@ type AvailableCallScopeDetectorsConfigs = {
5247
5620
  [CallConcurrentIssueDetector.NAME]: CallConcurrentIssueDetectorConfig;
5248
5621
  [IssueFanOutDetector.NAME]: IssueFanOutDetectorConfig;
5249
5622
  [PublisherFaultCorroborationDetector.NAME]: PublisherFaultCorroborationDetectorConfig;
5623
+ [AudioEchoDetector.NAME]: AudioEchoDetectorConfig;
5250
5624
  };
5251
5625
  type AvailableDetectorsConfigs = AvailableObserverScopeDetectorsConfigs | AvailableCallScopeDetectorsConfigs;
5252
5626
  declare class Detectors {
@@ -5446,6 +5820,8 @@ declare class ObservedCall<AppData extends Record<string, unknown> = Record<stri
5446
5820
  readonly settings: Pick<ObservedCallSettings, 'closeCallIfEmptyForMs' | 'autoUpdateOnClientUpdate'>;
5447
5821
  /** Ancestry base shared by all Observer-bus events originating at this call. */
5448
5822
  readonly eventScope: ObservedCallScope;
5823
+ readonly sinks: SinkRegistry;
5824
+ readonly extensionStats: ExtensionStatsRegistry;
5449
5825
  private closeTimer?;
5450
5826
  constructor(settings: ObservedCallSettings<AppData>, observer: Observer, activeIssuesRegistry: ActiveIssuesRegistry);
5451
5827
  get numberOfClients(): number;
@@ -5498,6 +5874,9 @@ declare class ObservedCall<AppData extends Record<string, unknown> = Record<stri
5498
5874
  */
5499
5875
  addIssue(issue: Omit<CallIssue, 'scope'>): void;
5500
5876
  close(): void;
5877
+ createExtensionStats<C extends SinkConstructor>(settings: CreateExtensionStatsSettings<C>): InstanceType<C> | undefined;
5878
+ acceptExtensionStats(id: string, data: unknown): void;
5879
+ getExtensionStats<T>(id: string): T | undefined;
5501
5880
  getObservedClient<ClientAppData extends Record<string, unknown> = Record<string, unknown>>(clientId: string): ObservedClient<ClientAppData> | undefined;
5502
5881
  createObservedClient<ClientAppData extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedClientSettings<ClientAppData>, acceptCtx?: AcceptContext): ObservedClient<ClientAppData> | undefined;
5503
5882
  getOrCreateObservedClient<ClientAppData extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedClientSettings<ClientAppData>, acceptCtx?: AcceptContext): ObservedClient<ClientAppData> | undefined;
@@ -6219,11 +6598,38 @@ type ObserverConfig<AppData extends Record<string, unknown> = Record<string, unk
6219
6598
  /** Same as `createCallAppData`, for clients. Receives the (already-created) parent call. */
6220
6599
  createClientAppData?: ClientAppDataFactory;
6221
6600
  /**
6222
- * Optional factory invoked when a client is created, producing a per-client sink that
6223
- * receives every sample the client accepts (or `undefined` for no sink). The destination
6224
- * can be derived from `callId` / `clientId`.
6601
+ * Write every sample a client accepts to a JSONL file: return the file path for a newly-created
6602
+ * client, or `undefined` for no file. The directory must exist. The `JsonlFileSink` is added to
6603
+ * `observedCall.sinks` under the client id and is ended when the client closes; `client-sink-created`
6604
+ * announces it, so a `close` handler can upload or move the file once it is flushed.
6605
+ *
6606
+ * For any other destination, add your own sink under the client id instead, e.g. on
6607
+ * `client-added`: `observedCall.sinks.create(MySink, { id: observedClient.clientId, ... })`.
6608
+ *
6609
+ * Replaces `createClientSink` (removed in 1.3.0), and with it `createJsonlFileSinkFactory`.
6610
+ * To migrate:
6611
+ *
6612
+ * ```ts
6613
+ * // before (1.2): JSONL files
6614
+ * new Observer({ createClientSink: createJsonlFileSinkFactory({ directory: './stats' }) });
6615
+ * // after: return the path; `${callId}__${clientId}.jsonl` was the factory's default file name
6616
+ * new Observer({
6617
+ * createClientJsonlSink: ({ observedCall, clientId }) => path.join('./stats', `${observedCall.callId}__${clientId}.jsonl`),
6618
+ * });
6619
+ *
6620
+ * // before (1.2): any other sink
6621
+ * new Observer({ createClientSink: ({ clientId, observedCall }) => new HttpSink(url(observedCall, clientId)) });
6622
+ * // after: create it under the client id when the client is added (HttpSink implements
6623
+ * // `ClientSampleSink`, takes `SinkSettings & { ... }` and emits `ended` from `end()`)
6624
+ * observer.on('client-added', ({ observedCall, observedClient }) => {
6625
+ * observedCall.sinks.create(HttpSink, { id: observedClient.clientId, url: url(observedCall, observedClient.clientId) });
6626
+ * });
6627
+ * ```
6225
6628
  */
6226
- createClientSink?: ClientSampleSinkFactory;
6629
+ createClientJsonlSink?: (params: {
6630
+ observedCall: ObservedCall;
6631
+ clientId: string;
6632
+ }) => string | undefined;
6227
6633
  /**
6228
6634
  * Optional factory invoked when a call is created, producing the call's `RemoteTrackResolver`
6229
6635
  * (or `undefined` for none). Use the built-ins
@@ -6242,6 +6648,8 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
6242
6648
  readonly observedTURN: ObservedTURN;
6243
6649
  readonly observedCalls: Map<string, ObservedCall<Record<string, unknown>>>;
6244
6650
  readonly observedMediasoupRouters: Map<string, ObservedMediasoupRouter<Record<string, unknown>>>;
6651
+ readonly sinks: SinkRegistry;
6652
+ readonly extensionStats: ExtensionStatsRegistry;
6245
6653
  /** Ancestry base shared by all Observer-bus events originating at the observer. */
6246
6654
  readonly eventScope: ObserverEventBase;
6247
6655
  readonly config: ObserverConfig<AppData>;
@@ -6292,7 +6700,7 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
6292
6700
  * });
6293
6701
  * ```
6294
6702
  */
6295
- readonly callDetectorConfigs: Map<keyof AvailableCallScopeDetectorsConfigs, Partial<UnconsumedTrackDetectorConfig | TrackDeliveryMismatchDetectorConfig | CallConcurrentIssueDetectorConfig | IssueFanOutDetectorConfig | PublisherFaultCorroborationDetectorConfig>>;
6703
+ readonly callDetectorConfigs: Map<keyof AvailableCallScopeDetectorsConfigs, Partial<UnconsumedTrackDetectorConfig | TrackDeliveryMismatchDetectorConfig | CallConcurrentIssueDetectorConfig | IssueFanOutDetectorConfig | PublisherFaultCorroborationDetectorConfig | AudioEchoDetectorConfig>>;
6296
6704
  /**
6297
6705
  * Owns every call's summary: the resolved `config.callSummary`, the bus subscriptions that keep
6298
6706
  * the summaries current (one per event type, not one per call), and the summaries themselves.
@@ -6392,6 +6800,9 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
6392
6800
  matchPeerConnectionByWebRtcTransportId?: boolean;
6393
6801
  }): ObservedMediasoupRouter<Record<string, unknown>> | undefined;
6394
6802
  close(): void;
6803
+ createExtensionStats<C extends SinkConstructor>(settings: CreateExtensionStatsSettings<C>): InstanceType<C> | undefined;
6804
+ acceptExtensionStats(id: string, data: unknown): void;
6805
+ getExtensionStats<T>(id: string): T | undefined;
6395
6806
  accept(sample: ClientSample, context?: AcceptContext): void;
6396
6807
  update(): void;
6397
6808
  /**
@@ -6785,52 +7196,38 @@ declare function createLogger(moduleName: string): Logger;
6785
7196
  */
6786
7197
  declare function setObserverLogger(logger: ObserverLogger): void;
6787
7198
 
6788
- type JsonlFileSinkOptions = {
6789
- /** Absolute or relative file path to append JSONL lines to. */
6790
- path: string;
6791
- /** Optional custom serializer; defaults to `JSON.stringify`. */
6792
- serializeSample?: (sample: ClientSample) => string;
6793
- };
6794
- /**
6795
- * A sink that serializes each sample to a JSON line and appends it to a file. It wraps an
6796
- * `fs.WriteStream` and re-emits its lifecycle events, so `close` fires once the file is flushed
6797
- * and its descriptor is closed (file ready) and `error` surfaces file errors.
6798
- */
6799
- declare class JsonlFileSink extends ClientSampleSink {
6800
- /** The file this sink writes to. Read it (e.g. in a `close` handler) to upload/move the file. */
6801
- readonly path: string;
6802
- private readonly _stream;
6803
- private readonly _serializeSample;
6804
- constructor(options: JsonlFileSinkOptions);
6805
- write(sample: ClientSample): boolean;
6806
- end(): void;
6807
- }
6808
- /** Build a JSONL file sink for a single path. */
6809
- declare function createJsonlFileSink(options: JsonlFileSinkOptions): ClientSampleSink;
6810
- type JsonlFileSinkFactoryOptions = {
6811
- /** Directory the per-client files are written into (must exists). */
6812
- directory: string;
6813
- /** Builds the file name from the call/client ids. Defaults to `${callId}__${clientId}.jsonl`. */
6814
- getFileName?: (params: {
6815
- callId: string;
6816
- clientId: string;
6817
- }) => string;
6818
- /** Optional custom serializer; defaults to `JSON.stringify`. */
6819
- serializeSample?: (sample: ClientSample) => string;
6820
- };
7199
+ /** The lifecycle events a client sample sink may emit. Same as {@link SinkEvents}. */
7200
+ type ClientSampleSinkEvents = SinkEvents;
6821
7201
  /**
6822
- * Convenience factory that writes one JSONL file per client, with the path derived from
6823
- * `callId` / `clientId`. Plug into `ObserverConfig.createClientSink`.
7202
+ * A per-client destination for accepted samples. Create one under the client id with
7203
+ * `observedCall.sinks.create(MySink, { id: clientId, ... })`, or let
7204
+ * `ObserverConfig.createClientJsonlSink` create a `JsonlFileSink` there.
7205
+ *
7206
+ * The library calls `write(sample)` synchronously per accepted sample (not awaited) and `end()`s
7207
+ * the sink when the client closes; the registry attaches an `error` listener so a failing sink
7208
+ * can't crash the process. Because `write` isn't awaited, backpressure and batching are the sink's
7209
+ * concern.
6824
7210
  */
6825
- declare function createJsonlFileSinkFactory(options: JsonlFileSinkFactoryOptions): ClientSampleSinkFactory;
7211
+ type ClientSampleSink = Sink<ClientSample>;
6826
7212
 
7213
+ declare interface InMemorySink {
7214
+ on<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
7215
+ once<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
7216
+ off<E extends keyof SinkEvents>(event: E, listener: (...args: SinkEvents[E]) => void): this;
7217
+ emit<E extends keyof SinkEvents>(event: E, ...args: SinkEvents[E]): boolean;
7218
+ }
6827
7219
  /**
6828
- * A `ClientSampleSink` that collects the accepted sample objects into an array. Useful for
6829
- * tests and offline replay. Emits `close` on `end()`.
7220
+ * A client sample sink that collects the accepted sample objects into an array. Useful for tests
7221
+ * and offline replay. Emits `ended` and `close` on `end()`.
6830
7222
  */
6831
- declare class InMemorySink extends ClientSampleSink {
7223
+ declare class InMemorySink extends EventEmitter implements Sink<ClientSample> {
7224
+ readonly id: string;
6832
7225
  readonly samples: ClientSample[];
6833
- constructor(samples?: ClientSample[]);
7226
+ private _ended;
7227
+ constructor(settings?: Partial<SinkSettings> & {
7228
+ samples?: ClientSample[];
7229
+ });
7230
+ get ended(): boolean;
6834
7231
  write(sample: ClientSample): boolean;
6835
7232
  end(): void;
6836
7233
  }
@@ -6839,4 +7236,4 @@ declare function createInMemorySink(samples?: ClientSample[]): InMemorySink;
6839
7236
  declare function createDefaultMediasoupRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
6840
7237
  declare function createP2pRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
6841
7238
 
6842
- export { type AcceptContext, type AcceptMiddleware, type AcceptMiddlewarePayload, type ActiveClientIssue, type ActiveIssueTracker, ActiveIssuesRegistry, type AvailableCallScopeDetectorsConfigs, type AvailableDetectorsConfigs, type AvailableObserverScopeDetectorsConfigs, type AvailableValidatorConfigs, CODEC_MISMATCH_ISSUE, type CalculatedScore, type CallAppDataFactory, CallConcurrentIssueDetector, type CallConcurrentIssueDetectorConfig, type CallConcurrentIssueGroup, CallConcurrentIssueTypes, type CallHealth, CallHealthAggregator, type CallIssue, type CallIssueSpread, type CallScopedEventName, type CallSummary, type CallSummaryClients, CallSummaryCollector, type CallSummaryConfig, type CallSummaryEnricher, type CallSummaryEnrichers, type CallSummaryScores, type CallSummarySection, type CallSummaryTruncation, type CallSummaryTurnServers, type CertificateStats, type ClientAppDataFactory, type ClientEvent, ClientEventTypes, type ClientHealth, type ClientHealthThresholds, type ClientIssue, type ClientLocation, type ClientLocationResolver, type ClientMetaData, ClientMetaTypes, type ClientPopulation, type ClientPopulationAxis, ClientPopulationIssueDetector, type ClientPopulationIssueDetectorConfig, ClientPopulationIssueTypes, type ClientSample, ClientSampleSink, type ClientSampleSinkEvents, type ClientSampleSinkFactory, type CodecConsistencyReportPayload, CodecConsistencyValidator, type CodecConsistencyValidatorConfig, type CodecEvidence, type CodecStats, type CorroboratedPublisherFault, type DataChannelStats, type Detector, Detectors, type ExtensionStat, GEOHASH_CELL_SIZES, type IceCandidatePairStats, type IceCandidateStats, type IceTransportStats, InMemorySink, type InboundRtpStats, type InboundTrackSample, type Issue, type IssueBase, type IssueConclusion, IssueFanOutDetector, type IssueFanOutDetectorConfig, IssueFanOutTypes, type IssueFaultDomain, JsonlFileSink, type JsonlFileSinkFactoryOptions, type JsonlFileSinkOptions, LOWEST_COMMON_DENOMINATOR_ISSUE, type Logger, type MannKendallResult, type MediaKind, type MediaPlayoutStats, type MediaSourceStats, type MediasoupConsumerSample, type MediasoupConsumerSampleEvent, type MediasoupDataConsumerSample, type MediasoupDataProducerSample, type MediasoupDirectTransportSample, type MediasoupDirectTransportSampleEventMap, type MediasoupPipeTransportSample, type MediasoupPipeTransportSampleEventMap, type MediasoupPlainTransportSample, type MediasoupPlainTransportSampleEventMap, type MediasoupProducerSample, type MediasoupProducerSampleEvent, type MediasoupRouterSample, type MediasoupSampleEnricher, type MediasoupTransportSample, type MediasoupWebRtcTransportSample, type MediasoupWebRtcTransportSampleEventMap, type Middleware, MiddlewareProcessor, ObservedCall, type ObservedCallScope, type ObservedCallSettings, ObservedCertificate, ObservedClient, ObservedClientIssueRegistry, type ObservedClientScope, type ObservedClientSettings, ObservedCodec, ObservedDataChannel, ObservedIceCandidate, ObservedIceCandidatePair, ObservedIceTransport, ObservedInboundRtp, ObservedInboundTrack, ObservedMediaPlayout, ObservedMediaSource, ObservedMediasoupRouter, type ObservedMediasoupRouterEvents, type ObservedMediasoupRouterScope, type ObservedMediasoupRouterSettings, ObservedOutboundRtp, ObservedOutboundTrack, ObservedPeerConnection, type ObservedPeerConnectionScope, ObservedPeerConnectionTransport, ObservedRemoteInboundRtp, ObservedRemoteOutboundRtp, ObservedTURN, type ObservedTURNEventMap, ObservedTurnServer, Observer, ObserverConcurrentIssueDetector, type ObserverConcurrentIssueDetectorConfig, type ObserverConcurrentIssueGroup, ObserverConcurrentIssueTypes, type ObserverEventBase, type ObserverEvents, type ObserverIssue, type ObserverIssueSpread, type ObserverLogger, type OutboundRtpStats, type OutboundTrackSample, type PageHinkleyResult, type PeerConnectionSample, type PeerConnectionTransportStats, type PsnrSum, PublisherFaultCorroborationDetector, type PublisherFaultCorroborationDetectorConfig, PublisherFaultTypes, type QualityLimitationDurations, RESOLVED_ISSUE_SUFFIX, type RemoteInboundRtpStats, type RemoteOutboundRtpStats, type RemoteTrackLinkEvidence, RemoteTrackResolver, type RemoteTrackResolverFactory, type RemoteTrackResolverReportPayload, RemoteTrackResolverValidator, type RemoteTrackResolverValidatorConfig, type RemoteTrackResolvers, type ResolvedActiveClientIssue, type RunningValidator, type SampleRejectedReason, type ScoreCalculator, SfuCongestionDetector, type SfuCongestionDetectorBucket, type SfuCongestionDetectorConfig, type SfuCongestionDetectorEvaluation, type SfuCongestionDetectorReport, type SimulcastReceiverEvidence, type SimulcastReceiverReportPayload, SimulcastReceiverValidator, type SimulcastReceiverValidatorConfig, SlidingWindow, type SlidingWindowEntry, type StatsSummary, TrackDeliveryMismatchDetector, type TrackDeliveryMismatchDetectorConfig, TrackDeliveryMismatchTypes, TrendTester, type TrendTesterConfig, type TurnServerHealth, TurnServerHealthDetector, type TurnServerHealthDetectorConfig, TurnServerHealthTypes, TurnServerOutageDetector, type TurnServerOutageDetectorConfig, TurnServerOutageTypes, UNRESOLVED_TRACK_LINKS_ISSUE, UnconsumedTrackDetector, type UnconsumedTrackDetectorConfig, UnconsumedTrackTypes, type ValidationReport, type Validator, type ValidatorName, baseIssueType, concludeCallIssue, concludeObserverIssue, correlation, counterDelta, createCallSummary, createDefaultMediasoupRemoteTrackResolverFactory, createInMemorySink, createJsonlFileSink, createJsonlFileSinkFactory, createLogger, createP2pRemoteTrackResolverFactory, defaultCallSummaryConfig, defaultClientHealthThresholds, geohash, isClientIssueResolutionEntry, isPublicIpAddress, issuePayloadAsString, mannKendall, mannKendallVerdict, median, medianAbsoluteDeviation, pageHinkley, percentile, percentileOfSorted, robustZScore, schemaVersion, setObserverLogger, summarize };
7239
+ export { type AcceptContext, type AcceptMiddleware, type AcceptMiddlewarePayload, type ActiveClientIssue, type ActiveIssueTracker, ActiveIssuesRegistry, AudioEchoDetector, type AudioEchoDetectorConfig, AudioEchoTypes, type AudioInputDevice, type AvailableCallScopeDetectorsConfigs, type AvailableDetectorsConfigs, type AvailableObserverScopeDetectorsConfigs, type AvailableValidatorConfigs, CODEC_MISMATCH_ISSUE, type CalculatedScore, type CallAppDataFactory, CallConcurrentIssueDetector, type CallConcurrentIssueDetectorConfig, type CallConcurrentIssueGroup, CallConcurrentIssueTypes, type CallHealth, CallHealthAggregator, type CallIssue, type CallIssueSpread, type CallScopedEventName, type CallSummary, type CallSummaryClients, CallSummaryCollector, type CallSummaryConfig, type CallSummaryEnricher, type CallSummaryEnrichers, type CallSummaryScores, type CallSummarySection, type CallSummaryTruncation, type CallSummaryTurnServers, type CertificateStats, type ClientAppDataFactory, type ClientEvent, ClientEventTypes, type ClientHealth, type ClientHealthThresholds, type ClientIssue, type ClientLocation, type ClientLocationResolver, type ClientMetaData, ClientMetaTypes, type ClientPopulation, type ClientPopulationAxis, ClientPopulationIssueDetector, type ClientPopulationIssueDetectorConfig, ClientPopulationIssueTypes, type ClientSample, type ClientSampleSink, type ClientSampleSinkEvents, type CodecConsistencyReportPayload, CodecConsistencyValidator, type CodecConsistencyValidatorConfig, type CodecEvidence, type CodecStats, type CorroboratedPublisherFault, type CreateExtensionStatsSettings, type DataChannelStats, type Detector, Detectors, type ExtensionStat, ExtensionStatsRegistry, GEOHASH_CELL_SIZES, type IceCandidatePairStats, type IceCandidateStats, type IceTransportStats, InMemorySink, type InboundRtpStats, type InboundTrackSample, type Issue, type IssueBase, type IssueConclusion, IssueFanOutDetector, type IssueFanOutDetectorConfig, IssueFanOutTypes, type IssueFaultDomain, JsonlFileSink, type JsonlFileSinkOptions, LOWEST_COMMON_DENOMINATOR_ISSUE, type Logger, type MannKendallResult, type MediaKind, type MediaPlayoutStats, type MediaSourceStats, type MediasoupConsumerSample, type MediasoupConsumerSampleEvent, type MediasoupDataConsumerSample, type MediasoupDataProducerSample, type MediasoupDirectTransportSample, type MediasoupDirectTransportSampleEventMap, type MediasoupPipeTransportSample, type MediasoupPipeTransportSampleEventMap, type MediasoupPlainTransportSample, type MediasoupPlainTransportSampleEventMap, type MediasoupProducerSample, type MediasoupProducerSampleEvent, type MediasoupRouterSample, type MediasoupSampleEnricher, type MediasoupTransportSample, type MediasoupWebRtcTransportSample, type MediasoupWebRtcTransportSampleEventMap, type Middleware, MiddlewareProcessor, ObservedCall, type ObservedCallScope, type ObservedCallSettings, ObservedCertificate, ObservedClient, ObservedClientIssueRegistry, type ObservedClientScope, type ObservedClientSettings, ObservedCodec, ObservedDataChannel, ObservedIceCandidate, ObservedIceCandidatePair, ObservedIceTransport, ObservedInboundRtp, ObservedInboundTrack, ObservedMediaPlayout, ObservedMediaSource, ObservedMediasoupRouter, type ObservedMediasoupRouterEvents, type ObservedMediasoupRouterScope, type ObservedMediasoupRouterSettings, ObservedOutboundRtp, ObservedOutboundTrack, ObservedPeerConnection, type ObservedPeerConnectionScope, ObservedPeerConnectionTransport, ObservedRemoteInboundRtp, ObservedRemoteOutboundRtp, ObservedTURN, type ObservedTURNEventMap, ObservedTurnServer, Observer, ObserverConcurrentIssueDetector, type ObserverConcurrentIssueDetectorConfig, type ObserverConcurrentIssueGroup, ObserverConcurrentIssueTypes, type ObserverEventBase, type ObserverEvents, type ObserverIssue, type ObserverIssueSpread, type ObserverLogger, type OutboundRtpStats, type OutboundTrackSample, type PageHinkleyResult, type PeerConnectionSample, type PeerConnectionTransportStats, type PsnrSum, PublisherFaultCorroborationDetector, type PublisherFaultCorroborationDetectorConfig, PublisherFaultTypes, type QualityLimitationDurations, RESOLVED_ISSUE_SUFFIX, RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS, RTT_HISTOGRAM_NUMBER_OF_BUCKETS, type RemoteInboundRtpStats, type RemoteOutboundRtpStats, type RemoteTrackLinkEvidence, RemoteTrackResolver, type RemoteTrackResolverFactory, type RemoteTrackResolverReportPayload, RemoteTrackResolverValidator, type RemoteTrackResolverValidatorConfig, type RemoteTrackResolvers, type ResolvedActiveClientIssue, type RttSummary, type RunningValidator, SCORE_HISTOGRAM_BUCKET_WIDTH, SCORE_HISTOGRAM_NUMBER_OF_BUCKETS, type SampleRejectedReason, type ScoreCalculator, type ScoreSummary, SfuCongestionDetector, type SfuCongestionDetectorBucket, type SfuCongestionDetectorConfig, type SfuCongestionDetectorEvaluation, type SfuCongestionDetectorReport, type SimulcastReceiverEvidence, type SimulcastReceiverReportPayload, SimulcastReceiverValidator, type SimulcastReceiverValidatorConfig, type Sink, type SinkConstructor, type SinkCreateSettings, type SinkEvents, SinkRegistry, type SinkSettings, SlidingWindow, type SlidingWindowEntry, type StatsSummary, TrackDeliveryMismatchDetector, type TrackDeliveryMismatchDetectorConfig, TrackDeliveryMismatchTypes, TrendTester, type TrendTesterConfig, type TurnServerHealth, TurnServerHealthDetector, type TurnServerHealthDetectorConfig, TurnServerHealthTypes, TurnServerOutageDetector, type TurnServerOutageDetectorConfig, TurnServerOutageTypes, UNRESOLVED_TRACK_LINKS_ISSUE, UnconsumedTrackDetector, type UnconsumedTrackDetectorConfig, UnconsumedTrackTypes, type ValidationReport, type Validator, type ValidatorName, baseIssueType, concludeCallIssue, concludeObserverIssue, correlation, counterDelta, createCallSummary, createDefaultMediasoupRemoteTrackResolverFactory, createInMemorySink, createJsonlFileSink, createLogger, createP2pRemoteTrackResolverFactory, defaultCallSummaryConfig, defaultClientHealthThresholds, geohash, isClientIssueResolutionEntry, isPublicIpAddress, issuePayloadAsString, mannKendall, mannKendallVerdict, median, medianAbsoluteDeviation, pageHinkley, percentile, percentileOfHistogram, percentileOfSorted, robustZScore, schemaVersion, setObserverLogger, summarize };