@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/README.md +160 -86
- package/dist/index.d.mts +530 -133
- package/dist/index.d.ts +530 -133
- package/dist/index.js +799 -177
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +790 -175
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -16
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
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
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
|
|
2984
|
-
*
|
|
2985
|
-
* `
|
|
2986
|
-
*
|
|
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
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
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
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
*
|
|
6223
|
-
*
|
|
6224
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
6789
|
-
|
|
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
|
-
*
|
|
6823
|
-
* `
|
|
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
|
-
|
|
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
|
|
6829
|
-
*
|
|
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
|
|
7223
|
+
declare class InMemorySink extends EventEmitter implements Sink<ClientSample> {
|
|
7224
|
+
readonly id: string;
|
|
6832
7225
|
readonly samples: ClientSample[];
|
|
6833
|
-
|
|
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
|
|
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 };
|