@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.js
CHANGED
|
@@ -31,6 +31,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
33
|
ActiveIssuesRegistry: () => ActiveIssuesRegistry,
|
|
34
|
+
AudioEchoDetector: () => AudioEchoDetector,
|
|
35
|
+
AudioEchoTypes: () => AudioEchoTypes,
|
|
34
36
|
CODEC_MISMATCH_ISSUE: () => CODEC_MISMATCH_ISSUE,
|
|
35
37
|
CallConcurrentIssueDetector: () => CallConcurrentIssueDetector,
|
|
36
38
|
CallConcurrentIssueTypes: () => CallConcurrentIssueTypes,
|
|
@@ -40,9 +42,9 @@ __export(src_exports, {
|
|
|
40
42
|
ClientMetaTypes: () => ClientMetaTypes,
|
|
41
43
|
ClientPopulationIssueDetector: () => ClientPopulationIssueDetector,
|
|
42
44
|
ClientPopulationIssueTypes: () => ClientPopulationIssueTypes,
|
|
43
|
-
ClientSampleSink: () => ClientSampleSink,
|
|
44
45
|
CodecConsistencyValidator: () => CodecConsistencyValidator,
|
|
45
46
|
Detectors: () => Detectors,
|
|
47
|
+
ExtensionStatsRegistry: () => ExtensionStatsRegistry,
|
|
46
48
|
GEOHASH_CELL_SIZES: () => GEOHASH_CELL_SIZES,
|
|
47
49
|
InMemorySink: () => InMemorySink,
|
|
48
50
|
IssueFanOutDetector: () => IssueFanOutDetector,
|
|
@@ -78,10 +80,15 @@ __export(src_exports, {
|
|
|
78
80
|
PublisherFaultCorroborationDetector: () => PublisherFaultCorroborationDetector,
|
|
79
81
|
PublisherFaultTypes: () => PublisherFaultTypes,
|
|
80
82
|
RESOLVED_ISSUE_SUFFIX: () => RESOLVED_ISSUE_SUFFIX,
|
|
83
|
+
RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS: () => RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS,
|
|
84
|
+
RTT_HISTOGRAM_NUMBER_OF_BUCKETS: () => RTT_HISTOGRAM_NUMBER_OF_BUCKETS,
|
|
81
85
|
RemoteTrackResolver: () => RemoteTrackResolver,
|
|
82
86
|
RemoteTrackResolverValidator: () => RemoteTrackResolverValidator,
|
|
87
|
+
SCORE_HISTOGRAM_BUCKET_WIDTH: () => SCORE_HISTOGRAM_BUCKET_WIDTH,
|
|
88
|
+
SCORE_HISTOGRAM_NUMBER_OF_BUCKETS: () => SCORE_HISTOGRAM_NUMBER_OF_BUCKETS,
|
|
83
89
|
SfuCongestionDetector: () => SfuCongestionDetector,
|
|
84
90
|
SimulcastReceiverValidator: () => SimulcastReceiverValidator,
|
|
91
|
+
SinkRegistry: () => SinkRegistry,
|
|
85
92
|
SlidingWindow: () => SlidingWindow,
|
|
86
93
|
TrackDeliveryMismatchDetector: () => TrackDeliveryMismatchDetector,
|
|
87
94
|
TrackDeliveryMismatchTypes: () => TrackDeliveryMismatchTypes,
|
|
@@ -102,7 +109,6 @@ __export(src_exports, {
|
|
|
102
109
|
createDefaultMediasoupRemoteTrackResolverFactory: () => createDefaultMediasoupRemoteTrackResolverFactory,
|
|
103
110
|
createInMemorySink: () => createInMemorySink,
|
|
104
111
|
createJsonlFileSink: () => createJsonlFileSink,
|
|
105
|
-
createJsonlFileSinkFactory: () => createJsonlFileSinkFactory,
|
|
106
112
|
createLogger: () => createLogger,
|
|
107
113
|
createP2pRemoteTrackResolverFactory: () => createP2pRemoteTrackResolverFactory,
|
|
108
114
|
defaultCallSummaryConfig: () => defaultCallSummaryConfig,
|
|
@@ -117,6 +123,7 @@ __export(src_exports, {
|
|
|
117
123
|
medianAbsoluteDeviation: () => medianAbsoluteDeviation,
|
|
118
124
|
pageHinkley: () => pageHinkley,
|
|
119
125
|
percentile: () => percentile,
|
|
126
|
+
percentileOfHistogram: () => percentileOfHistogram,
|
|
120
127
|
percentileOfSorted: () => percentileOfSorted,
|
|
121
128
|
robustZScore: () => robustZScore,
|
|
122
129
|
schemaVersion: () => schemaVersion,
|
|
@@ -164,18 +171,189 @@ function createLogger(moduleName) {
|
|
|
164
171
|
}
|
|
165
172
|
}();
|
|
166
173
|
}
|
|
167
|
-
function setObserverLogger(
|
|
168
|
-
mainLogger =
|
|
174
|
+
function setObserverLogger(logger13) {
|
|
175
|
+
mainLogger = logger13;
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
// src/ObservedCall.ts
|
|
172
|
-
var
|
|
179
|
+
var import_node_events4 = require("events");
|
|
180
|
+
|
|
181
|
+
// src/sinks/SinkRegistry.ts
|
|
182
|
+
var import_uuid = require("uuid");
|
|
183
|
+
var logger = createLogger("SinkRegistry");
|
|
184
|
+
var SinkRegistry = class {
|
|
185
|
+
constructor(parent) {
|
|
186
|
+
this.parent = parent;
|
|
187
|
+
}
|
|
188
|
+
parent;
|
|
189
|
+
// This registry's sinks and its descendants'.
|
|
190
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
191
|
+
_sinks = /* @__PURE__ */ new Map();
|
|
192
|
+
/** Number of open sinks in this registry and its descendants. */
|
|
193
|
+
get size() {
|
|
194
|
+
return this._sinks.size;
|
|
195
|
+
}
|
|
196
|
+
keys() {
|
|
197
|
+
return this._sinks.keys();
|
|
198
|
+
}
|
|
199
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
200
|
+
values() {
|
|
201
|
+
return this._sinks.values();
|
|
202
|
+
}
|
|
203
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
204
|
+
get(id) {
|
|
205
|
+
return this._sinks.get(id);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Construct `new SinkClass(settings)` and register it. The id (`settings.id`, generated when
|
|
209
|
+
* omitted) is checked first, so nothing is constructed — no file is opened — when it is taken:
|
|
210
|
+
* `undefined` is returned and a warning logged.
|
|
211
|
+
*/
|
|
212
|
+
create(SinkClass, settings) {
|
|
213
|
+
const lineage = this._lineage();
|
|
214
|
+
const id = settings.id ?? (0, import_uuid.v4)();
|
|
215
|
+
if (lineage[lineage.length - 1]._sinks.has(id)) {
|
|
216
|
+
logger.warn("Sink id %s is already in use; sink ids must be unique per observer. No sink was created", id);
|
|
217
|
+
return void 0;
|
|
218
|
+
}
|
|
219
|
+
const sink = new SinkClass({ ...settings, id });
|
|
220
|
+
const onError = (err) => logger.warn("Sink %s failed: %o", id, err);
|
|
221
|
+
sink.on("error", onError);
|
|
222
|
+
sink.once("close", () => sink.off("error", onError));
|
|
223
|
+
sink.once("ended", () => {
|
|
224
|
+
for (const registry of lineage) {
|
|
225
|
+
if (registry._sinks.get(id) === sink) registry._sinks.delete(id);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
for (const registry of lineage) registry._sinks.set(id, sink);
|
|
229
|
+
return sink;
|
|
230
|
+
}
|
|
231
|
+
/** Write `record` to the sink `id`. `false` when there is no such sink, or on backpressure. */
|
|
232
|
+
write(id, record) {
|
|
233
|
+
const sink = this._sinks.get(id);
|
|
234
|
+
if (!sink) {
|
|
235
|
+
logger.debug("No sink %s; record dropped", id);
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
return sink.write(record);
|
|
239
|
+
}
|
|
240
|
+
/** End every sink of this registry and of its descendants. */
|
|
241
|
+
clear() {
|
|
242
|
+
for (const sink of [...this._sinks.values()]) {
|
|
243
|
+
try {
|
|
244
|
+
sink.end();
|
|
245
|
+
} catch (err) {
|
|
246
|
+
logger.warn("Sink %s failed to end: %o", sink.id, err);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/** This registry and its ancestors, up to the root. */
|
|
251
|
+
_lineage() {
|
|
252
|
+
return [this, ...this.parent?._lineage() ?? []];
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// src/sinks/ExtensionStatsRegistry.ts
|
|
257
|
+
var logger2 = createLogger("ExtensionStats");
|
|
258
|
+
var ExtensionStatsRegistry = class {
|
|
259
|
+
constructor(id, _sinks) {
|
|
260
|
+
this.id = id;
|
|
261
|
+
this._sinks = _sinks;
|
|
262
|
+
}
|
|
263
|
+
id;
|
|
264
|
+
_sinks;
|
|
265
|
+
/** The last data accepted for each extension stats id. */
|
|
266
|
+
values = /* @__PURE__ */ new Map();
|
|
267
|
+
closed = false;
|
|
268
|
+
/**
|
|
269
|
+
* Refuse further `create` / `accept`, and end the extension stats' sinks (the sinks whose id is
|
|
270
|
+
* an extension stats id). Other sinks of the owner, e.g. client sample sinks, are left to the
|
|
271
|
+
* owner's `sinks.clear()`. Called by the owner when it closes.
|
|
272
|
+
*/
|
|
273
|
+
close() {
|
|
274
|
+
if (this.closed) return;
|
|
275
|
+
this.closed = true;
|
|
276
|
+
for (const id of this.values.keys()) {
|
|
277
|
+
this._sinks.get(id)?.end();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
create(settings) {
|
|
281
|
+
if (this.closed) {
|
|
282
|
+
logger2.warn("Attempted to create extension stats %s on closed %s", settings.id, this.id);
|
|
283
|
+
return void 0;
|
|
284
|
+
}
|
|
285
|
+
if (this.values.has(settings.id)) {
|
|
286
|
+
logger2.warn("Extension stats with id %s already exists on %s", settings.id, this.id);
|
|
287
|
+
return void 0;
|
|
288
|
+
}
|
|
289
|
+
this.values.set(settings.id, void 0);
|
|
290
|
+
if (!settings.createSink) return void 0;
|
|
291
|
+
const [SinkClass, sinkSettings] = settings.createSink;
|
|
292
|
+
this._sinks.get(settings.id)?.end();
|
|
293
|
+
return this._sinks.create(SinkClass, { ...sinkSettings, id: settings.id });
|
|
294
|
+
}
|
|
295
|
+
accept(id, data) {
|
|
296
|
+
if (this.closed) {
|
|
297
|
+
logger2.debug("Extension stats %s dropped: %s is closed", id, this.id);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
this.values.set(id, data);
|
|
301
|
+
try {
|
|
302
|
+
this._sinks.get(id)?.write(data);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
logger2.warn("Failed to write extension stats %s to sink: %o", id, err);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
get(id) {
|
|
308
|
+
return this.values.get(id);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// src/sinks/JsonlFileSink.ts
|
|
313
|
+
var fs = __toESM(require("fs"));
|
|
314
|
+
var import_node_events = require("events");
|
|
315
|
+
var JsonlFileSink = class extends import_node_events.EventEmitter {
|
|
316
|
+
id;
|
|
317
|
+
/** The file this sink writes to. Read it (e.g. in a `close` handler) to upload/move the file. */
|
|
318
|
+
path;
|
|
319
|
+
_stream;
|
|
320
|
+
_serializeSample;
|
|
321
|
+
_ended = false;
|
|
322
|
+
constructor(options) {
|
|
323
|
+
super();
|
|
324
|
+
this.id = options.id ?? options.path;
|
|
325
|
+
this.path = options.path;
|
|
326
|
+
this._stream = fs.createWriteStream(this.path, { flags: "a" });
|
|
327
|
+
this._stream.on("close", () => this.emit("close"));
|
|
328
|
+
this._stream.on("finish", () => this.emit("finish"));
|
|
329
|
+
this._stream.on("drain", () => this.emit("drain"));
|
|
330
|
+
this._stream.on("error", (err) => this.emit("error", err));
|
|
331
|
+
this._serializeSample = options.serializeSample ?? ((record) => JSON.stringify(record));
|
|
332
|
+
}
|
|
333
|
+
get ended() {
|
|
334
|
+
return this._ended;
|
|
335
|
+
}
|
|
336
|
+
write(record) {
|
|
337
|
+
if (this._ended) return false;
|
|
338
|
+
return this._stream.write(`${this._serializeSample(record)}
|
|
339
|
+
`);
|
|
340
|
+
}
|
|
341
|
+
end() {
|
|
342
|
+
if (this._ended) return;
|
|
343
|
+
this._ended = true;
|
|
344
|
+
this.emit("ended");
|
|
345
|
+
this._stream.end();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
function createJsonlFileSink(options) {
|
|
349
|
+
return new JsonlFileSink(options);
|
|
350
|
+
}
|
|
173
351
|
|
|
174
352
|
// src/ObservedClient.ts
|
|
175
|
-
var
|
|
353
|
+
var import_node_events3 = require("events");
|
|
176
354
|
|
|
177
355
|
// src/ObservedPeerConnection.ts
|
|
178
|
-
var
|
|
356
|
+
var import_node_events2 = require("events");
|
|
179
357
|
|
|
180
358
|
// src/utils/stats.ts
|
|
181
359
|
function percentile(values, p) {
|
|
@@ -230,6 +408,10 @@ function counterDelta(previous, current) {
|
|
|
230
408
|
if (current < previous) return 0;
|
|
231
409
|
return current - previous;
|
|
232
410
|
}
|
|
411
|
+
function rmsAudioLevel(deltaTotalAudioEnergy, deltaTotalSamplesDuration) {
|
|
412
|
+
if (!(0 < deltaTotalSamplesDuration)) return void 0;
|
|
413
|
+
return Math.sqrt(Math.max(0, deltaTotalAudioEnergy) / deltaTotalSamplesDuration);
|
|
414
|
+
}
|
|
233
415
|
function correlation(xs, ys) {
|
|
234
416
|
const n = Math.min(xs.length, ys.length);
|
|
235
417
|
if (n < 2) return 0;
|
|
@@ -262,8 +444,8 @@ function pageHinkley(values, delta = 0, lambda = 50) {
|
|
|
262
444
|
let runningMinimum = 0;
|
|
263
445
|
for (let i = 0; i < values.length; i++) {
|
|
264
446
|
sum += values[i];
|
|
265
|
-
const
|
|
266
|
-
cumulative += values[i] -
|
|
447
|
+
const mean2 = sum / (i + 1);
|
|
448
|
+
cumulative += values[i] - mean2 - delta;
|
|
267
449
|
if (cumulative < runningMinimum) runningMinimum = cumulative;
|
|
268
450
|
const ph = cumulative - runningMinimum;
|
|
269
451
|
statistic.push(ph);
|
|
@@ -315,6 +497,25 @@ function erf(x) {
|
|
|
315
497
|
const y = 1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-(ax * ax));
|
|
316
498
|
return sign * y;
|
|
317
499
|
}
|
|
500
|
+
function percentileOfHistogram(histogram, bucketWidth, p, options = {}) {
|
|
501
|
+
let total = 0;
|
|
502
|
+
for (const count of histogram) total += count;
|
|
503
|
+
if (total < 1) return void 0;
|
|
504
|
+
const rank = Math.min(Math.max(p, 0), 1) * total;
|
|
505
|
+
let cumulative = 0;
|
|
506
|
+
for (let index = 0; index < histogram.length; ++index) {
|
|
507
|
+
const count = histogram[index];
|
|
508
|
+
if (count < 1 || cumulative + count < rank) {
|
|
509
|
+
cumulative += count;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (options.openEnded && index === histogram.length - 1 && options.max !== void 0) return options.max;
|
|
513
|
+
const lower = Math.max(index * bucketWidth, options.min ?? -Infinity);
|
|
514
|
+
const upper = Math.min((index + 1) * bucketWidth, options.max ?? Infinity);
|
|
515
|
+
return lower + (rank - cumulative) / count * Math.max(0, upper - lower);
|
|
516
|
+
}
|
|
517
|
+
return options.max;
|
|
518
|
+
}
|
|
318
519
|
|
|
319
520
|
// src/ObservedInboundRtp.ts
|
|
320
521
|
var ObservedInboundRtp = class {
|
|
@@ -425,6 +626,15 @@ var ObservedInboundRtp = class {
|
|
|
425
626
|
deltaFecPacketsReceived = 0;
|
|
426
627
|
deltaFecPacketsDiscarded = 0;
|
|
427
628
|
deltaPausesDuration = 0;
|
|
629
|
+
deltaTotalAudioEnergy = 0;
|
|
630
|
+
deltaTotalSamplesDuration = 0;
|
|
631
|
+
/**
|
|
632
|
+
* RMS level (0..1) of the audio played out from this stream over the tick, from the two deltas
|
|
633
|
+
* above. This is the far end's outgoing level **as this receiver heard it**, which is the only
|
|
634
|
+
* send-side level available for publishers on browsers that do not report `media-source` audio
|
|
635
|
+
* levels (Firefox). `undefined` for video and for a tick with no audio accounted for.
|
|
636
|
+
*/
|
|
637
|
+
rmsAudioLevel;
|
|
428
638
|
/**
|
|
429
639
|
* Mean jitter-buffer delay for the frames/samples emitted in this tick (seconds), derived from
|
|
430
640
|
* the cumulative `jitterBufferDelay` / `jitterBufferEmittedCount` pair — the only correct way to
|
|
@@ -510,6 +720,9 @@ var ObservedInboundRtp = class {
|
|
|
510
720
|
this.jitterBufferDelayInMs = void 0;
|
|
511
721
|
this.concealmentRatio = void 0;
|
|
512
722
|
this.framesDroppedRatio = void 0;
|
|
723
|
+
this.deltaTotalAudioEnergy = 0;
|
|
724
|
+
this.deltaTotalSamplesDuration = 0;
|
|
725
|
+
this.rmsAudioLevel = void 0;
|
|
513
726
|
const elapsedTimeInMs = stats.timestamp - this.timestamp;
|
|
514
727
|
this.counterResetBoundary = this.codecId !== void 0 && stats.codecId !== void 0 && this.codecId !== stats.codecId || this.decoderImplementation !== void 0 && stats.decoderImplementation !== void 0 && this.decoderImplementation !== stats.decoderImplementation;
|
|
515
728
|
if (0 < elapsedTimeInMs && !this.counterResetBoundary) {
|
|
@@ -539,6 +752,9 @@ var ObservedInboundRtp = class {
|
|
|
539
752
|
this.deltaRetransmittedPacketsReceived = counterDelta(this.retransmittedPacketsReceived, stats.retransmittedPacketsReceived);
|
|
540
753
|
this.deltaFecPacketsReceived = counterDelta(this.fecPacketsReceived, stats.fecPacketsReceived);
|
|
541
754
|
this.deltaFecPacketsDiscarded = counterDelta(this.fecPacketsDiscarded, stats.fecPacketsDiscarded);
|
|
755
|
+
this.deltaTotalAudioEnergy = counterDelta(this.totalAudioEnergy, stats.totalAudioEnergy);
|
|
756
|
+
this.deltaTotalSamplesDuration = counterDelta(this.totalSamplesDuration, stats.totalSamplesDuration);
|
|
757
|
+
this.rmsAudioLevel = rmsAudioLevel(this.deltaTotalAudioEnergy, this.deltaTotalSamplesDuration);
|
|
542
758
|
this.bitrate = this.deltaBytesReceived * 8 / elapsedInSeconds;
|
|
543
759
|
const pixels = (stats.frameWidth ?? 0) * (stats.frameHeight ?? 0);
|
|
544
760
|
if (0 < pixels && 0 < this.deltaFramesReceived) {
|
|
@@ -596,6 +812,9 @@ var ObservedInboundRtp = class {
|
|
|
596
812
|
this.jitterBufferEmittedCount = stats.jitterBufferEmittedCount;
|
|
597
813
|
this.jitterBufferMinimumDelay = stats.jitterBufferMinimumDelay;
|
|
598
814
|
this.totalSamplesReceived = stats.totalSamplesReceived;
|
|
815
|
+
this.audioLevel = stats.audioLevel;
|
|
816
|
+
this.totalAudioEnergy = stats.totalAudioEnergy;
|
|
817
|
+
this.totalSamplesDuration = stats.totalSamplesDuration;
|
|
599
818
|
this.concealedSamples = stats.concealedSamples;
|
|
600
819
|
this.silentConcealedSamples = stats.silentConcealedSamples;
|
|
601
820
|
this.concealmentEvents = stats.concealmentEvents;
|
|
@@ -1250,6 +1469,16 @@ var ObservedMediaSource = class {
|
|
|
1250
1469
|
frames;
|
|
1251
1470
|
framesPerSecond;
|
|
1252
1471
|
attachments;
|
|
1472
|
+
/** Increase of `totalAudioEnergy` in this tick (audio only). */
|
|
1473
|
+
deltaTotalAudioEnergy = 0;
|
|
1474
|
+
/** Increase of `totalSamplesDuration` in this tick, in seconds (audio only). */
|
|
1475
|
+
deltaTotalSamplesDuration = 0;
|
|
1476
|
+
/**
|
|
1477
|
+
* RMS level (0..1) of what the capture device delivered over the tick, after the browser's own
|
|
1478
|
+
* processing (AEC, NS, AGC). Chrome and Safari report the inputs; Firefox does not, and there the
|
|
1479
|
+
* publisher's level has to come from its receivers — see `ObservedOutboundTrack.rmsAudioLevel`.
|
|
1480
|
+
*/
|
|
1481
|
+
rmsAudioLevel;
|
|
1253
1482
|
get visited() {
|
|
1254
1483
|
const visited = this._visited;
|
|
1255
1484
|
this._visited = false;
|
|
@@ -1263,6 +1492,14 @@ var ObservedMediaSource = class {
|
|
|
1263
1492
|
}
|
|
1264
1493
|
update(stats) {
|
|
1265
1494
|
this._visited = true;
|
|
1495
|
+
this.deltaTotalAudioEnergy = 0;
|
|
1496
|
+
this.deltaTotalSamplesDuration = 0;
|
|
1497
|
+
this.rmsAudioLevel = void 0;
|
|
1498
|
+
if (this.timestamp < stats.timestamp) {
|
|
1499
|
+
this.deltaTotalAudioEnergy = counterDelta(this.totalAudioEnergy, stats.totalAudioEnergy);
|
|
1500
|
+
this.deltaTotalSamplesDuration = counterDelta(this.totalSamplesDuration, stats.totalSamplesDuration);
|
|
1501
|
+
this.rmsAudioLevel = rmsAudioLevel(this.deltaTotalAudioEnergy, this.deltaTotalSamplesDuration);
|
|
1502
|
+
}
|
|
1266
1503
|
this.timestamp = stats.timestamp;
|
|
1267
1504
|
this.trackIdentifier = stats.trackIdentifier;
|
|
1268
1505
|
this.audioLevel = stats.audioLevel;
|
|
@@ -1661,6 +1898,20 @@ var ObservedOutboundTrack = class {
|
|
|
1661
1898
|
remoteFractionLost;
|
|
1662
1899
|
remoteRttInMs;
|
|
1663
1900
|
qualityLimitationReason;
|
|
1901
|
+
/**
|
|
1902
|
+
* This audio track's RMS level (0..1) over the latest tick — how loud the publisher was sending.
|
|
1903
|
+
* Taken from the publisher's own `media-source` when it reports one (Chrome, Safari), otherwise
|
|
1904
|
+
* the median of what its receivers heard ({@link receivedRmsAudioLevel}), which is what makes it
|
|
1905
|
+
* available for Firefox publishers too. `undefined` for video, or when neither is known.
|
|
1906
|
+
* {@link rmsAudioLevelSource} says which one it is.
|
|
1907
|
+
*/
|
|
1908
|
+
rmsAudioLevel;
|
|
1909
|
+
rmsAudioLevelSource;
|
|
1910
|
+
/**
|
|
1911
|
+
* Median RMS level (0..1) of this track across the receivers that played it out, each over its own
|
|
1912
|
+
* latest tick. Needs a `RemoteTrackResolver`: without links there are no receivers to ask.
|
|
1913
|
+
*/
|
|
1914
|
+
receivedRmsAudioLevel;
|
|
1664
1915
|
get score() {
|
|
1665
1916
|
return this.calculatedScore.value;
|
|
1666
1917
|
}
|
|
@@ -1681,6 +1932,10 @@ var ObservedOutboundTrack = class {
|
|
|
1681
1932
|
getMediaSource() {
|
|
1682
1933
|
return this._mediaSource;
|
|
1683
1934
|
}
|
|
1935
|
+
/** The capture device behind this track, when the client sent `AUDIO_INPUT_DEVICE` metadata for it. */
|
|
1936
|
+
get audioInputDevice() {
|
|
1937
|
+
return this._peerConnection.client.audioInputDevices.get(this.id);
|
|
1938
|
+
}
|
|
1684
1939
|
update(stats) {
|
|
1685
1940
|
this._visited = true;
|
|
1686
1941
|
this.timestamp = stats.timestamp;
|
|
@@ -1712,8 +1967,30 @@ var ObservedOutboundTrack = class {
|
|
|
1712
1967
|
if (this.muted !== true && this.deltaPacketsSent === 0) reasons.push("no-packets-sent");
|
|
1713
1968
|
this.degradedReasons = reasons.length === 0 ? void 0 : reasons;
|
|
1714
1969
|
this.receivingDistribution = this.createReceivingDistribution();
|
|
1970
|
+
this._updateAudioLevel();
|
|
1715
1971
|
this.detectors.update();
|
|
1716
1972
|
}
|
|
1973
|
+
_updateAudioLevel() {
|
|
1974
|
+
this.rmsAudioLevel = void 0;
|
|
1975
|
+
this.rmsAudioLevelSource = void 0;
|
|
1976
|
+
this.receivedRmsAudioLevel = void 0;
|
|
1977
|
+
if (this.kind !== "audio") return;
|
|
1978
|
+
if (0 < this.remoteInboundTracks.size) {
|
|
1979
|
+
const levels = [];
|
|
1980
|
+
for (const inboundTrack of this.remoteInboundTracks) {
|
|
1981
|
+
pushFinite(levels, inboundTrack.getInboundRtp()?.rmsAudioLevel);
|
|
1982
|
+
}
|
|
1983
|
+
this.receivedRmsAudioLevel = median(levels);
|
|
1984
|
+
}
|
|
1985
|
+
const sourceLevel = this._mediaSource?.rmsAudioLevel;
|
|
1986
|
+
if (sourceLevel !== void 0) {
|
|
1987
|
+
this.rmsAudioLevel = sourceLevel;
|
|
1988
|
+
this.rmsAudioLevelSource = "media-source";
|
|
1989
|
+
} else if (this.receivedRmsAudioLevel !== void 0) {
|
|
1990
|
+
this.rmsAudioLevel = this.receivedRmsAudioLevel;
|
|
1991
|
+
this.rmsAudioLevelSource = "receivers";
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1717
1994
|
createReceivingDistribution() {
|
|
1718
1995
|
if (this.remoteInboundTracks.size === 0) return void 0;
|
|
1719
1996
|
let freezeAffected = 0;
|
|
@@ -1769,14 +2046,14 @@ var ObservedOutboundTrack = class {
|
|
|
1769
2046
|
|
|
1770
2047
|
// src/ObservedPeerConnection.ts
|
|
1771
2048
|
var ascending2 = (a, b) => a - b;
|
|
1772
|
-
var
|
|
2049
|
+
var logger3 = createLogger("ObservedPeerConnection");
|
|
1773
2050
|
function publicAddressOfLocalCandidate(candidate) {
|
|
1774
2051
|
if (!candidate) return void 0;
|
|
1775
2052
|
if (candidate.candidateType !== "relay" && isPublicIpAddress(candidate.address)) return candidate.address;
|
|
1776
2053
|
if (isPublicIpAddress(candidate.relatedAddress)) return candidate.relatedAddress;
|
|
1777
2054
|
return void 0;
|
|
1778
2055
|
}
|
|
1779
|
-
var ObservedPeerConnection = class extends
|
|
2056
|
+
var ObservedPeerConnection = class extends import_node_events2.EventEmitter {
|
|
1780
2057
|
constructor(peerConnectionId, client) {
|
|
1781
2058
|
super();
|
|
1782
2059
|
this.peerConnectionId = peerConnectionId;
|
|
@@ -2358,7 +2635,7 @@ var ObservedPeerConnection = class extends import_events.EventEmitter {
|
|
|
2358
2635
|
let observedCertificate = this.observedCertificates.get(stats.id);
|
|
2359
2636
|
if (!observedCertificate) {
|
|
2360
2637
|
if (!stats.timestamp || !stats.id || !stats.fingerprint) {
|
|
2361
|
-
return
|
|
2638
|
+
return logger3.warn(
|
|
2362
2639
|
`ObservedPeerConnection received an invalid CertificateStats (missing timestamp OR id OR fingerprint field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2363
2640
|
stats
|
|
2364
2641
|
);
|
|
@@ -2377,7 +2654,7 @@ var ObservedPeerConnection = class extends import_events.EventEmitter {
|
|
|
2377
2654
|
let observedCodec = this.observedCodecs.get(stats.id);
|
|
2378
2655
|
if (!observedCodec) {
|
|
2379
2656
|
if (!stats.timestamp || !stats.id || !stats.mimeType) {
|
|
2380
|
-
return
|
|
2657
|
+
return logger3.warn(
|
|
2381
2658
|
`ObservedPeerConnection received an invalid CodecStats (missing timestamp OR id OR mimeType field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2382
2659
|
stats
|
|
2383
2660
|
);
|
|
@@ -2396,7 +2673,7 @@ var ObservedPeerConnection = class extends import_events.EventEmitter {
|
|
|
2396
2673
|
let observedDataChannel = this.observedDataChannels.get(stats.id);
|
|
2397
2674
|
if (!observedDataChannel) {
|
|
2398
2675
|
if (!stats.timestamp || !stats.id) {
|
|
2399
|
-
return
|
|
2676
|
+
return logger3.warn(
|
|
2400
2677
|
`ObservedPeerConnection received an invalid DataChannelStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2401
2678
|
stats
|
|
2402
2679
|
);
|
|
@@ -2415,7 +2692,7 @@ var ObservedPeerConnection = class extends import_events.EventEmitter {
|
|
|
2415
2692
|
let observedIceCandidate = this.observedIceCandidates.get(stats.id);
|
|
2416
2693
|
if (!observedIceCandidate) {
|
|
2417
2694
|
if (!stats.timestamp || !stats.id) {
|
|
2418
|
-
return
|
|
2695
|
+
return logger3.warn(
|
|
2419
2696
|
`ObservedPeerConnection received an invalid IceCandidateStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2420
2697
|
stats
|
|
2421
2698
|
);
|
|
@@ -2434,7 +2711,7 @@ var ObservedPeerConnection = class extends import_events.EventEmitter {
|
|
|
2434
2711
|
let observedIceCandidatePair = this.observedIceCandidatesPair.get(stats.id);
|
|
2435
2712
|
if (!observedIceCandidatePair) {
|
|
2436
2713
|
if (!stats.timestamp || !stats.id) {
|
|
2437
|
-
return
|
|
2714
|
+
return logger3.warn(
|
|
2438
2715
|
`ObservedPeerConnection received an invalid IceCandidateStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2439
2716
|
stats
|
|
2440
2717
|
);
|
|
@@ -2453,7 +2730,7 @@ var ObservedPeerConnection = class extends import_events.EventEmitter {
|
|
|
2453
2730
|
let observedIceTransport = this.observedIceTransports.get(stats.id);
|
|
2454
2731
|
if (!observedIceTransport) {
|
|
2455
2732
|
if (!stats.timestamp || !stats.id) {
|
|
2456
|
-
return
|
|
2733
|
+
return logger3.warn(
|
|
2457
2734
|
`ObservedPeerConnection received an invalid IceCandidateStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2458
2735
|
stats
|
|
2459
2736
|
);
|
|
@@ -2472,7 +2749,7 @@ var ObservedPeerConnection = class extends import_events.EventEmitter {
|
|
|
2472
2749
|
let observedInboundRtp = this.observedInboundRtps.get(stats.ssrc);
|
|
2473
2750
|
if (!observedInboundRtp) {
|
|
2474
2751
|
if (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind || !stats.trackIdentifier) {
|
|
2475
|
-
return
|
|
2752
|
+
return logger3.warn(
|
|
2476
2753
|
`ObservedPeerConnection received an invalid InboundRtpStats (missing timestamp OR id OR ssrc OR kind OR trackIdentifier field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2477
2754
|
stats
|
|
2478
2755
|
);
|
|
@@ -2498,7 +2775,7 @@ var ObservedPeerConnection = class extends import_events.EventEmitter {
|
|
|
2498
2775
|
let observedInboundTrack = this.observedInboundTracks.get(stats.id);
|
|
2499
2776
|
if (!observedInboundTrack) {
|
|
2500
2777
|
if (!stats.timestamp || !stats.id || !stats.kind) {
|
|
2501
|
-
return
|
|
2778
|
+
return logger3.warn(
|
|
2502
2779
|
`ObservedPeerConnection received an invalid InboundTrackSample (missing timestamp OR id OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2503
2780
|
stats
|
|
2504
2781
|
);
|
|
@@ -2526,7 +2803,7 @@ var ObservedPeerConnection = class extends import_events.EventEmitter {
|
|
|
2526
2803
|
let observedMediaPlayout = this.observedMediaPlayouts.get(stats.id);
|
|
2527
2804
|
if (!observedMediaPlayout) {
|
|
2528
2805
|
if (!stats.timestamp || !stats.id || !stats.kind) {
|
|
2529
|
-
return
|
|
2806
|
+
return logger3.warn(
|
|
2530
2807
|
`ObservedPeerConnection received an invalid InboundRtpStats (missing timestamp OR id OR kind OR trackIdentifier field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2531
2808
|
stats
|
|
2532
2809
|
);
|
|
@@ -2550,7 +2827,7 @@ var ObservedPeerConnection = class extends import_events.EventEmitter {
|
|
|
2550
2827
|
let observedMediaSource = this.observedMediaSources.get(stats.id);
|
|
2551
2828
|
if (!observedMediaSource) {
|
|
2552
2829
|
if (!stats.timestamp || !stats.id || !stats.kind) {
|
|
2553
|
-
return
|
|
2830
|
+
return logger3.warn(
|
|
2554
2831
|
`ObservedPeerConnection received an invalid InboundRtpStats (missing timestamp OR id OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2555
2832
|
stats
|
|
2556
2833
|
);
|
|
@@ -2574,7 +2851,7 @@ var ObservedPeerConnection = class extends import_events.EventEmitter {
|
|
|
2574
2851
|
let observedOutboundRtp = this.observedOutboundRtps.get(stats.ssrc);
|
|
2575
2852
|
if (!observedOutboundRtp) {
|
|
2576
2853
|
if (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind) {
|
|
2577
|
-
return
|
|
2854
|
+
return logger3.warn(
|
|
2578
2855
|
`ObservedPeerConnection received an invalid OutboundRtpStats (missing timestamp OR id OR ssrc OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2579
2856
|
stats
|
|
2580
2857
|
);
|
|
@@ -2599,7 +2876,7 @@ var ObservedPeerConnection = class extends import_events.EventEmitter {
|
|
|
2599
2876
|
let observedOutboundTrack = this.observedOutboundTracks.get(stats.id);
|
|
2600
2877
|
if (!observedOutboundTrack) {
|
|
2601
2878
|
if (!stats.timestamp || !stats.id || !stats.kind) {
|
|
2602
|
-
return
|
|
2879
|
+
return logger3.warn(
|
|
2603
2880
|
`ObservedPeerConnection received an invalid OutboundTrackSample (missing timestamp OR id OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2604
2881
|
stats
|
|
2605
2882
|
);
|
|
@@ -2627,7 +2904,7 @@ var ObservedPeerConnection = class extends import_events.EventEmitter {
|
|
|
2627
2904
|
let observedPeerConnectionTransport = this.observedPeerConnectionTransports.get(stats.id);
|
|
2628
2905
|
if (!observedPeerConnectionTransport) {
|
|
2629
2906
|
if (!stats.timestamp || !stats.id) {
|
|
2630
|
-
return
|
|
2907
|
+
return logger3.warn(
|
|
2631
2908
|
`ObservedPeerConnection received an invalid PeerConnectionTransportStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2632
2909
|
stats
|
|
2633
2910
|
);
|
|
@@ -2646,7 +2923,7 @@ var ObservedPeerConnection = class extends import_events.EventEmitter {
|
|
|
2646
2923
|
let observedRemoteInboundRtp = this.observedRemoteInboundRtps.get(stats.ssrc);
|
|
2647
2924
|
if (!observedRemoteInboundRtp) {
|
|
2648
2925
|
if (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind) {
|
|
2649
|
-
return
|
|
2926
|
+
return logger3.warn(
|
|
2650
2927
|
`ObservedPeerConnection received an invalid RemoteInboundRtpStats (missing timestamp OR id OR ssrc OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2651
2928
|
stats
|
|
2652
2929
|
);
|
|
@@ -2671,7 +2948,7 @@ var ObservedPeerConnection = class extends import_events.EventEmitter {
|
|
|
2671
2948
|
let observedRemoteOutboundRtp = this.observedRemoteOutboundRtps.get(stats.ssrc);
|
|
2672
2949
|
if (!observedRemoteOutboundRtp) {
|
|
2673
2950
|
if (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind) {
|
|
2674
|
-
return
|
|
2951
|
+
return logger3.warn(
|
|
2675
2952
|
`ObservedPeerConnection received an invalid RemoteOutboundRtpStats (missing timestamp OR id OR ssrc OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2676
2953
|
stats
|
|
2677
2954
|
);
|
|
@@ -2753,6 +3030,7 @@ var ClientMetaTypes = /* @__PURE__ */ ((ClientMetaTypes2) => {
|
|
|
2753
3030
|
ClientMetaTypes2["MEDIA_DEVICES_SUPPORTED_CONSTRAINTS"] = "MEDIA_DEVICES_SUPPORTED_CONSTRAINTS";
|
|
2754
3031
|
ClientMetaTypes2["USER_MEDIA_ERROR"] = "USER_MEDIA_ERROR";
|
|
2755
3032
|
ClientMetaTypes2["LOCAL_SDP"] = "LOCAL_SDP";
|
|
3033
|
+
ClientMetaTypes2["AUDIO_INPUT_DEVICE"] = "AUDIO_INPUT_DEVICE";
|
|
2756
3034
|
ClientMetaTypes2["OPERATION_SYSTEM"] = "OPERATION_SYSTEM";
|
|
2757
3035
|
ClientMetaTypes2["ENGINE"] = "ENGINE";
|
|
2758
3036
|
ClientMetaTypes2["PLATFORM"] = "PLATFORM";
|
|
@@ -2770,31 +3048,27 @@ function baseIssueType(type) {
|
|
|
2770
3048
|
}
|
|
2771
3049
|
|
|
2772
3050
|
// src/ObservedClient.ts
|
|
2773
|
-
var
|
|
2774
|
-
var
|
|
3051
|
+
var logger4 = createLogger("ObservedClient");
|
|
3052
|
+
var RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS = 20;
|
|
3053
|
+
var RTT_HISTOGRAM_NUMBER_OF_BUCKETS = 50;
|
|
3054
|
+
var SCORE_HISTOGRAM_BUCKET_WIDTH = 1;
|
|
3055
|
+
var SCORE_HISTOGRAM_NUMBER_OF_BUCKETS = 5;
|
|
3056
|
+
var ObservedClient = class extends import_node_events3.EventEmitter {
|
|
2775
3057
|
constructor(settings, call, activeIssues) {
|
|
2776
3058
|
super();
|
|
2777
3059
|
this.call = call;
|
|
2778
3060
|
this.setMaxListeners(Infinity);
|
|
2779
3061
|
this.eventScope = { observer: call.observer, observedCall: call, observedClient: this };
|
|
2780
|
-
this.sink = call.observer.config.createClientSink?.({ clientId: settings.clientId, observedCall: call });
|
|
2781
3062
|
this.clientId = settings.clientId;
|
|
2782
3063
|
this.appData = settings.appData ?? {};
|
|
2783
3064
|
this.settings = {
|
|
2784
3065
|
closeClientIfIdleForMs: settings.closeClientIfIdleForMs
|
|
2785
3066
|
};
|
|
2786
|
-
if (this.sink) {
|
|
2787
|
-
const onError = (err) => logger2.warn("ClientSampleSink error for client %s: %o", this.clientId, err);
|
|
2788
|
-
this.sink.once("close", () => this.sink?.off("error", onError));
|
|
2789
|
-
this.sink.on("error", onError);
|
|
2790
|
-
}
|
|
2791
3067
|
this.activeIssues = activeIssues;
|
|
2792
3068
|
}
|
|
2793
3069
|
call;
|
|
2794
3070
|
clientId;
|
|
2795
3071
|
observedPeerConnections = /* @__PURE__ */ new Map();
|
|
2796
|
-
/** Per-client sink (from `ObserverConfig.createClientSink`), if any. */
|
|
2797
|
-
sink;
|
|
2798
3072
|
/** Ancestry base shared by all Observer-bus events originating at this client. */
|
|
2799
3073
|
eventScope;
|
|
2800
3074
|
calculatedScore = {
|
|
@@ -2835,10 +3109,6 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
2835
3109
|
deltaOutboundPacketsSent = 0;
|
|
2836
3110
|
deltaTransportSentBytes = 0;
|
|
2837
3111
|
deltaTransportReceivedBytes = 0;
|
|
2838
|
-
deltaRttLt50Measurements = 0;
|
|
2839
|
-
deltaRttLt150Measurements = 0;
|
|
2840
|
-
deltaRttLt300Measurements = 0;
|
|
2841
|
-
deltaRttGtOrEq300Measurements = 0;
|
|
2842
3112
|
currentMaxRttInMs;
|
|
2843
3113
|
currentMinRttInMs;
|
|
2844
3114
|
currentAvgRttInMs;
|
|
@@ -2854,8 +3124,73 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
2854
3124
|
deltaNumberOfIssues = 0;
|
|
2855
3125
|
totalScoreSum = 0;
|
|
2856
3126
|
numberOfScoreMeasurements = 0;
|
|
2857
|
-
|
|
3127
|
+
minScore;
|
|
3128
|
+
maxScore;
|
|
3129
|
+
/**
|
|
3130
|
+
* Score measurements (one per accepted sample with a score) counted into
|
|
3131
|
+
* `SCORE_HISTOGRAM_NUMBER_OF_BUCKETS` buckets of `SCORE_HISTOGRAM_BUCKET_WIDTH`. Histograms of
|
|
3132
|
+
* several clients can be added bucket by bucket to get the distribution of a call.
|
|
3133
|
+
*/
|
|
3134
|
+
scoreHistogram = new Array(SCORE_HISTOGRAM_NUMBER_OF_BUCKETS).fill(0);
|
|
3135
|
+
/** Issues raised over the session, counted as in `issueHistogram`. */
|
|
3136
|
+
totalNumberOfIssues = 0;
|
|
3137
|
+
/**
|
|
3138
|
+
* Number of times each issue type was raised over the session, keyed by the base type (without
|
|
3139
|
+
* the `-resolved` suffix): `client.issueHistogram['loopback-audio-input']`. A resolution entry is
|
|
3140
|
+
* not counted, and neither is a re-raise of a keyed issue that is still open, so a stateful issue
|
|
3141
|
+
* reported on every tick counts once per open interval. A type never raised is `undefined`.
|
|
3142
|
+
*/
|
|
3143
|
+
issueHistogram = /* @__PURE__ */ Object.create(null);
|
|
3144
|
+
// Session totals. Unlike the `delta*` and `current*` fields they are never reset, and unlike the
|
|
3145
|
+
// `total*` fields of `ObservedPeerConnection` they survive a peer connection being closed.
|
|
3146
|
+
// Every RTT measurement is one peer connection's `currentRttInMs` in one accepted sample.
|
|
3147
|
+
totalRttSumInMs = 0;
|
|
3148
|
+
numberOfRttMeasurements = 0;
|
|
3149
|
+
maxRttInMs;
|
|
3150
|
+
minRttInMs;
|
|
3151
|
+
/**
|
|
3152
|
+
* RTT measurements counted into fixed buckets of `RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS`; the last bucket
|
|
3153
|
+
* holds every measurement of 980 ms and above. Constant memory, so percentiles can be estimated
|
|
3154
|
+
* over the whole session (see `rttSummary`) without keeping every measurement. Histograms
|
|
3155
|
+
* of several clients can be added bucket by bucket to get the distribution of a call.
|
|
3156
|
+
*/
|
|
3157
|
+
rttHistogram = new Array(RTT_HISTOGRAM_NUMBER_OF_BUCKETS).fill(0);
|
|
3158
|
+
_rttSummary;
|
|
3159
|
+
_scoreSummary;
|
|
3160
|
+
totalInboundPacketsLost = 0;
|
|
3161
|
+
totalInboundPacketsReceived = 0;
|
|
3162
|
+
totalOutboundPacketsSent = 0;
|
|
3163
|
+
totalReceivedAudioBytes = 0;
|
|
3164
|
+
totalReceivedVideoBytes = 0;
|
|
3165
|
+
totalSentAudioBytes = 0;
|
|
3166
|
+
totalSentVideoBytes = 0;
|
|
3167
|
+
totalTransportReceivedBytes = 0;
|
|
3168
|
+
totalTransportSentBytes = 0;
|
|
3169
|
+
totalDataChannelBytesReceived = 0;
|
|
3170
|
+
totalDataChannelBytesSent = 0;
|
|
3171
|
+
totalDataChannelMessagesReceived = 0;
|
|
3172
|
+
totalDataChannelMessagesSent = 0;
|
|
3173
|
+
/** Highest `sendingAudioBitrate + sendingVideoBitrate` of any sample, in bits per second. */
|
|
3174
|
+
peakSendingBitrate = 0;
|
|
3175
|
+
/** Highest `receivingAudioBitrate + receivingVideoBitrate` of any sample, in bits per second. */
|
|
3176
|
+
peakReceivingBitrate = 0;
|
|
3177
|
+
// Samples by the transport path of the selected ICE candidate pairs. A sample whose peer
|
|
3178
|
+
// connections use different paths is counted under each of them, so more than one non-zero
|
|
3179
|
+
// counter means the path changed or was mixed. A sample with no selected pair counts as unknown.
|
|
3180
|
+
numberOfSamplesOverUdp = 0;
|
|
3181
|
+
numberOfSamplesOverTcp = 0;
|
|
3182
|
+
numberOfSamplesOverTurnUdp = 0;
|
|
3183
|
+
numberOfSamplesOverTurnTcp = 0;
|
|
3184
|
+
numberOfSamplesWithUnknownPath = 0;
|
|
3185
|
+
numberOfPublicAddressChanges = 0;
|
|
2858
3186
|
mediaDevices = [];
|
|
3187
|
+
/**
|
|
3188
|
+
* The capture device behind each outbound audio track, keyed by track id, from the client's
|
|
3189
|
+
* `AUDIO_INPUT_DEVICE` metadata. Kept for the client's lifetime (a device switch is a new track
|
|
3190
|
+
* id), so a finding raised after a track ended can still name its device. Read it per track via
|
|
3191
|
+
* `ObservedOutboundTrack.audioInputDevice`.
|
|
3192
|
+
*/
|
|
3193
|
+
audioInputDevices = /* @__PURE__ */ new Map();
|
|
2859
3194
|
/**
|
|
2860
3195
|
* The client's currently **open** stateful issues, keyed by `ClientIssue.key` — the server-side
|
|
2861
3196
|
* mirror of the client monitor's own active-issue map (client-monitor-js >= 4.6.0).
|
|
@@ -2868,12 +3203,17 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
2868
3203
|
activeIssues;
|
|
2869
3204
|
_pendingInjections = {};
|
|
2870
3205
|
_activeSample;
|
|
2871
|
-
/** Peer connections the client reported closed (`PEER_CONNECTION_CLOSED`), closed once the current sample is applied. */
|
|
2872
3206
|
_peerConnectionsToClose = /* @__PURE__ */ new Set();
|
|
2873
|
-
/** Ids of peer connections closed by the client; late stats for them are ignored rather than resurrecting them. */
|
|
2874
3207
|
_closedPeerConnectionIds = /* @__PURE__ */ new Set();
|
|
2875
3208
|
closeTimer;
|
|
2876
3209
|
/** The distinct `publicAddress`es currently known by this client's open peer connections. */
|
|
3210
|
+
/**
|
|
3211
|
+
* The sink this client's samples are written to: whatever is registered under its client id in
|
|
3212
|
+
* `call.sinks` (the `createClientJsonlSink` file, or a sink the application added), if any.
|
|
3213
|
+
*/
|
|
3214
|
+
get sink() {
|
|
3215
|
+
return this.call.sinks.get(this.clientId);
|
|
3216
|
+
}
|
|
2877
3217
|
get publicAddresses() {
|
|
2878
3218
|
const result = /* @__PURE__ */ new Set();
|
|
2879
3219
|
for (const observedPeerConnection of this.observedPeerConnections.values()) {
|
|
@@ -2887,6 +3227,61 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
2887
3227
|
get score() {
|
|
2888
3228
|
return this.calculatedScore.value;
|
|
2889
3229
|
}
|
|
3230
|
+
/**
|
|
3231
|
+
* Percentiles (error at most one 20 ms bucket; a percentile in the overflow bucket is reported as
|
|
3232
|
+
* `maxRttInMs`) and mean of every RTT measurement of the session. Cached until the next measurement.
|
|
3233
|
+
*/
|
|
3234
|
+
get rttSummary() {
|
|
3235
|
+
if (!this._rttSummary) {
|
|
3236
|
+
const bounds = { min: this.minRttInMs, max: this.maxRttInMs, openEnded: true };
|
|
3237
|
+
const percentile2 = (p) => percentileOfHistogram(this.rttHistogram, RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS, p, bounds);
|
|
3238
|
+
this._rttSummary = Object.freeze({
|
|
3239
|
+
p50: percentile2(0.5),
|
|
3240
|
+
p75: percentile2(0.75),
|
|
3241
|
+
p95: percentile2(0.95),
|
|
3242
|
+
p99: percentile2(0.99),
|
|
3243
|
+
avg: 0 < this.numberOfRttMeasurements ? this.totalRttSumInMs / this.numberOfRttMeasurements : void 0
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
3246
|
+
return this._rttSummary;
|
|
3247
|
+
}
|
|
3248
|
+
/**
|
|
3249
|
+
* Percentiles (error at most one score point) and mean of every score of the session. Cached until
|
|
3250
|
+
* the next score.
|
|
3251
|
+
*/
|
|
3252
|
+
get scoreSummary() {
|
|
3253
|
+
if (!this._scoreSummary) {
|
|
3254
|
+
const bounds = { min: this.minScore, max: this.maxScore };
|
|
3255
|
+
const percentile2 = (p) => percentileOfHistogram(this.scoreHistogram, SCORE_HISTOGRAM_BUCKET_WIDTH, p, bounds);
|
|
3256
|
+
this._scoreSummary = Object.freeze({
|
|
3257
|
+
p5: percentile2(0.05),
|
|
3258
|
+
p25: percentile2(0.25),
|
|
3259
|
+
p50: percentile2(0.5),
|
|
3260
|
+
p75: percentile2(0.75),
|
|
3261
|
+
p95: percentile2(0.95),
|
|
3262
|
+
avg: 0 < this.numberOfScoreMeasurements ? this.totalScoreSum / this.numberOfScoreMeasurements : void 0
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
return this._scoreSummary;
|
|
3266
|
+
}
|
|
3267
|
+
_addScoreMeasurement(score) {
|
|
3268
|
+
this._scoreSummary = void 0;
|
|
3269
|
+
this.totalScoreSum += score;
|
|
3270
|
+
++this.numberOfScoreMeasurements;
|
|
3271
|
+
if (this.maxScore === void 0 || this.maxScore < score) this.maxScore = score;
|
|
3272
|
+
if (this.minScore === void 0 || score < this.minScore) this.minScore = score;
|
|
3273
|
+
const bucket = Math.min(Math.max(Math.floor(score / SCORE_HISTOGRAM_BUCKET_WIDTH), 0), SCORE_HISTOGRAM_NUMBER_OF_BUCKETS - 1);
|
|
3274
|
+
++this.scoreHistogram[bucket];
|
|
3275
|
+
}
|
|
3276
|
+
_addRttMeasurement(rttInMs) {
|
|
3277
|
+
this._rttSummary = void 0;
|
|
3278
|
+
this.totalRttSumInMs += rttInMs;
|
|
3279
|
+
++this.numberOfRttMeasurements;
|
|
3280
|
+
if (this.maxRttInMs === void 0 || this.maxRttInMs < rttInMs) this.maxRttInMs = rttInMs;
|
|
3281
|
+
if (this.minRttInMs === void 0 || rttInMs < this.minRttInMs) this.minRttInMs = rttInMs;
|
|
3282
|
+
const bucket = Math.min(Math.max(Math.floor(rttInMs / RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS), 0), RTT_HISTOGRAM_NUMBER_OF_BUCKETS - 1);
|
|
3283
|
+
++this.rttHistogram[bucket];
|
|
3284
|
+
}
|
|
2890
3285
|
close() {
|
|
2891
3286
|
if (this.closed) return;
|
|
2892
3287
|
if (this.closeTimer) {
|
|
@@ -2913,12 +3308,12 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
2913
3308
|
try {
|
|
2914
3309
|
this.sink?.end();
|
|
2915
3310
|
} catch (err) {
|
|
2916
|
-
|
|
3311
|
+
logger4.warn("Sink end failed for client %s: %o", this.clientId, err);
|
|
2917
3312
|
}
|
|
2918
3313
|
}
|
|
2919
3314
|
accept(sample, context) {
|
|
2920
3315
|
if (this.closed) {
|
|
2921
|
-
|
|
3316
|
+
logger4.warn("Attempted to accept a sample on a closed client %s", this.clientId);
|
|
2922
3317
|
return;
|
|
2923
3318
|
}
|
|
2924
3319
|
if (this.closeTimer) {
|
|
@@ -2930,6 +3325,10 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
2930
3325
|
const elapsedInSeconds = elapsedInMs / 1e3;
|
|
2931
3326
|
let sumOfRtts = 0;
|
|
2932
3327
|
let numberOfRttMeasurements = 0;
|
|
3328
|
+
let overUdp = false;
|
|
3329
|
+
let overTcp = false;
|
|
3330
|
+
let overTurnUdp = false;
|
|
3331
|
+
let overTurnTcp = false;
|
|
2933
3332
|
++this.acceptedSamples;
|
|
2934
3333
|
this.availableIncomingBitrate = 0;
|
|
2935
3334
|
this.availableOutgoingBitrate = 0;
|
|
@@ -2946,10 +3345,6 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
2946
3345
|
this.deltaSentVideoBytes = 0;
|
|
2947
3346
|
this.deltaTransportReceivedBytes = 0;
|
|
2948
3347
|
this.deltaTransportSentBytes = 0;
|
|
2949
|
-
this.deltaRttLt50Measurements = 0;
|
|
2950
|
-
this.deltaRttLt150Measurements = 0;
|
|
2951
|
-
this.deltaRttLt300Measurements = 0;
|
|
2952
|
-
this.deltaRttGtOrEq300Measurements = 0;
|
|
2953
3348
|
this.deltaNumberOfIssues = 0;
|
|
2954
3349
|
this.numberOfDataChannels = 0;
|
|
2955
3350
|
this.numberOfInboundRtpStreams = 0;
|
|
@@ -3021,6 +3416,18 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3021
3416
|
if (observedPeerConnection.usingTCP) {
|
|
3022
3417
|
this.usingTCP = true;
|
|
3023
3418
|
}
|
|
3419
|
+
for (const selectedPair of observedPeerConnection.selectedIceCandidatePairs) {
|
|
3420
|
+
const localCandidate = selectedPair.getLocalCandidate();
|
|
3421
|
+
if (!localCandidate) continue;
|
|
3422
|
+
if (localCandidate.candidateType === "relay") {
|
|
3423
|
+
if (localCandidate.relayProtocol === "tcp" || localCandidate.relayProtocol === "tls") overTurnTcp = true;
|
|
3424
|
+
else overTurnUdp = true;
|
|
3425
|
+
} else if (localCandidate.protocol === "tcp") {
|
|
3426
|
+
overTcp = true;
|
|
3427
|
+
} else {
|
|
3428
|
+
overUdp = true;
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3024
3431
|
if (observedPeerConnection.currentRttInMs) {
|
|
3025
3432
|
if (this.currentMinRttInMs === void 0 || observedPeerConnection.currentRttInMs < this.currentMinRttInMs) {
|
|
3026
3433
|
this.currentMinRttInMs = observedPeerConnection.currentRttInMs;
|
|
@@ -3028,17 +3435,9 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3028
3435
|
if (this.currentMaxRttInMs === void 0 || observedPeerConnection.currentRttInMs > this.currentMaxRttInMs) {
|
|
3029
3436
|
this.currentMaxRttInMs = observedPeerConnection.currentRttInMs;
|
|
3030
3437
|
}
|
|
3031
|
-
if (observedPeerConnection.currentRttInMs < 50) {
|
|
3032
|
-
this.deltaRttLt50Measurements += 1;
|
|
3033
|
-
} else if (observedPeerConnection.currentRttInMs < 150) {
|
|
3034
|
-
this.deltaRttLt150Measurements += 1;
|
|
3035
|
-
} else if (observedPeerConnection.currentRttInMs < 300) {
|
|
3036
|
-
this.deltaRttLt300Measurements += 1;
|
|
3037
|
-
} else if (300 <= observedPeerConnection.currentRttInMs) {
|
|
3038
|
-
this.deltaRttGtOrEq300Measurements += 1;
|
|
3039
|
-
}
|
|
3040
3438
|
sumOfRtts += observedPeerConnection.currentRttInMs;
|
|
3041
3439
|
++numberOfRttMeasurements;
|
|
3440
|
+
this._addRttMeasurement(observedPeerConnection.currentRttInMs);
|
|
3042
3441
|
}
|
|
3043
3442
|
}
|
|
3044
3443
|
for (const clientEvent of clientEventsPostBuffer) {
|
|
@@ -3048,6 +3447,7 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3048
3447
|
if (latestPublicAddress !== void 0 && latestPublicAddress !== this.publicAddress) {
|
|
3049
3448
|
const previousPublicAddress = this.publicAddress;
|
|
3050
3449
|
this.publicAddress = latestPublicAddress;
|
|
3450
|
+
if (previousPublicAddress !== void 0) ++this.numberOfPublicAddressChanges;
|
|
3051
3451
|
this._notify("client-public-address-changed", { ...this.eventScope, publicAddress: latestPublicAddress, previousPublicAddress });
|
|
3052
3452
|
}
|
|
3053
3453
|
this.attachments = sample.attachments;
|
|
@@ -3063,15 +3463,34 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3063
3463
|
this.sendingVideoBitrate = 0;
|
|
3064
3464
|
}
|
|
3065
3465
|
this.currentAvgRttInMs = 0 < numberOfRttMeasurements ? sumOfRtts / numberOfRttMeasurements : void 0;
|
|
3466
|
+
this.peakSendingBitrate = Math.max(this.peakSendingBitrate, this.sendingAudioBitrate + this.sendingVideoBitrate);
|
|
3467
|
+
this.peakReceivingBitrate = Math.max(this.peakReceivingBitrate, this.receivingAudioBitrate + this.receivingVideoBitrate);
|
|
3468
|
+
this.totalInboundPacketsLost += this.deltaInboundPacketsLost;
|
|
3469
|
+
this.totalInboundPacketsReceived += this.deltaInboundPacketsReceived;
|
|
3470
|
+
this.totalOutboundPacketsSent += this.deltaOutboundPacketsSent;
|
|
3471
|
+
this.totalReceivedAudioBytes += this.deltaReceivedAudioBytes;
|
|
3472
|
+
this.totalReceivedVideoBytes += this.deltaReceivedVideoBytes;
|
|
3473
|
+
this.totalSentAudioBytes += this.deltaSentAudioBytes;
|
|
3474
|
+
this.totalSentVideoBytes += this.deltaSentVideoBytes;
|
|
3475
|
+
this.totalTransportReceivedBytes += this.deltaTransportReceivedBytes;
|
|
3476
|
+
this.totalTransportSentBytes += this.deltaTransportSentBytes;
|
|
3477
|
+
this.totalDataChannelBytesReceived += this.deltaDataChannelBytesReceived;
|
|
3478
|
+
this.totalDataChannelBytesSent += this.deltaDataChannelBytesSent;
|
|
3479
|
+
this.totalDataChannelMessagesReceived += this.deltaDataChannelMessagesReceived;
|
|
3480
|
+
this.totalDataChannelMessagesSent += this.deltaDataChannelMessagesSent;
|
|
3481
|
+
if (overUdp) ++this.numberOfSamplesOverUdp;
|
|
3482
|
+
if (overTcp) ++this.numberOfSamplesOverTcp;
|
|
3483
|
+
if (overTurnUdp) ++this.numberOfSamplesOverTurnUdp;
|
|
3484
|
+
if (overTurnTcp) ++this.numberOfSamplesOverTurnTcp;
|
|
3485
|
+
if (!overUdp && !overTcp && !overTurnUdp && !overTurnTcp) ++this.numberOfSamplesWithUnknownPath;
|
|
3066
3486
|
this.calculatedScore.value = sample.score;
|
|
3067
3487
|
this.lastSampleTimestamp = sample.timestamp;
|
|
3068
3488
|
const elapsedTimeInMs = now - this.updated;
|
|
3069
3489
|
this.emit("update", sample, elapsedTimeInMs, context);
|
|
3070
3490
|
this._notify("client-updated", { ...this.eventScope, sample, elapsedTimeInMs, context });
|
|
3071
3491
|
this.updated = now;
|
|
3072
|
-
if (this.calculatedScore.value) {
|
|
3073
|
-
this.
|
|
3074
|
-
++this.numberOfScoreMeasurements;
|
|
3492
|
+
if (typeof this.calculatedScore.value === "number" && Number.isFinite(this.calculatedScore.value)) {
|
|
3493
|
+
this._addScoreMeasurement(this.calculatedScore.value);
|
|
3075
3494
|
}
|
|
3076
3495
|
if (this.settings.closeClientIfIdleForMs !== void 0) {
|
|
3077
3496
|
this.closeTimer = setTimeout(() => {
|
|
@@ -3084,7 +3503,7 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3084
3503
|
try {
|
|
3085
3504
|
this.sink?.write(sample);
|
|
3086
3505
|
} catch (err) {
|
|
3087
|
-
|
|
3506
|
+
logger4.warn("Sink write failed for client %s: %o", this.clientId, err);
|
|
3088
3507
|
}
|
|
3089
3508
|
}
|
|
3090
3509
|
injectMetaData(metaData) {
|
|
@@ -3162,6 +3581,13 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3162
3581
|
this.operationSystem = parseJsonAs(metadata.payload);
|
|
3163
3582
|
break;
|
|
3164
3583
|
}
|
|
3584
|
+
case "AUDIO_INPUT_DEVICE" /* AUDIO_INPUT_DEVICE */: {
|
|
3585
|
+
const device = parseJsonAs(metadata.payload);
|
|
3586
|
+
if (typeof device?.trackId === "string" && typeof device.label === "string") {
|
|
3587
|
+
this.audioInputDevices.set(device.trackId, device);
|
|
3588
|
+
}
|
|
3589
|
+
break;
|
|
3590
|
+
}
|
|
3165
3591
|
}
|
|
3166
3592
|
this._notify("client-metadata", { ...this.eventScope, metaData: metadata });
|
|
3167
3593
|
}
|
|
@@ -3182,6 +3608,11 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3182
3608
|
if (isClientIssueResolutionEntry(issue)) {
|
|
3183
3609
|
return this._resolveIssue(issue);
|
|
3184
3610
|
}
|
|
3611
|
+
if (!issue.key || !this.activeIssues.get(issue.key)) {
|
|
3612
|
+
const type = baseIssueType(issue.type);
|
|
3613
|
+
this.issueHistogram[type] = (this.issueHistogram[type] ?? 0) + 1;
|
|
3614
|
+
++this.totalNumberOfIssues;
|
|
3615
|
+
}
|
|
3185
3616
|
const now = Date.now();
|
|
3186
3617
|
if (issue.key) {
|
|
3187
3618
|
const payload = parseJsonObject(issue.payload);
|
|
@@ -3255,10 +3686,10 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3255
3686
|
this._notify("client-rejoined", { ...this.eventScope, timestamp: event.timestamp });
|
|
3256
3687
|
} else {
|
|
3257
3688
|
this.joinedAt = event.timestamp;
|
|
3258
|
-
|
|
3689
|
+
logger4.warn(`Client ${this.clientId} joinedAt timestamp was updated to ${event.timestamp}. the joined event will not be emitted.`);
|
|
3259
3690
|
}
|
|
3260
3691
|
}
|
|
3261
|
-
|
|
3692
|
+
logger4.debug("Client %s joined at %o", this.clientId, event);
|
|
3262
3693
|
break;
|
|
3263
3694
|
}
|
|
3264
3695
|
case "CLIENT_LEFT" /* CLIENT_LEFT */: {
|
|
@@ -3268,10 +3699,10 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3268
3699
|
this.emit("left");
|
|
3269
3700
|
this._notify("client-left", { ...this.eventScope });
|
|
3270
3701
|
} else {
|
|
3271
|
-
|
|
3702
|
+
logger4.warn(`Client ${this.clientId} leftAt timestamp was already set`);
|
|
3272
3703
|
}
|
|
3273
3704
|
}
|
|
3274
|
-
|
|
3705
|
+
logger4.debug("Client %s left at %o", this.clientId, event);
|
|
3275
3706
|
break;
|
|
3276
3707
|
}
|
|
3277
3708
|
case "PEER_CONNECTION_OPENED" /* PEER_CONNECTION_OPENED */: {
|
|
@@ -3283,7 +3714,7 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3283
3714
|
} else if (postBuffer) {
|
|
3284
3715
|
postBuffer.push(event);
|
|
3285
3716
|
} else {
|
|
3286
|
-
|
|
3717
|
+
logger4.warn("Received PEER_CONNECTION_OPENED event without a corresponding observedPeerConnection: %o", event);
|
|
3287
3718
|
}
|
|
3288
3719
|
}
|
|
3289
3720
|
break;
|
|
@@ -3300,7 +3731,7 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3300
3731
|
} else if (postBuffer) {
|
|
3301
3732
|
postBuffer.push(event);
|
|
3302
3733
|
} else {
|
|
3303
|
-
|
|
3734
|
+
logger4.warn("Received PEER_CONNECTION_CLOSED event without a corresponding observedPeerConnection: %o", event);
|
|
3304
3735
|
}
|
|
3305
3736
|
}
|
|
3306
3737
|
break;
|
|
@@ -3315,7 +3746,7 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3315
3746
|
} else if (postBuffer) {
|
|
3316
3747
|
postBuffer.push(event);
|
|
3317
3748
|
} else {
|
|
3318
|
-
|
|
3749
|
+
logger4.warn("Received MEDIA_TRACK_ADDED event without a corresponding observedPeerConnection or observedTrack: %o", event);
|
|
3319
3750
|
}
|
|
3320
3751
|
}
|
|
3321
3752
|
break;
|
|
@@ -3330,7 +3761,7 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3330
3761
|
} else if (postBuffer) {
|
|
3331
3762
|
postBuffer.push(event);
|
|
3332
3763
|
} else {
|
|
3333
|
-
|
|
3764
|
+
logger4.warn("Received MEDIA_TRACK_REMOVED event without a corresponding observedPeerConnection or observedTrack: %o", event);
|
|
3334
3765
|
}
|
|
3335
3766
|
}
|
|
3336
3767
|
break;
|
|
@@ -3345,7 +3776,7 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3345
3776
|
} else if (postBuffer) {
|
|
3346
3777
|
postBuffer.push(event);
|
|
3347
3778
|
} else {
|
|
3348
|
-
|
|
3779
|
+
logger4.warn("Received DATA_CHANNEL_OPENED event without a corresponding observedPeerConnection or observedDataChannel: %o", event);
|
|
3349
3780
|
}
|
|
3350
3781
|
}
|
|
3351
3782
|
break;
|
|
@@ -3360,7 +3791,7 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3360
3791
|
} else if (postBuffer) {
|
|
3361
3792
|
postBuffer.push(event);
|
|
3362
3793
|
} else {
|
|
3363
|
-
|
|
3794
|
+
logger4.warn("Received DATA_CHANNEL_CLOSE event without a corresponding observedPeerConnection or observedDataChannel: %o", event);
|
|
3364
3795
|
}
|
|
3365
3796
|
}
|
|
3366
3797
|
break;
|
|
@@ -3380,7 +3811,7 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3380
3811
|
} else if (postBuffer) {
|
|
3381
3812
|
postBuffer.push(event);
|
|
3382
3813
|
} else {
|
|
3383
|
-
|
|
3814
|
+
logger4.warn("Received MEDIA_TRACK_MUTED event without a corresponding observedPeerConnection or observedTrack: %o", event);
|
|
3384
3815
|
}
|
|
3385
3816
|
}
|
|
3386
3817
|
break;
|
|
@@ -3400,7 +3831,7 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3400
3831
|
} else if (postBuffer) {
|
|
3401
3832
|
postBuffer.push(event);
|
|
3402
3833
|
} else {
|
|
3403
|
-
|
|
3834
|
+
logger4.warn("Received MEDIA_TRACK_UNMUTED event without a corresponding observedPeerConnection or observedTrack: %o", event);
|
|
3404
3835
|
}
|
|
3405
3836
|
}
|
|
3406
3837
|
break;
|
|
@@ -3415,7 +3846,7 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3415
3846
|
} else if (postBuffer) {
|
|
3416
3847
|
postBuffer.push(event);
|
|
3417
3848
|
} else {
|
|
3418
|
-
|
|
3849
|
+
logger4.warn("Received ICE_CONNECTION_STATE_CHANGED event without a corresponding observedPeerConnection: %o", event);
|
|
3419
3850
|
}
|
|
3420
3851
|
}
|
|
3421
3852
|
break;
|
|
@@ -3430,7 +3861,7 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3430
3861
|
} else if (postBuffer) {
|
|
3431
3862
|
postBuffer.push(event);
|
|
3432
3863
|
} else {
|
|
3433
|
-
|
|
3864
|
+
logger4.warn("Received ICE_GATHERING_STATE_CHANGED event without a corresponding observedPeerConnection: %o", event);
|
|
3434
3865
|
}
|
|
3435
3866
|
}
|
|
3436
3867
|
break;
|
|
@@ -3445,7 +3876,7 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3445
3876
|
} else if (postBuffer) {
|
|
3446
3877
|
postBuffer.push(event);
|
|
3447
3878
|
} else {
|
|
3448
|
-
|
|
3879
|
+
logger4.warn("Received PEER_CONNECTION_STATE_CHANGED event without a corresponding observedPeerConnection: %o", event);
|
|
3449
3880
|
}
|
|
3450
3881
|
}
|
|
3451
3882
|
break;
|
|
@@ -3464,11 +3895,11 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3464
3895
|
let observedPeerConnection = this.observedPeerConnections.get(sample.peerConnectionId);
|
|
3465
3896
|
if (!observedPeerConnection) {
|
|
3466
3897
|
if (this._closedPeerConnectionIds.has(sample.peerConnectionId)) {
|
|
3467
|
-
|
|
3898
|
+
logger4.debug("Ignoring stats of peer connection %s, already closed by client %s", sample.peerConnectionId, this.clientId);
|
|
3468
3899
|
return void 0;
|
|
3469
3900
|
}
|
|
3470
3901
|
if (!sample.peerConnectionId) {
|
|
3471
|
-
return
|
|
3902
|
+
return logger4.warn(
|
|
3472
3903
|
`ObservedClient received an invalid PeerConnectionSample (missing peerConnectionId field). ClientId: ${this.clientId}, CallId: ${this.call.callId}`,
|
|
3473
3904
|
sample
|
|
3474
3905
|
), void 0;
|
|
@@ -3528,7 +3959,7 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3528
3959
|
try {
|
|
3529
3960
|
this.sink?.write(finalSample);
|
|
3530
3961
|
} catch (err) {
|
|
3531
|
-
|
|
3962
|
+
logger4.warn("Sink write failed during injection flush for client %s: %o", this.clientId, err);
|
|
3532
3963
|
}
|
|
3533
3964
|
}
|
|
3534
3965
|
/** Emit an Observer-bus event scoped to this client (or a peer connection under it). */
|
|
@@ -3556,7 +3987,7 @@ var DefaultCallScoreCalculator = class {
|
|
|
3556
3987
|
};
|
|
3557
3988
|
|
|
3558
3989
|
// src/detectors/Detectors.ts
|
|
3559
|
-
var
|
|
3990
|
+
var logger5 = createLogger("Detectors");
|
|
3560
3991
|
var Detectors = class {
|
|
3561
3992
|
_detectors;
|
|
3562
3993
|
constructor(...detectors) {
|
|
@@ -3637,7 +4068,7 @@ var Detectors = class {
|
|
|
3637
4068
|
try {
|
|
3638
4069
|
detector.update();
|
|
3639
4070
|
} catch (err) {
|
|
3640
|
-
|
|
4071
|
+
logger5.warn(`Error updating detector ${detector?.name}`, err);
|
|
3641
4072
|
}
|
|
3642
4073
|
}
|
|
3643
4074
|
}
|
|
@@ -3650,7 +4081,7 @@ var Detectors = class {
|
|
|
3650
4081
|
try {
|
|
3651
4082
|
detector.close?.();
|
|
3652
4083
|
} catch (err) {
|
|
3653
|
-
|
|
4084
|
+
logger5.warn(`Error closing detector ${detector?.name}`, err);
|
|
3654
4085
|
}
|
|
3655
4086
|
}
|
|
3656
4087
|
};
|
|
@@ -4453,9 +4884,172 @@ var PublisherFaultCorroborationDetector = class _PublisherFaultCorroborationDete
|
|
|
4453
4884
|
}
|
|
4454
4885
|
};
|
|
4455
4886
|
|
|
4887
|
+
// src/detectors/AudioEchoDetector.ts
|
|
4888
|
+
var AudioEchoTypes = {
|
|
4889
|
+
/**
|
|
4890
|
+
* One participant's outgoing audio follows another participant's voice: it rises when they speak,
|
|
4891
|
+
* in proportion to how loud they are. The echoing participant is sending the far end's playout
|
|
4892
|
+
* back into the call — speakers into an open mic, or a loopback capture device.
|
|
4893
|
+
*/
|
|
4894
|
+
audioEchoSuspected: "AUDIO_ECHO_SUSPECTED"
|
|
4895
|
+
};
|
|
4896
|
+
var DIRECTION_MARGIN = 0.1;
|
|
4897
|
+
var mean = (values) => values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
4898
|
+
var LEVEL_FLOOR = 1e-5;
|
|
4899
|
+
var toDb = (level) => 20 * Math.log10(Math.max(level, LEVEL_FLOOR));
|
|
4900
|
+
var AudioEchoDetector = class _AudioEchoDetector {
|
|
4901
|
+
constructor(call, config = {}) {
|
|
4902
|
+
this.call = call;
|
|
4903
|
+
this._config = {
|
|
4904
|
+
bucketMs: 5e3,
|
|
4905
|
+
windowBuckets: 24,
|
|
4906
|
+
activeRmsLevel: 0.01,
|
|
4907
|
+
minSourceActiveBuckets: 8,
|
|
4908
|
+
minFollowRatio: 0.8,
|
|
4909
|
+
minCorrelation: 0.6,
|
|
4910
|
+
cooldownMs: 12e4,
|
|
4911
|
+
loopbackAudioInputIssueType: "loopback-audio-input",
|
|
4912
|
+
...config
|
|
4913
|
+
};
|
|
4914
|
+
}
|
|
4915
|
+
call;
|
|
4916
|
+
static NAME = "audio-echo-detector";
|
|
4917
|
+
name = _AudioEchoDetector.NAME;
|
|
4918
|
+
_config;
|
|
4919
|
+
_series = /* @__PURE__ */ new Map();
|
|
4920
|
+
_lastRaisedAt = /* @__PURE__ */ new Map();
|
|
4921
|
+
_lastEvaluatedBucket;
|
|
4922
|
+
close() {
|
|
4923
|
+
this._series.clear();
|
|
4924
|
+
this._lastRaisedAt.clear();
|
|
4925
|
+
}
|
|
4926
|
+
update() {
|
|
4927
|
+
const now = Date.now();
|
|
4928
|
+
const bucket = Math.floor(now / this._config.bucketMs);
|
|
4929
|
+
if (this._lastEvaluatedBucket === bucket) return;
|
|
4930
|
+
const closedBucket = bucket - 1;
|
|
4931
|
+
this._lastEvaluatedBucket = bucket;
|
|
4932
|
+
this._collect(now, closedBucket);
|
|
4933
|
+
this._evaluate(now, bucket);
|
|
4934
|
+
}
|
|
4935
|
+
_collect(now, bucket) {
|
|
4936
|
+
for (const client of this.call.observedClients.values()) {
|
|
4937
|
+
for (const peerConnection of client.observedPeerConnections.values()) {
|
|
4938
|
+
for (const track of peerConnection.observedOutboundTracks.values()) {
|
|
4939
|
+
if (track.kind !== "audio") continue;
|
|
4940
|
+
let series = this._series.get(track.id);
|
|
4941
|
+
if (!series) {
|
|
4942
|
+
series = { track, clientId: client.clientId, lastTimestamp: -1, samples: [], lastSeenAt: now };
|
|
4943
|
+
this._series.set(track.id, series);
|
|
4944
|
+
}
|
|
4945
|
+
series.lastSeenAt = now;
|
|
4946
|
+
if (series.lastTimestamp === track.timestamp) continue;
|
|
4947
|
+
series.lastTimestamp = track.timestamp;
|
|
4948
|
+
const level = track.rmsAudioLevel;
|
|
4949
|
+
if (level === void 0) continue;
|
|
4950
|
+
series.samples.push({ bucket, level });
|
|
4951
|
+
const oldest = bucket - this._config.windowBuckets;
|
|
4952
|
+
while (series.samples.length && series.samples[0].bucket <= oldest) series.samples.shift();
|
|
4953
|
+
}
|
|
4954
|
+
}
|
|
4955
|
+
}
|
|
4956
|
+
for (const [trackId, series] of this._series) {
|
|
4957
|
+
if (series.lastSeenAt !== now) this._series.delete(trackId);
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4960
|
+
_evaluate(now, currentBucket) {
|
|
4961
|
+
if (this._series.size < 2) return;
|
|
4962
|
+
const tops = /* @__PURE__ */ new Map();
|
|
4963
|
+
for (const [trackId, series] of this._series) {
|
|
4964
|
+
for (const { bucket, level } of series.samples) {
|
|
4965
|
+
if (currentBucket <= bucket) continue;
|
|
4966
|
+
const top = tops.get(bucket);
|
|
4967
|
+
const entry = { id: trackId, level };
|
|
4968
|
+
if (!top) tops.set(bucket, [entry, void 0]);
|
|
4969
|
+
else if (top[0].level < level) tops.set(bucket, [entry, top[0]]);
|
|
4970
|
+
else if (!top[1] || top[1].level < level) top[1] = entry;
|
|
4971
|
+
}
|
|
4972
|
+
}
|
|
4973
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
4974
|
+
for (const [trackId, series] of this._series) {
|
|
4975
|
+
const suspectDb = [];
|
|
4976
|
+
const sourceDb = [];
|
|
4977
|
+
const sourceCounts = /* @__PURE__ */ new Map();
|
|
4978
|
+
let followed = 0;
|
|
4979
|
+
for (const { bucket, level } of series.samples) {
|
|
4980
|
+
const top = tops.get(bucket);
|
|
4981
|
+
if (!top) continue;
|
|
4982
|
+
const source = top[0].id === trackId ? top[1] : top[0];
|
|
4983
|
+
if (!source || source.level < this._config.activeRmsLevel) continue;
|
|
4984
|
+
if (this._config.activeRmsLevel <= level) followed += 1;
|
|
4985
|
+
suspectDb.push(toDb(level));
|
|
4986
|
+
sourceDb.push(toDb(source.level));
|
|
4987
|
+
sourceCounts.set(source.id, (sourceCounts.get(source.id) ?? 0) + 1);
|
|
4988
|
+
}
|
|
4989
|
+
const sourceActiveBuckets = sourceDb.length;
|
|
4990
|
+
if (sourceActiveBuckets < this._config.minSourceActiveBuckets) continue;
|
|
4991
|
+
const followRatio = followed / sourceActiveBuckets;
|
|
4992
|
+
if (followRatio < this._config.minFollowRatio) continue;
|
|
4993
|
+
const levelCorrelation = correlation(suspectDb, sourceDb);
|
|
4994
|
+
if (levelCorrelation < this._config.minCorrelation) continue;
|
|
4995
|
+
const [sourceTrackId] = [...sourceCounts].reduce((a, b) => a[1] < b[1] ? b : a);
|
|
4996
|
+
const client = this.call.observedClients.get(series.clientId);
|
|
4997
|
+
candidates.set(trackId, {
|
|
4998
|
+
trackId,
|
|
4999
|
+
series,
|
|
5000
|
+
sourceTrackId,
|
|
5001
|
+
followRatio,
|
|
5002
|
+
levelCorrelation,
|
|
5003
|
+
sourceActiveBuckets,
|
|
5004
|
+
// How much quieter than its source the suspect is, in dB. An echo is at most as loud as
|
|
5005
|
+
// what it echoes, give or take AGC.
|
|
5006
|
+
relativeLevelDb: mean(suspectDb) - mean(sourceDb),
|
|
5007
|
+
loopbackAudioInput: client?.activeIssues.get(`${this._config.loopbackAudioInputIssueType}-track-${trackId}`) !== void 0
|
|
5008
|
+
});
|
|
5009
|
+
}
|
|
5010
|
+
for (const candidate of candidates.values()) {
|
|
5011
|
+
const reverse = candidates.get(candidate.sourceTrackId);
|
|
5012
|
+
let ambiguousDirection = false;
|
|
5013
|
+
if (reverse && reverse.sourceTrackId === candidate.trackId) {
|
|
5014
|
+
if (candidate.loopbackAudioInput !== reverse.loopbackAudioInput) {
|
|
5015
|
+
if (!candidate.loopbackAudioInput) continue;
|
|
5016
|
+
} else if (DIRECTION_MARGIN < Math.abs(candidate.followRatio - reverse.followRatio)) {
|
|
5017
|
+
if (candidate.followRatio < reverse.followRatio) continue;
|
|
5018
|
+
} else {
|
|
5019
|
+
ambiguousDirection = true;
|
|
5020
|
+
if (reverse.relativeLevelDb < candidate.relativeLevelDb) continue;
|
|
5021
|
+
if (reverse.relativeLevelDb === candidate.relativeLevelDb && reverse.trackId < candidate.trackId) continue;
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
5024
|
+
if (now - (this._lastRaisedAt.get(candidate.trackId) ?? -Infinity) < this._config.cooldownMs) continue;
|
|
5025
|
+
this._lastRaisedAt.set(candidate.trackId, now);
|
|
5026
|
+
const { series } = candidate;
|
|
5027
|
+
this.call.addIssue({
|
|
5028
|
+
type: AudioEchoTypes.audioEchoSuspected,
|
|
5029
|
+
timestamp: now,
|
|
5030
|
+
payload: {
|
|
5031
|
+
trackId: candidate.trackId,
|
|
5032
|
+
clientId: series.clientId,
|
|
5033
|
+
sourceTrackId: candidate.sourceTrackId,
|
|
5034
|
+
sourceClientId: this._series.get(candidate.sourceTrackId)?.clientId,
|
|
5035
|
+
followRatio: candidate.followRatio,
|
|
5036
|
+
levelCorrelation: candidate.levelCorrelation,
|
|
5037
|
+
relativeLevelDb: candidate.relativeLevelDb,
|
|
5038
|
+
sourceActiveBuckets: candidate.sourceActiveBuckets,
|
|
5039
|
+
bucketMs: this._config.bucketMs,
|
|
5040
|
+
ambiguousDirection,
|
|
5041
|
+
levelSource: series.track.rmsAudioLevelSource,
|
|
5042
|
+
audioInputLabel: series.track.audioInputDevice?.label,
|
|
5043
|
+
loopbackAudioInput: candidate.loopbackAudioInput
|
|
5044
|
+
}
|
|
5045
|
+
});
|
|
5046
|
+
}
|
|
5047
|
+
}
|
|
5048
|
+
};
|
|
5049
|
+
|
|
4456
5050
|
// src/ObservedCall.ts
|
|
4457
|
-
var
|
|
4458
|
-
var ObservedCall = class extends
|
|
5051
|
+
var logger6 = createLogger("ObservedCall");
|
|
5052
|
+
var ObservedCall = class extends import_node_events4.EventEmitter {
|
|
4459
5053
|
constructor(settings, observer, activeIssuesRegistry) {
|
|
4460
5054
|
super();
|
|
4461
5055
|
this.observer = observer;
|
|
@@ -4463,6 +5057,8 @@ var ObservedCall = class extends import_events3.EventEmitter {
|
|
|
4463
5057
|
this.setMaxListeners(Infinity);
|
|
4464
5058
|
this.eventScope = { observer: this.observer, observedCall: this };
|
|
4465
5059
|
this.callId = settings.callId;
|
|
5060
|
+
this.sinks = new SinkRegistry(this.observer.sinks);
|
|
5061
|
+
this.extensionStats = new ExtensionStatsRegistry(this.callId, this.sinks);
|
|
4466
5062
|
this.appData = settings.appData ?? {};
|
|
4467
5063
|
this.scoreCalculator = new DefaultCallScoreCalculator(this);
|
|
4468
5064
|
this.detectors = new Detectors();
|
|
@@ -4511,10 +5107,6 @@ var ObservedCall = class extends import_events3.EventEmitter {
|
|
|
4511
5107
|
numberOfDataChannels = 0;
|
|
4512
5108
|
maxNumberOfClients = 0;
|
|
4513
5109
|
deltaNumberOfIssues = 0;
|
|
4514
|
-
// public deltaRttLt50Measurements = 0;
|
|
4515
|
-
// public deltaRttLt150Measurements = 0;
|
|
4516
|
-
// public deltaRttLt300Measurements = 0;
|
|
4517
|
-
// public deltaRttGtOrEq300Measurements = 0;
|
|
4518
5110
|
appData;
|
|
4519
5111
|
closed = false;
|
|
4520
5112
|
startedAt;
|
|
@@ -4523,6 +5115,8 @@ var ObservedCall = class extends import_events3.EventEmitter {
|
|
|
4523
5115
|
settings;
|
|
4524
5116
|
/** Ancestry base shared by all Observer-bus events originating at this call. */
|
|
4525
5117
|
eventScope;
|
|
5118
|
+
sinks;
|
|
5119
|
+
extensionStats;
|
|
4526
5120
|
closeTimer;
|
|
4527
5121
|
get numberOfClients() {
|
|
4528
5122
|
return this.observedClients.size;
|
|
@@ -4560,8 +5154,12 @@ var ObservedCall = class extends import_events3.EventEmitter {
|
|
|
4560
5154
|
detector = new PublisherFaultCorroborationDetector(this, config);
|
|
4561
5155
|
break;
|
|
4562
5156
|
}
|
|
5157
|
+
case AudioEchoDetector.NAME: {
|
|
5158
|
+
detector = new AudioEchoDetector(this, config);
|
|
5159
|
+
break;
|
|
5160
|
+
}
|
|
4563
5161
|
default: {
|
|
4564
|
-
|
|
5162
|
+
logger6.warn("Unknown detector name %s; skipping", name);
|
|
4565
5163
|
return this;
|
|
4566
5164
|
}
|
|
4567
5165
|
}
|
|
@@ -4631,6 +5229,8 @@ var ObservedCall = class extends import_events3.EventEmitter {
|
|
|
4631
5229
|
if (client.joinedAt) minSampleTimestamps = Math.min(minSampleTimestamps ?? client.joinedAt, client.joinedAt);
|
|
4632
5230
|
if (client.leftAt) maxSampleTimestamps = Math.max(maxSampleTimestamps ?? client.leftAt, client.leftAt);
|
|
4633
5231
|
}
|
|
5232
|
+
this.extensionStats.close();
|
|
5233
|
+
this.sinks.clear();
|
|
4634
5234
|
if (this.startedAt === void 0) this.startedAt = minSampleTimestamps;
|
|
4635
5235
|
if (this.endedAt === void 0) this.endedAt = maxSampleTimestamps;
|
|
4636
5236
|
this.closedAt = Date.now();
|
|
@@ -4643,17 +5243,26 @@ var ObservedCall = class extends import_events3.EventEmitter {
|
|
|
4643
5243
|
this.emit("close");
|
|
4644
5244
|
this._notify("call-closed", { ...this.eventScope });
|
|
4645
5245
|
}
|
|
5246
|
+
createExtensionStats(settings) {
|
|
5247
|
+
return this.extensionStats.create(settings);
|
|
5248
|
+
}
|
|
5249
|
+
acceptExtensionStats(id, data) {
|
|
5250
|
+
this.extensionStats.accept(id, data);
|
|
5251
|
+
}
|
|
5252
|
+
getExtensionStats(id) {
|
|
5253
|
+
return this.extensionStats.get(id);
|
|
5254
|
+
}
|
|
4646
5255
|
getObservedClient(clientId) {
|
|
4647
5256
|
if (this.closed || !this.observedClients.has(clientId)) return;
|
|
4648
5257
|
return this.observedClients.get(clientId);
|
|
4649
5258
|
}
|
|
4650
5259
|
createObservedClient(settings, acceptCtx) {
|
|
4651
5260
|
if (this.closed) {
|
|
4652
|
-
|
|
5261
|
+
logger6.warn("Attempted to create a client (clientId: %s) on a closed call %s", settings.clientId, this.callId);
|
|
4653
5262
|
return void 0;
|
|
4654
5263
|
}
|
|
4655
5264
|
if (this.observedClients.has(settings.clientId)) {
|
|
4656
|
-
|
|
5265
|
+
logger6.warn("Client with id %s already exists in call %s; returning the existing instance", settings.clientId, this.callId);
|
|
4657
5266
|
return this.observedClients.get(settings.clientId);
|
|
4658
5267
|
}
|
|
4659
5268
|
const clientSettings = {
|
|
@@ -4703,10 +5312,17 @@ var ObservedCall = class extends import_events3.EventEmitter {
|
|
|
4703
5312
|
clearTimeout(this.closeTimer);
|
|
4704
5313
|
this.closeTimer = void 0;
|
|
4705
5314
|
}
|
|
5315
|
+
const jsonlPath = this.observer.config.createClientJsonlSink?.({ observedCall: this, clientId: settings.clientId });
|
|
5316
|
+
let jsonlSink;
|
|
5317
|
+
if (jsonlPath !== void 0 && this.observer.sinks.get(settings.clientId)) {
|
|
5318
|
+
logger6.warn("No sample file for client %s in call %s: sink id %s is already in use", settings.clientId, this.callId, settings.clientId);
|
|
5319
|
+
} else if (jsonlPath !== void 0) {
|
|
5320
|
+
jsonlSink = this.sinks.create(JsonlFileSink, { id: settings.clientId, path: jsonlPath });
|
|
5321
|
+
}
|
|
4706
5322
|
this.emit("newclient", result);
|
|
4707
5323
|
this._notify("client-added", { ...this.eventScope, observedClient: result });
|
|
4708
|
-
if (
|
|
4709
|
-
this._notify("client-sink-created", { ...this.eventScope, observedClient: result, sink:
|
|
5324
|
+
if (jsonlSink) {
|
|
5325
|
+
this._notify("client-sink-created", { ...this.eventScope, observedClient: result, sink: jsonlSink });
|
|
4710
5326
|
}
|
|
4711
5327
|
if (wasEmpty) {
|
|
4712
5328
|
this.emit("not-empty");
|
|
@@ -4759,10 +5375,10 @@ var ObservedCall = class extends import_events3.EventEmitter {
|
|
|
4759
5375
|
};
|
|
4760
5376
|
|
|
4761
5377
|
// src/Observer.ts
|
|
4762
|
-
var
|
|
5378
|
+
var import_node_events7 = require("events");
|
|
4763
5379
|
|
|
4764
5380
|
// src/ObservedTURN.ts
|
|
4765
|
-
var
|
|
5381
|
+
var import_node_events5 = require("events");
|
|
4766
5382
|
|
|
4767
5383
|
// src/ObservedTurnServer.ts
|
|
4768
5384
|
var ObservedTurnServer = class {
|
|
@@ -4822,8 +5438,8 @@ var ObservedTurnServer = class {
|
|
|
4822
5438
|
};
|
|
4823
5439
|
|
|
4824
5440
|
// src/ObservedTURN.ts
|
|
4825
|
-
var
|
|
4826
|
-
var ObservedTURN = class extends
|
|
5441
|
+
var logger7 = createLogger("ObservedTURN");
|
|
5442
|
+
var ObservedTURN = class extends import_node_events5.EventEmitter {
|
|
4827
5443
|
totalBytesSent = 0;
|
|
4828
5444
|
totalBytesReceived = 0;
|
|
4829
5445
|
totalPacketsSent = 0;
|
|
@@ -4852,12 +5468,12 @@ var ObservedTURN = class extends import_events4.EventEmitter {
|
|
|
4852
5468
|
addPeerConnection(peerConnection) {
|
|
4853
5469
|
const turnPairs = peerConnection.selectedIceCandiadtePairForTurn;
|
|
4854
5470
|
if (turnPairs.length !== 1) {
|
|
4855
|
-
return
|
|
5471
|
+
return logger7.warn(`Expected exactly one TURN pair, but found for peerconnection ${peerConnection.peerConnectionId}`, turnPairs.length), void 0;
|
|
4856
5472
|
}
|
|
4857
5473
|
const candidatePair = turnPairs[0];
|
|
4858
5474
|
const rawUrl = candidatePair.getLocalCandidate()?.url;
|
|
4859
5475
|
if (!rawUrl) {
|
|
4860
|
-
return
|
|
5476
|
+
return logger7.warn(`No local candidate URL found for peerconnection ${peerConnection.peerConnectionId}`), void 0;
|
|
4861
5477
|
}
|
|
4862
5478
|
const turnServerUrl = rawUrl.split("?")[0];
|
|
4863
5479
|
let turnServer = this.servers.get(turnServerUrl);
|
|
@@ -4933,14 +5549,16 @@ var MiddlewareProcessor = class {
|
|
|
4933
5549
|
};
|
|
4934
5550
|
|
|
4935
5551
|
// src/ObservedMediasoupRouter.ts
|
|
4936
|
-
var
|
|
4937
|
-
var
|
|
4938
|
-
var ObservedMediasoupRouter = class extends
|
|
5552
|
+
var import_node_events6 = require("events");
|
|
5553
|
+
var logger8 = createLogger("ObservedMediasoupRouter");
|
|
5554
|
+
var ObservedMediasoupRouter = class extends import_node_events6.EventEmitter {
|
|
4939
5555
|
router;
|
|
4940
5556
|
appData;
|
|
4941
5557
|
sample;
|
|
4942
5558
|
webrtcTransportIds = /* @__PURE__ */ new Set();
|
|
4943
5559
|
closed = false;
|
|
5560
|
+
sinks;
|
|
5561
|
+
extensionStats;
|
|
4944
5562
|
// Id -> sample indexes over the arrays in `this.sample`, so an entity can be reached in O(1)
|
|
4945
5563
|
// instead of scanning (`sample.producers.find(...)`). They hold the *same* objects as the arrays.
|
|
4946
5564
|
_transportSamples = /* @__PURE__ */ new Map();
|
|
@@ -4949,11 +5567,17 @@ var ObservedMediasoupRouter = class extends import_events5.EventEmitter {
|
|
|
4949
5567
|
_dataProducerSamples = /* @__PURE__ */ new Map();
|
|
4950
5568
|
_dataConsumerSamples = /* @__PURE__ */ new Map();
|
|
4951
5569
|
_enrich;
|
|
4952
|
-
|
|
5570
|
+
/**
|
|
5571
|
+
* @param parentSinks the parent of `sinks`: `observer.sinks`, passed by
|
|
5572
|
+
* `Observer.createObservedMediasoupRouter`. Internal; a router constructed directly has none.
|
|
5573
|
+
*/
|
|
5574
|
+
constructor(settings, parentSinks) {
|
|
4953
5575
|
super();
|
|
4954
5576
|
this.router = settings.router;
|
|
4955
5577
|
this.appData = settings.appData ?? {};
|
|
4956
5578
|
this._enrich = settings.enrich;
|
|
5579
|
+
this.sinks = new SinkRegistry(parentSinks);
|
|
5580
|
+
this.extensionStats = new ExtensionStatsRegistry(this.router.id, this.sinks);
|
|
4957
5581
|
this.sample = {
|
|
4958
5582
|
routerId: this.router.id,
|
|
4959
5583
|
attachments: settings.attachments ?? {},
|
|
@@ -5022,8 +5646,19 @@ var ObservedMediasoupRouter = class extends import_events5.EventEmitter {
|
|
|
5022
5646
|
if (this.closed) return;
|
|
5023
5647
|
this.closed = true;
|
|
5024
5648
|
if (this.sample.closedAt === void 0) this.sample.closedAt = Date.now();
|
|
5649
|
+
this.extensionStats.close();
|
|
5650
|
+
this.sinks.clear();
|
|
5025
5651
|
this.emit("close");
|
|
5026
5652
|
}
|
|
5653
|
+
createExtensionStats(settings) {
|
|
5654
|
+
return this.extensionStats.create(settings);
|
|
5655
|
+
}
|
|
5656
|
+
acceptExtensionStats(id, data) {
|
|
5657
|
+
this.extensionStats.accept(id, data);
|
|
5658
|
+
}
|
|
5659
|
+
getExtensionStats(id) {
|
|
5660
|
+
return this.extensionStats.get(id);
|
|
5661
|
+
}
|
|
5027
5662
|
addTransport = (transport) => {
|
|
5028
5663
|
let transportType = transport.type;
|
|
5029
5664
|
if (!transportType) {
|
|
@@ -5032,7 +5667,7 @@ var ObservedMediasoupRouter = class extends import_events5.EventEmitter {
|
|
|
5032
5667
|
else if (transport.constructor.name === "PipeTransport") transportType = "pipe";
|
|
5033
5668
|
else if (transport.constructor.name === "DirectTransport") transportType = "direct";
|
|
5034
5669
|
}
|
|
5035
|
-
if (!transportType) return
|
|
5670
|
+
if (!transportType) return logger8.warn(`Unknown transport type for transport ${transport.id}: ${transport.constructor.name}`);
|
|
5036
5671
|
switch (transportType) {
|
|
5037
5672
|
case "webrtc":
|
|
5038
5673
|
return this.addWebRtcTransport(transport);
|
|
@@ -5043,7 +5678,7 @@ var ObservedMediasoupRouter = class extends import_events5.EventEmitter {
|
|
|
5043
5678
|
case "direct":
|
|
5044
5679
|
return this.addDirectTransport(transport);
|
|
5045
5680
|
default:
|
|
5046
|
-
return
|
|
5681
|
+
return logger8.warn(`Unsupported transport type for transport ${transport.id}: ${transportType}`);
|
|
5047
5682
|
}
|
|
5048
5683
|
};
|
|
5049
5684
|
addWebRtcTransport(transport) {
|
|
@@ -5285,7 +5920,7 @@ var ObservedMediasoupRouter = class extends import_events5.EventEmitter {
|
|
|
5285
5920
|
const attachments = enrich();
|
|
5286
5921
|
if (attachments) sample.attachments = { ...sample.attachments, ...attachments };
|
|
5287
5922
|
} catch (err) {
|
|
5288
|
-
|
|
5923
|
+
logger8.warn("Mediasoup sample enricher threw for router %s: %o", this.id, err);
|
|
5289
5924
|
}
|
|
5290
5925
|
}
|
|
5291
5926
|
_attachTransportObserverListeners(transport, transportSample, onClose) {
|
|
@@ -5310,7 +5945,7 @@ var ObservedMediasoupRouter = class extends import_events5.EventEmitter {
|
|
|
5310
5945
|
};
|
|
5311
5946
|
|
|
5312
5947
|
// src/issues/ActiveIssuesRegistry.ts
|
|
5313
|
-
var
|
|
5948
|
+
var logger9 = createLogger("ActiveIssuesRegistry");
|
|
5314
5949
|
var ActiveIssuesRegistry = class {
|
|
5315
5950
|
constructor(parent) {
|
|
5316
5951
|
this.parent = parent;
|
|
@@ -5396,7 +6031,7 @@ var ActiveIssuesRegistry = class {
|
|
|
5396
6031
|
try {
|
|
5397
6032
|
apply(target);
|
|
5398
6033
|
} catch (err) {
|
|
5399
|
-
|
|
6034
|
+
logger9.warn("An issue tracker threw while being notified; continuing with the rest. %o", err);
|
|
5400
6035
|
}
|
|
5401
6036
|
}
|
|
5402
6037
|
};
|
|
@@ -5425,7 +6060,7 @@ function createCallSummary(callId, config) {
|
|
|
5425
6060
|
}
|
|
5426
6061
|
|
|
5427
6062
|
// src/summaries/CallSummaryCollector.ts
|
|
5428
|
-
var
|
|
6063
|
+
var logger10 = createLogger("CallSummaryCollector");
|
|
5429
6064
|
var CallSummaryCollector = class {
|
|
5430
6065
|
constructor(_observer, _config) {
|
|
5431
6066
|
this._observer = _observer;
|
|
@@ -5500,7 +6135,7 @@ var CallSummaryCollector = class {
|
|
|
5500
6135
|
try {
|
|
5501
6136
|
handler(summary, scratch, ...args);
|
|
5502
6137
|
} catch (err) {
|
|
5503
|
-
|
|
6138
|
+
logger10.warn("A call-summary handler for %s threw; continuing. %o", event, err);
|
|
5504
6139
|
}
|
|
5505
6140
|
};
|
|
5506
6141
|
this._observer.on(event, listener);
|
|
@@ -5925,7 +6560,7 @@ function geohash(location, precision = 3) {
|
|
|
5925
6560
|
}
|
|
5926
6561
|
|
|
5927
6562
|
// src/detectors/ClientPopulationIssueDetector.ts
|
|
5928
|
-
var
|
|
6563
|
+
var logger11 = createLogger("ClientPopulationIssueDetector");
|
|
5929
6564
|
var ClientPopulationIssueTypes = {
|
|
5930
6565
|
/** One issue type is concentrated on one client population while the rest of the fleet is fine. */
|
|
5931
6566
|
clientPopulationIssue: "CLIENT_POPULATION_ISSUE"
|
|
@@ -5947,7 +6582,7 @@ var ClientPopulationIssueDetector = class _ClientPopulationIssueDetector {
|
|
|
5947
6582
|
...config
|
|
5948
6583
|
};
|
|
5949
6584
|
if (this._config.groupBy === "location" && !this._config.resolveClientLocation) {
|
|
5950
|
-
|
|
6585
|
+
logger11.warn(
|
|
5951
6586
|
"%s is configured with groupBy: 'location' but no resolveClientLocation, so no client can be placed and nothing will ever be detected",
|
|
5952
6587
|
this.name
|
|
5953
6588
|
);
|
|
@@ -6781,11 +7416,13 @@ function mediaKindOf(mimeType) {
|
|
|
6781
7416
|
}
|
|
6782
7417
|
|
|
6783
7418
|
// src/Observer.ts
|
|
6784
|
-
var
|
|
6785
|
-
var Observer = class extends
|
|
7419
|
+
var logger12 = createLogger("Observer");
|
|
7420
|
+
var Observer = class extends import_node_events7.EventEmitter {
|
|
6786
7421
|
observedTURN = new ObservedTURN();
|
|
6787
7422
|
observedCalls = /* @__PURE__ */ new Map();
|
|
6788
7423
|
observedMediasoupRouters = /* @__PURE__ */ new Map();
|
|
7424
|
+
sinks = new SinkRegistry();
|
|
7425
|
+
extensionStats = new ExtensionStatsRegistry("observer", this.sinks);
|
|
6789
7426
|
/** Ancestry base shared by all Observer-bus events originating at the observer. */
|
|
6790
7427
|
eventScope = { observer: this };
|
|
6791
7428
|
config;
|
|
@@ -6900,7 +7537,7 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6900
7537
|
break;
|
|
6901
7538
|
}
|
|
6902
7539
|
default: {
|
|
6903
|
-
|
|
7540
|
+
logger12.warn("Unknown detector name %s; skipping", name);
|
|
6904
7541
|
return this;
|
|
6905
7542
|
}
|
|
6906
7543
|
}
|
|
@@ -6985,7 +7622,7 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6985
7622
|
break;
|
|
6986
7623
|
}
|
|
6987
7624
|
if (!validator) {
|
|
6988
|
-
|
|
7625
|
+
logger12.warn("Unknown validator name %s; skipping", name);
|
|
6989
7626
|
return this;
|
|
6990
7627
|
}
|
|
6991
7628
|
this.validators.add(validator);
|
|
@@ -7016,7 +7653,7 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
7016
7653
|
try {
|
|
7017
7654
|
validator.cancel(reason);
|
|
7018
7655
|
} catch (err) {
|
|
7019
|
-
|
|
7656
|
+
logger12.warn("Error cancelling validator %s: %o", validator.name, err);
|
|
7020
7657
|
}
|
|
7021
7658
|
}
|
|
7022
7659
|
return matching.length;
|
|
@@ -7032,11 +7669,11 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
7032
7669
|
*/
|
|
7033
7670
|
createObservedCall(settings, acceptCtx) {
|
|
7034
7671
|
if (this.closed) {
|
|
7035
|
-
|
|
7672
|
+
logger12.warn("Attempted to create a call (callId: %s) on a closed observer", settings.callId);
|
|
7036
7673
|
return void 0;
|
|
7037
7674
|
}
|
|
7038
7675
|
if (this.observedCalls.has(settings.callId)) {
|
|
7039
|
-
|
|
7676
|
+
logger12.warn("Observed Call with id %s already exists; returning the existing instance", settings.callId);
|
|
7040
7677
|
return this.observedCalls.get(settings.callId);
|
|
7041
7678
|
}
|
|
7042
7679
|
const callSettings = {
|
|
@@ -7074,14 +7711,14 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
7074
7711
|
}
|
|
7075
7712
|
createObservedMediasoupRouter(settings) {
|
|
7076
7713
|
if (this.closed) {
|
|
7077
|
-
|
|
7714
|
+
logger12.warn("Attempted to create mediasoup router (id: %d) on a closed observer", settings.router.id);
|
|
7078
7715
|
return void 0;
|
|
7079
7716
|
}
|
|
7080
7717
|
if (this.observedMediasoupRouters.has(settings.router.id)) {
|
|
7081
|
-
|
|
7718
|
+
logger12.warn("Observed Mediasoup Router (id %s) already exists; returning the existing instance", settings.router.id);
|
|
7082
7719
|
return this.observedMediasoupRouters.get(settings.router.id);
|
|
7083
7720
|
}
|
|
7084
|
-
const observedMediasoupRouter = new ObservedMediasoupRouter(settings);
|
|
7721
|
+
const observedMediasoupRouter = new ObservedMediasoupRouter(settings, this.sinks);
|
|
7085
7722
|
const observedMediasoupRouterScope = {
|
|
7086
7723
|
observedMediasoupRouter,
|
|
7087
7724
|
observer: this
|
|
@@ -7109,7 +7746,7 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
7109
7746
|
}
|
|
7110
7747
|
close() {
|
|
7111
7748
|
if (this.closed) {
|
|
7112
|
-
return
|
|
7749
|
+
return logger12.debug("Attempted to close twice");
|
|
7113
7750
|
}
|
|
7114
7751
|
this.closed = true;
|
|
7115
7752
|
for (const call of [...this.observedCalls.values()]) call.close();
|
|
@@ -7118,8 +7755,19 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
7118
7755
|
this.validators.clear();
|
|
7119
7756
|
this.callSummaryCollector?.close();
|
|
7120
7757
|
this.activeIssuesRegistry.clear();
|
|
7758
|
+
this.extensionStats.close();
|
|
7759
|
+
this.sinks.clear();
|
|
7121
7760
|
this._notify("observer-closed", { ...this.eventScope });
|
|
7122
7761
|
}
|
|
7762
|
+
createExtensionStats(settings) {
|
|
7763
|
+
return this.extensionStats.create(settings);
|
|
7764
|
+
}
|
|
7765
|
+
acceptExtensionStats(id, data) {
|
|
7766
|
+
this.extensionStats.accept(id, data);
|
|
7767
|
+
}
|
|
7768
|
+
getExtensionStats(id) {
|
|
7769
|
+
return this.extensionStats.get(id);
|
|
7770
|
+
}
|
|
7123
7771
|
accept(sample, context) {
|
|
7124
7772
|
if (this.closed) {
|
|
7125
7773
|
return this._notify("sample-rejected", { ...this.eventScope, reason: "observer-closed", sample });
|
|
@@ -7127,7 +7775,7 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
7127
7775
|
try {
|
|
7128
7776
|
this.acceptMiddlewares.process({ sample, context });
|
|
7129
7777
|
} catch (err) {
|
|
7130
|
-
|
|
7778
|
+
logger12.warn("An accept middleware threw; dropping the sample. %o", err);
|
|
7131
7779
|
}
|
|
7132
7780
|
if (!sample.callId) {
|
|
7133
7781
|
this._notify("sample-rejected", { ...this.eventScope, reason: "missing-callId", sample });
|
|
@@ -7167,7 +7815,7 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
7167
7815
|
try {
|
|
7168
7816
|
validator.update();
|
|
7169
7817
|
} catch (err) {
|
|
7170
|
-
|
|
7818
|
+
logger12.warn("Error running validator %s: %o", validator.name, err);
|
|
7171
7819
|
}
|
|
7172
7820
|
}
|
|
7173
7821
|
this._notify("observer-updated", { ...this.eventScope });
|
|
@@ -7403,67 +8051,34 @@ var TrendTester = class {
|
|
|
7403
8051
|
}
|
|
7404
8052
|
};
|
|
7405
8053
|
|
|
7406
|
-
// src/sinks/ClientSampleSink.ts
|
|
7407
|
-
var import_events7 = require("events");
|
|
7408
|
-
var ClientSampleSink = class extends import_events7.EventEmitter {
|
|
7409
|
-
};
|
|
7410
|
-
|
|
7411
|
-
// src/sinks/JsonlFileSink.ts
|
|
7412
|
-
var fs = __toESM(require("fs"));
|
|
7413
|
-
var path = __toESM(require("path"));
|
|
7414
|
-
var JsonlFileSink = class extends ClientSampleSink {
|
|
7415
|
-
/** The file this sink writes to. Read it (e.g. in a `close` handler) to upload/move the file. */
|
|
7416
|
-
path;
|
|
7417
|
-
_stream;
|
|
7418
|
-
_serializeSample;
|
|
7419
|
-
constructor(options) {
|
|
7420
|
-
super();
|
|
7421
|
-
this.path = options.path;
|
|
7422
|
-
this._stream = fs.createWriteStream(this.path, { flags: "a" });
|
|
7423
|
-
this._stream.on("close", () => this.emit("close"));
|
|
7424
|
-
this._stream.on("finish", () => this.emit("finish"));
|
|
7425
|
-
this._stream.on("drain", () => this.emit("drain"));
|
|
7426
|
-
this._stream.on("error", (err) => this.emit("error", err));
|
|
7427
|
-
this._serializeSample = options.serializeSample ?? ((sample) => JSON.stringify(sample));
|
|
7428
|
-
}
|
|
7429
|
-
write(sample) {
|
|
7430
|
-
return this._stream.write(`${this._serializeSample(sample)}
|
|
7431
|
-
`);
|
|
7432
|
-
}
|
|
7433
|
-
end() {
|
|
7434
|
-
this._stream.end();
|
|
7435
|
-
}
|
|
7436
|
-
};
|
|
7437
|
-
function createJsonlFileSink(options) {
|
|
7438
|
-
return new JsonlFileSink(options);
|
|
7439
|
-
}
|
|
7440
|
-
function createJsonlFileSinkFactory(options) {
|
|
7441
|
-
return ({ clientId, observedCall }) => {
|
|
7442
|
-
const filename = options.getFileName?.({ callId: observedCall.callId, clientId }) ?? `${observedCall.callId}__${clientId}.jsonl`;
|
|
7443
|
-
return createJsonlFileSink({
|
|
7444
|
-
path: path.join(options.directory, filename),
|
|
7445
|
-
serializeSample: options.serializeSample
|
|
7446
|
-
});
|
|
7447
|
-
};
|
|
7448
|
-
}
|
|
7449
|
-
|
|
7450
8054
|
// src/sinks/InMemorySink.ts
|
|
7451
|
-
var
|
|
7452
|
-
|
|
8055
|
+
var import_node_events8 = require("events");
|
|
8056
|
+
var InMemorySink = class extends import_node_events8.EventEmitter {
|
|
8057
|
+
id;
|
|
8058
|
+
samples;
|
|
8059
|
+
_ended = false;
|
|
8060
|
+
constructor(settings = {}) {
|
|
7453
8061
|
super();
|
|
7454
|
-
this.
|
|
8062
|
+
this.id = settings.id ?? "in-memory";
|
|
8063
|
+
this.samples = settings.samples ?? [];
|
|
8064
|
+
}
|
|
8065
|
+
get ended() {
|
|
8066
|
+
return this._ended;
|
|
7455
8067
|
}
|
|
7456
|
-
samples;
|
|
7457
8068
|
write(sample) {
|
|
8069
|
+
if (this._ended) return false;
|
|
7458
8070
|
this.samples.push(sample);
|
|
7459
8071
|
return true;
|
|
7460
8072
|
}
|
|
7461
8073
|
end() {
|
|
8074
|
+
if (this._ended) return;
|
|
8075
|
+
this._ended = true;
|
|
8076
|
+
this.emit("ended");
|
|
7462
8077
|
this.emit("close");
|
|
7463
8078
|
}
|
|
7464
8079
|
};
|
|
7465
8080
|
function createInMemorySink(samples) {
|
|
7466
|
-
return new InMemorySink(samples);
|
|
8081
|
+
return new InMemorySink({ samples });
|
|
7467
8082
|
}
|
|
7468
8083
|
|
|
7469
8084
|
// src/resolvers/RemoteTrackResolver.ts
|
|
@@ -7641,6 +8256,8 @@ function createP2pRemoteTrackResolverFactory() {
|
|
|
7641
8256
|
// Annotate the CommonJS export names for ESM import in node:
|
|
7642
8257
|
0 && (module.exports = {
|
|
7643
8258
|
ActiveIssuesRegistry,
|
|
8259
|
+
AudioEchoDetector,
|
|
8260
|
+
AudioEchoTypes,
|
|
7644
8261
|
CODEC_MISMATCH_ISSUE,
|
|
7645
8262
|
CallConcurrentIssueDetector,
|
|
7646
8263
|
CallConcurrentIssueTypes,
|
|
@@ -7650,9 +8267,9 @@ function createP2pRemoteTrackResolverFactory() {
|
|
|
7650
8267
|
ClientMetaTypes,
|
|
7651
8268
|
ClientPopulationIssueDetector,
|
|
7652
8269
|
ClientPopulationIssueTypes,
|
|
7653
|
-
ClientSampleSink,
|
|
7654
8270
|
CodecConsistencyValidator,
|
|
7655
8271
|
Detectors,
|
|
8272
|
+
ExtensionStatsRegistry,
|
|
7656
8273
|
GEOHASH_CELL_SIZES,
|
|
7657
8274
|
InMemorySink,
|
|
7658
8275
|
IssueFanOutDetector,
|
|
@@ -7688,10 +8305,15 @@ function createP2pRemoteTrackResolverFactory() {
|
|
|
7688
8305
|
PublisherFaultCorroborationDetector,
|
|
7689
8306
|
PublisherFaultTypes,
|
|
7690
8307
|
RESOLVED_ISSUE_SUFFIX,
|
|
8308
|
+
RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS,
|
|
8309
|
+
RTT_HISTOGRAM_NUMBER_OF_BUCKETS,
|
|
7691
8310
|
RemoteTrackResolver,
|
|
7692
8311
|
RemoteTrackResolverValidator,
|
|
8312
|
+
SCORE_HISTOGRAM_BUCKET_WIDTH,
|
|
8313
|
+
SCORE_HISTOGRAM_NUMBER_OF_BUCKETS,
|
|
7693
8314
|
SfuCongestionDetector,
|
|
7694
8315
|
SimulcastReceiverValidator,
|
|
8316
|
+
SinkRegistry,
|
|
7695
8317
|
SlidingWindow,
|
|
7696
8318
|
TrackDeliveryMismatchDetector,
|
|
7697
8319
|
TrackDeliveryMismatchTypes,
|
|
@@ -7712,7 +8334,6 @@ function createP2pRemoteTrackResolverFactory() {
|
|
|
7712
8334
|
createDefaultMediasoupRemoteTrackResolverFactory,
|
|
7713
8335
|
createInMemorySink,
|
|
7714
8336
|
createJsonlFileSink,
|
|
7715
|
-
createJsonlFileSinkFactory,
|
|
7716
8337
|
createLogger,
|
|
7717
8338
|
createP2pRemoteTrackResolverFactory,
|
|
7718
8339
|
defaultCallSummaryConfig,
|
|
@@ -7727,6 +8348,7 @@ function createP2pRemoteTrackResolverFactory() {
|
|
|
7727
8348
|
medianAbsoluteDeviation,
|
|
7728
8349
|
pageHinkley,
|
|
7729
8350
|
percentile,
|
|
8351
|
+
percentileOfHistogram,
|
|
7730
8352
|
percentileOfSorted,
|
|
7731
8353
|
robustZScore,
|
|
7732
8354
|
schemaVersion,
|