@observertc/observer-js 1.0.0-beta.13 → 1.0.0-beta.14
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 +396 -9
- package/dist/index.d.mts +1253 -8
- package/dist/index.d.ts +1253 -8
- package/dist/index.js +1952 -59
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1915 -58
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -5
- package/llms-full.txt +0 -1632
package/dist/index.d.mts
CHANGED
|
@@ -52,6 +52,12 @@ type ClientIssue = {
|
|
|
52
52
|
*/
|
|
53
53
|
type: string;
|
|
54
54
|
/**
|
|
55
|
+
* Identity of a **stateful** issue, shared by its raise entry and its `<type>-resolved`
|
|
56
|
+
* companion, so a server can open an active issue on the raise and close it on the match
|
|
57
|
+
* (client-monitor-js >= 4.6.0, `sendResolvedIssuesToServer`). One-shot issues have no key.
|
|
58
|
+
*/
|
|
59
|
+
key?: string;
|
|
60
|
+
/**
|
|
55
61
|
* The value associated with the event, if applicable.
|
|
56
62
|
*/
|
|
57
63
|
payload?: string;
|
|
@@ -1585,6 +1591,24 @@ declare class ObservedOutboundRtp implements OutboundRtpStats {
|
|
|
1585
1591
|
bitPerPixel: number;
|
|
1586
1592
|
deltaPacketsSent: number;
|
|
1587
1593
|
deltaBytesSent: number;
|
|
1594
|
+
deltaFramesSent: number;
|
|
1595
|
+
deltaFramesEncoded: number;
|
|
1596
|
+
deltaKeyFramesEncoded: number;
|
|
1597
|
+
deltaNackCount: number;
|
|
1598
|
+
deltaPliCount: number;
|
|
1599
|
+
deltaFirCount: number;
|
|
1600
|
+
deltaRetransmittedPacketsSent: number;
|
|
1601
|
+
deltaRetransmittedBytesSent: number;
|
|
1602
|
+
deltaEncodeTime: number;
|
|
1603
|
+
deltaQualityLimitationResolutionChanges: number;
|
|
1604
|
+
/**
|
|
1605
|
+
* `true` when the codec, encoder implementation or scalability mode changed in this tick.
|
|
1606
|
+
*
|
|
1607
|
+
* Chrome resets `packetsSent`/`bytesSent` on the SSRC when the codec switches
|
|
1608
|
+
* (crbug.com/webrtc/5361), producing sawtooth spikes and negative bitrates. All deltas for the
|
|
1609
|
+
* tick are suppressed so a codec change is never mistaken for a traffic event.
|
|
1610
|
+
*/
|
|
1611
|
+
counterResetBoundary: boolean;
|
|
1588
1612
|
remoteRttInMs?: number;
|
|
1589
1613
|
remoteFractionLost?: number;
|
|
1590
1614
|
remoteJitter?: number;
|
|
@@ -1752,6 +1776,46 @@ declare class ObservedInboundRtp implements InboundRtpStats {
|
|
|
1752
1776
|
deltaBytesReceived: number;
|
|
1753
1777
|
deltaReceivedSamples: number;
|
|
1754
1778
|
deltaSilentConcealedSamples: number;
|
|
1779
|
+
deltaConcealedSamples: number;
|
|
1780
|
+
deltaConcealmentEvents: number;
|
|
1781
|
+
deltaFreezeCount: number;
|
|
1782
|
+
deltaFreezesDuration: number;
|
|
1783
|
+
deltaPliCount: number;
|
|
1784
|
+
deltaNackCount: number;
|
|
1785
|
+
deltaFirCount: number;
|
|
1786
|
+
deltaPacketsDiscarded: number;
|
|
1787
|
+
deltaFramesDecoded: number;
|
|
1788
|
+
deltaFramesReceived: number;
|
|
1789
|
+
deltaFramesRendered: number;
|
|
1790
|
+
deltaFramesDropped: number;
|
|
1791
|
+
deltaKeyFramesDecoded: number;
|
|
1792
|
+
deltaDecodeTime: number;
|
|
1793
|
+
deltaJitterBufferDelay: number;
|
|
1794
|
+
deltaJitterBufferEmittedCount: number;
|
|
1795
|
+
deltaRetransmittedPacketsReceived: number;
|
|
1796
|
+
deltaFecPacketsReceived: number;
|
|
1797
|
+
deltaFecPacketsDiscarded: number;
|
|
1798
|
+
deltaPausesDuration: number;
|
|
1799
|
+
/**
|
|
1800
|
+
* Mean jitter-buffer delay for the frames/samples emitted in this tick (seconds), derived from
|
|
1801
|
+
* the cumulative `jitterBufferDelay` / `jitterBufferEmittedCount` pair — the only correct way to
|
|
1802
|
+
* read those two counters.
|
|
1803
|
+
*/
|
|
1804
|
+
jitterBufferDelayInMs?: number;
|
|
1805
|
+
/** Fraction of the samples received in this tick that were concealed (0..1). */
|
|
1806
|
+
concealmentRatio?: number;
|
|
1807
|
+
/** Fraction of the frames received in this tick that were dropped before rendering (0..1). */
|
|
1808
|
+
framesDroppedRatio?: number;
|
|
1809
|
+
/**
|
|
1810
|
+
* `true` when the codec or decoder implementation changed in this tick.
|
|
1811
|
+
*
|
|
1812
|
+
* Chrome resets `packetsReceived`/`bytesReceived` on an SSRC when the codec switches
|
|
1813
|
+
* (crbug.com/webrtc/5361, open since 2015), which shows up as a sawtooth spike or a negative
|
|
1814
|
+
* rate. Every delta in this tick is therefore suppressed to `0` rather than reported as traffic
|
|
1815
|
+
* — otherwise a room-wide codec rollout produces a synchronized fake-degradation alert across
|
|
1816
|
+
* every participant at once.
|
|
1817
|
+
*/
|
|
1818
|
+
counterResetBoundary: boolean;
|
|
1755
1819
|
remoteRttInMs?: number;
|
|
1756
1820
|
remoteBytesSent?: number;
|
|
1757
1821
|
remotePacketsSent?: number;
|
|
@@ -1941,7 +2005,34 @@ declare class ObservedPeerConnection extends EventEmitter {
|
|
|
1941
2005
|
sendingVideoBitrate: number;
|
|
1942
2006
|
receivingAudioBitrate: number;
|
|
1943
2007
|
receivingVideoBitrate: number;
|
|
2008
|
+
/**
|
|
2009
|
+
* Median round-trip time of the tick, in ms.
|
|
2010
|
+
*
|
|
2011
|
+
* Prefers {@link rtcpRttInMs} and falls back to {@link iceRttInMs}, so within one tick it always
|
|
2012
|
+
* reports **one** kind of round trip. It used to be the median of both mixed together, which was
|
|
2013
|
+
* a bug: the mixing ratio changed as streams came and went, so the value moved for reasons that
|
|
2014
|
+
* had nothing to do with the network.
|
|
2015
|
+
*/
|
|
1944
2016
|
currentRttInMs?: number;
|
|
2017
|
+
/**
|
|
2018
|
+
* RTT measured by ICE/STUN consent checks, in ms — the trip to **whatever terminates ICE**. In an
|
|
2019
|
+
* SFU topology that is the SFU, so this is the client↔SFU leg, not client↔client.
|
|
2020
|
+
*/
|
|
2021
|
+
iceRttInMs?: number;
|
|
2022
|
+
/**
|
|
2023
|
+
* RTT reported by RTCP receiver reports, in ms — an **end-to-end** media-path round trip.
|
|
2024
|
+
*
|
|
2025
|
+
* Not the same trip as {@link iceRttInMs}; the difference between the two is roughly the far side
|
|
2026
|
+
* of the SFU (see {@link sfuHopRttInMs}).
|
|
2027
|
+
*/
|
|
2028
|
+
rtcpRttInMs?: number;
|
|
2029
|
+
/**
|
|
2030
|
+
* `rtcpRttInMs - iceRttInMs`, when both are known — an estimate of everything *past* the SFU.
|
|
2031
|
+
*
|
|
2032
|
+
* Useful for splitting "this client's own last mile is slow" (high `iceRttInMs`) from "the path
|
|
2033
|
+
* beyond the SFU is slow" (low ICE, high hop).
|
|
2034
|
+
*/
|
|
2035
|
+
sfuHopRttInMs?: number;
|
|
1945
2036
|
currentJitter?: number;
|
|
1946
2037
|
usingTCP: boolean;
|
|
1947
2038
|
usingTURN: boolean;
|
|
@@ -2036,6 +2127,72 @@ type OperationSystem = {
|
|
|
2036
2127
|
version: string;
|
|
2037
2128
|
};
|
|
2038
2129
|
|
|
2130
|
+
/** The suffix client-monitor-js appends to the type of a resolution entry. */
|
|
2131
|
+
declare const RESOLVED_ISSUE_SUFFIX = "-resolved";
|
|
2132
|
+
/**
|
|
2133
|
+
* A **stateful** client issue the server currently believes to be open.
|
|
2134
|
+
*
|
|
2135
|
+
* client-monitor-js (>= 4.6.0, `sendResolvedIssuesToServer`) puts an issue's whole lifecycle on the
|
|
2136
|
+
* wire as two entries sharing a `key`:
|
|
2137
|
+
*
|
|
2138
|
+
* ```
|
|
2139
|
+
* raise: { type: 'stuck-decoder', key, payload, timestamp: raisedAt }
|
|
2140
|
+
* resolution: { type: 'stuck-decoder-resolved', key, payload: { raisedAt, comment, …final }, timestamp: resolvedAt }
|
|
2141
|
+
* ```
|
|
2142
|
+
*
|
|
2143
|
+
* The observer opens an `ActiveClientIssue` on the raise and closes it on the matching key, which
|
|
2144
|
+
* turns a stream of point-in-time symptom reports into **intervals**. That is what makes the
|
|
2145
|
+
* difference between "several clients reported congestion in the last 10 seconds" (a guess built on
|
|
2146
|
+
* an arbitrary window) and "several clients are congested *right now, simultaneously*" — the latter
|
|
2147
|
+
* being real evidence of a shared cause.
|
|
2148
|
+
*
|
|
2149
|
+
* Issues still active when the client monitor closes are auto-resolved by the client, so a clean
|
|
2150
|
+
* departure does not leak. An unclean one (crash, network death) can, which is why the registry
|
|
2151
|
+
* also expires entries — see `IssueRegistry`.
|
|
2152
|
+
*/
|
|
2153
|
+
type ActiveClientIssue = {
|
|
2154
|
+
/** Identity shared by the raise and its resolution. Unique per client. */
|
|
2155
|
+
key: string;
|
|
2156
|
+
/** The issue type **without** the `-resolved` suffix (e.g. `'congestion'`). */
|
|
2157
|
+
type: string;
|
|
2158
|
+
/** The client that reported it. */
|
|
2159
|
+
clientId: string;
|
|
2160
|
+
/** When the client raised it (client clock). */
|
|
2161
|
+
raisedAt: number;
|
|
2162
|
+
/** When the observer first saw it (server clock) — skew-free, use this for cross-client timing. */
|
|
2163
|
+
observedAt: number;
|
|
2164
|
+
/** Parsed raise payload, when it was JSON. */
|
|
2165
|
+
payload?: Record<string, unknown>;
|
|
2166
|
+
/** `payload.peerConnectionId`, when present — most client detectors report it. */
|
|
2167
|
+
peerConnectionId?: string;
|
|
2168
|
+
/** `payload.trackId`, when present — the join key to a track (and thus to a publisher). */
|
|
2169
|
+
trackId?: string;
|
|
2170
|
+
};
|
|
2171
|
+
/** A closed interval: an {@link ActiveClientIssue} plus how it ended. */
|
|
2172
|
+
type ResolvedClientIssue = ActiveClientIssue & {
|
|
2173
|
+
/** When the client resolved it (client clock). */
|
|
2174
|
+
resolvedAt: number;
|
|
2175
|
+
/** Observer-clock resolution time. */
|
|
2176
|
+
observedResolvedAt: number;
|
|
2177
|
+
/** `resolvedAt - raisedAt` as reported by the client, else derived from observer clocks. */
|
|
2178
|
+
durationInMs: number;
|
|
2179
|
+
/** Free-form note passed to `resolveIssue`. */
|
|
2180
|
+
comment?: string;
|
|
2181
|
+
/** Payload explicitly passed at resolution (the built-in detectors pass their final payload). */
|
|
2182
|
+
resolutionPayload?: Record<string, unknown>;
|
|
2183
|
+
/**
|
|
2184
|
+
* How the interval ended: the client said so, the observer expired it, or the client left
|
|
2185
|
+
* without resolving.
|
|
2186
|
+
*/
|
|
2187
|
+
resolvedBy: 'client' | 'timeout' | 'client-closed';
|
|
2188
|
+
};
|
|
2189
|
+
/** `true` when the entry is a resolution companion rather than a raise. */
|
|
2190
|
+
declare function isResolutionEntry(issue: ClientIssue): boolean;
|
|
2191
|
+
/** Strip the `-resolved` suffix, so both entries of a lifecycle share one logical type. */
|
|
2192
|
+
declare function baseIssueType(type: string): string;
|
|
2193
|
+
/** Best-effort parse of the JSON-string payload the schema carries. */
|
|
2194
|
+
declare function parseIssuePayload(payload?: string): Record<string, unknown> | undefined;
|
|
2195
|
+
|
|
2039
2196
|
/** The lifecycle events a sink may emit (a subset of Node's writable-stream events). */
|
|
2040
2197
|
type ClientSampleSinkEvents = {
|
|
2041
2198
|
/** The destination is fully written and closed (e.g. a file flushed and its fd closed). */
|
|
@@ -2103,11 +2260,11 @@ type RtpCodecParameters = {
|
|
|
2103
2260
|
parameter?: string;
|
|
2104
2261
|
}[];
|
|
2105
2262
|
};
|
|
2106
|
-
type SampleHistoryItem<T extends string> =
|
|
2263
|
+
type SampleHistoryItem<T extends string> = {
|
|
2107
2264
|
type: T;
|
|
2108
2265
|
timestamp: number;
|
|
2109
2266
|
};
|
|
2110
|
-
type MediasoupRouterSample =
|
|
2267
|
+
type MediasoupRouterSample = {
|
|
2111
2268
|
routerId: string;
|
|
2112
2269
|
attachments: Record<string, unknown>;
|
|
2113
2270
|
createdAt: number;
|
|
@@ -2151,7 +2308,9 @@ type MediasoupDirectTransportSample = {
|
|
|
2151
2308
|
type: 'direct';
|
|
2152
2309
|
history: MediasoupDirectTransportSampleEventMap[];
|
|
2153
2310
|
};
|
|
2154
|
-
type MediasoupTransportSample =
|
|
2311
|
+
type MediasoupTransportSample = {
|
|
2312
|
+
/** Free-form application data. Attach anything here — see `ObservedMediasoupRouter`. */
|
|
2313
|
+
attachments?: Record<string, unknown>;
|
|
2155
2314
|
id: string;
|
|
2156
2315
|
createdAt: number;
|
|
2157
2316
|
connectedAt?: number;
|
|
@@ -2167,7 +2326,9 @@ type MediasoupProducerSampleEventMap = {
|
|
|
2167
2326
|
type MediasoupProducerSampleEvent = {
|
|
2168
2327
|
[K in keyof MediasoupProducerSampleEventMap]: SampleHistoryItem<K>;
|
|
2169
2328
|
}[keyof MediasoupProducerSampleEventMap];
|
|
2170
|
-
type MediasoupProducerSample =
|
|
2329
|
+
type MediasoupProducerSample = {
|
|
2330
|
+
/** Free-form application data. Attach anything here — see `ObservedMediasoupRouter`. */
|
|
2331
|
+
attachments?: Record<string, unknown>;
|
|
2171
2332
|
id: string;
|
|
2172
2333
|
transportId: string;
|
|
2173
2334
|
createdAt: number;
|
|
@@ -2191,7 +2352,9 @@ type MediasoupConsumerSampleEventMap = {
|
|
|
2191
2352
|
type MediasoupConsumerSampleEvent = {
|
|
2192
2353
|
[K in keyof MediasoupConsumerSampleEventMap]: SampleHistoryItem<K>;
|
|
2193
2354
|
}[keyof MediasoupConsumerSampleEventMap];
|
|
2194
|
-
type MediasoupConsumerSample =
|
|
2355
|
+
type MediasoupConsumerSample = {
|
|
2356
|
+
/** Free-form application data. Attach anything here — see `ObservedMediasoupRouter`. */
|
|
2357
|
+
attachments?: Record<string, unknown>;
|
|
2195
2358
|
id: string;
|
|
2196
2359
|
producerId: string;
|
|
2197
2360
|
transportId: string;
|
|
@@ -2200,7 +2363,9 @@ type MediasoupConsumerSample = Record<string, unknown> & {
|
|
|
2200
2363
|
kind: 'audio' | 'video';
|
|
2201
2364
|
history: MediasoupConsumerSampleEvent[];
|
|
2202
2365
|
};
|
|
2203
|
-
type MediasoupDataProducerSample =
|
|
2366
|
+
type MediasoupDataProducerSample = {
|
|
2367
|
+
/** Free-form application data. Attach anything here — see `ObservedMediasoupRouter`. */
|
|
2368
|
+
attachments?: Record<string, unknown>;
|
|
2204
2369
|
id: string;
|
|
2205
2370
|
transportId: string;
|
|
2206
2371
|
createdAt: number;
|
|
@@ -2208,7 +2373,9 @@ type MediasoupDataProducerSample = Record<string, unknown> & {
|
|
|
2208
2373
|
label: string;
|
|
2209
2374
|
protocol: string;
|
|
2210
2375
|
};
|
|
2211
|
-
type MediasoupDataConsumerSample =
|
|
2376
|
+
type MediasoupDataConsumerSample = {
|
|
2377
|
+
/** Free-form application data. Attach anything here — see `ObservedMediasoupRouter`. */
|
|
2378
|
+
attachments?: Record<string, unknown>;
|
|
2212
2379
|
id: string;
|
|
2213
2380
|
dataProducerId: string;
|
|
2214
2381
|
transportId: string;
|
|
@@ -2218,13 +2385,86 @@ type MediasoupDataConsumerSample = Record<string, unknown> & {
|
|
|
2218
2385
|
protocol: string;
|
|
2219
2386
|
};
|
|
2220
2387
|
|
|
2388
|
+
/**
|
|
2389
|
+
* Declarative enrichment: return the `attachments` to stamp onto an entity's sample the moment it is
|
|
2390
|
+
* created. Called once per entity, before the corresponding `*-sample-added` event.
|
|
2391
|
+
*
|
|
2392
|
+
* The mediasoup object is handed in, so the common case — mirroring mediasoup's own `appData`, where
|
|
2393
|
+
* applications already keep `participantId`, `purpose` and friends — is a one-liner. Returning
|
|
2394
|
+
* `undefined` attaches nothing.
|
|
2395
|
+
*/
|
|
2396
|
+
type MediasoupSampleEnricher = {
|
|
2397
|
+
transport?: (transport: types.Transport) => Record<string, unknown> | undefined;
|
|
2398
|
+
producer?: (producer: types.Producer, transport: types.Transport) => Record<string, unknown> | undefined;
|
|
2399
|
+
consumer?: (consumer: types.Consumer, transport: types.Transport) => Record<string, unknown> | undefined;
|
|
2400
|
+
dataProducer?: (dataProducer: types.DataProducer, transport: types.Transport) => Record<string, unknown> | undefined;
|
|
2401
|
+
dataConsumer?: (dataConsumer: types.DataConsumer, transport: types.Transport) => Record<string, unknown> | undefined;
|
|
2402
|
+
};
|
|
2221
2403
|
type ObservedMediasoupRouterSettings<AppData extends Record<string, unknown> = Record<string, unknown>> = {
|
|
2222
2404
|
router: types.Router;
|
|
2223
2405
|
appData?: AppData;
|
|
2224
2406
|
attachments?: Record<string, unknown>;
|
|
2407
|
+
/** Stamp `attachments` onto each entity sample as it is created. See {@link MediasoupSampleEnricher}. */
|
|
2408
|
+
enrich?: MediasoupSampleEnricher;
|
|
2225
2409
|
};
|
|
2410
|
+
/**
|
|
2411
|
+
* Lifecycle hooks for building your own report.
|
|
2412
|
+
*
|
|
2413
|
+
* Each entity announces itself as `<entity>-sample-added` when it appears and
|
|
2414
|
+
* `<entity>-sample-closed` when it goes away, carrying **the live sample object** plus the mediasoup
|
|
2415
|
+
* object it came from. Mutating `sample.attachments` inside a handler is the intended way to extend
|
|
2416
|
+
* a sample on the fly — the object you receive is the one held in `observedRouter.sample`, not a copy.
|
|
2417
|
+
*/
|
|
2226
2418
|
type ObservedMediasoupRouterEvents = {
|
|
2227
2419
|
close: [];
|
|
2420
|
+
'transport-sample-added': [{
|
|
2421
|
+
sample: MediasoupTransportSample;
|
|
2422
|
+
transport: types.Transport;
|
|
2423
|
+
}];
|
|
2424
|
+
'transport-sample-closed': [{
|
|
2425
|
+
sample: MediasoupTransportSample;
|
|
2426
|
+
transport: types.Transport;
|
|
2427
|
+
}];
|
|
2428
|
+
'producer-sample-added': [{
|
|
2429
|
+
sample: MediasoupProducerSample;
|
|
2430
|
+
producer: types.Producer;
|
|
2431
|
+
transport: types.Transport;
|
|
2432
|
+
}];
|
|
2433
|
+
'producer-sample-closed': [{
|
|
2434
|
+
sample: MediasoupProducerSample;
|
|
2435
|
+
producer: types.Producer;
|
|
2436
|
+
transport: types.Transport;
|
|
2437
|
+
}];
|
|
2438
|
+
'consumer-sample-added': [{
|
|
2439
|
+
sample: MediasoupConsumerSample;
|
|
2440
|
+
consumer: types.Consumer;
|
|
2441
|
+
transport: types.Transport;
|
|
2442
|
+
}];
|
|
2443
|
+
'consumer-sample-closed': [{
|
|
2444
|
+
sample: MediasoupConsumerSample;
|
|
2445
|
+
consumer: types.Consumer;
|
|
2446
|
+
transport: types.Transport;
|
|
2447
|
+
}];
|
|
2448
|
+
'data-producer-sample-added': [{
|
|
2449
|
+
sample: MediasoupDataProducerSample;
|
|
2450
|
+
dataProducer: types.DataProducer;
|
|
2451
|
+
transport: types.Transport;
|
|
2452
|
+
}];
|
|
2453
|
+
'data-producer-sample-closed': [{
|
|
2454
|
+
sample: MediasoupDataProducerSample;
|
|
2455
|
+
dataProducer: types.DataProducer;
|
|
2456
|
+
transport: types.Transport;
|
|
2457
|
+
}];
|
|
2458
|
+
'data-consumer-sample-added': [{
|
|
2459
|
+
sample: MediasoupDataConsumerSample;
|
|
2460
|
+
dataConsumer: types.DataConsumer;
|
|
2461
|
+
transport: types.Transport;
|
|
2462
|
+
}];
|
|
2463
|
+
'data-consumer-sample-closed': [{
|
|
2464
|
+
sample: MediasoupDataConsumerSample;
|
|
2465
|
+
dataConsumer: types.DataConsumer;
|
|
2466
|
+
transport: types.Transport;
|
|
2467
|
+
}];
|
|
2228
2468
|
};
|
|
2229
2469
|
declare interface ObservedMediasoupRouter {
|
|
2230
2470
|
on<U extends keyof ObservedMediasoupRouterEvents>(event: U, listener: (...args: ObservedMediasoupRouterEvents[U]) => void): this;
|
|
@@ -2250,9 +2490,36 @@ declare class ObservedMediasoupRouter<AppData extends Record<string, unknown> =
|
|
|
2250
2490
|
readonly sample: MediasoupRouterSample;
|
|
2251
2491
|
readonly webrtcTransportIds: Set<string>;
|
|
2252
2492
|
closed: boolean;
|
|
2493
|
+
private readonly _transportSamples;
|
|
2494
|
+
private readonly _producerSamples;
|
|
2495
|
+
private readonly _consumerSamples;
|
|
2496
|
+
private readonly _dataProducerSamples;
|
|
2497
|
+
private readonly _dataConsumerSamples;
|
|
2498
|
+
private readonly _enrich?;
|
|
2253
2499
|
constructor(settings: ObservedMediasoupRouterSettings<AppData>);
|
|
2254
2500
|
get id(): string;
|
|
2255
2501
|
get attachments(): Record<string, unknown>;
|
|
2502
|
+
getTransportSample(id: string): MediasoupTransportSample | undefined;
|
|
2503
|
+
getProducerSample(id: string): MediasoupProducerSample | undefined;
|
|
2504
|
+
getConsumerSample(id: string): MediasoupConsumerSample | undefined;
|
|
2505
|
+
getDataProducerSample(id: string): MediasoupDataProducerSample | undefined;
|
|
2506
|
+
getDataConsumerSample(id: string): MediasoupDataConsumerSample | undefined;
|
|
2507
|
+
/**
|
|
2508
|
+
* Merge `attachments` into an entity's sample, whichever kind it is.
|
|
2509
|
+
*
|
|
2510
|
+
* Ids are unique across mediasoup entity kinds, so one method covers all of them. Returns `false`
|
|
2511
|
+
* when the id is unknown — a real answer instead of failing quietly, which matters when the
|
|
2512
|
+
* annotation is driven by application events that may race the mediasoup ones.
|
|
2513
|
+
*/
|
|
2514
|
+
attachTo(id: string, attachments: Record<string, unknown>): boolean;
|
|
2515
|
+
/**
|
|
2516
|
+
* A **detached deep copy** of the current sample — the basis for building your own report.
|
|
2517
|
+
*
|
|
2518
|
+
* `this.sample` is live: its arrays grow and its `history` entries are appended as the router
|
|
2519
|
+
* runs, so a report built directly on it keeps changing after you think you're done. This returns
|
|
2520
|
+
* a snapshot that never moves.
|
|
2521
|
+
*/
|
|
2522
|
+
snapshot(): MediasoupRouterSample;
|
|
2256
2523
|
close(): void;
|
|
2257
2524
|
addTransport: (transport: types.Transport) => void;
|
|
2258
2525
|
addWebRtcTransport(transport: types.WebRtcTransport): void;
|
|
@@ -2264,6 +2531,18 @@ declare class ObservedMediasoupRouter<AppData extends Record<string, unknown> =
|
|
|
2264
2531
|
addDataProducer(transport: types.Transport, dataProducer: types.DataProducer): void;
|
|
2265
2532
|
addDataConsumer(transport: types.Transport, dataConsumer: types.DataConsumer): void;
|
|
2266
2533
|
private attachRouterListeners;
|
|
2534
|
+
private _addTransportSample;
|
|
2535
|
+
private _addProducerSample;
|
|
2536
|
+
private _addConsumerSample;
|
|
2537
|
+
private _addDataProducerSample;
|
|
2538
|
+
private _addDataConsumerSample;
|
|
2539
|
+
/**
|
|
2540
|
+
* Run an enricher and merge what it returns.
|
|
2541
|
+
*
|
|
2542
|
+
* Takes a thunk rather than a value so the **invocation** is inside the guard — application code
|
|
2543
|
+
* runs here, and a throwing enricher must not take the router's bookkeeping down with it.
|
|
2544
|
+
*/
|
|
2545
|
+
private _applyEnrichment;
|
|
2267
2546
|
private _attachTransportObserverListeners;
|
|
2268
2547
|
}
|
|
2269
2548
|
|
|
@@ -2304,6 +2583,10 @@ type ObserverEvents = {
|
|
|
2304
2583
|
reason: SampleRejectedReason;
|
|
2305
2584
|
sample: ClientSample;
|
|
2306
2585
|
}];
|
|
2586
|
+
/** An observer-scoped (cross-call / SFU-wide) finding raised by `observer.addIssue(...)`. */
|
|
2587
|
+
'observer-issue': [ObserverEventBase & {
|
|
2588
|
+
issue: ClientIssue;
|
|
2589
|
+
}];
|
|
2307
2590
|
'mediasoup-router-added': [ObservedMediasoupRouterScope];
|
|
2308
2591
|
'mediasoup-router-removed': [ObservedMediasoupRouterScope];
|
|
2309
2592
|
'mediasoup-router-matched-with-peer-connection': [ObservedMediasoupRouterScope & ObservedPeerConnectionScope];
|
|
@@ -2332,6 +2615,14 @@ type ObserverEvents = {
|
|
|
2332
2615
|
'client-issue': [ObservedClientScope & {
|
|
2333
2616
|
issue: ClientIssue;
|
|
2334
2617
|
}];
|
|
2618
|
+
/**
|
|
2619
|
+
* A stateful client issue ended — the client sent its `<type>-resolved` companion, or the
|
|
2620
|
+
* observer force-closed it because the client went away. Carries the finished **interval**
|
|
2621
|
+
* (`raisedAt` → `resolvedAt`, `durationInMs`).
|
|
2622
|
+
*/
|
|
2623
|
+
'client-issue-resolved': [ObservedClientScope & {
|
|
2624
|
+
resolvedIssue: ResolvedClientIssue;
|
|
2625
|
+
}];
|
|
2335
2626
|
'client-metadata': [ObservedClientScope & {
|
|
2336
2627
|
metaData: ClientMetaData;
|
|
2337
2628
|
}];
|
|
@@ -2582,6 +2873,16 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
|
|
|
2582
2873
|
numberOfScoreMeasurements: number;
|
|
2583
2874
|
readonly mediaDevices: MediaDeviceInfo[];
|
|
2584
2875
|
issues: ClientIssue[];
|
|
2876
|
+
/**
|
|
2877
|
+
* The client's currently **open** stateful issues, keyed by `ClientIssue.key` — the server-side
|
|
2878
|
+
* mirror of the client monitor's own active-issue map (client-monitor-js >= 4.6.0).
|
|
2879
|
+
*
|
|
2880
|
+
* This turns point-in-time symptom reports into intervals, which is what lets detectors ask
|
|
2881
|
+
* "are these clients broken *at the same time*" instead of "did they both report something
|
|
2882
|
+
* recently". Entries are opened by a raise, closed by the matching `<type>-resolved` entry, and
|
|
2883
|
+
* force-closed when the client closes.
|
|
2884
|
+
*/
|
|
2885
|
+
readonly activeIssues: Map<string, ActiveClientIssue>;
|
|
2585
2886
|
private _pendingInjections;
|
|
2586
2887
|
private _activeSample?;
|
|
2587
2888
|
private closeTimer?;
|
|
@@ -2596,8 +2897,28 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
|
|
|
2596
2897
|
injectExtensionStat(stat: ExtensionStat): void;
|
|
2597
2898
|
injectAttachment(attachments: Record<string, unknown>): void;
|
|
2598
2899
|
addMetadata(metadata: ClientMetaData): void;
|
|
2900
|
+
/**
|
|
2901
|
+
* Process one `clientIssues[]` entry.
|
|
2902
|
+
*
|
|
2903
|
+
* Entries come in two flavours (client-monitor-js >= 4.6.0):
|
|
2904
|
+
*
|
|
2905
|
+
* - a **raise** — opens an {@link ActiveClientIssue} under `issue.key` and emits `client-issue`;
|
|
2906
|
+
* - a **resolution** — `type` ends in `-resolved` and carries the same `key`; it closes the
|
|
2907
|
+
* matching active issue and emits `client-issue-resolved`.
|
|
2908
|
+
*
|
|
2909
|
+
* Keyless entries are one-shot: reported, never tracked. A re-raise of a key already active
|
|
2910
|
+
* refreshes the payload rather than opening a second interval.
|
|
2911
|
+
*/
|
|
2599
2912
|
addIssue(issue: ClientIssue): void;
|
|
2600
2913
|
addExtensionStats(stats: ExtensionStat): void;
|
|
2914
|
+
/**
|
|
2915
|
+
* Close every still-open issue, e.g. because the client is going away. Emits
|
|
2916
|
+
* `client-issue-resolved` for each with the given `resolvedBy`, so correlators relying on the
|
|
2917
|
+
* active set don't keep a departed client's issues open forever.
|
|
2918
|
+
*/
|
|
2919
|
+
resolveActiveIssues(resolvedBy?: ResolvedClientIssue['resolvedBy']): void;
|
|
2920
|
+
/** Close the active issue a `<type>-resolved` entry refers to, and announce the finished interval. */
|
|
2921
|
+
private _resolveIssue;
|
|
2601
2922
|
private _processClientEvent;
|
|
2602
2923
|
private _updatePeerConnection;
|
|
2603
2924
|
private _mergePendingInjections;
|
|
@@ -2665,6 +2986,12 @@ interface Detector {
|
|
|
2665
2986
|
readonly name: string;
|
|
2666
2987
|
/** Called on every entity update; may raise issues via the entity it observes. */
|
|
2667
2988
|
update(): void;
|
|
2989
|
+
/**
|
|
2990
|
+
* Optional teardown, called when the detector is removed from its registry (or the registry is
|
|
2991
|
+
* cleared, which happens when the owning call/observer closes). Implement it when the detector
|
|
2992
|
+
* subscribes to events or holds timers, so it doesn't leak.
|
|
2993
|
+
*/
|
|
2994
|
+
close?(): void;
|
|
2668
2995
|
}
|
|
2669
2996
|
|
|
2670
2997
|
declare class Detectors {
|
|
@@ -2675,6 +3002,7 @@ declare class Detectors {
|
|
|
2675
3002
|
remove(detector: Detector): void;
|
|
2676
3003
|
update(): void;
|
|
2677
3004
|
clear(): void;
|
|
3005
|
+
private _close;
|
|
2678
3006
|
}
|
|
2679
3007
|
|
|
2680
3008
|
type ObservedCallUpdateConfig = {
|
|
@@ -2844,6 +3172,13 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
|
|
|
2844
3172
|
numberOfPeerConnections: number;
|
|
2845
3173
|
/** Global, pre-dispatch middleware chain run on every accepted sample. */
|
|
2846
3174
|
readonly acceptMiddlewares: MiddlewareProcessor<AcceptMiddlewarePayload>;
|
|
3175
|
+
/**
|
|
3176
|
+
* Observer-scoped detector registry (ships empty), run on every `observer.update()` — the place
|
|
3177
|
+
* for findings that span **calls**, e.g. "many calls on the same SFU degraded at once". Detectors
|
|
3178
|
+
* raise findings with `observer.addIssue(...)`, surfaced on the bus as `observer-issue`.
|
|
3179
|
+
* (For findings within a single call use `observedCall.detectors`.)
|
|
3180
|
+
*/
|
|
3181
|
+
readonly detectors: Detectors;
|
|
2847
3182
|
constructor(config?: ObserverConfig<AppData>);
|
|
2848
3183
|
get numberOfCalls(): number;
|
|
2849
3184
|
get appData(): AppData | undefined;
|
|
@@ -2856,6 +3191,11 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
|
|
|
2856
3191
|
close(): void;
|
|
2857
3192
|
accept(sample: ClientSample, context?: AcceptContext): void;
|
|
2858
3193
|
update(): void;
|
|
3194
|
+
/**
|
|
3195
|
+
* Raise an observer-scoped (cross-call / SFU-wide) finding. Emitted on the bus as
|
|
3196
|
+
* `observer-issue`. Intended for `observer.detectors`, but the application may call it too.
|
|
3197
|
+
*/
|
|
3198
|
+
addIssue(issue: ClientIssue): void;
|
|
2859
3199
|
/** Emit an Observer-bus event. */
|
|
2860
3200
|
private _notify;
|
|
2861
3201
|
}
|
|
@@ -2894,6 +3234,911 @@ declare enum ClientEventTypes {
|
|
|
2894
3234
|
DATA_CONSUMER_CLOSED = "DATA_CONSUMER_CLOSED"
|
|
2895
3235
|
}
|
|
2896
3236
|
|
|
3237
|
+
/**
|
|
3238
|
+
* Small statistics helpers used by the aggregators and detectors.
|
|
3239
|
+
*
|
|
3240
|
+
* Rationale: a mean is a poor summary for call telemetry — one participant with a 1500 ms RTT
|
|
3241
|
+
* skews the average for nine healthy ones. Detectors should reason with medians, high percentiles
|
|
3242
|
+
* and "affected ratios" instead.
|
|
3243
|
+
*/
|
|
3244
|
+
/** A distribution summary of a numeric sample set. */
|
|
3245
|
+
type StatsSummary = {
|
|
3246
|
+
count: number;
|
|
3247
|
+
min: number;
|
|
3248
|
+
max: number;
|
|
3249
|
+
mean: number;
|
|
3250
|
+
median: number;
|
|
3251
|
+
p75: number;
|
|
3252
|
+
p95: number;
|
|
3253
|
+
};
|
|
3254
|
+
/**
|
|
3255
|
+
* The p-th percentile (0..1) using linear interpolation between closest ranks.
|
|
3256
|
+
* Returns `undefined` for an empty input.
|
|
3257
|
+
*/
|
|
3258
|
+
declare function percentile(values: number[], p: number): number | undefined;
|
|
3259
|
+
/** The median (50th percentile). `undefined` for an empty input. */
|
|
3260
|
+
declare function median(values: number[]): number | undefined;
|
|
3261
|
+
/** Summarize a numeric sample set. Returns `undefined` for an empty input. */
|
|
3262
|
+
declare function summarize(values: number[]): StatsSummary | undefined;
|
|
3263
|
+
/**
|
|
3264
|
+
* A counter-reset-safe delta: the increase of a cumulative counter between two observations.
|
|
3265
|
+
*
|
|
3266
|
+
* Returns `0` when the counter went backwards (reset / SSRC reuse) or when either side is missing.
|
|
3267
|
+
* NOTE the guard is `>=` on **defined** values — a previous value of `0` is a perfectly valid
|
|
3268
|
+
* baseline, so `0 -> 5` correctly yields `5` (using a truthiness check here silently drops the
|
|
3269
|
+
* first interval of every counter, which is exactly when the first loss/freeze event happens).
|
|
3270
|
+
*/
|
|
3271
|
+
declare function counterDelta(previous: number | undefined, current: number | undefined): number;
|
|
3272
|
+
/** An entry retained by {@link SlidingWindow}. */
|
|
3273
|
+
type SlidingWindowEntry<T> = {
|
|
3274
|
+
timestamp: number;
|
|
3275
|
+
value: T;
|
|
3276
|
+
};
|
|
3277
|
+
/**
|
|
3278
|
+
* A time-bounded ring buffer used by detectors that reason over a window ("N of M clients degraded
|
|
3279
|
+
* within 10 s"). Entries older than `windowMs` are evicted on write and on read.
|
|
3280
|
+
*/
|
|
3281
|
+
declare class SlidingWindow<T> {
|
|
3282
|
+
readonly windowMs: number;
|
|
3283
|
+
/** Optional hard cap on retained entries, to bound memory on very chatty inputs. */
|
|
3284
|
+
readonly maxEntries: number;
|
|
3285
|
+
private _entries;
|
|
3286
|
+
constructor(windowMs: number,
|
|
3287
|
+
/** Optional hard cap on retained entries, to bound memory on very chatty inputs. */
|
|
3288
|
+
maxEntries?: number);
|
|
3289
|
+
/** Add an entry (defaults to `Date.now()`), then evict anything outside the window. */
|
|
3290
|
+
add(value: T, timestamp?: number): void;
|
|
3291
|
+
/** The entries still inside the window, oldest first. */
|
|
3292
|
+
entries(now?: number): SlidingWindowEntry<T>[];
|
|
3293
|
+
/** The values still inside the window, oldest first. */
|
|
3294
|
+
values(now?: number): T[];
|
|
3295
|
+
get size(): number;
|
|
3296
|
+
clear(): void;
|
|
3297
|
+
private _evict;
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
/**
|
|
3301
|
+
* Thresholds deciding when a single receiver counts as "degraded". A receiver is degraded when it
|
|
3302
|
+
* trips **any** of these in the current tick.
|
|
3303
|
+
*/
|
|
3304
|
+
type ReceiverHealthThresholds = {
|
|
3305
|
+
/** Fraction of packets lost in the tick (0..1). */
|
|
3306
|
+
fractionLost: number;
|
|
3307
|
+
/** Freezes observed in the tick. */
|
|
3308
|
+
freezeCount: number;
|
|
3309
|
+
/** Fraction of received frames dropped before rendering (0..1). */
|
|
3310
|
+
framesDroppedRatio: number;
|
|
3311
|
+
/** Fraction of received audio samples concealed (0..1). */
|
|
3312
|
+
concealmentRatio: number;
|
|
3313
|
+
/** Mean jitter-buffer delay of the tick (ms). */
|
|
3314
|
+
jitterBufferDelayInMs: number;
|
|
3315
|
+
/** Round-trip time reported for the receiving peer connection (ms). */
|
|
3316
|
+
rttInMs: number;
|
|
3317
|
+
};
|
|
3318
|
+
declare const defaultReceiverHealthThresholds: ReceiverHealthThresholds;
|
|
3319
|
+
/** The per-receiver view the aggregator builds for one subscribed track. */
|
|
3320
|
+
type ReceiverDistributionEntry = {
|
|
3321
|
+
observedInboundTrack: ObservedInboundTrack;
|
|
3322
|
+
clientId: string;
|
|
3323
|
+
peerConnectionId: string;
|
|
3324
|
+
degraded: boolean;
|
|
3325
|
+
/** Why it was marked degraded (empty when healthy). */
|
|
3326
|
+
reasons: string[];
|
|
3327
|
+
bitrate: number;
|
|
3328
|
+
fractionLost?: number;
|
|
3329
|
+
jitter?: number;
|
|
3330
|
+
rttInMs?: number;
|
|
3331
|
+
jitterBufferDelayInMs?: number;
|
|
3332
|
+
concealmentRatio?: number;
|
|
3333
|
+
framesDroppedRatio?: number;
|
|
3334
|
+
deltaFreezeCount: number;
|
|
3335
|
+
deltaPliCount: number;
|
|
3336
|
+
deltaNackCount: number;
|
|
3337
|
+
deltaPacketsReceived: number;
|
|
3338
|
+
};
|
|
3339
|
+
/** The publisher side of the distribution. */
|
|
3340
|
+
type PublisherDistributionEntry = {
|
|
3341
|
+
observedOutboundTrack: ObservedOutboundTrack;
|
|
3342
|
+
clientId: string;
|
|
3343
|
+
peerConnectionId: string;
|
|
3344
|
+
/** `true` when the publisher's own egress looks fine (so degradation is downstream). */
|
|
3345
|
+
healthy: boolean;
|
|
3346
|
+
reasons: string[];
|
|
3347
|
+
bitrate: number;
|
|
3348
|
+
/** Loss reported back by the SFU/remote via RTCP (0..1). */
|
|
3349
|
+
remoteFractionLost?: number;
|
|
3350
|
+
remoteRttInMs?: number;
|
|
3351
|
+
qualityLimitationReason?: string;
|
|
3352
|
+
deltaPacketsSent: number;
|
|
3353
|
+
};
|
|
3354
|
+
/**
|
|
3355
|
+
* One published track and everything observed about how it was delivered to its subscribers.
|
|
3356
|
+
* This is the primitive most cross-client detectors are built on.
|
|
3357
|
+
*/
|
|
3358
|
+
type ObservedTrackDistribution = {
|
|
3359
|
+
trackId: string;
|
|
3360
|
+
kind: string;
|
|
3361
|
+
publisher: PublisherDistributionEntry;
|
|
3362
|
+
receivers: ReceiverDistributionEntry[];
|
|
3363
|
+
numberOfReceivers: number;
|
|
3364
|
+
numberOfHealthyReceivers: number;
|
|
3365
|
+
numberOfDegradedReceivers: number;
|
|
3366
|
+
/** degradedReceivers / receivers (0..1); `0` when there are no receivers. */
|
|
3367
|
+
degradedRatio: number;
|
|
3368
|
+
/** Distribution summaries across receivers (undefined when no receiver reported the metric). */
|
|
3369
|
+
bitrate?: StatsSummary;
|
|
3370
|
+
fractionLost?: StatsSummary;
|
|
3371
|
+
jitter?: StatsSummary;
|
|
3372
|
+
rttInMs?: StatsSummary;
|
|
3373
|
+
jitterBufferDelayInMs?: StatsSummary;
|
|
3374
|
+
concealmentRatio?: StatsSummary;
|
|
3375
|
+
/** Fan-out counters: how many receivers saw the symptom, and the total across them. */
|
|
3376
|
+
freezes: {
|
|
3377
|
+
affectedReceivers: number;
|
|
3378
|
+
total: number;
|
|
3379
|
+
};
|
|
3380
|
+
plis: {
|
|
3381
|
+
affectedReceivers: number;
|
|
3382
|
+
total: number;
|
|
3383
|
+
};
|
|
3384
|
+
concealment: {
|
|
3385
|
+
affectedReceivers: number;
|
|
3386
|
+
};
|
|
3387
|
+
};
|
|
3388
|
+
/**
|
|
3389
|
+
* Builds {@link ObservedTrackDistribution}s by walking
|
|
3390
|
+
* `ObservedOutboundTrack.remoteInboundTracks` — the publisher → subscribers links maintained by a
|
|
3391
|
+
* `RemoteTrackResolver`. Without a configured resolver there are no links and the aggregator
|
|
3392
|
+
* yields nothing.
|
|
3393
|
+
*
|
|
3394
|
+
* It is stateless per call: build it once and call `aggregate()` on each `call.update()`.
|
|
3395
|
+
*/
|
|
3396
|
+
declare class TrackDistributionAggregator {
|
|
3397
|
+
private readonly _call;
|
|
3398
|
+
readonly thresholds: ReceiverHealthThresholds;
|
|
3399
|
+
constructor(_call: ObservedCall, thresholds?: ReceiverHealthThresholds);
|
|
3400
|
+
/** Aggregate every published track in the call that currently has linked subscribers. */
|
|
3401
|
+
aggregate(): ObservedTrackDistribution[];
|
|
3402
|
+
/** Aggregate a single published track, or `undefined` when it has no linked subscribers. */
|
|
3403
|
+
aggregateTrack(outboundTrack: ObservedOutboundTrack): ObservedTrackDistribution | undefined;
|
|
3404
|
+
private _publisherEntry;
|
|
3405
|
+
private _receiverEntry;
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
/** Thresholds deciding when a client counts as degraded on the receiving / sending side. */
|
|
3409
|
+
type ClientHealthThresholds = {
|
|
3410
|
+
/** Inbound loss fraction across the client's received streams (0..1). */
|
|
3411
|
+
inboundFractionLost: number;
|
|
3412
|
+
/** Loss fraction reported back about the client's sent streams via RTCP (0..1). */
|
|
3413
|
+
outboundFractionLost: number;
|
|
3414
|
+
/** Round-trip time (ms). */
|
|
3415
|
+
rttInMs: number;
|
|
3416
|
+
/** Freezes observed across the client's inbound video in the tick. */
|
|
3417
|
+
freezeCount: number;
|
|
3418
|
+
/** Concealment fraction across the client's inbound audio (0..1). */
|
|
3419
|
+
concealmentRatio: number;
|
|
3420
|
+
};
|
|
3421
|
+
declare const defaultClientHealthThresholds: ClientHealthThresholds;
|
|
3422
|
+
/** The per-client health view, split by direction. */
|
|
3423
|
+
type ClientHealth = {
|
|
3424
|
+
observedClient: ObservedClient;
|
|
3425
|
+
clientId: string;
|
|
3426
|
+
/** Receiving (download) side is impaired. */
|
|
3427
|
+
inboundDegraded: boolean;
|
|
3428
|
+
/** Sending (upload) side is impaired. */
|
|
3429
|
+
outboundDegraded: boolean;
|
|
3430
|
+
/** `inboundDegraded || outboundDegraded`. */
|
|
3431
|
+
degraded: boolean;
|
|
3432
|
+
reasons: string[];
|
|
3433
|
+
inboundFractionLost?: number;
|
|
3434
|
+
outboundFractionLost?: number;
|
|
3435
|
+
rttInMs?: number;
|
|
3436
|
+
deltaFreezeCount: number;
|
|
3437
|
+
concealmentRatio?: number;
|
|
3438
|
+
/** Quality-limitation reasons seen on this client's outbound video ('cpu' | 'bandwidth' | …). */
|
|
3439
|
+
qualityLimitationReasons: string[];
|
|
3440
|
+
usingTURN: boolean;
|
|
3441
|
+
usingTCP: boolean;
|
|
3442
|
+
};
|
|
3443
|
+
/** Call-level rollup of the per-client health, using percentiles rather than means. */
|
|
3444
|
+
type CallHealth = {
|
|
3445
|
+
callId: string;
|
|
3446
|
+
clients: ClientHealth[];
|
|
3447
|
+
numberOfClients: number;
|
|
3448
|
+
numberOfDegradedClients: number;
|
|
3449
|
+
numberOfInboundDegradedClients: number;
|
|
3450
|
+
numberOfOutboundDegradedClients: number;
|
|
3451
|
+
/** degradedClients / clients (0..1). */
|
|
3452
|
+
degradedRatio: number;
|
|
3453
|
+
inboundDegradedRatio: number;
|
|
3454
|
+
outboundDegradedRatio: number;
|
|
3455
|
+
rttInMs?: StatsSummary;
|
|
3456
|
+
inboundFractionLost?: StatsSummary;
|
|
3457
|
+
concealmentRatio?: StatsSummary;
|
|
3458
|
+
/** How many clients reported each quality-limitation reason on their outbound video. */
|
|
3459
|
+
qualityLimitation: {
|
|
3460
|
+
cpu: number;
|
|
3461
|
+
bandwidth: number;
|
|
3462
|
+
other: number;
|
|
3463
|
+
};
|
|
3464
|
+
freezes: {
|
|
3465
|
+
affectedClients: number;
|
|
3466
|
+
total: number;
|
|
3467
|
+
};
|
|
3468
|
+
};
|
|
3469
|
+
/**
|
|
3470
|
+
* Aggregates a call along the **client** axis (as `TrackDistributionAggregator` does along the
|
|
3471
|
+
* publisher→subscriber axis): per-client health split into sending vs receiving, plus percentile
|
|
3472
|
+
* rollups and "affected ratio" counts for the whole call.
|
|
3473
|
+
*
|
|
3474
|
+
* Build once per call and call `aggregate()` on each `call.update()`.
|
|
3475
|
+
*/
|
|
3476
|
+
declare class CallHealthAggregator {
|
|
3477
|
+
private readonly _call;
|
|
3478
|
+
readonly thresholds: ClientHealthThresholds;
|
|
3479
|
+
constructor(_call: ObservedCall, thresholds?: ClientHealthThresholds);
|
|
3480
|
+
aggregate(): CallHealth;
|
|
3481
|
+
private _clientHealth;
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
/** The finding types this detector raises (as `call-issue.type`). */
|
|
3485
|
+
declare const CommonSourceDegradationTypes: {
|
|
3486
|
+
/** The publisher's egress looks fine, yet most subscribers are degraded → downstream/SFU suspected. */
|
|
3487
|
+
readonly publisherHealthySubscribersDegraded: "PUBLISHER_HEALTHY_SUBSCRIBERS_DEGRADED";
|
|
3488
|
+
/** The publisher itself is impaired and every subscriber sees it → source-side problem. */
|
|
3489
|
+
readonly publisherDegradedForAllSubscribers: "PUBLISHER_DEGRADED_FOR_ALL_SUBSCRIBERS";
|
|
3490
|
+
/** Exactly one subscriber is degraded while the rest are fine → that receiver's own problem. */
|
|
3491
|
+
readonly singleSubscriberDegraded: "SINGLE_SUBSCRIBER_DEGRADED";
|
|
3492
|
+
/** Several (but not most) subscribers degraded on the same source. */
|
|
3493
|
+
readonly multipleSubscribersDegraded: "MULTIPLE_SUBSCRIBERS_DEGRADED";
|
|
3494
|
+
};
|
|
3495
|
+
type CommonSourceDegradationDetectorConfig = {
|
|
3496
|
+
/** Minimum subscribers before a ratio is meaningful. Default `3`. */
|
|
3497
|
+
minReceivers: number;
|
|
3498
|
+
/** degradedRatio at/above which the problem is treated as common to the source. Default `0.6`. */
|
|
3499
|
+
degradedRatioThreshold: number;
|
|
3500
|
+
/** Per-receiver health thresholds (forwarded to the aggregator). */
|
|
3501
|
+
thresholds?: Partial<ReceiverHealthThresholds>;
|
|
3502
|
+
/**
|
|
3503
|
+
* Consecutive ticks a condition must hold before an issue is raised, to avoid flapping on a
|
|
3504
|
+
* single bad sample. Default `2`.
|
|
3505
|
+
*/
|
|
3506
|
+
consecutiveTicks: number;
|
|
3507
|
+
};
|
|
3508
|
+
/**
|
|
3509
|
+
* Compares a published track against **all** of its subscribers and decides where the fault lies.
|
|
3510
|
+
*
|
|
3511
|
+
* This is the detector that only a server-side observer can run: a single browser cannot know
|
|
3512
|
+
* whether the other participants receiving the same source see the same thing. Requires a
|
|
3513
|
+
* `RemoteTrackResolver` (`ObserverConfig.createTrackResolver`) — without publisher↔subscriber links
|
|
3514
|
+
* there is nothing to compare and the detector stays silent.
|
|
3515
|
+
*/
|
|
3516
|
+
declare class CommonSourceDegradationDetector implements Detector {
|
|
3517
|
+
private readonly _call;
|
|
3518
|
+
readonly name = "common-source-degradation-detector";
|
|
3519
|
+
private readonly _config;
|
|
3520
|
+
private readonly _aggregator;
|
|
3521
|
+
/** trackId -> consecutive ticks the same finding held. */
|
|
3522
|
+
private readonly _streaks;
|
|
3523
|
+
/** The distributions computed on the most recent `update()` (handy for dashboards). */
|
|
3524
|
+
lastDistributions: ObservedTrackDistribution[];
|
|
3525
|
+
constructor(_call: ObservedCall, config?: Partial<CommonSourceDegradationDetectorConfig>);
|
|
3526
|
+
update(): void;
|
|
3527
|
+
private _classify;
|
|
3528
|
+
private _payload;
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
declare const CallWideDegradationTypes: {
|
|
3532
|
+
/** Most participants degraded at once → shared cause (call/SFU/network), not individual endpoints. */
|
|
3533
|
+
readonly callWideQualityDegradation: "CALL_WIDE_QUALITY_DEGRADATION";
|
|
3534
|
+
/** Most participants' **receiving** side degraded → downstream/egress suspected. */
|
|
3535
|
+
readonly callWideInboundDegradation: "CALL_WIDE_INBOUND_DEGRADATION";
|
|
3536
|
+
/** Most participants' **sending** side degraded → ingress suspected. */
|
|
3537
|
+
readonly callWideOutboundDegradation: "CALL_WIDE_OUTBOUND_DEGRADATION";
|
|
3538
|
+
};
|
|
3539
|
+
type CallWideDegradationDetectorConfig = {
|
|
3540
|
+
/** Minimum participants before a ratio means anything. Default `3`. */
|
|
3541
|
+
minClients: number;
|
|
3542
|
+
/** Affected-client ratio at/above which the call is considered call-wide degraded. Default `0.5`. */
|
|
3543
|
+
degradedRatioThreshold: number;
|
|
3544
|
+
/** Per-client health thresholds. */
|
|
3545
|
+
thresholds?: Partial<ClientHealthThresholds>;
|
|
3546
|
+
/** Consecutive ticks the condition must hold before raising. Default `2`. */
|
|
3547
|
+
consecutiveTicks: number;
|
|
3548
|
+
};
|
|
3549
|
+
/**
|
|
3550
|
+
* Raises a call-level finding when a **majority of participants** are degraded in the same window —
|
|
3551
|
+
* the signal that something shared is wrong rather than one person's Wi-Fi.
|
|
3552
|
+
*
|
|
3553
|
+
* It also splits by direction, because that is what makes the finding actionable: if most clients'
|
|
3554
|
+
* receiving side is bad the suspicion is egress/downstream; if most clients' sending side is bad it
|
|
3555
|
+
* points at ingress. Reported with medians/percentiles, never means, so a single 1500 ms outlier
|
|
3556
|
+
* can't manufacture (or mask) a call-wide alert.
|
|
3557
|
+
*/
|
|
3558
|
+
declare class CallWideDegradationDetector implements Detector {
|
|
3559
|
+
private readonly _call;
|
|
3560
|
+
readonly name = "call-wide-degradation-detector";
|
|
3561
|
+
private readonly _config;
|
|
3562
|
+
private readonly _aggregator;
|
|
3563
|
+
private _streak?;
|
|
3564
|
+
/** The health rollup computed on the most recent `update()`. */
|
|
3565
|
+
lastHealth?: CallHealth;
|
|
3566
|
+
constructor(_call: ObservedCall, config?: Partial<CallWideDegradationDetectorConfig>);
|
|
3567
|
+
update(): void;
|
|
3568
|
+
private _classify;
|
|
3569
|
+
private _payload;
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
declare const PliAndFreezeFanOutTypes: {
|
|
3573
|
+
/** Many receivers of the same publisher requested keyframes at once. */
|
|
3574
|
+
readonly publisherPliStorm: "PUBLISHER_PLI_STORM";
|
|
3575
|
+
/** Many receivers of the same publisher froze at once. */
|
|
3576
|
+
readonly publishedVideoFrozenForMultipleReceivers: "PUBLISHED_VIDEO_FROZEN_FOR_MULTIPLE_RECEIVERS";
|
|
3577
|
+
};
|
|
3578
|
+
type PliAndFreezeFanOutDetectorConfig = {
|
|
3579
|
+
/** Minimum receivers of the track before a fan-out ratio is meaningful. Default `3`. */
|
|
3580
|
+
minReceivers: number;
|
|
3581
|
+
/** Fraction of receivers that must be affected within the window. Default `0.5`. */
|
|
3582
|
+
affectedRatioThreshold: number;
|
|
3583
|
+
/** The correlation window (ms) symptoms are counted over. Default `10_000`. */
|
|
3584
|
+
windowMs: number;
|
|
3585
|
+
/** Minimum PLIs across receivers inside the window before a storm is declared. Default `5`. */
|
|
3586
|
+
minPliCount: number;
|
|
3587
|
+
/** Re-arm time (ms) before the same finding can be raised again for a track. Default `30_000`. */
|
|
3588
|
+
cooldownMs: number;
|
|
3589
|
+
thresholds?: Partial<ReceiverHealthThresholds>;
|
|
3590
|
+
};
|
|
3591
|
+
/**
|
|
3592
|
+
* Detects **fan-out** symptoms: one publisher's stream causing many receivers to request keyframes
|
|
3593
|
+
* (PLI) or to freeze within the same window.
|
|
3594
|
+
*
|
|
3595
|
+
* A single receiver sending PLIs is unremarkable — it lost some packets. Nineteen of twenty
|
|
3596
|
+
* receivers of the *same* source doing it inside ten seconds is not twenty coincidental network
|
|
3597
|
+
* faults; it points at the source's output, the SFU's forwarding, or a keyframe/burst problem
|
|
3598
|
+
* upstream. That distinction requires seeing every subscriber of a track at once, which is why this
|
|
3599
|
+
* belongs server-side.
|
|
3600
|
+
*
|
|
3601
|
+
* Symptoms are accumulated in a sliding window rather than judged per tick, because a burst is
|
|
3602
|
+
* spread over a few samples.
|
|
3603
|
+
*/
|
|
3604
|
+
declare class PliAndFreezeFanOutDetector implements Detector {
|
|
3605
|
+
private readonly _call;
|
|
3606
|
+
readonly name = "pli-and-freeze-fan-out-detector";
|
|
3607
|
+
private readonly _config;
|
|
3608
|
+
private readonly _aggregator;
|
|
3609
|
+
private readonly _windows;
|
|
3610
|
+
constructor(_call: ObservedCall, config?: Partial<PliAndFreezeFanOutDetectorConfig>);
|
|
3611
|
+
update(): void;
|
|
3612
|
+
close(): void;
|
|
3613
|
+
private _evaluate;
|
|
3614
|
+
private _windowOf;
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
declare const AudioImpairmentFanOutTypes: {
|
|
3618
|
+
/** Most receivers of one microphone are concealing audio → the problem follows that source. */
|
|
3619
|
+
readonly publishedAudioDegradedForMajority: "PUBLISHED_AUDIO_DEGRADED_FOR_MAJORITY";
|
|
3620
|
+
/** Receivers across the call are under jitter-buffer pressure → shared delivery problem. */
|
|
3621
|
+
readonly callWideAudioJitterBufferStress: "CALL_WIDE_AUDIO_JITTER_BUFFER_STRESS";
|
|
3622
|
+
};
|
|
3623
|
+
type AudioImpairmentFanOutDetectorConfig = {
|
|
3624
|
+
/** Minimum receivers of a track before a ratio is meaningful. Default `3`. */
|
|
3625
|
+
minReceivers: number;
|
|
3626
|
+
/** Fraction of receivers that must be impaired. Default `0.6`. */
|
|
3627
|
+
affectedRatioThreshold: number;
|
|
3628
|
+
/** Jitter-buffer delay (ms) above which a receiver counts as stressed. Default `500`. */
|
|
3629
|
+
jitterBufferDelayInMs: number;
|
|
3630
|
+
/** Minimum audio receivers in the call before the call-wide check runs. Default `5`. */
|
|
3631
|
+
minCallReceivers: number;
|
|
3632
|
+
/** Consecutive ticks the condition must hold before raising. Default `2`. */
|
|
3633
|
+
consecutiveTicks: number;
|
|
3634
|
+
thresholds?: Partial<ReceiverHealthThresholds>;
|
|
3635
|
+
};
|
|
3636
|
+
/**
|
|
3637
|
+
* Audio-side fan-out analysis, using the concealment / jitter-buffer metrics WebRTC exposes on the
|
|
3638
|
+
* receiver (NetEQ's own account of how hard it is working to keep audio smooth).
|
|
3639
|
+
*
|
|
3640
|
+
* Two questions a browser can't answer:
|
|
3641
|
+
*
|
|
3642
|
+
* 1. *Does the impairment follow the source?* If every receiver of Alice's microphone is concealing
|
|
3643
|
+
* ~20% of samples while every receiver of Bob's is at ~0.1%, the fault is on Alice's path, not on
|
|
3644
|
+
* the receivers.
|
|
3645
|
+
* 2. *Is the whole call's audio delivery under pressure?* One receiver with a huge jitter buffer is
|
|
3646
|
+
* their own network; fifteen of twenty at once is shared.
|
|
3647
|
+
*/
|
|
3648
|
+
declare class AudioImpairmentFanOutDetector implements Detector {
|
|
3649
|
+
private readonly _call;
|
|
3650
|
+
readonly name = "audio-impairment-fan-out-detector";
|
|
3651
|
+
private readonly _config;
|
|
3652
|
+
private readonly _aggregator;
|
|
3653
|
+
private readonly _trackStreaks;
|
|
3654
|
+
private _callStreak;
|
|
3655
|
+
constructor(_call: ObservedCall, config?: Partial<AudioImpairmentFanOutDetectorConfig>);
|
|
3656
|
+
update(): void;
|
|
3657
|
+
close(): void;
|
|
3658
|
+
private _evaluateCallWide;
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
declare const IceDisruptionTypes: {
|
|
3662
|
+
/** Many participants lost/failed their ICE connection inside the same window. */
|
|
3663
|
+
readonly callIceDisruption: "CALL_ICE_DISRUPTION";
|
|
3664
|
+
};
|
|
3665
|
+
type IceDisruptionDetectorConfig = {
|
|
3666
|
+
/** Minimum participants before a ratio is meaningful. Default `3`. */
|
|
3667
|
+
minClients: number;
|
|
3668
|
+
/** Fraction of participants that must be disrupted within the window. Default `0.5`. */
|
|
3669
|
+
affectedRatioThreshold: number;
|
|
3670
|
+
/** Correlation window (ms). Default `10_000`. */
|
|
3671
|
+
windowMs: number;
|
|
3672
|
+
/** Re-arm time (ms) before raising again. Default `60_000`. */
|
|
3673
|
+
cooldownMs: number;
|
|
3674
|
+
};
|
|
3675
|
+
/**
|
|
3676
|
+
* Detects an **ICE disruption storm**: many participants of a call losing connectivity inside the
|
|
3677
|
+
* same short window.
|
|
3678
|
+
*
|
|
3679
|
+
* One client losing ICE is routine (they walked out of Wi-Fi range). Twenty-eight of thirty-five
|
|
3680
|
+
* doing it within five seconds is an infrastructure event, and that conclusion is only reachable by
|
|
3681
|
+
* correlating clients — which is the whole point of doing it here rather than in the browser.
|
|
3682
|
+
*
|
|
3683
|
+
* ### Prefer the issue-driven path
|
|
3684
|
+
*
|
|
3685
|
+
* This detector works from **raw ICE state transitions**, which is the fallback for clients that do
|
|
3686
|
+
* not report issues. If your clients run client-monitor-js >= 4.6.0, prefer
|
|
3687
|
+
* `ConcurrentIssueDetector` configured with the ICE issue types instead:
|
|
3688
|
+
*
|
|
3689
|
+
* ```ts
|
|
3690
|
+
* new ConcurrentIssueDetector(observedCall, {
|
|
3691
|
+
* issueTypes: [ 'ice-disconnected', 'ice-connection-failed', 'ice-transport-stalled', 'unstable-ice-path' ],
|
|
3692
|
+
* });
|
|
3693
|
+
* ```
|
|
3694
|
+
*
|
|
3695
|
+
* The client's own detector is better at deciding *whether* a transport is really disrupted: it
|
|
3696
|
+
* raises `ice-disconnected` only once `disconnected` has persisted past a threshold, so the transient
|
|
3697
|
+
* blips ICE routinely heals on its own never produce an issue at all, and it distinguishes a terminal
|
|
3698
|
+
* `failed` from a stalled transport from an unstable path. This detector cannot make those
|
|
3699
|
+
* distinctions — a raw `disconnected` that recovers in 200 ms looks identical to one that never does.
|
|
3700
|
+
* It also gains resolution intervals, so "they all recovered together" becomes observable.
|
|
3701
|
+
*
|
|
3702
|
+
* ### Why this one subscribes to the bus
|
|
3703
|
+
*
|
|
3704
|
+
* ICE transitions are discrete events that can occur and revert between two `update()` ticks;
|
|
3705
|
+
* polling `iceConnectionState` per tick would miss short flaps. It therefore implements `close()`
|
|
3706
|
+
* to drop its listeners (called automatically when the call closes or the detector is removed).
|
|
3707
|
+
*/
|
|
3708
|
+
declare class IceDisruptionDetector implements Detector {
|
|
3709
|
+
private readonly _call;
|
|
3710
|
+
readonly name = "ice-disruption-detector";
|
|
3711
|
+
private readonly _config;
|
|
3712
|
+
/** clientId -> the last time that client was seen disrupted, inside the window. */
|
|
3713
|
+
private readonly _disruptions;
|
|
3714
|
+
private _lastRaisedAt;
|
|
3715
|
+
private readonly _onIceStateChanged;
|
|
3716
|
+
private readonly _onConnectionStateChanged;
|
|
3717
|
+
constructor(_call: ObservedCall, config?: Partial<IceDisruptionDetectorConfig>);
|
|
3718
|
+
update(): void;
|
|
3719
|
+
close(): void;
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
declare const TurnServerHealthTypes: {
|
|
3723
|
+
/** One TURN server's clients are degraded while other servers' clients are fine. */
|
|
3724
|
+
readonly turnServerDegraded: "TURN_SERVER_DEGRADED";
|
|
3725
|
+
};
|
|
3726
|
+
type TurnServerHealthDetectorConfig = {
|
|
3727
|
+
/** Minimum clients on a server before a ratio is meaningful. Default `5`. */
|
|
3728
|
+
minClientsPerServer: number;
|
|
3729
|
+
/** Fraction of a server's clients that must be degraded. Default `0.5`. */
|
|
3730
|
+
degradedRatioThreshold: number;
|
|
3731
|
+
/** RTT (ms) above which a relayed peer connection counts as degraded. Default `400`. */
|
|
3732
|
+
rttInMs: number;
|
|
3733
|
+
/** Inbound loss fraction above which a relayed peer connection counts as degraded. Default `0.03`. */
|
|
3734
|
+
fractionLost: number;
|
|
3735
|
+
/** Consecutive ticks the condition must hold before raising. Default `2`. */
|
|
3736
|
+
consecutiveTicks: number;
|
|
3737
|
+
/** Re-arm time (ms) before raising again for the same server. Default `60_000`. */
|
|
3738
|
+
cooldownMs: number;
|
|
3739
|
+
};
|
|
3740
|
+
/** The per-server view this detector builds. */
|
|
3741
|
+
type TurnServerHealth = {
|
|
3742
|
+
serverUrl: string;
|
|
3743
|
+
peerConnections: number;
|
|
3744
|
+
degradedPeerConnections: number;
|
|
3745
|
+
degradedRatio: number;
|
|
3746
|
+
rttInMs?: StatsSummary;
|
|
3747
|
+
fractionLost?: StatsSummary;
|
|
3748
|
+
affectedClientIds: string[];
|
|
3749
|
+
};
|
|
3750
|
+
/**
|
|
3751
|
+
* An **observer-level** detector that groups relayed peer connections by the TURN server carrying
|
|
3752
|
+
* them and compares the servers against each other.
|
|
3753
|
+
*
|
|
3754
|
+
* Counting TURN usage is not useful on its own; knowing that `turn-eu-1` has 22 of 30 clients in
|
|
3755
|
+
* trouble while `turn-eu-2` has 1 of 34 is. Because the comparison spans calls, it lives on
|
|
3756
|
+
* `observer.detectors` and raises `observer-issue` — a single actionable alert instead of fifty
|
|
3757
|
+
* per-client ones.
|
|
3758
|
+
*/
|
|
3759
|
+
declare class TurnServerHealthDetector implements Detector {
|
|
3760
|
+
private readonly _observer;
|
|
3761
|
+
readonly name = "turn-server-health-detector";
|
|
3762
|
+
private readonly _config;
|
|
3763
|
+
private readonly _streaks;
|
|
3764
|
+
private readonly _lastRaisedAt;
|
|
3765
|
+
/** The per-server rollup computed on the most recent `update()`. */
|
|
3766
|
+
lastServers: TurnServerHealth[];
|
|
3767
|
+
constructor(_observer: Observer, config?: Partial<TurnServerHealthDetectorConfig>);
|
|
3768
|
+
update(): void;
|
|
3769
|
+
close(): void;
|
|
3770
|
+
private _serverHealth;
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
/** A snapshot of everyone currently reporting one issue type. */
|
|
3774
|
+
type IssueCohort = {
|
|
3775
|
+
/** The issue type, without the `-resolved` suffix. */
|
|
3776
|
+
type: string;
|
|
3777
|
+
/** The open intervals of that type, one per (client, key). */
|
|
3778
|
+
issues: ActiveClientIssue[];
|
|
3779
|
+
/** Distinct clients with at least one open interval of this type. */
|
|
3780
|
+
clientIds: string[];
|
|
3781
|
+
/** `clientIds.length / totalClients` (0..1). */
|
|
3782
|
+
affectedRatio: number;
|
|
3783
|
+
/** Clients considered for the ratio. */
|
|
3784
|
+
totalClients: number;
|
|
3785
|
+
/**
|
|
3786
|
+
* Spread of the onsets, in **observer** time (ms) — `max(observedAt) - min(observedAt)`.
|
|
3787
|
+
*
|
|
3788
|
+
* Measured on the observer clock on purpose: `raisedAt` comes from each client's own clock, and
|
|
3789
|
+
* comparing those across machines makes clock skew look like an infrastructure event. A small
|
|
3790
|
+
* spread means the issues began together, which is the signature of a shared cause.
|
|
3791
|
+
*/
|
|
3792
|
+
onsetSpreadInMs: number;
|
|
3793
|
+
/** The earliest onset (observer clock). */
|
|
3794
|
+
firstObservedAt: number;
|
|
3795
|
+
};
|
|
3796
|
+
/** Options controlling how long an unresolved issue is trusted. */
|
|
3797
|
+
type IssueRegistryConfig = {
|
|
3798
|
+
/**
|
|
3799
|
+
* Drop an active issue this long after it was opened, even without a resolution (ms).
|
|
3800
|
+
*
|
|
3801
|
+
* A safety net for the case the lifecycle can't cover: a client that dies without its monitor
|
|
3802
|
+
* running `close()` never sends the `-resolved` companion, and a stuck "active" issue would make
|
|
3803
|
+
* every concurrency detector fire forever. Default `120_000`.
|
|
3804
|
+
*/
|
|
3805
|
+
maxIssueAgeInMs: number;
|
|
3806
|
+
};
|
|
3807
|
+
/**
|
|
3808
|
+
* Read-side index over the **active** client issues in a call (or across an observer's calls).
|
|
3809
|
+
*
|
|
3810
|
+
* `ObservedClient.activeIssues` holds the raw per-client state; this puts the questions detectors
|
|
3811
|
+
* actually ask on top of it:
|
|
3812
|
+
*
|
|
3813
|
+
* - *who currently has issue X?* → {@link cohortOf}
|
|
3814
|
+
* - *which issue types are shared by several clients right now?* → {@link cohorts}
|
|
3815
|
+
* - *which receivers of this published track are broken?* → {@link byTrackIds}
|
|
3816
|
+
*
|
|
3817
|
+
* The distinction that matters is **concurrency**. A window-based count ("N clients reported
|
|
3818
|
+
* congestion in the last 10 s") is a heuristic that has to guess whether the symptoms are still
|
|
3819
|
+
* happening; an active-issue set is ground truth, because the client tells the server when the
|
|
3820
|
+
* episode ends. Overlapping intervals are much stronger evidence of a common cause than
|
|
3821
|
+
* near-in-time reports.
|
|
3822
|
+
*
|
|
3823
|
+
* It is a *view*, holding no state of its own beyond configuration, so it can be constructed per
|
|
3824
|
+
* detector and queried on every `update()`.
|
|
3825
|
+
*/
|
|
3826
|
+
declare class IssueRegistry {
|
|
3827
|
+
private readonly _source;
|
|
3828
|
+
private readonly _config;
|
|
3829
|
+
constructor(_source: ObservedCall | Observer, config?: Partial<IssueRegistryConfig>);
|
|
3830
|
+
/** Every open issue in scope, with stale entries filtered out. */
|
|
3831
|
+
activeIssues(now?: number): ActiveClientIssue[];
|
|
3832
|
+
/** The number of clients in scope — the denominator for every ratio. */
|
|
3833
|
+
get totalClients(): number;
|
|
3834
|
+
/** The cohort for one issue type (empty `issues` when nobody has it open). */
|
|
3835
|
+
cohortOf(type: string, now?: number): IssueCohort;
|
|
3836
|
+
/** One cohort per issue type currently open somewhere in scope, largest first. */
|
|
3837
|
+
cohorts(now?: number): IssueCohort[];
|
|
3838
|
+
/**
|
|
3839
|
+
* Open issues attributed to any of the given track ids — the join that turns a receiver-side
|
|
3840
|
+
* issue into a statement about a **published** track (`trackId` → inbound track →
|
|
3841
|
+
* `remoteOutboundTrack` → publisher).
|
|
3842
|
+
*/
|
|
3843
|
+
byTrackIds(trackIds: Iterable<string>, now?: number): ActiveClientIssue[];
|
|
3844
|
+
/** Open issues reported by one client. */
|
|
3845
|
+
byClientId(clientId: string, now?: number): ActiveClientIssue[];
|
|
3846
|
+
private _toCohort;
|
|
3847
|
+
/** Works for both scopes: a call yields its clients, an observer yields every call's clients. */
|
|
3848
|
+
private _clients;
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3851
|
+
declare const ConcurrentIssueTypes: {
|
|
3852
|
+
/** Several participants have the same issue open **at the same time**. */
|
|
3853
|
+
readonly concurrentClientIssues: "CONCURRENT_CLIENT_ISSUES";
|
|
3854
|
+
/** Those issues also *began* together — the signature of one infrastructure event. */
|
|
3855
|
+
readonly issueOnsetBurst: "ISSUE_ONSET_BURST";
|
|
3856
|
+
};
|
|
3857
|
+
type ConcurrentIssueDetectorConfig = {
|
|
3858
|
+
/**
|
|
3859
|
+
* Only consider these issue types. Empty (default) means every type the clients report — which
|
|
3860
|
+
* is usually what you want, since the detector is generic over the client's vocabulary.
|
|
3861
|
+
*/
|
|
3862
|
+
issueTypes: string[];
|
|
3863
|
+
/** Minimum participants in scope before a ratio is meaningful. Default `3`. */
|
|
3864
|
+
minClients: number;
|
|
3865
|
+
/** Minimum distinct clients sharing the open issue. Default `3`. */
|
|
3866
|
+
minAffectedClients: number;
|
|
3867
|
+
/** Fraction of participants that must share it. Default `0.5`. */
|
|
3868
|
+
affectedRatioThreshold: number;
|
|
3869
|
+
/**
|
|
3870
|
+
* When the onsets of a qualifying cohort fall within this span (ms), the finding is escalated to
|
|
3871
|
+
* `ISSUE_ONSET_BURST` — they didn't just overlap, they started together. Default `2_000`.
|
|
3872
|
+
*/
|
|
3873
|
+
onsetBurstWindowInMs: number;
|
|
3874
|
+
/** Re-arm time (ms) per issue type. Default `60_000`. */
|
|
3875
|
+
cooldownMs: number;
|
|
3876
|
+
/** Forwarded to the {@link IssueRegistry} (stale-issue expiry). */
|
|
3877
|
+
registry?: Partial<IssueRegistryConfig>;
|
|
3878
|
+
};
|
|
3879
|
+
/**
|
|
3880
|
+
* Raises a finding when **several participants have the same issue open simultaneously**.
|
|
3881
|
+
*
|
|
3882
|
+
* This is the generic replacement for a family of symptom-specific detectors. The client already
|
|
3883
|
+
* decides *what* is wrong for itself — `congestion`, `ice-disconnected`, `audio-concealment`,
|
|
3884
|
+
* `video-decoder-overloaded`, and so on — with detectors that have hysteresis and multi-signal
|
|
3885
|
+
* confirmation behind them. Re-deriving those verdicts from raw counters server-side would be
|
|
3886
|
+
* strictly worse. What the server uniquely knows is *how many other participants are in the same
|
|
3887
|
+
* state right now*, which is exactly the difference between "one person's Wi-Fi" and "our problem".
|
|
3888
|
+
*
|
|
3889
|
+
* Concurrency is judged from the **active issue set** (open interval per key), not from a sliding
|
|
3890
|
+
* window of recent reports. That distinction matters: a window has to guess whether a symptom is
|
|
3891
|
+
* still happening, whereas an interval is closed by the client when the episode actually ends
|
|
3892
|
+
* (client-monitor-js >= 4.6.0 ships the `<type>-resolved` companion for this purpose).
|
|
3893
|
+
*
|
|
3894
|
+
* When the onsets also cluster inside `onsetBurstWindowInMs`, the finding is escalated to
|
|
3895
|
+
* `ISSUE_ONSET_BURST`: participants degrading *together within a couple of seconds* is far more
|
|
3896
|
+
* likely to be a deploy, a TURN failover or a link flap than a coincidence. Onsets are compared on
|
|
3897
|
+
* the **observer clock** (`observedAt`), never on client clocks, because clock skew between
|
|
3898
|
+
* machines would otherwise masquerade as a synchronized event.
|
|
3899
|
+
*
|
|
3900
|
+
* Works at both scopes — pass an `ObservedCall` for "this meeting", or the `Observer` for
|
|
3901
|
+
* "everything this SFU is serving".
|
|
3902
|
+
*/
|
|
3903
|
+
declare class ConcurrentIssueDetector implements Detector {
|
|
3904
|
+
private readonly _scope;
|
|
3905
|
+
readonly name = "concurrent-issue-detector";
|
|
3906
|
+
private readonly _config;
|
|
3907
|
+
private readonly _registry;
|
|
3908
|
+
private readonly _lastRaisedAt;
|
|
3909
|
+
private readonly _isObserverScope;
|
|
3910
|
+
/** The cohorts that qualified on the most recent `update()`. */
|
|
3911
|
+
lastCohorts: IssueCohort[];
|
|
3912
|
+
constructor(_scope: ObservedCall | Observer, config?: Partial<ConcurrentIssueDetectorConfig>);
|
|
3913
|
+
update(): void;
|
|
3914
|
+
close(): void;
|
|
3915
|
+
private _raise;
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3918
|
+
declare const IssueFanOutTypes: {
|
|
3919
|
+
/** Most receivers of one published track have the same issue open → the fault follows the source. */
|
|
3920
|
+
readonly publishedTrackIssueFanOut: "PUBLISHED_TRACK_ISSUE_FAN_OUT";
|
|
3921
|
+
/** Exactly one receiver of a track has it → that receiver's own problem. */
|
|
3922
|
+
readonly singleReceiverIssue: "SINGLE_RECEIVER_ISSUE";
|
|
3923
|
+
};
|
|
3924
|
+
type IssueFanOutDetectorConfig = {
|
|
3925
|
+
/** Only consider these issue types. Empty (default) = every type the receivers report. */
|
|
3926
|
+
issueTypes: string[];
|
|
3927
|
+
/** Minimum receivers of the track before a ratio is meaningful. Default `3`. */
|
|
3928
|
+
minReceivers: number;
|
|
3929
|
+
/** Fraction of a track's receivers that must share the issue. Default `0.6`. */
|
|
3930
|
+
affectedRatioThreshold: number;
|
|
3931
|
+
/** Also report the "only one receiver is affected" case. Default `true`. */
|
|
3932
|
+
reportSingleReceiver: boolean;
|
|
3933
|
+
/** Re-arm time (ms) per (track, issue type). Default `60_000`. */
|
|
3934
|
+
cooldownMs: number;
|
|
3935
|
+
registry?: Partial<IssueRegistryConfig>;
|
|
3936
|
+
};
|
|
3937
|
+
/**
|
|
3938
|
+
* Attributes **client-reported issues to the published track they are about**, then asks how far
|
|
3939
|
+
* the problem fans out across that track's receivers.
|
|
3940
|
+
*
|
|
3941
|
+
* The join is what makes this possible: a receiver-side issue payload carries `trackId` (the client
|
|
3942
|
+
* detectors report it for every track-scoped issue), the observer resolves that to an inbound track,
|
|
3943
|
+
* and `RemoteTrackResolver` links the inbound track to the `remoteOutboundTrack` that published it.
|
|
3944
|
+
* With the whole subscriber set of one source in hand, the verdict is straightforward and is the
|
|
3945
|
+
* single most useful thing a server can say:
|
|
3946
|
+
*
|
|
3947
|
+
* - **most receivers of Alice's track are affected** → the fault is on Alice's path — her uplink, the
|
|
3948
|
+
* SFU's ingress, or its forwarding of that stream. Corroborated by whether the publisher's own
|
|
3949
|
+
* egress looks healthy.
|
|
3950
|
+
* - **one receiver of Alice's track is affected** → that receiver's downlink. Nothing to do with
|
|
3951
|
+
* Alice, even though the symptom is reported against her stream.
|
|
3952
|
+
*
|
|
3953
|
+
* Note this is deliberately generic over the issue vocabulary: `freezed-video-track`,
|
|
3954
|
+
* `keyframe-storm`, `audio-concealment`, `video-decoder-overloaded`, `stuck-decoder` and anything a
|
|
3955
|
+
* custom client detector invents all fan out the same way, so one mechanism replaces a family of
|
|
3956
|
+
* symptom-specific detectors.
|
|
3957
|
+
*/
|
|
3958
|
+
declare class IssueFanOutDetector implements Detector {
|
|
3959
|
+
private readonly _call;
|
|
3960
|
+
readonly name = "issue-fan-out-detector";
|
|
3961
|
+
private readonly _config;
|
|
3962
|
+
private readonly _registry;
|
|
3963
|
+
private readonly _aggregator;
|
|
3964
|
+
private readonly _lastRaisedAt;
|
|
3965
|
+
constructor(_call: ObservedCall, config?: Partial<IssueFanOutDetectorConfig>);
|
|
3966
|
+
update(): void;
|
|
3967
|
+
close(): void;
|
|
3968
|
+
/** Exposed for tests/dashboards: the distributions this detector reasons over. */
|
|
3969
|
+
distributionsOf(): ObservedOutboundTrack[];
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3972
|
+
declare const WorstReceiverContagionTypes: {
|
|
3973
|
+
/**
|
|
3974
|
+
* A publisher's sending bitrate is tracking its **worst** receiver — one bad downlink is
|
|
3975
|
+
* dragging the quality everyone else gets.
|
|
3976
|
+
*/
|
|
3977
|
+
readonly worstReceiverContagion: "WORST_RECEIVER_CONTAGION";
|
|
3978
|
+
};
|
|
3979
|
+
type WorstReceiverContagionDetectorConfig = {
|
|
3980
|
+
/** Minimum receivers before the comparison means anything. Default `3`. */
|
|
3981
|
+
minReceivers: number;
|
|
3982
|
+
/** Observation window (ms) the correlation is computed over. Default `30_000`. */
|
|
3983
|
+
windowMs: number;
|
|
3984
|
+
/** Minimum samples inside the window before judging. Default `4`. */
|
|
3985
|
+
minSamples: number;
|
|
3986
|
+
/**
|
|
3987
|
+
* How closely the publisher's bitrate must track the worst receiver's, relative to the spread
|
|
3988
|
+
* between the worst and the median receiver. Default `0.75`.
|
|
3989
|
+
*/
|
|
3990
|
+
trackingRatioThreshold: number;
|
|
3991
|
+
/**
|
|
3992
|
+
* The worst receiver must be this much worse than the median receiver before the situation even
|
|
3993
|
+
* counts as "one bad apple" (0..1 of the median). Default `0.5` — i.e. at most half.
|
|
3994
|
+
*/
|
|
3995
|
+
outlierRatioThreshold: number;
|
|
3996
|
+
/** Re-arm time (ms) per track. Default `120_000`. */
|
|
3997
|
+
cooldownMs: number;
|
|
3998
|
+
};
|
|
3999
|
+
/**
|
|
4000
|
+
* Detects the classic SFU misconfiguration: **the sender adapting to the worst receiver**.
|
|
4001
|
+
*
|
|
4002
|
+
* In a correctly built SFU the RTCP feedback loop is *terminated* at the server — each receiver's
|
|
4003
|
+
* reports drive what that receiver is sent, and the publisher encodes for the server, not for the
|
|
4004
|
+
* unluckiest participant. When the loop is instead relayed end to end, the publisher's bandwidth
|
|
4005
|
+
* estimate collapses to the minimum across all receivers, so a single participant on a bad 3G link
|
|
4006
|
+
* silently downgrades the stream *everyone* sees. Simulcast exists precisely to prevent this
|
|
4007
|
+
* "lowest common denominator" outcome, and its absence (or an SFU that forwards RR/REMB verbatim)
|
|
4008
|
+
* reproduces it.
|
|
4009
|
+
*
|
|
4010
|
+
* The signature is a correlation, not a threshold, so it is judged over a window: the publisher's
|
|
4011
|
+
* outbound bitrate moving in lockstep with the *worst* receiver's inbound bitrate, while the median
|
|
4012
|
+
* receiver has ample headroom. A publisher that drops because of its own uplink or CPU shows no such
|
|
4013
|
+
* relationship — every receiver falls together and there is no outlier to track.
|
|
4014
|
+
*
|
|
4015
|
+
* This is arguably the most valuable thing an observer can detect, because the damage is invisible
|
|
4016
|
+
* from every individual endpoint: the publisher sees "my bitrate went down", each healthy receiver
|
|
4017
|
+
* sees "my video got worse", and nobody can see the causal link except the server.
|
|
4018
|
+
*/
|
|
4019
|
+
declare class WorstReceiverContagionDetector implements Detector {
|
|
4020
|
+
private readonly _call;
|
|
4021
|
+
readonly name = "worst-receiver-contagion-detector";
|
|
4022
|
+
private readonly _config;
|
|
4023
|
+
private readonly _aggregator;
|
|
4024
|
+
private readonly _windows;
|
|
4025
|
+
private readonly _lastRaisedAt;
|
|
4026
|
+
constructor(_call: ObservedCall, config?: Partial<WorstReceiverContagionDetectorConfig>);
|
|
4027
|
+
update(): void;
|
|
4028
|
+
close(): void;
|
|
4029
|
+
private _windowOf;
|
|
4030
|
+
}
|
|
4031
|
+
|
|
4032
|
+
declare const TrackDeliveryMismatchTypes: {
|
|
4033
|
+
/**
|
|
4034
|
+
* The source is sending, but **none** of its subscribers are receiving → the media is being lost
|
|
4035
|
+
* between the publisher and the receivers. In an SFU that means the forwarding path.
|
|
4036
|
+
*/
|
|
4037
|
+
readonly publishedTrackNotDelivered: "PUBLISHED_TRACK_NOT_DELIVERED";
|
|
4038
|
+
/**
|
|
4039
|
+
* The source is sending and most subscribers are fine, but **some** are dry → those consumers are
|
|
4040
|
+
* broken individually (in mediasoup, the usual mitigation is recreating the consumer).
|
|
4041
|
+
*/
|
|
4042
|
+
readonly receiverTrackNotDelivered: "RECEIVER_TRACK_NOT_DELIVERED";
|
|
4043
|
+
/**
|
|
4044
|
+
* The source itself stopped producing, so its subscribers being dry is expected and **not** an
|
|
4045
|
+
* SFU fault. Reported so the other two verdicts can be trusted as *not* being this.
|
|
4046
|
+
*/
|
|
4047
|
+
readonly publisherTrackDry: "PUBLISHER_TRACK_DRY";
|
|
4048
|
+
};
|
|
4049
|
+
type TrackDeliveryMismatchDetectorConfig = {
|
|
4050
|
+
/** The receiver-side issue type meaning "no media arriving". Default `'dry-inbound-track'`. */
|
|
4051
|
+
dryInboundIssueType: string;
|
|
4052
|
+
/** The publisher-side issue type meaning "not producing". Default `'dry-outbound-track'`. */
|
|
4053
|
+
dryOutboundIssueType: string;
|
|
4054
|
+
/** Minimum subscribers before "all of them" means anything. Default `2`. */
|
|
4055
|
+
minReceivers: number;
|
|
4056
|
+
/** Fraction of subscribers that must be dry to call it a whole-track delivery failure. Default `1`. */
|
|
4057
|
+
allReceiversRatio: number;
|
|
4058
|
+
/** Re-arm time (ms) per (track, verdict). Default `60_000`. */
|
|
4059
|
+
cooldownMs: number;
|
|
4060
|
+
registry?: Partial<IssueRegistryConfig>;
|
|
4061
|
+
};
|
|
4062
|
+
/**
|
|
4063
|
+
* Answers **"is the media actually getting through?"** by joining the two ends of a published track.
|
|
4064
|
+
*
|
|
4065
|
+
* A dry track is the clearest possible symptom — no bytes are arriving — but on its own it is
|
|
4066
|
+
* ambiguous, and the ambiguity is precisely what a single endpoint cannot resolve. A receiver seeing
|
|
4067
|
+
* silence cannot tell whether the camera was switched off, the SFU stopped forwarding, or its own
|
|
4068
|
+
* consumer wedged. All three look identical from the browser.
|
|
4069
|
+
*
|
|
4070
|
+
* With the publisher↔subscriber links this becomes a three-way decision:
|
|
4071
|
+
*
|
|
4072
|
+
* | publisher | subscribers | verdict |
|
|
4073
|
+
* |---|---|---|
|
|
4074
|
+
* | sending | **all** dry | `PUBLISHED_TRACK_NOT_DELIVERED` — the SFU/forwarding path |
|
|
4075
|
+
* | sending | **some** dry | `RECEIVER_TRACK_NOT_DELIVERED` — those consumers (recreate them) |
|
|
4076
|
+
* | dry | any dry | `PUBLISHER_TRACK_DRY` — the source stopped; not an SFU fault |
|
|
4077
|
+
*
|
|
4078
|
+
* The publisher side is judged from **both** signals available: its own `dry-outbound-track` issue
|
|
4079
|
+
* when the client reports one, and — as the fallback, and the corroboration when it does not — the
|
|
4080
|
+
* observed outbound RTP (`deltaPacketsSent`). That combination is what makes the first row
|
|
4081
|
+
* trustworthy: the server can state that packets demonstrably left the publisher during the same
|
|
4082
|
+
* interval in which every receiver got nothing.
|
|
4083
|
+
*
|
|
4084
|
+
* This is the "SFU forwarding mismatch" check, and notably it needs **no** mediasoup instrumentation
|
|
4085
|
+
* — the client's own dry-track verdicts plus the resolver links are sufficient.
|
|
4086
|
+
*/
|
|
4087
|
+
declare class TrackDeliveryMismatchDetector implements Detector {
|
|
4088
|
+
private readonly _call;
|
|
4089
|
+
readonly name = "track-delivery-mismatch-detector";
|
|
4090
|
+
private readonly _config;
|
|
4091
|
+
private readonly _registry;
|
|
4092
|
+
private readonly _aggregator;
|
|
4093
|
+
private readonly _lastRaisedAt;
|
|
4094
|
+
constructor(_call: ObservedCall, config?: Partial<TrackDeliveryMismatchDetectorConfig>);
|
|
4095
|
+
update(): void;
|
|
4096
|
+
close(): void;
|
|
4097
|
+
}
|
|
4098
|
+
|
|
4099
|
+
declare const UnconsumedTrackTypes: {
|
|
4100
|
+
/** A track is being published to the SFU that nobody is subscribed to — pure wasted uplink. */
|
|
4101
|
+
readonly unconsumedPublishedTrack: "UNCONSUMED_PUBLISHED_TRACK";
|
|
4102
|
+
};
|
|
4103
|
+
type UnconsumedTrackDetectorConfig = {
|
|
4104
|
+
/** How long a track must stay unconsumed while sending before reporting (ms). Default `30_000`. */
|
|
4105
|
+
minUnconsumedDurationInMs: number;
|
|
4106
|
+
/** Ignore tracks below this bitrate — a trickle isn't worth an alert (bps). Default `50_000`. */
|
|
4107
|
+
minBitrate: number;
|
|
4108
|
+
/** Re-arm time (ms) per track. Default `300_000`. */
|
|
4109
|
+
cooldownMs: number;
|
|
4110
|
+
};
|
|
4111
|
+
/**
|
|
4112
|
+
* Finds tracks that are **published but consumed by nobody** — uplink and SFU ingress spent on media
|
|
4113
|
+
* that is never forwarded anywhere.
|
|
4114
|
+
*
|
|
4115
|
+
* This is the one detector that reads the resolver's *silence* as the signal: an outbound track with
|
|
4116
|
+
* an empty `remoteInboundTracks` set, still pushing packets. The usual causes are a participant
|
|
4117
|
+
* publishing while everyone has them hidden or muted-in-UI, a simulcast layer no viewer's bandwidth
|
|
4118
|
+
* ever selects, or an application that forgot to stop a track after the last subscriber left.
|
|
4119
|
+
*
|
|
4120
|
+
* It is deliberately slow to fire: `minUnconsumedDurationInMs` must elapse with the track still
|
|
4121
|
+
* sending, because a brief gap between publishing and the first subscription is completely normal at
|
|
4122
|
+
* join time.
|
|
4123
|
+
*
|
|
4124
|
+
* ### Careful: this detector is only sound with a resolver
|
|
4125
|
+
*
|
|
4126
|
+
* "No subscribers" and "no resolver configured" produce the identical observation — an empty link
|
|
4127
|
+
* set. Without a `RemoteTrackResolver` this would report *every* published track in the call as
|
|
4128
|
+
* unconsumed, so it checks `call.remoteTrackResolver` at runtime and does nothing without one.
|
|
4129
|
+
*/
|
|
4130
|
+
declare class UnconsumedTrackDetector implements Detector {
|
|
4131
|
+
private readonly _call;
|
|
4132
|
+
readonly name = "unconsumed-track-detector";
|
|
4133
|
+
private readonly _config;
|
|
4134
|
+
/** trackId -> when it was first seen sending with no subscribers. */
|
|
4135
|
+
private readonly _unconsumedSince;
|
|
4136
|
+
private readonly _lastRaisedAt;
|
|
4137
|
+
constructor(_call: ObservedCall, config?: Partial<UnconsumedTrackDetectorConfig>);
|
|
4138
|
+
update(): void;
|
|
4139
|
+
close(): void;
|
|
4140
|
+
}
|
|
4141
|
+
|
|
2897
4142
|
interface Logger {
|
|
2898
4143
|
trace(...args: any[]): void;
|
|
2899
4144
|
debug(...args: any[]): void;
|
|
@@ -2965,4 +4210,4 @@ declare function createInMemorySink(samples?: ClientSample[]): InMemorySink;
|
|
|
2965
4210
|
declare function createDefaultMediasoupRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
|
|
2966
4211
|
declare function createP2pRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
|
|
2967
4212
|
|
|
2968
|
-
export { type AcceptContext, type AcceptMiddleware, type AcceptMiddlewarePayload, type CallAppDataFactory, type ClientAppDataFactory, type ClientEvent, ClientEventTypes, type ClientIssue, type ClientMetaData, ClientMetaTypes, type ClientSample, ClientSampleSink, type ClientSampleSinkEvents, type ClientSampleSinkFactory, type Detector, Detectors, InMemorySink, JsonlFileSink, type JsonlFileSinkFactoryOptions, type JsonlFileSinkOptions, type Logger, type 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 MediasoupTransportSample, type MediasoupWebRtcTransportSample, type MediasoupWebRtcTransportSampleEventMap, type Middleware, ObservedCall, type ObservedCallScope, ObservedCertificate, ObservedClient, type ObservedClientScope, ObservedCodec, ObservedDataChannel, ObservedIceCandidate, ObservedIceCandidatePair, ObservedIceTransport, ObservedInboundRtp, ObservedInboundTrack, ObservedMediaPlayout, ObservedMediaSource, ObservedMediasoupRouter, type ObservedMediasoupRouterEvents, type ObservedMediasoupRouterScope, type ObservedMediasoupRouterSettings, ObservedOutboundRtp, ObservedOutboundTrack, ObservedPeerConnection, type ObservedPeerConnectionScope, ObservedPeerConnectionTransport, ObservedRemoteInboundRtp, ObservedRemoteOutboundRtp, Observer, type ObserverEventBase, type ObserverEvents, type ObserverLogger, RemoteTrackResolver, type RemoteTrackResolverFactory, type RemoteTrackResolvers, type SampleRejectedReason, type ScoreCalculator, createDefaultMediasoupRemoteTrackResolverFactory, createInMemorySink, createJsonlFileSink, createJsonlFileSinkFactory, createLogger, createP2pRemoteTrackResolverFactory, setObserverLogger };
|
|
4213
|
+
export { type AcceptContext, type AcceptMiddleware, type AcceptMiddlewarePayload, type ActiveClientIssue, AudioImpairmentFanOutDetector, type AudioImpairmentFanOutDetectorConfig, AudioImpairmentFanOutTypes, type CallAppDataFactory, type CallHealth, CallHealthAggregator, CallWideDegradationDetector, type CallWideDegradationDetectorConfig, CallWideDegradationTypes, type ClientAppDataFactory, type ClientEvent, ClientEventTypes, type ClientHealth, type ClientHealthThresholds, type ClientIssue, type ClientMetaData, ClientMetaTypes, type ClientSample, ClientSampleSink, type ClientSampleSinkEvents, type ClientSampleSinkFactory, CommonSourceDegradationDetector, type CommonSourceDegradationDetectorConfig, CommonSourceDegradationTypes, ConcurrentIssueDetector, type ConcurrentIssueDetectorConfig, ConcurrentIssueTypes, type Detector, Detectors, IceDisruptionDetector, type IceDisruptionDetectorConfig, IceDisruptionTypes, InMemorySink, type IssueCohort, IssueFanOutDetector, type IssueFanOutDetectorConfig, IssueFanOutTypes, IssueRegistry, type IssueRegistryConfig, JsonlFileSink, type JsonlFileSinkFactoryOptions, type JsonlFileSinkOptions, type Logger, 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, ObservedCall, type ObservedCallScope, ObservedCertificate, ObservedClient, type ObservedClientScope, ObservedCodec, ObservedDataChannel, ObservedIceCandidate, ObservedIceCandidatePair, ObservedIceTransport, ObservedInboundRtp, ObservedInboundTrack, ObservedMediaPlayout, ObservedMediaSource, ObservedMediasoupRouter, type ObservedMediasoupRouterEvents, type ObservedMediasoupRouterScope, type ObservedMediasoupRouterSettings, ObservedOutboundRtp, ObservedOutboundTrack, ObservedPeerConnection, type ObservedPeerConnectionScope, ObservedPeerConnectionTransport, ObservedRemoteInboundRtp, ObservedRemoteOutboundRtp, type ObservedTrackDistribution, Observer, type ObserverEventBase, type ObserverEvents, type ObserverLogger, PliAndFreezeFanOutDetector, type PliAndFreezeFanOutDetectorConfig, PliAndFreezeFanOutTypes, type PublisherDistributionEntry, RESOLVED_ISSUE_SUFFIX, type ReceiverDistributionEntry, type ReceiverHealthThresholds, RemoteTrackResolver, type RemoteTrackResolverFactory, type RemoteTrackResolvers, type ResolvedClientIssue, type SampleRejectedReason, type ScoreCalculator, SlidingWindow, type SlidingWindowEntry, type StatsSummary, TrackDeliveryMismatchDetector, type TrackDeliveryMismatchDetectorConfig, TrackDeliveryMismatchTypes, TrackDistributionAggregator, type TurnServerHealth, TurnServerHealthDetector, type TurnServerHealthDetectorConfig, TurnServerHealthTypes, UnconsumedTrackDetector, type UnconsumedTrackDetectorConfig, UnconsumedTrackTypes, WorstReceiverContagionDetector, type WorstReceiverContagionDetectorConfig, WorstReceiverContagionTypes, baseIssueType, counterDelta, createDefaultMediasoupRemoteTrackResolverFactory, createInMemorySink, createJsonlFileSink, createJsonlFileSinkFactory, createLogger, createP2pRemoteTrackResolverFactory, defaultClientHealthThresholds, defaultReceiverHealthThresholds, isResolutionEntry, median, parseIssuePayload, percentile, setObserverLogger, summarize };
|