@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.mjs
CHANGED
|
@@ -37,18 +37,189 @@ function createLogger(moduleName) {
|
|
|
37
37
|
}
|
|
38
38
|
}();
|
|
39
39
|
}
|
|
40
|
-
function setObserverLogger(
|
|
41
|
-
mainLogger =
|
|
40
|
+
function setObserverLogger(logger13) {
|
|
41
|
+
mainLogger = logger13;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// src/ObservedCall.ts
|
|
45
|
-
import { EventEmitter as
|
|
45
|
+
import { EventEmitter as EventEmitter4 } from "events";
|
|
46
|
+
|
|
47
|
+
// src/sinks/SinkRegistry.ts
|
|
48
|
+
import { v4 as uuidv4 } from "uuid";
|
|
49
|
+
var logger = createLogger("SinkRegistry");
|
|
50
|
+
var SinkRegistry = class {
|
|
51
|
+
constructor(parent) {
|
|
52
|
+
this.parent = parent;
|
|
53
|
+
}
|
|
54
|
+
parent;
|
|
55
|
+
// This registry's sinks and its descendants'.
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
+
_sinks = /* @__PURE__ */ new Map();
|
|
58
|
+
/** Number of open sinks in this registry and its descendants. */
|
|
59
|
+
get size() {
|
|
60
|
+
return this._sinks.size;
|
|
61
|
+
}
|
|
62
|
+
keys() {
|
|
63
|
+
return this._sinks.keys();
|
|
64
|
+
}
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
|
+
values() {
|
|
67
|
+
return this._sinks.values();
|
|
68
|
+
}
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
get(id) {
|
|
71
|
+
return this._sinks.get(id);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Construct `new SinkClass(settings)` and register it. The id (`settings.id`, generated when
|
|
75
|
+
* omitted) is checked first, so nothing is constructed — no file is opened — when it is taken:
|
|
76
|
+
* `undefined` is returned and a warning logged.
|
|
77
|
+
*/
|
|
78
|
+
create(SinkClass, settings) {
|
|
79
|
+
const lineage = this._lineage();
|
|
80
|
+
const id = settings.id ?? uuidv4();
|
|
81
|
+
if (lineage[lineage.length - 1]._sinks.has(id)) {
|
|
82
|
+
logger.warn("Sink id %s is already in use; sink ids must be unique per observer. No sink was created", id);
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
const sink = new SinkClass({ ...settings, id });
|
|
86
|
+
const onError = (err) => logger.warn("Sink %s failed: %o", id, err);
|
|
87
|
+
sink.on("error", onError);
|
|
88
|
+
sink.once("close", () => sink.off("error", onError));
|
|
89
|
+
sink.once("ended", () => {
|
|
90
|
+
for (const registry of lineage) {
|
|
91
|
+
if (registry._sinks.get(id) === sink) registry._sinks.delete(id);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
for (const registry of lineage) registry._sinks.set(id, sink);
|
|
95
|
+
return sink;
|
|
96
|
+
}
|
|
97
|
+
/** Write `record` to the sink `id`. `false` when there is no such sink, or on backpressure. */
|
|
98
|
+
write(id, record) {
|
|
99
|
+
const sink = this._sinks.get(id);
|
|
100
|
+
if (!sink) {
|
|
101
|
+
logger.debug("No sink %s; record dropped", id);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
return sink.write(record);
|
|
105
|
+
}
|
|
106
|
+
/** End every sink of this registry and of its descendants. */
|
|
107
|
+
clear() {
|
|
108
|
+
for (const sink of [...this._sinks.values()]) {
|
|
109
|
+
try {
|
|
110
|
+
sink.end();
|
|
111
|
+
} catch (err) {
|
|
112
|
+
logger.warn("Sink %s failed to end: %o", sink.id, err);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** This registry and its ancestors, up to the root. */
|
|
117
|
+
_lineage() {
|
|
118
|
+
return [this, ...this.parent?._lineage() ?? []];
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// src/sinks/ExtensionStatsRegistry.ts
|
|
123
|
+
var logger2 = createLogger("ExtensionStats");
|
|
124
|
+
var ExtensionStatsRegistry = class {
|
|
125
|
+
constructor(id, _sinks) {
|
|
126
|
+
this.id = id;
|
|
127
|
+
this._sinks = _sinks;
|
|
128
|
+
}
|
|
129
|
+
id;
|
|
130
|
+
_sinks;
|
|
131
|
+
/** The last data accepted for each extension stats id. */
|
|
132
|
+
values = /* @__PURE__ */ new Map();
|
|
133
|
+
closed = false;
|
|
134
|
+
/**
|
|
135
|
+
* Refuse further `create` / `accept`, and end the extension stats' sinks (the sinks whose id is
|
|
136
|
+
* an extension stats id). Other sinks of the owner, e.g. client sample sinks, are left to the
|
|
137
|
+
* owner's `sinks.clear()`. Called by the owner when it closes.
|
|
138
|
+
*/
|
|
139
|
+
close() {
|
|
140
|
+
if (this.closed) return;
|
|
141
|
+
this.closed = true;
|
|
142
|
+
for (const id of this.values.keys()) {
|
|
143
|
+
this._sinks.get(id)?.end();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
create(settings) {
|
|
147
|
+
if (this.closed) {
|
|
148
|
+
logger2.warn("Attempted to create extension stats %s on closed %s", settings.id, this.id);
|
|
149
|
+
return void 0;
|
|
150
|
+
}
|
|
151
|
+
if (this.values.has(settings.id)) {
|
|
152
|
+
logger2.warn("Extension stats with id %s already exists on %s", settings.id, this.id);
|
|
153
|
+
return void 0;
|
|
154
|
+
}
|
|
155
|
+
this.values.set(settings.id, void 0);
|
|
156
|
+
if (!settings.createSink) return void 0;
|
|
157
|
+
const [SinkClass, sinkSettings] = settings.createSink;
|
|
158
|
+
this._sinks.get(settings.id)?.end();
|
|
159
|
+
return this._sinks.create(SinkClass, { ...sinkSettings, id: settings.id });
|
|
160
|
+
}
|
|
161
|
+
accept(id, data) {
|
|
162
|
+
if (this.closed) {
|
|
163
|
+
logger2.debug("Extension stats %s dropped: %s is closed", id, this.id);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.values.set(id, data);
|
|
167
|
+
try {
|
|
168
|
+
this._sinks.get(id)?.write(data);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
logger2.warn("Failed to write extension stats %s to sink: %o", id, err);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
get(id) {
|
|
174
|
+
return this.values.get(id);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// src/sinks/JsonlFileSink.ts
|
|
179
|
+
import * as fs from "fs";
|
|
180
|
+
import { EventEmitter } from "events";
|
|
181
|
+
var JsonlFileSink = class extends EventEmitter {
|
|
182
|
+
id;
|
|
183
|
+
/** The file this sink writes to. Read it (e.g. in a `close` handler) to upload/move the file. */
|
|
184
|
+
path;
|
|
185
|
+
_stream;
|
|
186
|
+
_serializeSample;
|
|
187
|
+
_ended = false;
|
|
188
|
+
constructor(options) {
|
|
189
|
+
super();
|
|
190
|
+
this.id = options.id ?? options.path;
|
|
191
|
+
this.path = options.path;
|
|
192
|
+
this._stream = fs.createWriteStream(this.path, { flags: "a" });
|
|
193
|
+
this._stream.on("close", () => this.emit("close"));
|
|
194
|
+
this._stream.on("finish", () => this.emit("finish"));
|
|
195
|
+
this._stream.on("drain", () => this.emit("drain"));
|
|
196
|
+
this._stream.on("error", (err) => this.emit("error", err));
|
|
197
|
+
this._serializeSample = options.serializeSample ?? ((record) => JSON.stringify(record));
|
|
198
|
+
}
|
|
199
|
+
get ended() {
|
|
200
|
+
return this._ended;
|
|
201
|
+
}
|
|
202
|
+
write(record) {
|
|
203
|
+
if (this._ended) return false;
|
|
204
|
+
return this._stream.write(`${this._serializeSample(record)}
|
|
205
|
+
`);
|
|
206
|
+
}
|
|
207
|
+
end() {
|
|
208
|
+
if (this._ended) return;
|
|
209
|
+
this._ended = true;
|
|
210
|
+
this.emit("ended");
|
|
211
|
+
this._stream.end();
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
function createJsonlFileSink(options) {
|
|
215
|
+
return new JsonlFileSink(options);
|
|
216
|
+
}
|
|
46
217
|
|
|
47
218
|
// src/ObservedClient.ts
|
|
48
|
-
import { EventEmitter as
|
|
219
|
+
import { EventEmitter as EventEmitter3 } from "events";
|
|
49
220
|
|
|
50
221
|
// src/ObservedPeerConnection.ts
|
|
51
|
-
import { EventEmitter } from "events";
|
|
222
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
52
223
|
|
|
53
224
|
// src/utils/stats.ts
|
|
54
225
|
function percentile(values, p) {
|
|
@@ -103,6 +274,10 @@ function counterDelta(previous, current) {
|
|
|
103
274
|
if (current < previous) return 0;
|
|
104
275
|
return current - previous;
|
|
105
276
|
}
|
|
277
|
+
function rmsAudioLevel(deltaTotalAudioEnergy, deltaTotalSamplesDuration) {
|
|
278
|
+
if (!(0 < deltaTotalSamplesDuration)) return void 0;
|
|
279
|
+
return Math.sqrt(Math.max(0, deltaTotalAudioEnergy) / deltaTotalSamplesDuration);
|
|
280
|
+
}
|
|
106
281
|
function correlation(xs, ys) {
|
|
107
282
|
const n = Math.min(xs.length, ys.length);
|
|
108
283
|
if (n < 2) return 0;
|
|
@@ -135,8 +310,8 @@ function pageHinkley(values, delta = 0, lambda = 50) {
|
|
|
135
310
|
let runningMinimum = 0;
|
|
136
311
|
for (let i = 0; i < values.length; i++) {
|
|
137
312
|
sum += values[i];
|
|
138
|
-
const
|
|
139
|
-
cumulative += values[i] -
|
|
313
|
+
const mean2 = sum / (i + 1);
|
|
314
|
+
cumulative += values[i] - mean2 - delta;
|
|
140
315
|
if (cumulative < runningMinimum) runningMinimum = cumulative;
|
|
141
316
|
const ph = cumulative - runningMinimum;
|
|
142
317
|
statistic.push(ph);
|
|
@@ -188,6 +363,25 @@ function erf(x) {
|
|
|
188
363
|
const y = 1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-(ax * ax));
|
|
189
364
|
return sign * y;
|
|
190
365
|
}
|
|
366
|
+
function percentileOfHistogram(histogram, bucketWidth, p, options = {}) {
|
|
367
|
+
let total = 0;
|
|
368
|
+
for (const count of histogram) total += count;
|
|
369
|
+
if (total < 1) return void 0;
|
|
370
|
+
const rank = Math.min(Math.max(p, 0), 1) * total;
|
|
371
|
+
let cumulative = 0;
|
|
372
|
+
for (let index = 0; index < histogram.length; ++index) {
|
|
373
|
+
const count = histogram[index];
|
|
374
|
+
if (count < 1 || cumulative + count < rank) {
|
|
375
|
+
cumulative += count;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (options.openEnded && index === histogram.length - 1 && options.max !== void 0) return options.max;
|
|
379
|
+
const lower = Math.max(index * bucketWidth, options.min ?? -Infinity);
|
|
380
|
+
const upper = Math.min((index + 1) * bucketWidth, options.max ?? Infinity);
|
|
381
|
+
return lower + (rank - cumulative) / count * Math.max(0, upper - lower);
|
|
382
|
+
}
|
|
383
|
+
return options.max;
|
|
384
|
+
}
|
|
191
385
|
|
|
192
386
|
// src/ObservedInboundRtp.ts
|
|
193
387
|
var ObservedInboundRtp = class {
|
|
@@ -298,6 +492,15 @@ var ObservedInboundRtp = class {
|
|
|
298
492
|
deltaFecPacketsReceived = 0;
|
|
299
493
|
deltaFecPacketsDiscarded = 0;
|
|
300
494
|
deltaPausesDuration = 0;
|
|
495
|
+
deltaTotalAudioEnergy = 0;
|
|
496
|
+
deltaTotalSamplesDuration = 0;
|
|
497
|
+
/**
|
|
498
|
+
* RMS level (0..1) of the audio played out from this stream over the tick, from the two deltas
|
|
499
|
+
* above. This is the far end's outgoing level **as this receiver heard it**, which is the only
|
|
500
|
+
* send-side level available for publishers on browsers that do not report `media-source` audio
|
|
501
|
+
* levels (Firefox). `undefined` for video and for a tick with no audio accounted for.
|
|
502
|
+
*/
|
|
503
|
+
rmsAudioLevel;
|
|
301
504
|
/**
|
|
302
505
|
* Mean jitter-buffer delay for the frames/samples emitted in this tick (seconds), derived from
|
|
303
506
|
* the cumulative `jitterBufferDelay` / `jitterBufferEmittedCount` pair — the only correct way to
|
|
@@ -383,6 +586,9 @@ var ObservedInboundRtp = class {
|
|
|
383
586
|
this.jitterBufferDelayInMs = void 0;
|
|
384
587
|
this.concealmentRatio = void 0;
|
|
385
588
|
this.framesDroppedRatio = void 0;
|
|
589
|
+
this.deltaTotalAudioEnergy = 0;
|
|
590
|
+
this.deltaTotalSamplesDuration = 0;
|
|
591
|
+
this.rmsAudioLevel = void 0;
|
|
386
592
|
const elapsedTimeInMs = stats.timestamp - this.timestamp;
|
|
387
593
|
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;
|
|
388
594
|
if (0 < elapsedTimeInMs && !this.counterResetBoundary) {
|
|
@@ -412,6 +618,9 @@ var ObservedInboundRtp = class {
|
|
|
412
618
|
this.deltaRetransmittedPacketsReceived = counterDelta(this.retransmittedPacketsReceived, stats.retransmittedPacketsReceived);
|
|
413
619
|
this.deltaFecPacketsReceived = counterDelta(this.fecPacketsReceived, stats.fecPacketsReceived);
|
|
414
620
|
this.deltaFecPacketsDiscarded = counterDelta(this.fecPacketsDiscarded, stats.fecPacketsDiscarded);
|
|
621
|
+
this.deltaTotalAudioEnergy = counterDelta(this.totalAudioEnergy, stats.totalAudioEnergy);
|
|
622
|
+
this.deltaTotalSamplesDuration = counterDelta(this.totalSamplesDuration, stats.totalSamplesDuration);
|
|
623
|
+
this.rmsAudioLevel = rmsAudioLevel(this.deltaTotalAudioEnergy, this.deltaTotalSamplesDuration);
|
|
415
624
|
this.bitrate = this.deltaBytesReceived * 8 / elapsedInSeconds;
|
|
416
625
|
const pixels = (stats.frameWidth ?? 0) * (stats.frameHeight ?? 0);
|
|
417
626
|
if (0 < pixels && 0 < this.deltaFramesReceived) {
|
|
@@ -469,6 +678,9 @@ var ObservedInboundRtp = class {
|
|
|
469
678
|
this.jitterBufferEmittedCount = stats.jitterBufferEmittedCount;
|
|
470
679
|
this.jitterBufferMinimumDelay = stats.jitterBufferMinimumDelay;
|
|
471
680
|
this.totalSamplesReceived = stats.totalSamplesReceived;
|
|
681
|
+
this.audioLevel = stats.audioLevel;
|
|
682
|
+
this.totalAudioEnergy = stats.totalAudioEnergy;
|
|
683
|
+
this.totalSamplesDuration = stats.totalSamplesDuration;
|
|
472
684
|
this.concealedSamples = stats.concealedSamples;
|
|
473
685
|
this.silentConcealedSamples = stats.silentConcealedSamples;
|
|
474
686
|
this.concealmentEvents = stats.concealmentEvents;
|
|
@@ -1123,6 +1335,16 @@ var ObservedMediaSource = class {
|
|
|
1123
1335
|
frames;
|
|
1124
1336
|
framesPerSecond;
|
|
1125
1337
|
attachments;
|
|
1338
|
+
/** Increase of `totalAudioEnergy` in this tick (audio only). */
|
|
1339
|
+
deltaTotalAudioEnergy = 0;
|
|
1340
|
+
/** Increase of `totalSamplesDuration` in this tick, in seconds (audio only). */
|
|
1341
|
+
deltaTotalSamplesDuration = 0;
|
|
1342
|
+
/**
|
|
1343
|
+
* RMS level (0..1) of what the capture device delivered over the tick, after the browser's own
|
|
1344
|
+
* processing (AEC, NS, AGC). Chrome and Safari report the inputs; Firefox does not, and there the
|
|
1345
|
+
* publisher's level has to come from its receivers — see `ObservedOutboundTrack.rmsAudioLevel`.
|
|
1346
|
+
*/
|
|
1347
|
+
rmsAudioLevel;
|
|
1126
1348
|
get visited() {
|
|
1127
1349
|
const visited = this._visited;
|
|
1128
1350
|
this._visited = false;
|
|
@@ -1136,6 +1358,14 @@ var ObservedMediaSource = class {
|
|
|
1136
1358
|
}
|
|
1137
1359
|
update(stats) {
|
|
1138
1360
|
this._visited = true;
|
|
1361
|
+
this.deltaTotalAudioEnergy = 0;
|
|
1362
|
+
this.deltaTotalSamplesDuration = 0;
|
|
1363
|
+
this.rmsAudioLevel = void 0;
|
|
1364
|
+
if (this.timestamp < stats.timestamp) {
|
|
1365
|
+
this.deltaTotalAudioEnergy = counterDelta(this.totalAudioEnergy, stats.totalAudioEnergy);
|
|
1366
|
+
this.deltaTotalSamplesDuration = counterDelta(this.totalSamplesDuration, stats.totalSamplesDuration);
|
|
1367
|
+
this.rmsAudioLevel = rmsAudioLevel(this.deltaTotalAudioEnergy, this.deltaTotalSamplesDuration);
|
|
1368
|
+
}
|
|
1139
1369
|
this.timestamp = stats.timestamp;
|
|
1140
1370
|
this.trackIdentifier = stats.trackIdentifier;
|
|
1141
1371
|
this.audioLevel = stats.audioLevel;
|
|
@@ -1534,6 +1764,20 @@ var ObservedOutboundTrack = class {
|
|
|
1534
1764
|
remoteFractionLost;
|
|
1535
1765
|
remoteRttInMs;
|
|
1536
1766
|
qualityLimitationReason;
|
|
1767
|
+
/**
|
|
1768
|
+
* This audio track's RMS level (0..1) over the latest tick — how loud the publisher was sending.
|
|
1769
|
+
* Taken from the publisher's own `media-source` when it reports one (Chrome, Safari), otherwise
|
|
1770
|
+
* the median of what its receivers heard ({@link receivedRmsAudioLevel}), which is what makes it
|
|
1771
|
+
* available for Firefox publishers too. `undefined` for video, or when neither is known.
|
|
1772
|
+
* {@link rmsAudioLevelSource} says which one it is.
|
|
1773
|
+
*/
|
|
1774
|
+
rmsAudioLevel;
|
|
1775
|
+
rmsAudioLevelSource;
|
|
1776
|
+
/**
|
|
1777
|
+
* Median RMS level (0..1) of this track across the receivers that played it out, each over its own
|
|
1778
|
+
* latest tick. Needs a `RemoteTrackResolver`: without links there are no receivers to ask.
|
|
1779
|
+
*/
|
|
1780
|
+
receivedRmsAudioLevel;
|
|
1537
1781
|
get score() {
|
|
1538
1782
|
return this.calculatedScore.value;
|
|
1539
1783
|
}
|
|
@@ -1554,6 +1798,10 @@ var ObservedOutboundTrack = class {
|
|
|
1554
1798
|
getMediaSource() {
|
|
1555
1799
|
return this._mediaSource;
|
|
1556
1800
|
}
|
|
1801
|
+
/** The capture device behind this track, when the client sent `AUDIO_INPUT_DEVICE` metadata for it. */
|
|
1802
|
+
get audioInputDevice() {
|
|
1803
|
+
return this._peerConnection.client.audioInputDevices.get(this.id);
|
|
1804
|
+
}
|
|
1557
1805
|
update(stats) {
|
|
1558
1806
|
this._visited = true;
|
|
1559
1807
|
this.timestamp = stats.timestamp;
|
|
@@ -1585,8 +1833,30 @@ var ObservedOutboundTrack = class {
|
|
|
1585
1833
|
if (this.muted !== true && this.deltaPacketsSent === 0) reasons.push("no-packets-sent");
|
|
1586
1834
|
this.degradedReasons = reasons.length === 0 ? void 0 : reasons;
|
|
1587
1835
|
this.receivingDistribution = this.createReceivingDistribution();
|
|
1836
|
+
this._updateAudioLevel();
|
|
1588
1837
|
this.detectors.update();
|
|
1589
1838
|
}
|
|
1839
|
+
_updateAudioLevel() {
|
|
1840
|
+
this.rmsAudioLevel = void 0;
|
|
1841
|
+
this.rmsAudioLevelSource = void 0;
|
|
1842
|
+
this.receivedRmsAudioLevel = void 0;
|
|
1843
|
+
if (this.kind !== "audio") return;
|
|
1844
|
+
if (0 < this.remoteInboundTracks.size) {
|
|
1845
|
+
const levels = [];
|
|
1846
|
+
for (const inboundTrack of this.remoteInboundTracks) {
|
|
1847
|
+
pushFinite(levels, inboundTrack.getInboundRtp()?.rmsAudioLevel);
|
|
1848
|
+
}
|
|
1849
|
+
this.receivedRmsAudioLevel = median(levels);
|
|
1850
|
+
}
|
|
1851
|
+
const sourceLevel = this._mediaSource?.rmsAudioLevel;
|
|
1852
|
+
if (sourceLevel !== void 0) {
|
|
1853
|
+
this.rmsAudioLevel = sourceLevel;
|
|
1854
|
+
this.rmsAudioLevelSource = "media-source";
|
|
1855
|
+
} else if (this.receivedRmsAudioLevel !== void 0) {
|
|
1856
|
+
this.rmsAudioLevel = this.receivedRmsAudioLevel;
|
|
1857
|
+
this.rmsAudioLevelSource = "receivers";
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1590
1860
|
createReceivingDistribution() {
|
|
1591
1861
|
if (this.remoteInboundTracks.size === 0) return void 0;
|
|
1592
1862
|
let freezeAffected = 0;
|
|
@@ -1642,14 +1912,14 @@ var ObservedOutboundTrack = class {
|
|
|
1642
1912
|
|
|
1643
1913
|
// src/ObservedPeerConnection.ts
|
|
1644
1914
|
var ascending2 = (a, b) => a - b;
|
|
1645
|
-
var
|
|
1915
|
+
var logger3 = createLogger("ObservedPeerConnection");
|
|
1646
1916
|
function publicAddressOfLocalCandidate(candidate) {
|
|
1647
1917
|
if (!candidate) return void 0;
|
|
1648
1918
|
if (candidate.candidateType !== "relay" && isPublicIpAddress(candidate.address)) return candidate.address;
|
|
1649
1919
|
if (isPublicIpAddress(candidate.relatedAddress)) return candidate.relatedAddress;
|
|
1650
1920
|
return void 0;
|
|
1651
1921
|
}
|
|
1652
|
-
var ObservedPeerConnection = class extends
|
|
1922
|
+
var ObservedPeerConnection = class extends EventEmitter2 {
|
|
1653
1923
|
constructor(peerConnectionId, client) {
|
|
1654
1924
|
super();
|
|
1655
1925
|
this.peerConnectionId = peerConnectionId;
|
|
@@ -2231,7 +2501,7 @@ var ObservedPeerConnection = class extends EventEmitter {
|
|
|
2231
2501
|
let observedCertificate = this.observedCertificates.get(stats.id);
|
|
2232
2502
|
if (!observedCertificate) {
|
|
2233
2503
|
if (!stats.timestamp || !stats.id || !stats.fingerprint) {
|
|
2234
|
-
return
|
|
2504
|
+
return logger3.warn(
|
|
2235
2505
|
`ObservedPeerConnection received an invalid CertificateStats (missing timestamp OR id OR fingerprint field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2236
2506
|
stats
|
|
2237
2507
|
);
|
|
@@ -2250,7 +2520,7 @@ var ObservedPeerConnection = class extends EventEmitter {
|
|
|
2250
2520
|
let observedCodec = this.observedCodecs.get(stats.id);
|
|
2251
2521
|
if (!observedCodec) {
|
|
2252
2522
|
if (!stats.timestamp || !stats.id || !stats.mimeType) {
|
|
2253
|
-
return
|
|
2523
|
+
return logger3.warn(
|
|
2254
2524
|
`ObservedPeerConnection received an invalid CodecStats (missing timestamp OR id OR mimeType field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2255
2525
|
stats
|
|
2256
2526
|
);
|
|
@@ -2269,7 +2539,7 @@ var ObservedPeerConnection = class extends EventEmitter {
|
|
|
2269
2539
|
let observedDataChannel = this.observedDataChannels.get(stats.id);
|
|
2270
2540
|
if (!observedDataChannel) {
|
|
2271
2541
|
if (!stats.timestamp || !stats.id) {
|
|
2272
|
-
return
|
|
2542
|
+
return logger3.warn(
|
|
2273
2543
|
`ObservedPeerConnection received an invalid DataChannelStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2274
2544
|
stats
|
|
2275
2545
|
);
|
|
@@ -2288,7 +2558,7 @@ var ObservedPeerConnection = class extends EventEmitter {
|
|
|
2288
2558
|
let observedIceCandidate = this.observedIceCandidates.get(stats.id);
|
|
2289
2559
|
if (!observedIceCandidate) {
|
|
2290
2560
|
if (!stats.timestamp || !stats.id) {
|
|
2291
|
-
return
|
|
2561
|
+
return logger3.warn(
|
|
2292
2562
|
`ObservedPeerConnection received an invalid IceCandidateStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2293
2563
|
stats
|
|
2294
2564
|
);
|
|
@@ -2307,7 +2577,7 @@ var ObservedPeerConnection = class extends EventEmitter {
|
|
|
2307
2577
|
let observedIceCandidatePair = this.observedIceCandidatesPair.get(stats.id);
|
|
2308
2578
|
if (!observedIceCandidatePair) {
|
|
2309
2579
|
if (!stats.timestamp || !stats.id) {
|
|
2310
|
-
return
|
|
2580
|
+
return logger3.warn(
|
|
2311
2581
|
`ObservedPeerConnection received an invalid IceCandidateStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2312
2582
|
stats
|
|
2313
2583
|
);
|
|
@@ -2326,7 +2596,7 @@ var ObservedPeerConnection = class extends EventEmitter {
|
|
|
2326
2596
|
let observedIceTransport = this.observedIceTransports.get(stats.id);
|
|
2327
2597
|
if (!observedIceTransport) {
|
|
2328
2598
|
if (!stats.timestamp || !stats.id) {
|
|
2329
|
-
return
|
|
2599
|
+
return logger3.warn(
|
|
2330
2600
|
`ObservedPeerConnection received an invalid IceCandidateStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2331
2601
|
stats
|
|
2332
2602
|
);
|
|
@@ -2345,7 +2615,7 @@ var ObservedPeerConnection = class extends EventEmitter {
|
|
|
2345
2615
|
let observedInboundRtp = this.observedInboundRtps.get(stats.ssrc);
|
|
2346
2616
|
if (!observedInboundRtp) {
|
|
2347
2617
|
if (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind || !stats.trackIdentifier) {
|
|
2348
|
-
return
|
|
2618
|
+
return logger3.warn(
|
|
2349
2619
|
`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}`,
|
|
2350
2620
|
stats
|
|
2351
2621
|
);
|
|
@@ -2371,7 +2641,7 @@ var ObservedPeerConnection = class extends EventEmitter {
|
|
|
2371
2641
|
let observedInboundTrack = this.observedInboundTracks.get(stats.id);
|
|
2372
2642
|
if (!observedInboundTrack) {
|
|
2373
2643
|
if (!stats.timestamp || !stats.id || !stats.kind) {
|
|
2374
|
-
return
|
|
2644
|
+
return logger3.warn(
|
|
2375
2645
|
`ObservedPeerConnection received an invalid InboundTrackSample (missing timestamp OR id OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2376
2646
|
stats
|
|
2377
2647
|
);
|
|
@@ -2399,7 +2669,7 @@ var ObservedPeerConnection = class extends EventEmitter {
|
|
|
2399
2669
|
let observedMediaPlayout = this.observedMediaPlayouts.get(stats.id);
|
|
2400
2670
|
if (!observedMediaPlayout) {
|
|
2401
2671
|
if (!stats.timestamp || !stats.id || !stats.kind) {
|
|
2402
|
-
return
|
|
2672
|
+
return logger3.warn(
|
|
2403
2673
|
`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}`,
|
|
2404
2674
|
stats
|
|
2405
2675
|
);
|
|
@@ -2423,7 +2693,7 @@ var ObservedPeerConnection = class extends EventEmitter {
|
|
|
2423
2693
|
let observedMediaSource = this.observedMediaSources.get(stats.id);
|
|
2424
2694
|
if (!observedMediaSource) {
|
|
2425
2695
|
if (!stats.timestamp || !stats.id || !stats.kind) {
|
|
2426
|
-
return
|
|
2696
|
+
return logger3.warn(
|
|
2427
2697
|
`ObservedPeerConnection received an invalid InboundRtpStats (missing timestamp OR id OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2428
2698
|
stats
|
|
2429
2699
|
);
|
|
@@ -2447,7 +2717,7 @@ var ObservedPeerConnection = class extends EventEmitter {
|
|
|
2447
2717
|
let observedOutboundRtp = this.observedOutboundRtps.get(stats.ssrc);
|
|
2448
2718
|
if (!observedOutboundRtp) {
|
|
2449
2719
|
if (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind) {
|
|
2450
|
-
return
|
|
2720
|
+
return logger3.warn(
|
|
2451
2721
|
`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}`,
|
|
2452
2722
|
stats
|
|
2453
2723
|
);
|
|
@@ -2472,7 +2742,7 @@ var ObservedPeerConnection = class extends EventEmitter {
|
|
|
2472
2742
|
let observedOutboundTrack = this.observedOutboundTracks.get(stats.id);
|
|
2473
2743
|
if (!observedOutboundTrack) {
|
|
2474
2744
|
if (!stats.timestamp || !stats.id || !stats.kind) {
|
|
2475
|
-
return
|
|
2745
|
+
return logger3.warn(
|
|
2476
2746
|
`ObservedPeerConnection received an invalid OutboundTrackSample (missing timestamp OR id OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2477
2747
|
stats
|
|
2478
2748
|
);
|
|
@@ -2500,7 +2770,7 @@ var ObservedPeerConnection = class extends EventEmitter {
|
|
|
2500
2770
|
let observedPeerConnectionTransport = this.observedPeerConnectionTransports.get(stats.id);
|
|
2501
2771
|
if (!observedPeerConnectionTransport) {
|
|
2502
2772
|
if (!stats.timestamp || !stats.id) {
|
|
2503
|
-
return
|
|
2773
|
+
return logger3.warn(
|
|
2504
2774
|
`ObservedPeerConnection received an invalid PeerConnectionTransportStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
|
|
2505
2775
|
stats
|
|
2506
2776
|
);
|
|
@@ -2519,7 +2789,7 @@ var ObservedPeerConnection = class extends EventEmitter {
|
|
|
2519
2789
|
let observedRemoteInboundRtp = this.observedRemoteInboundRtps.get(stats.ssrc);
|
|
2520
2790
|
if (!observedRemoteInboundRtp) {
|
|
2521
2791
|
if (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind) {
|
|
2522
|
-
return
|
|
2792
|
+
return logger3.warn(
|
|
2523
2793
|
`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}`,
|
|
2524
2794
|
stats
|
|
2525
2795
|
);
|
|
@@ -2544,7 +2814,7 @@ var ObservedPeerConnection = class extends EventEmitter {
|
|
|
2544
2814
|
let observedRemoteOutboundRtp = this.observedRemoteOutboundRtps.get(stats.ssrc);
|
|
2545
2815
|
if (!observedRemoteOutboundRtp) {
|
|
2546
2816
|
if (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind) {
|
|
2547
|
-
return
|
|
2817
|
+
return logger3.warn(
|
|
2548
2818
|
`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}`,
|
|
2549
2819
|
stats
|
|
2550
2820
|
);
|
|
@@ -2626,6 +2896,7 @@ var ClientMetaTypes = /* @__PURE__ */ ((ClientMetaTypes2) => {
|
|
|
2626
2896
|
ClientMetaTypes2["MEDIA_DEVICES_SUPPORTED_CONSTRAINTS"] = "MEDIA_DEVICES_SUPPORTED_CONSTRAINTS";
|
|
2627
2897
|
ClientMetaTypes2["USER_MEDIA_ERROR"] = "USER_MEDIA_ERROR";
|
|
2628
2898
|
ClientMetaTypes2["LOCAL_SDP"] = "LOCAL_SDP";
|
|
2899
|
+
ClientMetaTypes2["AUDIO_INPUT_DEVICE"] = "AUDIO_INPUT_DEVICE";
|
|
2629
2900
|
ClientMetaTypes2["OPERATION_SYSTEM"] = "OPERATION_SYSTEM";
|
|
2630
2901
|
ClientMetaTypes2["ENGINE"] = "ENGINE";
|
|
2631
2902
|
ClientMetaTypes2["PLATFORM"] = "PLATFORM";
|
|
@@ -2643,31 +2914,27 @@ function baseIssueType(type) {
|
|
|
2643
2914
|
}
|
|
2644
2915
|
|
|
2645
2916
|
// src/ObservedClient.ts
|
|
2646
|
-
var
|
|
2647
|
-
var
|
|
2917
|
+
var logger4 = createLogger("ObservedClient");
|
|
2918
|
+
var RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS = 20;
|
|
2919
|
+
var RTT_HISTOGRAM_NUMBER_OF_BUCKETS = 50;
|
|
2920
|
+
var SCORE_HISTOGRAM_BUCKET_WIDTH = 1;
|
|
2921
|
+
var SCORE_HISTOGRAM_NUMBER_OF_BUCKETS = 5;
|
|
2922
|
+
var ObservedClient = class extends EventEmitter3 {
|
|
2648
2923
|
constructor(settings, call, activeIssues) {
|
|
2649
2924
|
super();
|
|
2650
2925
|
this.call = call;
|
|
2651
2926
|
this.setMaxListeners(Infinity);
|
|
2652
2927
|
this.eventScope = { observer: call.observer, observedCall: call, observedClient: this };
|
|
2653
|
-
this.sink = call.observer.config.createClientSink?.({ clientId: settings.clientId, observedCall: call });
|
|
2654
2928
|
this.clientId = settings.clientId;
|
|
2655
2929
|
this.appData = settings.appData ?? {};
|
|
2656
2930
|
this.settings = {
|
|
2657
2931
|
closeClientIfIdleForMs: settings.closeClientIfIdleForMs
|
|
2658
2932
|
};
|
|
2659
|
-
if (this.sink) {
|
|
2660
|
-
const onError = (err) => logger2.warn("ClientSampleSink error for client %s: %o", this.clientId, err);
|
|
2661
|
-
this.sink.once("close", () => this.sink?.off("error", onError));
|
|
2662
|
-
this.sink.on("error", onError);
|
|
2663
|
-
}
|
|
2664
2933
|
this.activeIssues = activeIssues;
|
|
2665
2934
|
}
|
|
2666
2935
|
call;
|
|
2667
2936
|
clientId;
|
|
2668
2937
|
observedPeerConnections = /* @__PURE__ */ new Map();
|
|
2669
|
-
/** Per-client sink (from `ObserverConfig.createClientSink`), if any. */
|
|
2670
|
-
sink;
|
|
2671
2938
|
/** Ancestry base shared by all Observer-bus events originating at this client. */
|
|
2672
2939
|
eventScope;
|
|
2673
2940
|
calculatedScore = {
|
|
@@ -2708,10 +2975,6 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2708
2975
|
deltaOutboundPacketsSent = 0;
|
|
2709
2976
|
deltaTransportSentBytes = 0;
|
|
2710
2977
|
deltaTransportReceivedBytes = 0;
|
|
2711
|
-
deltaRttLt50Measurements = 0;
|
|
2712
|
-
deltaRttLt150Measurements = 0;
|
|
2713
|
-
deltaRttLt300Measurements = 0;
|
|
2714
|
-
deltaRttGtOrEq300Measurements = 0;
|
|
2715
2978
|
currentMaxRttInMs;
|
|
2716
2979
|
currentMinRttInMs;
|
|
2717
2980
|
currentAvgRttInMs;
|
|
@@ -2727,8 +2990,73 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2727
2990
|
deltaNumberOfIssues = 0;
|
|
2728
2991
|
totalScoreSum = 0;
|
|
2729
2992
|
numberOfScoreMeasurements = 0;
|
|
2730
|
-
|
|
2993
|
+
minScore;
|
|
2994
|
+
maxScore;
|
|
2995
|
+
/**
|
|
2996
|
+
* Score measurements (one per accepted sample with a score) counted into
|
|
2997
|
+
* `SCORE_HISTOGRAM_NUMBER_OF_BUCKETS` buckets of `SCORE_HISTOGRAM_BUCKET_WIDTH`. Histograms of
|
|
2998
|
+
* several clients can be added bucket by bucket to get the distribution of a call.
|
|
2999
|
+
*/
|
|
3000
|
+
scoreHistogram = new Array(SCORE_HISTOGRAM_NUMBER_OF_BUCKETS).fill(0);
|
|
3001
|
+
/** Issues raised over the session, counted as in `issueHistogram`. */
|
|
3002
|
+
totalNumberOfIssues = 0;
|
|
3003
|
+
/**
|
|
3004
|
+
* Number of times each issue type was raised over the session, keyed by the base type (without
|
|
3005
|
+
* the `-resolved` suffix): `client.issueHistogram['loopback-audio-input']`. A resolution entry is
|
|
3006
|
+
* not counted, and neither is a re-raise of a keyed issue that is still open, so a stateful issue
|
|
3007
|
+
* reported on every tick counts once per open interval. A type never raised is `undefined`.
|
|
3008
|
+
*/
|
|
3009
|
+
issueHistogram = /* @__PURE__ */ Object.create(null);
|
|
3010
|
+
// Session totals. Unlike the `delta*` and `current*` fields they are never reset, and unlike the
|
|
3011
|
+
// `total*` fields of `ObservedPeerConnection` they survive a peer connection being closed.
|
|
3012
|
+
// Every RTT measurement is one peer connection's `currentRttInMs` in one accepted sample.
|
|
3013
|
+
totalRttSumInMs = 0;
|
|
3014
|
+
numberOfRttMeasurements = 0;
|
|
3015
|
+
maxRttInMs;
|
|
3016
|
+
minRttInMs;
|
|
3017
|
+
/**
|
|
3018
|
+
* RTT measurements counted into fixed buckets of `RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS`; the last bucket
|
|
3019
|
+
* holds every measurement of 980 ms and above. Constant memory, so percentiles can be estimated
|
|
3020
|
+
* over the whole session (see `rttSummary`) without keeping every measurement. Histograms
|
|
3021
|
+
* of several clients can be added bucket by bucket to get the distribution of a call.
|
|
3022
|
+
*/
|
|
3023
|
+
rttHistogram = new Array(RTT_HISTOGRAM_NUMBER_OF_BUCKETS).fill(0);
|
|
3024
|
+
_rttSummary;
|
|
3025
|
+
_scoreSummary;
|
|
3026
|
+
totalInboundPacketsLost = 0;
|
|
3027
|
+
totalInboundPacketsReceived = 0;
|
|
3028
|
+
totalOutboundPacketsSent = 0;
|
|
3029
|
+
totalReceivedAudioBytes = 0;
|
|
3030
|
+
totalReceivedVideoBytes = 0;
|
|
3031
|
+
totalSentAudioBytes = 0;
|
|
3032
|
+
totalSentVideoBytes = 0;
|
|
3033
|
+
totalTransportReceivedBytes = 0;
|
|
3034
|
+
totalTransportSentBytes = 0;
|
|
3035
|
+
totalDataChannelBytesReceived = 0;
|
|
3036
|
+
totalDataChannelBytesSent = 0;
|
|
3037
|
+
totalDataChannelMessagesReceived = 0;
|
|
3038
|
+
totalDataChannelMessagesSent = 0;
|
|
3039
|
+
/** Highest `sendingAudioBitrate + sendingVideoBitrate` of any sample, in bits per second. */
|
|
3040
|
+
peakSendingBitrate = 0;
|
|
3041
|
+
/** Highest `receivingAudioBitrate + receivingVideoBitrate` of any sample, in bits per second. */
|
|
3042
|
+
peakReceivingBitrate = 0;
|
|
3043
|
+
// Samples by the transport path of the selected ICE candidate pairs. A sample whose peer
|
|
3044
|
+
// connections use different paths is counted under each of them, so more than one non-zero
|
|
3045
|
+
// counter means the path changed or was mixed. A sample with no selected pair counts as unknown.
|
|
3046
|
+
numberOfSamplesOverUdp = 0;
|
|
3047
|
+
numberOfSamplesOverTcp = 0;
|
|
3048
|
+
numberOfSamplesOverTurnUdp = 0;
|
|
3049
|
+
numberOfSamplesOverTurnTcp = 0;
|
|
3050
|
+
numberOfSamplesWithUnknownPath = 0;
|
|
3051
|
+
numberOfPublicAddressChanges = 0;
|
|
2731
3052
|
mediaDevices = [];
|
|
3053
|
+
/**
|
|
3054
|
+
* The capture device behind each outbound audio track, keyed by track id, from the client's
|
|
3055
|
+
* `AUDIO_INPUT_DEVICE` metadata. Kept for the client's lifetime (a device switch is a new track
|
|
3056
|
+
* id), so a finding raised after a track ended can still name its device. Read it per track via
|
|
3057
|
+
* `ObservedOutboundTrack.audioInputDevice`.
|
|
3058
|
+
*/
|
|
3059
|
+
audioInputDevices = /* @__PURE__ */ new Map();
|
|
2732
3060
|
/**
|
|
2733
3061
|
* The client's currently **open** stateful issues, keyed by `ClientIssue.key` — the server-side
|
|
2734
3062
|
* mirror of the client monitor's own active-issue map (client-monitor-js >= 4.6.0).
|
|
@@ -2741,12 +3069,17 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2741
3069
|
activeIssues;
|
|
2742
3070
|
_pendingInjections = {};
|
|
2743
3071
|
_activeSample;
|
|
2744
|
-
/** Peer connections the client reported closed (`PEER_CONNECTION_CLOSED`), closed once the current sample is applied. */
|
|
2745
3072
|
_peerConnectionsToClose = /* @__PURE__ */ new Set();
|
|
2746
|
-
/** Ids of peer connections closed by the client; late stats for them are ignored rather than resurrecting them. */
|
|
2747
3073
|
_closedPeerConnectionIds = /* @__PURE__ */ new Set();
|
|
2748
3074
|
closeTimer;
|
|
2749
3075
|
/** The distinct `publicAddress`es currently known by this client's open peer connections. */
|
|
3076
|
+
/**
|
|
3077
|
+
* The sink this client's samples are written to: whatever is registered under its client id in
|
|
3078
|
+
* `call.sinks` (the `createClientJsonlSink` file, or a sink the application added), if any.
|
|
3079
|
+
*/
|
|
3080
|
+
get sink() {
|
|
3081
|
+
return this.call.sinks.get(this.clientId);
|
|
3082
|
+
}
|
|
2750
3083
|
get publicAddresses() {
|
|
2751
3084
|
const result = /* @__PURE__ */ new Set();
|
|
2752
3085
|
for (const observedPeerConnection of this.observedPeerConnections.values()) {
|
|
@@ -2760,6 +3093,61 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2760
3093
|
get score() {
|
|
2761
3094
|
return this.calculatedScore.value;
|
|
2762
3095
|
}
|
|
3096
|
+
/**
|
|
3097
|
+
* Percentiles (error at most one 20 ms bucket; a percentile in the overflow bucket is reported as
|
|
3098
|
+
* `maxRttInMs`) and mean of every RTT measurement of the session. Cached until the next measurement.
|
|
3099
|
+
*/
|
|
3100
|
+
get rttSummary() {
|
|
3101
|
+
if (!this._rttSummary) {
|
|
3102
|
+
const bounds = { min: this.minRttInMs, max: this.maxRttInMs, openEnded: true };
|
|
3103
|
+
const percentile2 = (p) => percentileOfHistogram(this.rttHistogram, RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS, p, bounds);
|
|
3104
|
+
this._rttSummary = Object.freeze({
|
|
3105
|
+
p50: percentile2(0.5),
|
|
3106
|
+
p75: percentile2(0.75),
|
|
3107
|
+
p95: percentile2(0.95),
|
|
3108
|
+
p99: percentile2(0.99),
|
|
3109
|
+
avg: 0 < this.numberOfRttMeasurements ? this.totalRttSumInMs / this.numberOfRttMeasurements : void 0
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3112
|
+
return this._rttSummary;
|
|
3113
|
+
}
|
|
3114
|
+
/**
|
|
3115
|
+
* Percentiles (error at most one score point) and mean of every score of the session. Cached until
|
|
3116
|
+
* the next score.
|
|
3117
|
+
*/
|
|
3118
|
+
get scoreSummary() {
|
|
3119
|
+
if (!this._scoreSummary) {
|
|
3120
|
+
const bounds = { min: this.minScore, max: this.maxScore };
|
|
3121
|
+
const percentile2 = (p) => percentileOfHistogram(this.scoreHistogram, SCORE_HISTOGRAM_BUCKET_WIDTH, p, bounds);
|
|
3122
|
+
this._scoreSummary = Object.freeze({
|
|
3123
|
+
p5: percentile2(0.05),
|
|
3124
|
+
p25: percentile2(0.25),
|
|
3125
|
+
p50: percentile2(0.5),
|
|
3126
|
+
p75: percentile2(0.75),
|
|
3127
|
+
p95: percentile2(0.95),
|
|
3128
|
+
avg: 0 < this.numberOfScoreMeasurements ? this.totalScoreSum / this.numberOfScoreMeasurements : void 0
|
|
3129
|
+
});
|
|
3130
|
+
}
|
|
3131
|
+
return this._scoreSummary;
|
|
3132
|
+
}
|
|
3133
|
+
_addScoreMeasurement(score) {
|
|
3134
|
+
this._scoreSummary = void 0;
|
|
3135
|
+
this.totalScoreSum += score;
|
|
3136
|
+
++this.numberOfScoreMeasurements;
|
|
3137
|
+
if (this.maxScore === void 0 || this.maxScore < score) this.maxScore = score;
|
|
3138
|
+
if (this.minScore === void 0 || score < this.minScore) this.minScore = score;
|
|
3139
|
+
const bucket = Math.min(Math.max(Math.floor(score / SCORE_HISTOGRAM_BUCKET_WIDTH), 0), SCORE_HISTOGRAM_NUMBER_OF_BUCKETS - 1);
|
|
3140
|
+
++this.scoreHistogram[bucket];
|
|
3141
|
+
}
|
|
3142
|
+
_addRttMeasurement(rttInMs) {
|
|
3143
|
+
this._rttSummary = void 0;
|
|
3144
|
+
this.totalRttSumInMs += rttInMs;
|
|
3145
|
+
++this.numberOfRttMeasurements;
|
|
3146
|
+
if (this.maxRttInMs === void 0 || this.maxRttInMs < rttInMs) this.maxRttInMs = rttInMs;
|
|
3147
|
+
if (this.minRttInMs === void 0 || rttInMs < this.minRttInMs) this.minRttInMs = rttInMs;
|
|
3148
|
+
const bucket = Math.min(Math.max(Math.floor(rttInMs / RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS), 0), RTT_HISTOGRAM_NUMBER_OF_BUCKETS - 1);
|
|
3149
|
+
++this.rttHistogram[bucket];
|
|
3150
|
+
}
|
|
2763
3151
|
close() {
|
|
2764
3152
|
if (this.closed) return;
|
|
2765
3153
|
if (this.closeTimer) {
|
|
@@ -2786,12 +3174,12 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2786
3174
|
try {
|
|
2787
3175
|
this.sink?.end();
|
|
2788
3176
|
} catch (err) {
|
|
2789
|
-
|
|
3177
|
+
logger4.warn("Sink end failed for client %s: %o", this.clientId, err);
|
|
2790
3178
|
}
|
|
2791
3179
|
}
|
|
2792
3180
|
accept(sample, context) {
|
|
2793
3181
|
if (this.closed) {
|
|
2794
|
-
|
|
3182
|
+
logger4.warn("Attempted to accept a sample on a closed client %s", this.clientId);
|
|
2795
3183
|
return;
|
|
2796
3184
|
}
|
|
2797
3185
|
if (this.closeTimer) {
|
|
@@ -2803,6 +3191,10 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2803
3191
|
const elapsedInSeconds = elapsedInMs / 1e3;
|
|
2804
3192
|
let sumOfRtts = 0;
|
|
2805
3193
|
let numberOfRttMeasurements = 0;
|
|
3194
|
+
let overUdp = false;
|
|
3195
|
+
let overTcp = false;
|
|
3196
|
+
let overTurnUdp = false;
|
|
3197
|
+
let overTurnTcp = false;
|
|
2806
3198
|
++this.acceptedSamples;
|
|
2807
3199
|
this.availableIncomingBitrate = 0;
|
|
2808
3200
|
this.availableOutgoingBitrate = 0;
|
|
@@ -2819,10 +3211,6 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2819
3211
|
this.deltaSentVideoBytes = 0;
|
|
2820
3212
|
this.deltaTransportReceivedBytes = 0;
|
|
2821
3213
|
this.deltaTransportSentBytes = 0;
|
|
2822
|
-
this.deltaRttLt50Measurements = 0;
|
|
2823
|
-
this.deltaRttLt150Measurements = 0;
|
|
2824
|
-
this.deltaRttLt300Measurements = 0;
|
|
2825
|
-
this.deltaRttGtOrEq300Measurements = 0;
|
|
2826
3214
|
this.deltaNumberOfIssues = 0;
|
|
2827
3215
|
this.numberOfDataChannels = 0;
|
|
2828
3216
|
this.numberOfInboundRtpStreams = 0;
|
|
@@ -2894,6 +3282,18 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2894
3282
|
if (observedPeerConnection.usingTCP) {
|
|
2895
3283
|
this.usingTCP = true;
|
|
2896
3284
|
}
|
|
3285
|
+
for (const selectedPair of observedPeerConnection.selectedIceCandidatePairs) {
|
|
3286
|
+
const localCandidate = selectedPair.getLocalCandidate();
|
|
3287
|
+
if (!localCandidate) continue;
|
|
3288
|
+
if (localCandidate.candidateType === "relay") {
|
|
3289
|
+
if (localCandidate.relayProtocol === "tcp" || localCandidate.relayProtocol === "tls") overTurnTcp = true;
|
|
3290
|
+
else overTurnUdp = true;
|
|
3291
|
+
} else if (localCandidate.protocol === "tcp") {
|
|
3292
|
+
overTcp = true;
|
|
3293
|
+
} else {
|
|
3294
|
+
overUdp = true;
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
2897
3297
|
if (observedPeerConnection.currentRttInMs) {
|
|
2898
3298
|
if (this.currentMinRttInMs === void 0 || observedPeerConnection.currentRttInMs < this.currentMinRttInMs) {
|
|
2899
3299
|
this.currentMinRttInMs = observedPeerConnection.currentRttInMs;
|
|
@@ -2901,17 +3301,9 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2901
3301
|
if (this.currentMaxRttInMs === void 0 || observedPeerConnection.currentRttInMs > this.currentMaxRttInMs) {
|
|
2902
3302
|
this.currentMaxRttInMs = observedPeerConnection.currentRttInMs;
|
|
2903
3303
|
}
|
|
2904
|
-
if (observedPeerConnection.currentRttInMs < 50) {
|
|
2905
|
-
this.deltaRttLt50Measurements += 1;
|
|
2906
|
-
} else if (observedPeerConnection.currentRttInMs < 150) {
|
|
2907
|
-
this.deltaRttLt150Measurements += 1;
|
|
2908
|
-
} else if (observedPeerConnection.currentRttInMs < 300) {
|
|
2909
|
-
this.deltaRttLt300Measurements += 1;
|
|
2910
|
-
} else if (300 <= observedPeerConnection.currentRttInMs) {
|
|
2911
|
-
this.deltaRttGtOrEq300Measurements += 1;
|
|
2912
|
-
}
|
|
2913
3304
|
sumOfRtts += observedPeerConnection.currentRttInMs;
|
|
2914
3305
|
++numberOfRttMeasurements;
|
|
3306
|
+
this._addRttMeasurement(observedPeerConnection.currentRttInMs);
|
|
2915
3307
|
}
|
|
2916
3308
|
}
|
|
2917
3309
|
for (const clientEvent of clientEventsPostBuffer) {
|
|
@@ -2921,6 +3313,7 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2921
3313
|
if (latestPublicAddress !== void 0 && latestPublicAddress !== this.publicAddress) {
|
|
2922
3314
|
const previousPublicAddress = this.publicAddress;
|
|
2923
3315
|
this.publicAddress = latestPublicAddress;
|
|
3316
|
+
if (previousPublicAddress !== void 0) ++this.numberOfPublicAddressChanges;
|
|
2924
3317
|
this._notify("client-public-address-changed", { ...this.eventScope, publicAddress: latestPublicAddress, previousPublicAddress });
|
|
2925
3318
|
}
|
|
2926
3319
|
this.attachments = sample.attachments;
|
|
@@ -2936,15 +3329,34 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2936
3329
|
this.sendingVideoBitrate = 0;
|
|
2937
3330
|
}
|
|
2938
3331
|
this.currentAvgRttInMs = 0 < numberOfRttMeasurements ? sumOfRtts / numberOfRttMeasurements : void 0;
|
|
3332
|
+
this.peakSendingBitrate = Math.max(this.peakSendingBitrate, this.sendingAudioBitrate + this.sendingVideoBitrate);
|
|
3333
|
+
this.peakReceivingBitrate = Math.max(this.peakReceivingBitrate, this.receivingAudioBitrate + this.receivingVideoBitrate);
|
|
3334
|
+
this.totalInboundPacketsLost += this.deltaInboundPacketsLost;
|
|
3335
|
+
this.totalInboundPacketsReceived += this.deltaInboundPacketsReceived;
|
|
3336
|
+
this.totalOutboundPacketsSent += this.deltaOutboundPacketsSent;
|
|
3337
|
+
this.totalReceivedAudioBytes += this.deltaReceivedAudioBytes;
|
|
3338
|
+
this.totalReceivedVideoBytes += this.deltaReceivedVideoBytes;
|
|
3339
|
+
this.totalSentAudioBytes += this.deltaSentAudioBytes;
|
|
3340
|
+
this.totalSentVideoBytes += this.deltaSentVideoBytes;
|
|
3341
|
+
this.totalTransportReceivedBytes += this.deltaTransportReceivedBytes;
|
|
3342
|
+
this.totalTransportSentBytes += this.deltaTransportSentBytes;
|
|
3343
|
+
this.totalDataChannelBytesReceived += this.deltaDataChannelBytesReceived;
|
|
3344
|
+
this.totalDataChannelBytesSent += this.deltaDataChannelBytesSent;
|
|
3345
|
+
this.totalDataChannelMessagesReceived += this.deltaDataChannelMessagesReceived;
|
|
3346
|
+
this.totalDataChannelMessagesSent += this.deltaDataChannelMessagesSent;
|
|
3347
|
+
if (overUdp) ++this.numberOfSamplesOverUdp;
|
|
3348
|
+
if (overTcp) ++this.numberOfSamplesOverTcp;
|
|
3349
|
+
if (overTurnUdp) ++this.numberOfSamplesOverTurnUdp;
|
|
3350
|
+
if (overTurnTcp) ++this.numberOfSamplesOverTurnTcp;
|
|
3351
|
+
if (!overUdp && !overTcp && !overTurnUdp && !overTurnTcp) ++this.numberOfSamplesWithUnknownPath;
|
|
2939
3352
|
this.calculatedScore.value = sample.score;
|
|
2940
3353
|
this.lastSampleTimestamp = sample.timestamp;
|
|
2941
3354
|
const elapsedTimeInMs = now - this.updated;
|
|
2942
3355
|
this.emit("update", sample, elapsedTimeInMs, context);
|
|
2943
3356
|
this._notify("client-updated", { ...this.eventScope, sample, elapsedTimeInMs, context });
|
|
2944
3357
|
this.updated = now;
|
|
2945
|
-
if (this.calculatedScore.value) {
|
|
2946
|
-
this.
|
|
2947
|
-
++this.numberOfScoreMeasurements;
|
|
3358
|
+
if (typeof this.calculatedScore.value === "number" && Number.isFinite(this.calculatedScore.value)) {
|
|
3359
|
+
this._addScoreMeasurement(this.calculatedScore.value);
|
|
2948
3360
|
}
|
|
2949
3361
|
if (this.settings.closeClientIfIdleForMs !== void 0) {
|
|
2950
3362
|
this.closeTimer = setTimeout(() => {
|
|
@@ -2957,7 +3369,7 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2957
3369
|
try {
|
|
2958
3370
|
this.sink?.write(sample);
|
|
2959
3371
|
} catch (err) {
|
|
2960
|
-
|
|
3372
|
+
logger4.warn("Sink write failed for client %s: %o", this.clientId, err);
|
|
2961
3373
|
}
|
|
2962
3374
|
}
|
|
2963
3375
|
injectMetaData(metaData) {
|
|
@@ -3035,6 +3447,13 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3035
3447
|
this.operationSystem = parseJsonAs(metadata.payload);
|
|
3036
3448
|
break;
|
|
3037
3449
|
}
|
|
3450
|
+
case "AUDIO_INPUT_DEVICE" /* AUDIO_INPUT_DEVICE */: {
|
|
3451
|
+
const device = parseJsonAs(metadata.payload);
|
|
3452
|
+
if (typeof device?.trackId === "string" && typeof device.label === "string") {
|
|
3453
|
+
this.audioInputDevices.set(device.trackId, device);
|
|
3454
|
+
}
|
|
3455
|
+
break;
|
|
3456
|
+
}
|
|
3038
3457
|
}
|
|
3039
3458
|
this._notify("client-metadata", { ...this.eventScope, metaData: metadata });
|
|
3040
3459
|
}
|
|
@@ -3055,6 +3474,11 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3055
3474
|
if (isClientIssueResolutionEntry(issue)) {
|
|
3056
3475
|
return this._resolveIssue(issue);
|
|
3057
3476
|
}
|
|
3477
|
+
if (!issue.key || !this.activeIssues.get(issue.key)) {
|
|
3478
|
+
const type = baseIssueType(issue.type);
|
|
3479
|
+
this.issueHistogram[type] = (this.issueHistogram[type] ?? 0) + 1;
|
|
3480
|
+
++this.totalNumberOfIssues;
|
|
3481
|
+
}
|
|
3058
3482
|
const now = Date.now();
|
|
3059
3483
|
if (issue.key) {
|
|
3060
3484
|
const payload = parseJsonObject(issue.payload);
|
|
@@ -3128,10 +3552,10 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3128
3552
|
this._notify("client-rejoined", { ...this.eventScope, timestamp: event.timestamp });
|
|
3129
3553
|
} else {
|
|
3130
3554
|
this.joinedAt = event.timestamp;
|
|
3131
|
-
|
|
3555
|
+
logger4.warn(`Client ${this.clientId} joinedAt timestamp was updated to ${event.timestamp}. the joined event will not be emitted.`);
|
|
3132
3556
|
}
|
|
3133
3557
|
}
|
|
3134
|
-
|
|
3558
|
+
logger4.debug("Client %s joined at %o", this.clientId, event);
|
|
3135
3559
|
break;
|
|
3136
3560
|
}
|
|
3137
3561
|
case "CLIENT_LEFT" /* CLIENT_LEFT */: {
|
|
@@ -3141,10 +3565,10 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3141
3565
|
this.emit("left");
|
|
3142
3566
|
this._notify("client-left", { ...this.eventScope });
|
|
3143
3567
|
} else {
|
|
3144
|
-
|
|
3568
|
+
logger4.warn(`Client ${this.clientId} leftAt timestamp was already set`);
|
|
3145
3569
|
}
|
|
3146
3570
|
}
|
|
3147
|
-
|
|
3571
|
+
logger4.debug("Client %s left at %o", this.clientId, event);
|
|
3148
3572
|
break;
|
|
3149
3573
|
}
|
|
3150
3574
|
case "PEER_CONNECTION_OPENED" /* PEER_CONNECTION_OPENED */: {
|
|
@@ -3156,7 +3580,7 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3156
3580
|
} else if (postBuffer) {
|
|
3157
3581
|
postBuffer.push(event);
|
|
3158
3582
|
} else {
|
|
3159
|
-
|
|
3583
|
+
logger4.warn("Received PEER_CONNECTION_OPENED event without a corresponding observedPeerConnection: %o", event);
|
|
3160
3584
|
}
|
|
3161
3585
|
}
|
|
3162
3586
|
break;
|
|
@@ -3173,7 +3597,7 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3173
3597
|
} else if (postBuffer) {
|
|
3174
3598
|
postBuffer.push(event);
|
|
3175
3599
|
} else {
|
|
3176
|
-
|
|
3600
|
+
logger4.warn("Received PEER_CONNECTION_CLOSED event without a corresponding observedPeerConnection: %o", event);
|
|
3177
3601
|
}
|
|
3178
3602
|
}
|
|
3179
3603
|
break;
|
|
@@ -3188,7 +3612,7 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3188
3612
|
} else if (postBuffer) {
|
|
3189
3613
|
postBuffer.push(event);
|
|
3190
3614
|
} else {
|
|
3191
|
-
|
|
3615
|
+
logger4.warn("Received MEDIA_TRACK_ADDED event without a corresponding observedPeerConnection or observedTrack: %o", event);
|
|
3192
3616
|
}
|
|
3193
3617
|
}
|
|
3194
3618
|
break;
|
|
@@ -3203,7 +3627,7 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3203
3627
|
} else if (postBuffer) {
|
|
3204
3628
|
postBuffer.push(event);
|
|
3205
3629
|
} else {
|
|
3206
|
-
|
|
3630
|
+
logger4.warn("Received MEDIA_TRACK_REMOVED event without a corresponding observedPeerConnection or observedTrack: %o", event);
|
|
3207
3631
|
}
|
|
3208
3632
|
}
|
|
3209
3633
|
break;
|
|
@@ -3218,7 +3642,7 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3218
3642
|
} else if (postBuffer) {
|
|
3219
3643
|
postBuffer.push(event);
|
|
3220
3644
|
} else {
|
|
3221
|
-
|
|
3645
|
+
logger4.warn("Received DATA_CHANNEL_OPENED event without a corresponding observedPeerConnection or observedDataChannel: %o", event);
|
|
3222
3646
|
}
|
|
3223
3647
|
}
|
|
3224
3648
|
break;
|
|
@@ -3233,7 +3657,7 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3233
3657
|
} else if (postBuffer) {
|
|
3234
3658
|
postBuffer.push(event);
|
|
3235
3659
|
} else {
|
|
3236
|
-
|
|
3660
|
+
logger4.warn("Received DATA_CHANNEL_CLOSE event without a corresponding observedPeerConnection or observedDataChannel: %o", event);
|
|
3237
3661
|
}
|
|
3238
3662
|
}
|
|
3239
3663
|
break;
|
|
@@ -3253,7 +3677,7 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3253
3677
|
} else if (postBuffer) {
|
|
3254
3678
|
postBuffer.push(event);
|
|
3255
3679
|
} else {
|
|
3256
|
-
|
|
3680
|
+
logger4.warn("Received MEDIA_TRACK_MUTED event without a corresponding observedPeerConnection or observedTrack: %o", event);
|
|
3257
3681
|
}
|
|
3258
3682
|
}
|
|
3259
3683
|
break;
|
|
@@ -3273,7 +3697,7 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3273
3697
|
} else if (postBuffer) {
|
|
3274
3698
|
postBuffer.push(event);
|
|
3275
3699
|
} else {
|
|
3276
|
-
|
|
3700
|
+
logger4.warn("Received MEDIA_TRACK_UNMUTED event without a corresponding observedPeerConnection or observedTrack: %o", event);
|
|
3277
3701
|
}
|
|
3278
3702
|
}
|
|
3279
3703
|
break;
|
|
@@ -3288,7 +3712,7 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3288
3712
|
} else if (postBuffer) {
|
|
3289
3713
|
postBuffer.push(event);
|
|
3290
3714
|
} else {
|
|
3291
|
-
|
|
3715
|
+
logger4.warn("Received ICE_CONNECTION_STATE_CHANGED event without a corresponding observedPeerConnection: %o", event);
|
|
3292
3716
|
}
|
|
3293
3717
|
}
|
|
3294
3718
|
break;
|
|
@@ -3303,7 +3727,7 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3303
3727
|
} else if (postBuffer) {
|
|
3304
3728
|
postBuffer.push(event);
|
|
3305
3729
|
} else {
|
|
3306
|
-
|
|
3730
|
+
logger4.warn("Received ICE_GATHERING_STATE_CHANGED event without a corresponding observedPeerConnection: %o", event);
|
|
3307
3731
|
}
|
|
3308
3732
|
}
|
|
3309
3733
|
break;
|
|
@@ -3318,7 +3742,7 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3318
3742
|
} else if (postBuffer) {
|
|
3319
3743
|
postBuffer.push(event);
|
|
3320
3744
|
} else {
|
|
3321
|
-
|
|
3745
|
+
logger4.warn("Received PEER_CONNECTION_STATE_CHANGED event without a corresponding observedPeerConnection: %o", event);
|
|
3322
3746
|
}
|
|
3323
3747
|
}
|
|
3324
3748
|
break;
|
|
@@ -3337,11 +3761,11 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3337
3761
|
let observedPeerConnection = this.observedPeerConnections.get(sample.peerConnectionId);
|
|
3338
3762
|
if (!observedPeerConnection) {
|
|
3339
3763
|
if (this._closedPeerConnectionIds.has(sample.peerConnectionId)) {
|
|
3340
|
-
|
|
3764
|
+
logger4.debug("Ignoring stats of peer connection %s, already closed by client %s", sample.peerConnectionId, this.clientId);
|
|
3341
3765
|
return void 0;
|
|
3342
3766
|
}
|
|
3343
3767
|
if (!sample.peerConnectionId) {
|
|
3344
|
-
return
|
|
3768
|
+
return logger4.warn(
|
|
3345
3769
|
`ObservedClient received an invalid PeerConnectionSample (missing peerConnectionId field). ClientId: ${this.clientId}, CallId: ${this.call.callId}`,
|
|
3346
3770
|
sample
|
|
3347
3771
|
), void 0;
|
|
@@ -3401,7 +3825,7 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3401
3825
|
try {
|
|
3402
3826
|
this.sink?.write(finalSample);
|
|
3403
3827
|
} catch (err) {
|
|
3404
|
-
|
|
3828
|
+
logger4.warn("Sink write failed during injection flush for client %s: %o", this.clientId, err);
|
|
3405
3829
|
}
|
|
3406
3830
|
}
|
|
3407
3831
|
/** Emit an Observer-bus event scoped to this client (or a peer connection under it). */
|
|
@@ -3429,7 +3853,7 @@ var DefaultCallScoreCalculator = class {
|
|
|
3429
3853
|
};
|
|
3430
3854
|
|
|
3431
3855
|
// src/detectors/Detectors.ts
|
|
3432
|
-
var
|
|
3856
|
+
var logger5 = createLogger("Detectors");
|
|
3433
3857
|
var Detectors = class {
|
|
3434
3858
|
_detectors;
|
|
3435
3859
|
constructor(...detectors) {
|
|
@@ -3510,7 +3934,7 @@ var Detectors = class {
|
|
|
3510
3934
|
try {
|
|
3511
3935
|
detector.update();
|
|
3512
3936
|
} catch (err) {
|
|
3513
|
-
|
|
3937
|
+
logger5.warn(`Error updating detector ${detector?.name}`, err);
|
|
3514
3938
|
}
|
|
3515
3939
|
}
|
|
3516
3940
|
}
|
|
@@ -3523,7 +3947,7 @@ var Detectors = class {
|
|
|
3523
3947
|
try {
|
|
3524
3948
|
detector.close?.();
|
|
3525
3949
|
} catch (err) {
|
|
3526
|
-
|
|
3950
|
+
logger5.warn(`Error closing detector ${detector?.name}`, err);
|
|
3527
3951
|
}
|
|
3528
3952
|
}
|
|
3529
3953
|
};
|
|
@@ -4326,9 +4750,172 @@ var PublisherFaultCorroborationDetector = class _PublisherFaultCorroborationDete
|
|
|
4326
4750
|
}
|
|
4327
4751
|
};
|
|
4328
4752
|
|
|
4753
|
+
// src/detectors/AudioEchoDetector.ts
|
|
4754
|
+
var AudioEchoTypes = {
|
|
4755
|
+
/**
|
|
4756
|
+
* One participant's outgoing audio follows another participant's voice: it rises when they speak,
|
|
4757
|
+
* in proportion to how loud they are. The echoing participant is sending the far end's playout
|
|
4758
|
+
* back into the call — speakers into an open mic, or a loopback capture device.
|
|
4759
|
+
*/
|
|
4760
|
+
audioEchoSuspected: "AUDIO_ECHO_SUSPECTED"
|
|
4761
|
+
};
|
|
4762
|
+
var DIRECTION_MARGIN = 0.1;
|
|
4763
|
+
var mean = (values) => values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
4764
|
+
var LEVEL_FLOOR = 1e-5;
|
|
4765
|
+
var toDb = (level) => 20 * Math.log10(Math.max(level, LEVEL_FLOOR));
|
|
4766
|
+
var AudioEchoDetector = class _AudioEchoDetector {
|
|
4767
|
+
constructor(call, config = {}) {
|
|
4768
|
+
this.call = call;
|
|
4769
|
+
this._config = {
|
|
4770
|
+
bucketMs: 5e3,
|
|
4771
|
+
windowBuckets: 24,
|
|
4772
|
+
activeRmsLevel: 0.01,
|
|
4773
|
+
minSourceActiveBuckets: 8,
|
|
4774
|
+
minFollowRatio: 0.8,
|
|
4775
|
+
minCorrelation: 0.6,
|
|
4776
|
+
cooldownMs: 12e4,
|
|
4777
|
+
loopbackAudioInputIssueType: "loopback-audio-input",
|
|
4778
|
+
...config
|
|
4779
|
+
};
|
|
4780
|
+
}
|
|
4781
|
+
call;
|
|
4782
|
+
static NAME = "audio-echo-detector";
|
|
4783
|
+
name = _AudioEchoDetector.NAME;
|
|
4784
|
+
_config;
|
|
4785
|
+
_series = /* @__PURE__ */ new Map();
|
|
4786
|
+
_lastRaisedAt = /* @__PURE__ */ new Map();
|
|
4787
|
+
_lastEvaluatedBucket;
|
|
4788
|
+
close() {
|
|
4789
|
+
this._series.clear();
|
|
4790
|
+
this._lastRaisedAt.clear();
|
|
4791
|
+
}
|
|
4792
|
+
update() {
|
|
4793
|
+
const now = Date.now();
|
|
4794
|
+
const bucket = Math.floor(now / this._config.bucketMs);
|
|
4795
|
+
if (this._lastEvaluatedBucket === bucket) return;
|
|
4796
|
+
const closedBucket = bucket - 1;
|
|
4797
|
+
this._lastEvaluatedBucket = bucket;
|
|
4798
|
+
this._collect(now, closedBucket);
|
|
4799
|
+
this._evaluate(now, bucket);
|
|
4800
|
+
}
|
|
4801
|
+
_collect(now, bucket) {
|
|
4802
|
+
for (const client of this.call.observedClients.values()) {
|
|
4803
|
+
for (const peerConnection of client.observedPeerConnections.values()) {
|
|
4804
|
+
for (const track of peerConnection.observedOutboundTracks.values()) {
|
|
4805
|
+
if (track.kind !== "audio") continue;
|
|
4806
|
+
let series = this._series.get(track.id);
|
|
4807
|
+
if (!series) {
|
|
4808
|
+
series = { track, clientId: client.clientId, lastTimestamp: -1, samples: [], lastSeenAt: now };
|
|
4809
|
+
this._series.set(track.id, series);
|
|
4810
|
+
}
|
|
4811
|
+
series.lastSeenAt = now;
|
|
4812
|
+
if (series.lastTimestamp === track.timestamp) continue;
|
|
4813
|
+
series.lastTimestamp = track.timestamp;
|
|
4814
|
+
const level = track.rmsAudioLevel;
|
|
4815
|
+
if (level === void 0) continue;
|
|
4816
|
+
series.samples.push({ bucket, level });
|
|
4817
|
+
const oldest = bucket - this._config.windowBuckets;
|
|
4818
|
+
while (series.samples.length && series.samples[0].bucket <= oldest) series.samples.shift();
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4821
|
+
}
|
|
4822
|
+
for (const [trackId, series] of this._series) {
|
|
4823
|
+
if (series.lastSeenAt !== now) this._series.delete(trackId);
|
|
4824
|
+
}
|
|
4825
|
+
}
|
|
4826
|
+
_evaluate(now, currentBucket) {
|
|
4827
|
+
if (this._series.size < 2) return;
|
|
4828
|
+
const tops = /* @__PURE__ */ new Map();
|
|
4829
|
+
for (const [trackId, series] of this._series) {
|
|
4830
|
+
for (const { bucket, level } of series.samples) {
|
|
4831
|
+
if (currentBucket <= bucket) continue;
|
|
4832
|
+
const top = tops.get(bucket);
|
|
4833
|
+
const entry = { id: trackId, level };
|
|
4834
|
+
if (!top) tops.set(bucket, [entry, void 0]);
|
|
4835
|
+
else if (top[0].level < level) tops.set(bucket, [entry, top[0]]);
|
|
4836
|
+
else if (!top[1] || top[1].level < level) top[1] = entry;
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
4840
|
+
for (const [trackId, series] of this._series) {
|
|
4841
|
+
const suspectDb = [];
|
|
4842
|
+
const sourceDb = [];
|
|
4843
|
+
const sourceCounts = /* @__PURE__ */ new Map();
|
|
4844
|
+
let followed = 0;
|
|
4845
|
+
for (const { bucket, level } of series.samples) {
|
|
4846
|
+
const top = tops.get(bucket);
|
|
4847
|
+
if (!top) continue;
|
|
4848
|
+
const source = top[0].id === trackId ? top[1] : top[0];
|
|
4849
|
+
if (!source || source.level < this._config.activeRmsLevel) continue;
|
|
4850
|
+
if (this._config.activeRmsLevel <= level) followed += 1;
|
|
4851
|
+
suspectDb.push(toDb(level));
|
|
4852
|
+
sourceDb.push(toDb(source.level));
|
|
4853
|
+
sourceCounts.set(source.id, (sourceCounts.get(source.id) ?? 0) + 1);
|
|
4854
|
+
}
|
|
4855
|
+
const sourceActiveBuckets = sourceDb.length;
|
|
4856
|
+
if (sourceActiveBuckets < this._config.minSourceActiveBuckets) continue;
|
|
4857
|
+
const followRatio = followed / sourceActiveBuckets;
|
|
4858
|
+
if (followRatio < this._config.minFollowRatio) continue;
|
|
4859
|
+
const levelCorrelation = correlation(suspectDb, sourceDb);
|
|
4860
|
+
if (levelCorrelation < this._config.minCorrelation) continue;
|
|
4861
|
+
const [sourceTrackId] = [...sourceCounts].reduce((a, b) => a[1] < b[1] ? b : a);
|
|
4862
|
+
const client = this.call.observedClients.get(series.clientId);
|
|
4863
|
+
candidates.set(trackId, {
|
|
4864
|
+
trackId,
|
|
4865
|
+
series,
|
|
4866
|
+
sourceTrackId,
|
|
4867
|
+
followRatio,
|
|
4868
|
+
levelCorrelation,
|
|
4869
|
+
sourceActiveBuckets,
|
|
4870
|
+
// How much quieter than its source the suspect is, in dB. An echo is at most as loud as
|
|
4871
|
+
// what it echoes, give or take AGC.
|
|
4872
|
+
relativeLevelDb: mean(suspectDb) - mean(sourceDb),
|
|
4873
|
+
loopbackAudioInput: client?.activeIssues.get(`${this._config.loopbackAudioInputIssueType}-track-${trackId}`) !== void 0
|
|
4874
|
+
});
|
|
4875
|
+
}
|
|
4876
|
+
for (const candidate of candidates.values()) {
|
|
4877
|
+
const reverse = candidates.get(candidate.sourceTrackId);
|
|
4878
|
+
let ambiguousDirection = false;
|
|
4879
|
+
if (reverse && reverse.sourceTrackId === candidate.trackId) {
|
|
4880
|
+
if (candidate.loopbackAudioInput !== reverse.loopbackAudioInput) {
|
|
4881
|
+
if (!candidate.loopbackAudioInput) continue;
|
|
4882
|
+
} else if (DIRECTION_MARGIN < Math.abs(candidate.followRatio - reverse.followRatio)) {
|
|
4883
|
+
if (candidate.followRatio < reverse.followRatio) continue;
|
|
4884
|
+
} else {
|
|
4885
|
+
ambiguousDirection = true;
|
|
4886
|
+
if (reverse.relativeLevelDb < candidate.relativeLevelDb) continue;
|
|
4887
|
+
if (reverse.relativeLevelDb === candidate.relativeLevelDb && reverse.trackId < candidate.trackId) continue;
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
if (now - (this._lastRaisedAt.get(candidate.trackId) ?? -Infinity) < this._config.cooldownMs) continue;
|
|
4891
|
+
this._lastRaisedAt.set(candidate.trackId, now);
|
|
4892
|
+
const { series } = candidate;
|
|
4893
|
+
this.call.addIssue({
|
|
4894
|
+
type: AudioEchoTypes.audioEchoSuspected,
|
|
4895
|
+
timestamp: now,
|
|
4896
|
+
payload: {
|
|
4897
|
+
trackId: candidate.trackId,
|
|
4898
|
+
clientId: series.clientId,
|
|
4899
|
+
sourceTrackId: candidate.sourceTrackId,
|
|
4900
|
+
sourceClientId: this._series.get(candidate.sourceTrackId)?.clientId,
|
|
4901
|
+
followRatio: candidate.followRatio,
|
|
4902
|
+
levelCorrelation: candidate.levelCorrelation,
|
|
4903
|
+
relativeLevelDb: candidate.relativeLevelDb,
|
|
4904
|
+
sourceActiveBuckets: candidate.sourceActiveBuckets,
|
|
4905
|
+
bucketMs: this._config.bucketMs,
|
|
4906
|
+
ambiguousDirection,
|
|
4907
|
+
levelSource: series.track.rmsAudioLevelSource,
|
|
4908
|
+
audioInputLabel: series.track.audioInputDevice?.label,
|
|
4909
|
+
loopbackAudioInput: candidate.loopbackAudioInput
|
|
4910
|
+
}
|
|
4911
|
+
});
|
|
4912
|
+
}
|
|
4913
|
+
}
|
|
4914
|
+
};
|
|
4915
|
+
|
|
4329
4916
|
// src/ObservedCall.ts
|
|
4330
|
-
var
|
|
4331
|
-
var ObservedCall = class extends
|
|
4917
|
+
var logger6 = createLogger("ObservedCall");
|
|
4918
|
+
var ObservedCall = class extends EventEmitter4 {
|
|
4332
4919
|
constructor(settings, observer, activeIssuesRegistry) {
|
|
4333
4920
|
super();
|
|
4334
4921
|
this.observer = observer;
|
|
@@ -4336,6 +4923,8 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4336
4923
|
this.setMaxListeners(Infinity);
|
|
4337
4924
|
this.eventScope = { observer: this.observer, observedCall: this };
|
|
4338
4925
|
this.callId = settings.callId;
|
|
4926
|
+
this.sinks = new SinkRegistry(this.observer.sinks);
|
|
4927
|
+
this.extensionStats = new ExtensionStatsRegistry(this.callId, this.sinks);
|
|
4339
4928
|
this.appData = settings.appData ?? {};
|
|
4340
4929
|
this.scoreCalculator = new DefaultCallScoreCalculator(this);
|
|
4341
4930
|
this.detectors = new Detectors();
|
|
@@ -4384,10 +4973,6 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4384
4973
|
numberOfDataChannels = 0;
|
|
4385
4974
|
maxNumberOfClients = 0;
|
|
4386
4975
|
deltaNumberOfIssues = 0;
|
|
4387
|
-
// public deltaRttLt50Measurements = 0;
|
|
4388
|
-
// public deltaRttLt150Measurements = 0;
|
|
4389
|
-
// public deltaRttLt300Measurements = 0;
|
|
4390
|
-
// public deltaRttGtOrEq300Measurements = 0;
|
|
4391
4976
|
appData;
|
|
4392
4977
|
closed = false;
|
|
4393
4978
|
startedAt;
|
|
@@ -4396,6 +4981,8 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4396
4981
|
settings;
|
|
4397
4982
|
/** Ancestry base shared by all Observer-bus events originating at this call. */
|
|
4398
4983
|
eventScope;
|
|
4984
|
+
sinks;
|
|
4985
|
+
extensionStats;
|
|
4399
4986
|
closeTimer;
|
|
4400
4987
|
get numberOfClients() {
|
|
4401
4988
|
return this.observedClients.size;
|
|
@@ -4433,8 +5020,12 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4433
5020
|
detector = new PublisherFaultCorroborationDetector(this, config);
|
|
4434
5021
|
break;
|
|
4435
5022
|
}
|
|
5023
|
+
case AudioEchoDetector.NAME: {
|
|
5024
|
+
detector = new AudioEchoDetector(this, config);
|
|
5025
|
+
break;
|
|
5026
|
+
}
|
|
4436
5027
|
default: {
|
|
4437
|
-
|
|
5028
|
+
logger6.warn("Unknown detector name %s; skipping", name);
|
|
4438
5029
|
return this;
|
|
4439
5030
|
}
|
|
4440
5031
|
}
|
|
@@ -4504,6 +5095,8 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4504
5095
|
if (client.joinedAt) minSampleTimestamps = Math.min(minSampleTimestamps ?? client.joinedAt, client.joinedAt);
|
|
4505
5096
|
if (client.leftAt) maxSampleTimestamps = Math.max(maxSampleTimestamps ?? client.leftAt, client.leftAt);
|
|
4506
5097
|
}
|
|
5098
|
+
this.extensionStats.close();
|
|
5099
|
+
this.sinks.clear();
|
|
4507
5100
|
if (this.startedAt === void 0) this.startedAt = minSampleTimestamps;
|
|
4508
5101
|
if (this.endedAt === void 0) this.endedAt = maxSampleTimestamps;
|
|
4509
5102
|
this.closedAt = Date.now();
|
|
@@ -4516,17 +5109,26 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4516
5109
|
this.emit("close");
|
|
4517
5110
|
this._notify("call-closed", { ...this.eventScope });
|
|
4518
5111
|
}
|
|
5112
|
+
createExtensionStats(settings) {
|
|
5113
|
+
return this.extensionStats.create(settings);
|
|
5114
|
+
}
|
|
5115
|
+
acceptExtensionStats(id, data) {
|
|
5116
|
+
this.extensionStats.accept(id, data);
|
|
5117
|
+
}
|
|
5118
|
+
getExtensionStats(id) {
|
|
5119
|
+
return this.extensionStats.get(id);
|
|
5120
|
+
}
|
|
4519
5121
|
getObservedClient(clientId) {
|
|
4520
5122
|
if (this.closed || !this.observedClients.has(clientId)) return;
|
|
4521
5123
|
return this.observedClients.get(clientId);
|
|
4522
5124
|
}
|
|
4523
5125
|
createObservedClient(settings, acceptCtx) {
|
|
4524
5126
|
if (this.closed) {
|
|
4525
|
-
|
|
5127
|
+
logger6.warn("Attempted to create a client (clientId: %s) on a closed call %s", settings.clientId, this.callId);
|
|
4526
5128
|
return void 0;
|
|
4527
5129
|
}
|
|
4528
5130
|
if (this.observedClients.has(settings.clientId)) {
|
|
4529
|
-
|
|
5131
|
+
logger6.warn("Client with id %s already exists in call %s; returning the existing instance", settings.clientId, this.callId);
|
|
4530
5132
|
return this.observedClients.get(settings.clientId);
|
|
4531
5133
|
}
|
|
4532
5134
|
const clientSettings = {
|
|
@@ -4576,10 +5178,17 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4576
5178
|
clearTimeout(this.closeTimer);
|
|
4577
5179
|
this.closeTimer = void 0;
|
|
4578
5180
|
}
|
|
5181
|
+
const jsonlPath = this.observer.config.createClientJsonlSink?.({ observedCall: this, clientId: settings.clientId });
|
|
5182
|
+
let jsonlSink;
|
|
5183
|
+
if (jsonlPath !== void 0 && this.observer.sinks.get(settings.clientId)) {
|
|
5184
|
+
logger6.warn("No sample file for client %s in call %s: sink id %s is already in use", settings.clientId, this.callId, settings.clientId);
|
|
5185
|
+
} else if (jsonlPath !== void 0) {
|
|
5186
|
+
jsonlSink = this.sinks.create(JsonlFileSink, { id: settings.clientId, path: jsonlPath });
|
|
5187
|
+
}
|
|
4579
5188
|
this.emit("newclient", result);
|
|
4580
5189
|
this._notify("client-added", { ...this.eventScope, observedClient: result });
|
|
4581
|
-
if (
|
|
4582
|
-
this._notify("client-sink-created", { ...this.eventScope, observedClient: result, sink:
|
|
5190
|
+
if (jsonlSink) {
|
|
5191
|
+
this._notify("client-sink-created", { ...this.eventScope, observedClient: result, sink: jsonlSink });
|
|
4583
5192
|
}
|
|
4584
5193
|
if (wasEmpty) {
|
|
4585
5194
|
this.emit("not-empty");
|
|
@@ -4632,10 +5241,10 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4632
5241
|
};
|
|
4633
5242
|
|
|
4634
5243
|
// src/Observer.ts
|
|
4635
|
-
import { EventEmitter as
|
|
5244
|
+
import { EventEmitter as EventEmitter7 } from "events";
|
|
4636
5245
|
|
|
4637
5246
|
// src/ObservedTURN.ts
|
|
4638
|
-
import { EventEmitter as
|
|
5247
|
+
import { EventEmitter as EventEmitter5 } from "events";
|
|
4639
5248
|
|
|
4640
5249
|
// src/ObservedTurnServer.ts
|
|
4641
5250
|
var ObservedTurnServer = class {
|
|
@@ -4695,8 +5304,8 @@ var ObservedTurnServer = class {
|
|
|
4695
5304
|
};
|
|
4696
5305
|
|
|
4697
5306
|
// src/ObservedTURN.ts
|
|
4698
|
-
var
|
|
4699
|
-
var ObservedTURN = class extends
|
|
5307
|
+
var logger7 = createLogger("ObservedTURN");
|
|
5308
|
+
var ObservedTURN = class extends EventEmitter5 {
|
|
4700
5309
|
totalBytesSent = 0;
|
|
4701
5310
|
totalBytesReceived = 0;
|
|
4702
5311
|
totalPacketsSent = 0;
|
|
@@ -4725,12 +5334,12 @@ var ObservedTURN = class extends EventEmitter4 {
|
|
|
4725
5334
|
addPeerConnection(peerConnection) {
|
|
4726
5335
|
const turnPairs = peerConnection.selectedIceCandiadtePairForTurn;
|
|
4727
5336
|
if (turnPairs.length !== 1) {
|
|
4728
|
-
return
|
|
5337
|
+
return logger7.warn(`Expected exactly one TURN pair, but found for peerconnection ${peerConnection.peerConnectionId}`, turnPairs.length), void 0;
|
|
4729
5338
|
}
|
|
4730
5339
|
const candidatePair = turnPairs[0];
|
|
4731
5340
|
const rawUrl = candidatePair.getLocalCandidate()?.url;
|
|
4732
5341
|
if (!rawUrl) {
|
|
4733
|
-
return
|
|
5342
|
+
return logger7.warn(`No local candidate URL found for peerconnection ${peerConnection.peerConnectionId}`), void 0;
|
|
4734
5343
|
}
|
|
4735
5344
|
const turnServerUrl = rawUrl.split("?")[0];
|
|
4736
5345
|
let turnServer = this.servers.get(turnServerUrl);
|
|
@@ -4806,14 +5415,16 @@ var MiddlewareProcessor = class {
|
|
|
4806
5415
|
};
|
|
4807
5416
|
|
|
4808
5417
|
// src/ObservedMediasoupRouter.ts
|
|
4809
|
-
import { EventEmitter as
|
|
4810
|
-
var
|
|
4811
|
-
var ObservedMediasoupRouter = class extends
|
|
5418
|
+
import { EventEmitter as EventEmitter6 } from "events";
|
|
5419
|
+
var logger8 = createLogger("ObservedMediasoupRouter");
|
|
5420
|
+
var ObservedMediasoupRouter = class extends EventEmitter6 {
|
|
4812
5421
|
router;
|
|
4813
5422
|
appData;
|
|
4814
5423
|
sample;
|
|
4815
5424
|
webrtcTransportIds = /* @__PURE__ */ new Set();
|
|
4816
5425
|
closed = false;
|
|
5426
|
+
sinks;
|
|
5427
|
+
extensionStats;
|
|
4817
5428
|
// Id -> sample indexes over the arrays in `this.sample`, so an entity can be reached in O(1)
|
|
4818
5429
|
// instead of scanning (`sample.producers.find(...)`). They hold the *same* objects as the arrays.
|
|
4819
5430
|
_transportSamples = /* @__PURE__ */ new Map();
|
|
@@ -4822,11 +5433,17 @@ var ObservedMediasoupRouter = class extends EventEmitter5 {
|
|
|
4822
5433
|
_dataProducerSamples = /* @__PURE__ */ new Map();
|
|
4823
5434
|
_dataConsumerSamples = /* @__PURE__ */ new Map();
|
|
4824
5435
|
_enrich;
|
|
4825
|
-
|
|
5436
|
+
/**
|
|
5437
|
+
* @param parentSinks the parent of `sinks`: `observer.sinks`, passed by
|
|
5438
|
+
* `Observer.createObservedMediasoupRouter`. Internal; a router constructed directly has none.
|
|
5439
|
+
*/
|
|
5440
|
+
constructor(settings, parentSinks) {
|
|
4826
5441
|
super();
|
|
4827
5442
|
this.router = settings.router;
|
|
4828
5443
|
this.appData = settings.appData ?? {};
|
|
4829
5444
|
this._enrich = settings.enrich;
|
|
5445
|
+
this.sinks = new SinkRegistry(parentSinks);
|
|
5446
|
+
this.extensionStats = new ExtensionStatsRegistry(this.router.id, this.sinks);
|
|
4830
5447
|
this.sample = {
|
|
4831
5448
|
routerId: this.router.id,
|
|
4832
5449
|
attachments: settings.attachments ?? {},
|
|
@@ -4895,8 +5512,19 @@ var ObservedMediasoupRouter = class extends EventEmitter5 {
|
|
|
4895
5512
|
if (this.closed) return;
|
|
4896
5513
|
this.closed = true;
|
|
4897
5514
|
if (this.sample.closedAt === void 0) this.sample.closedAt = Date.now();
|
|
5515
|
+
this.extensionStats.close();
|
|
5516
|
+
this.sinks.clear();
|
|
4898
5517
|
this.emit("close");
|
|
4899
5518
|
}
|
|
5519
|
+
createExtensionStats(settings) {
|
|
5520
|
+
return this.extensionStats.create(settings);
|
|
5521
|
+
}
|
|
5522
|
+
acceptExtensionStats(id, data) {
|
|
5523
|
+
this.extensionStats.accept(id, data);
|
|
5524
|
+
}
|
|
5525
|
+
getExtensionStats(id) {
|
|
5526
|
+
return this.extensionStats.get(id);
|
|
5527
|
+
}
|
|
4900
5528
|
addTransport = (transport) => {
|
|
4901
5529
|
let transportType = transport.type;
|
|
4902
5530
|
if (!transportType) {
|
|
@@ -4905,7 +5533,7 @@ var ObservedMediasoupRouter = class extends EventEmitter5 {
|
|
|
4905
5533
|
else if (transport.constructor.name === "PipeTransport") transportType = "pipe";
|
|
4906
5534
|
else if (transport.constructor.name === "DirectTransport") transportType = "direct";
|
|
4907
5535
|
}
|
|
4908
|
-
if (!transportType) return
|
|
5536
|
+
if (!transportType) return logger8.warn(`Unknown transport type for transport ${transport.id}: ${transport.constructor.name}`);
|
|
4909
5537
|
switch (transportType) {
|
|
4910
5538
|
case "webrtc":
|
|
4911
5539
|
return this.addWebRtcTransport(transport);
|
|
@@ -4916,7 +5544,7 @@ var ObservedMediasoupRouter = class extends EventEmitter5 {
|
|
|
4916
5544
|
case "direct":
|
|
4917
5545
|
return this.addDirectTransport(transport);
|
|
4918
5546
|
default:
|
|
4919
|
-
return
|
|
5547
|
+
return logger8.warn(`Unsupported transport type for transport ${transport.id}: ${transportType}`);
|
|
4920
5548
|
}
|
|
4921
5549
|
};
|
|
4922
5550
|
addWebRtcTransport(transport) {
|
|
@@ -5158,7 +5786,7 @@ var ObservedMediasoupRouter = class extends EventEmitter5 {
|
|
|
5158
5786
|
const attachments = enrich();
|
|
5159
5787
|
if (attachments) sample.attachments = { ...sample.attachments, ...attachments };
|
|
5160
5788
|
} catch (err) {
|
|
5161
|
-
|
|
5789
|
+
logger8.warn("Mediasoup sample enricher threw for router %s: %o", this.id, err);
|
|
5162
5790
|
}
|
|
5163
5791
|
}
|
|
5164
5792
|
_attachTransportObserverListeners(transport, transportSample, onClose) {
|
|
@@ -5183,7 +5811,7 @@ var ObservedMediasoupRouter = class extends EventEmitter5 {
|
|
|
5183
5811
|
};
|
|
5184
5812
|
|
|
5185
5813
|
// src/issues/ActiveIssuesRegistry.ts
|
|
5186
|
-
var
|
|
5814
|
+
var logger9 = createLogger("ActiveIssuesRegistry");
|
|
5187
5815
|
var ActiveIssuesRegistry = class {
|
|
5188
5816
|
constructor(parent) {
|
|
5189
5817
|
this.parent = parent;
|
|
@@ -5269,7 +5897,7 @@ var ActiveIssuesRegistry = class {
|
|
|
5269
5897
|
try {
|
|
5270
5898
|
apply(target);
|
|
5271
5899
|
} catch (err) {
|
|
5272
|
-
|
|
5900
|
+
logger9.warn("An issue tracker threw while being notified; continuing with the rest. %o", err);
|
|
5273
5901
|
}
|
|
5274
5902
|
}
|
|
5275
5903
|
};
|
|
@@ -5298,7 +5926,7 @@ function createCallSummary(callId, config) {
|
|
|
5298
5926
|
}
|
|
5299
5927
|
|
|
5300
5928
|
// src/summaries/CallSummaryCollector.ts
|
|
5301
|
-
var
|
|
5929
|
+
var logger10 = createLogger("CallSummaryCollector");
|
|
5302
5930
|
var CallSummaryCollector = class {
|
|
5303
5931
|
constructor(_observer, _config) {
|
|
5304
5932
|
this._observer = _observer;
|
|
@@ -5373,7 +6001,7 @@ var CallSummaryCollector = class {
|
|
|
5373
6001
|
try {
|
|
5374
6002
|
handler(summary, scratch, ...args);
|
|
5375
6003
|
} catch (err) {
|
|
5376
|
-
|
|
6004
|
+
logger10.warn("A call-summary handler for %s threw; continuing. %o", event, err);
|
|
5377
6005
|
}
|
|
5378
6006
|
};
|
|
5379
6007
|
this._observer.on(event, listener);
|
|
@@ -5798,7 +6426,7 @@ function geohash(location, precision = 3) {
|
|
|
5798
6426
|
}
|
|
5799
6427
|
|
|
5800
6428
|
// src/detectors/ClientPopulationIssueDetector.ts
|
|
5801
|
-
var
|
|
6429
|
+
var logger11 = createLogger("ClientPopulationIssueDetector");
|
|
5802
6430
|
var ClientPopulationIssueTypes = {
|
|
5803
6431
|
/** One issue type is concentrated on one client population while the rest of the fleet is fine. */
|
|
5804
6432
|
clientPopulationIssue: "CLIENT_POPULATION_ISSUE"
|
|
@@ -5820,7 +6448,7 @@ var ClientPopulationIssueDetector = class _ClientPopulationIssueDetector {
|
|
|
5820
6448
|
...config
|
|
5821
6449
|
};
|
|
5822
6450
|
if (this._config.groupBy === "location" && !this._config.resolveClientLocation) {
|
|
5823
|
-
|
|
6451
|
+
logger11.warn(
|
|
5824
6452
|
"%s is configured with groupBy: 'location' but no resolveClientLocation, so no client can be placed and nothing will ever be detected",
|
|
5825
6453
|
this.name
|
|
5826
6454
|
);
|
|
@@ -6654,11 +7282,13 @@ function mediaKindOf(mimeType) {
|
|
|
6654
7282
|
}
|
|
6655
7283
|
|
|
6656
7284
|
// src/Observer.ts
|
|
6657
|
-
var
|
|
6658
|
-
var Observer = class extends
|
|
7285
|
+
var logger12 = createLogger("Observer");
|
|
7286
|
+
var Observer = class extends EventEmitter7 {
|
|
6659
7287
|
observedTURN = new ObservedTURN();
|
|
6660
7288
|
observedCalls = /* @__PURE__ */ new Map();
|
|
6661
7289
|
observedMediasoupRouters = /* @__PURE__ */ new Map();
|
|
7290
|
+
sinks = new SinkRegistry();
|
|
7291
|
+
extensionStats = new ExtensionStatsRegistry("observer", this.sinks);
|
|
6662
7292
|
/** Ancestry base shared by all Observer-bus events originating at the observer. */
|
|
6663
7293
|
eventScope = { observer: this };
|
|
6664
7294
|
config;
|
|
@@ -6773,7 +7403,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6773
7403
|
break;
|
|
6774
7404
|
}
|
|
6775
7405
|
default: {
|
|
6776
|
-
|
|
7406
|
+
logger12.warn("Unknown detector name %s; skipping", name);
|
|
6777
7407
|
return this;
|
|
6778
7408
|
}
|
|
6779
7409
|
}
|
|
@@ -6858,7 +7488,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6858
7488
|
break;
|
|
6859
7489
|
}
|
|
6860
7490
|
if (!validator) {
|
|
6861
|
-
|
|
7491
|
+
logger12.warn("Unknown validator name %s; skipping", name);
|
|
6862
7492
|
return this;
|
|
6863
7493
|
}
|
|
6864
7494
|
this.validators.add(validator);
|
|
@@ -6889,7 +7519,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6889
7519
|
try {
|
|
6890
7520
|
validator.cancel(reason);
|
|
6891
7521
|
} catch (err) {
|
|
6892
|
-
|
|
7522
|
+
logger12.warn("Error cancelling validator %s: %o", validator.name, err);
|
|
6893
7523
|
}
|
|
6894
7524
|
}
|
|
6895
7525
|
return matching.length;
|
|
@@ -6905,11 +7535,11 @@ var Observer = class extends EventEmitter6 {
|
|
|
6905
7535
|
*/
|
|
6906
7536
|
createObservedCall(settings, acceptCtx) {
|
|
6907
7537
|
if (this.closed) {
|
|
6908
|
-
|
|
7538
|
+
logger12.warn("Attempted to create a call (callId: %s) on a closed observer", settings.callId);
|
|
6909
7539
|
return void 0;
|
|
6910
7540
|
}
|
|
6911
7541
|
if (this.observedCalls.has(settings.callId)) {
|
|
6912
|
-
|
|
7542
|
+
logger12.warn("Observed Call with id %s already exists; returning the existing instance", settings.callId);
|
|
6913
7543
|
return this.observedCalls.get(settings.callId);
|
|
6914
7544
|
}
|
|
6915
7545
|
const callSettings = {
|
|
@@ -6947,14 +7577,14 @@ var Observer = class extends EventEmitter6 {
|
|
|
6947
7577
|
}
|
|
6948
7578
|
createObservedMediasoupRouter(settings) {
|
|
6949
7579
|
if (this.closed) {
|
|
6950
|
-
|
|
7580
|
+
logger12.warn("Attempted to create mediasoup router (id: %d) on a closed observer", settings.router.id);
|
|
6951
7581
|
return void 0;
|
|
6952
7582
|
}
|
|
6953
7583
|
if (this.observedMediasoupRouters.has(settings.router.id)) {
|
|
6954
|
-
|
|
7584
|
+
logger12.warn("Observed Mediasoup Router (id %s) already exists; returning the existing instance", settings.router.id);
|
|
6955
7585
|
return this.observedMediasoupRouters.get(settings.router.id);
|
|
6956
7586
|
}
|
|
6957
|
-
const observedMediasoupRouter = new ObservedMediasoupRouter(settings);
|
|
7587
|
+
const observedMediasoupRouter = new ObservedMediasoupRouter(settings, this.sinks);
|
|
6958
7588
|
const observedMediasoupRouterScope = {
|
|
6959
7589
|
observedMediasoupRouter,
|
|
6960
7590
|
observer: this
|
|
@@ -6982,7 +7612,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6982
7612
|
}
|
|
6983
7613
|
close() {
|
|
6984
7614
|
if (this.closed) {
|
|
6985
|
-
return
|
|
7615
|
+
return logger12.debug("Attempted to close twice");
|
|
6986
7616
|
}
|
|
6987
7617
|
this.closed = true;
|
|
6988
7618
|
for (const call of [...this.observedCalls.values()]) call.close();
|
|
@@ -6991,8 +7621,19 @@ var Observer = class extends EventEmitter6 {
|
|
|
6991
7621
|
this.validators.clear();
|
|
6992
7622
|
this.callSummaryCollector?.close();
|
|
6993
7623
|
this.activeIssuesRegistry.clear();
|
|
7624
|
+
this.extensionStats.close();
|
|
7625
|
+
this.sinks.clear();
|
|
6994
7626
|
this._notify("observer-closed", { ...this.eventScope });
|
|
6995
7627
|
}
|
|
7628
|
+
createExtensionStats(settings) {
|
|
7629
|
+
return this.extensionStats.create(settings);
|
|
7630
|
+
}
|
|
7631
|
+
acceptExtensionStats(id, data) {
|
|
7632
|
+
this.extensionStats.accept(id, data);
|
|
7633
|
+
}
|
|
7634
|
+
getExtensionStats(id) {
|
|
7635
|
+
return this.extensionStats.get(id);
|
|
7636
|
+
}
|
|
6996
7637
|
accept(sample, context) {
|
|
6997
7638
|
if (this.closed) {
|
|
6998
7639
|
return this._notify("sample-rejected", { ...this.eventScope, reason: "observer-closed", sample });
|
|
@@ -7000,7 +7641,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
7000
7641
|
try {
|
|
7001
7642
|
this.acceptMiddlewares.process({ sample, context });
|
|
7002
7643
|
} catch (err) {
|
|
7003
|
-
|
|
7644
|
+
logger12.warn("An accept middleware threw; dropping the sample. %o", err);
|
|
7004
7645
|
}
|
|
7005
7646
|
if (!sample.callId) {
|
|
7006
7647
|
this._notify("sample-rejected", { ...this.eventScope, reason: "missing-callId", sample });
|
|
@@ -7040,7 +7681,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
7040
7681
|
try {
|
|
7041
7682
|
validator.update();
|
|
7042
7683
|
} catch (err) {
|
|
7043
|
-
|
|
7684
|
+
logger12.warn("Error running validator %s: %o", validator.name, err);
|
|
7044
7685
|
}
|
|
7045
7686
|
}
|
|
7046
7687
|
this._notify("observer-updated", { ...this.eventScope });
|
|
@@ -7276,67 +7917,34 @@ var TrendTester = class {
|
|
|
7276
7917
|
}
|
|
7277
7918
|
};
|
|
7278
7919
|
|
|
7279
|
-
// src/sinks/ClientSampleSink.ts
|
|
7280
|
-
import { EventEmitter as EventEmitter7 } from "events";
|
|
7281
|
-
var ClientSampleSink = class extends EventEmitter7 {
|
|
7282
|
-
};
|
|
7283
|
-
|
|
7284
|
-
// src/sinks/JsonlFileSink.ts
|
|
7285
|
-
import * as fs from "fs";
|
|
7286
|
-
import * as path from "path";
|
|
7287
|
-
var JsonlFileSink = class extends ClientSampleSink {
|
|
7288
|
-
/** The file this sink writes to. Read it (e.g. in a `close` handler) to upload/move the file. */
|
|
7289
|
-
path;
|
|
7290
|
-
_stream;
|
|
7291
|
-
_serializeSample;
|
|
7292
|
-
constructor(options) {
|
|
7293
|
-
super();
|
|
7294
|
-
this.path = options.path;
|
|
7295
|
-
this._stream = fs.createWriteStream(this.path, { flags: "a" });
|
|
7296
|
-
this._stream.on("close", () => this.emit("close"));
|
|
7297
|
-
this._stream.on("finish", () => this.emit("finish"));
|
|
7298
|
-
this._stream.on("drain", () => this.emit("drain"));
|
|
7299
|
-
this._stream.on("error", (err) => this.emit("error", err));
|
|
7300
|
-
this._serializeSample = options.serializeSample ?? ((sample) => JSON.stringify(sample));
|
|
7301
|
-
}
|
|
7302
|
-
write(sample) {
|
|
7303
|
-
return this._stream.write(`${this._serializeSample(sample)}
|
|
7304
|
-
`);
|
|
7305
|
-
}
|
|
7306
|
-
end() {
|
|
7307
|
-
this._stream.end();
|
|
7308
|
-
}
|
|
7309
|
-
};
|
|
7310
|
-
function createJsonlFileSink(options) {
|
|
7311
|
-
return new JsonlFileSink(options);
|
|
7312
|
-
}
|
|
7313
|
-
function createJsonlFileSinkFactory(options) {
|
|
7314
|
-
return ({ clientId, observedCall }) => {
|
|
7315
|
-
const filename = options.getFileName?.({ callId: observedCall.callId, clientId }) ?? `${observedCall.callId}__${clientId}.jsonl`;
|
|
7316
|
-
return createJsonlFileSink({
|
|
7317
|
-
path: path.join(options.directory, filename),
|
|
7318
|
-
serializeSample: options.serializeSample
|
|
7319
|
-
});
|
|
7320
|
-
};
|
|
7321
|
-
}
|
|
7322
|
-
|
|
7323
7920
|
// src/sinks/InMemorySink.ts
|
|
7324
|
-
|
|
7325
|
-
|
|
7921
|
+
import { EventEmitter as EventEmitter8 } from "events";
|
|
7922
|
+
var InMemorySink = class extends EventEmitter8 {
|
|
7923
|
+
id;
|
|
7924
|
+
samples;
|
|
7925
|
+
_ended = false;
|
|
7926
|
+
constructor(settings = {}) {
|
|
7326
7927
|
super();
|
|
7327
|
-
this.
|
|
7928
|
+
this.id = settings.id ?? "in-memory";
|
|
7929
|
+
this.samples = settings.samples ?? [];
|
|
7930
|
+
}
|
|
7931
|
+
get ended() {
|
|
7932
|
+
return this._ended;
|
|
7328
7933
|
}
|
|
7329
|
-
samples;
|
|
7330
7934
|
write(sample) {
|
|
7935
|
+
if (this._ended) return false;
|
|
7331
7936
|
this.samples.push(sample);
|
|
7332
7937
|
return true;
|
|
7333
7938
|
}
|
|
7334
7939
|
end() {
|
|
7940
|
+
if (this._ended) return;
|
|
7941
|
+
this._ended = true;
|
|
7942
|
+
this.emit("ended");
|
|
7335
7943
|
this.emit("close");
|
|
7336
7944
|
}
|
|
7337
7945
|
};
|
|
7338
7946
|
function createInMemorySink(samples) {
|
|
7339
|
-
return new InMemorySink(samples);
|
|
7947
|
+
return new InMemorySink({ samples });
|
|
7340
7948
|
}
|
|
7341
7949
|
|
|
7342
7950
|
// src/resolvers/RemoteTrackResolver.ts
|
|
@@ -7513,6 +8121,8 @@ function createP2pRemoteTrackResolverFactory() {
|
|
|
7513
8121
|
}
|
|
7514
8122
|
export {
|
|
7515
8123
|
ActiveIssuesRegistry,
|
|
8124
|
+
AudioEchoDetector,
|
|
8125
|
+
AudioEchoTypes,
|
|
7516
8126
|
CODEC_MISMATCH_ISSUE,
|
|
7517
8127
|
CallConcurrentIssueDetector,
|
|
7518
8128
|
CallConcurrentIssueTypes,
|
|
@@ -7522,9 +8132,9 @@ export {
|
|
|
7522
8132
|
ClientMetaTypes,
|
|
7523
8133
|
ClientPopulationIssueDetector,
|
|
7524
8134
|
ClientPopulationIssueTypes,
|
|
7525
|
-
ClientSampleSink,
|
|
7526
8135
|
CodecConsistencyValidator,
|
|
7527
8136
|
Detectors,
|
|
8137
|
+
ExtensionStatsRegistry,
|
|
7528
8138
|
GEOHASH_CELL_SIZES,
|
|
7529
8139
|
InMemorySink,
|
|
7530
8140
|
IssueFanOutDetector,
|
|
@@ -7560,10 +8170,15 @@ export {
|
|
|
7560
8170
|
PublisherFaultCorroborationDetector,
|
|
7561
8171
|
PublisherFaultTypes,
|
|
7562
8172
|
RESOLVED_ISSUE_SUFFIX,
|
|
8173
|
+
RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS,
|
|
8174
|
+
RTT_HISTOGRAM_NUMBER_OF_BUCKETS,
|
|
7563
8175
|
RemoteTrackResolver,
|
|
7564
8176
|
RemoteTrackResolverValidator,
|
|
8177
|
+
SCORE_HISTOGRAM_BUCKET_WIDTH,
|
|
8178
|
+
SCORE_HISTOGRAM_NUMBER_OF_BUCKETS,
|
|
7565
8179
|
SfuCongestionDetector,
|
|
7566
8180
|
SimulcastReceiverValidator,
|
|
8181
|
+
SinkRegistry,
|
|
7567
8182
|
SlidingWindow,
|
|
7568
8183
|
TrackDeliveryMismatchDetector,
|
|
7569
8184
|
TrackDeliveryMismatchTypes,
|
|
@@ -7584,7 +8199,6 @@ export {
|
|
|
7584
8199
|
createDefaultMediasoupRemoteTrackResolverFactory,
|
|
7585
8200
|
createInMemorySink,
|
|
7586
8201
|
createJsonlFileSink,
|
|
7587
|
-
createJsonlFileSinkFactory,
|
|
7588
8202
|
createLogger,
|
|
7589
8203
|
createP2pRemoteTrackResolverFactory,
|
|
7590
8204
|
defaultCallSummaryConfig,
|
|
@@ -7599,6 +8213,7 @@ export {
|
|
|
7599
8213
|
medianAbsoluteDeviation,
|
|
7600
8214
|
pageHinkley,
|
|
7601
8215
|
percentile,
|
|
8216
|
+
percentileOfHistogram,
|
|
7602
8217
|
percentileOfSorted,
|
|
7603
8218
|
robustZScore,
|
|
7604
8219
|
schemaVersion,
|