@observertc/observer-js 1.1.0 → 1.2.0
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 +11 -0
- package/dist/index.d.mts +295 -43
- package/dist/index.d.ts +295 -43
- package/dist/index.js +473 -43
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +452 -29
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -2
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,
|
|
@@ -78,8 +80,12 @@ __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,
|
|
85
91
|
SlidingWindow: () => SlidingWindow,
|
|
@@ -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,
|
|
@@ -169,13 +176,13 @@ function setObserverLogger(logger11) {
|
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
// src/ObservedCall.ts
|
|
172
|
-
var
|
|
179
|
+
var import_node_events3 = require("events");
|
|
173
180
|
|
|
174
181
|
// src/ObservedClient.ts
|
|
175
|
-
var
|
|
182
|
+
var import_node_events2 = require("events");
|
|
176
183
|
|
|
177
184
|
// src/ObservedPeerConnection.ts
|
|
178
|
-
var
|
|
185
|
+
var import_node_events = require("events");
|
|
179
186
|
|
|
180
187
|
// src/utils/stats.ts
|
|
181
188
|
function percentile(values, p) {
|
|
@@ -230,6 +237,10 @@ function counterDelta(previous, current) {
|
|
|
230
237
|
if (current < previous) return 0;
|
|
231
238
|
return current - previous;
|
|
232
239
|
}
|
|
240
|
+
function rmsAudioLevel(deltaTotalAudioEnergy, deltaTotalSamplesDuration) {
|
|
241
|
+
if (!(0 < deltaTotalSamplesDuration)) return void 0;
|
|
242
|
+
return Math.sqrt(Math.max(0, deltaTotalAudioEnergy) / deltaTotalSamplesDuration);
|
|
243
|
+
}
|
|
233
244
|
function correlation(xs, ys) {
|
|
234
245
|
const n = Math.min(xs.length, ys.length);
|
|
235
246
|
if (n < 2) return 0;
|
|
@@ -262,8 +273,8 @@ function pageHinkley(values, delta = 0, lambda = 50) {
|
|
|
262
273
|
let runningMinimum = 0;
|
|
263
274
|
for (let i = 0; i < values.length; i++) {
|
|
264
275
|
sum += values[i];
|
|
265
|
-
const
|
|
266
|
-
cumulative += values[i] -
|
|
276
|
+
const mean2 = sum / (i + 1);
|
|
277
|
+
cumulative += values[i] - mean2 - delta;
|
|
267
278
|
if (cumulative < runningMinimum) runningMinimum = cumulative;
|
|
268
279
|
const ph = cumulative - runningMinimum;
|
|
269
280
|
statistic.push(ph);
|
|
@@ -315,6 +326,24 @@ function erf(x) {
|
|
|
315
326
|
const y = 1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-(ax * ax));
|
|
316
327
|
return sign * y;
|
|
317
328
|
}
|
|
329
|
+
function percentileOfHistogram(histogram, bucketWidth, p, options = {}) {
|
|
330
|
+
let total = 0;
|
|
331
|
+
for (const count of histogram) total += count;
|
|
332
|
+
if (total < 1) return void 0;
|
|
333
|
+
const rank = Math.min(Math.max(p, 0), 1) * total;
|
|
334
|
+
let cumulative = 0;
|
|
335
|
+
for (let index = 0; index < histogram.length; ++index) {
|
|
336
|
+
const count = histogram[index];
|
|
337
|
+
if (count < 1 || cumulative + count < rank) {
|
|
338
|
+
cumulative += count;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (options.openEnded && index === histogram.length - 1 && options.max !== void 0) return options.max;
|
|
342
|
+
const estimate = index * bucketWidth + (rank - cumulative) / count * bucketWidth;
|
|
343
|
+
return Math.min(Math.max(estimate, options.min ?? estimate), options.max ?? estimate);
|
|
344
|
+
}
|
|
345
|
+
return options.max;
|
|
346
|
+
}
|
|
318
347
|
|
|
319
348
|
// src/ObservedInboundRtp.ts
|
|
320
349
|
var ObservedInboundRtp = class {
|
|
@@ -425,6 +454,15 @@ var ObservedInboundRtp = class {
|
|
|
425
454
|
deltaFecPacketsReceived = 0;
|
|
426
455
|
deltaFecPacketsDiscarded = 0;
|
|
427
456
|
deltaPausesDuration = 0;
|
|
457
|
+
deltaTotalAudioEnergy = 0;
|
|
458
|
+
deltaTotalSamplesDuration = 0;
|
|
459
|
+
/**
|
|
460
|
+
* RMS level (0..1) of the audio played out from this stream over the tick, from the two deltas
|
|
461
|
+
* above. This is the far end's outgoing level **as this receiver heard it**, which is the only
|
|
462
|
+
* send-side level available for publishers on browsers that do not report `media-source` audio
|
|
463
|
+
* levels (Firefox). `undefined` for video and for a tick with no audio accounted for.
|
|
464
|
+
*/
|
|
465
|
+
rmsAudioLevel;
|
|
428
466
|
/**
|
|
429
467
|
* Mean jitter-buffer delay for the frames/samples emitted in this tick (seconds), derived from
|
|
430
468
|
* the cumulative `jitterBufferDelay` / `jitterBufferEmittedCount` pair — the only correct way to
|
|
@@ -510,6 +548,9 @@ var ObservedInboundRtp = class {
|
|
|
510
548
|
this.jitterBufferDelayInMs = void 0;
|
|
511
549
|
this.concealmentRatio = void 0;
|
|
512
550
|
this.framesDroppedRatio = void 0;
|
|
551
|
+
this.deltaTotalAudioEnergy = 0;
|
|
552
|
+
this.deltaTotalSamplesDuration = 0;
|
|
553
|
+
this.rmsAudioLevel = void 0;
|
|
513
554
|
const elapsedTimeInMs = stats.timestamp - this.timestamp;
|
|
514
555
|
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
556
|
if (0 < elapsedTimeInMs && !this.counterResetBoundary) {
|
|
@@ -539,6 +580,9 @@ var ObservedInboundRtp = class {
|
|
|
539
580
|
this.deltaRetransmittedPacketsReceived = counterDelta(this.retransmittedPacketsReceived, stats.retransmittedPacketsReceived);
|
|
540
581
|
this.deltaFecPacketsReceived = counterDelta(this.fecPacketsReceived, stats.fecPacketsReceived);
|
|
541
582
|
this.deltaFecPacketsDiscarded = counterDelta(this.fecPacketsDiscarded, stats.fecPacketsDiscarded);
|
|
583
|
+
this.deltaTotalAudioEnergy = counterDelta(this.totalAudioEnergy, stats.totalAudioEnergy);
|
|
584
|
+
this.deltaTotalSamplesDuration = counterDelta(this.totalSamplesDuration, stats.totalSamplesDuration);
|
|
585
|
+
this.rmsAudioLevel = rmsAudioLevel(this.deltaTotalAudioEnergy, this.deltaTotalSamplesDuration);
|
|
542
586
|
this.bitrate = this.deltaBytesReceived * 8 / elapsedInSeconds;
|
|
543
587
|
const pixels = (stats.frameWidth ?? 0) * (stats.frameHeight ?? 0);
|
|
544
588
|
if (0 < pixels && 0 < this.deltaFramesReceived) {
|
|
@@ -596,6 +640,9 @@ var ObservedInboundRtp = class {
|
|
|
596
640
|
this.jitterBufferEmittedCount = stats.jitterBufferEmittedCount;
|
|
597
641
|
this.jitterBufferMinimumDelay = stats.jitterBufferMinimumDelay;
|
|
598
642
|
this.totalSamplesReceived = stats.totalSamplesReceived;
|
|
643
|
+
this.audioLevel = stats.audioLevel;
|
|
644
|
+
this.totalAudioEnergy = stats.totalAudioEnergy;
|
|
645
|
+
this.totalSamplesDuration = stats.totalSamplesDuration;
|
|
599
646
|
this.concealedSamples = stats.concealedSamples;
|
|
600
647
|
this.silentConcealedSamples = stats.silentConcealedSamples;
|
|
601
648
|
this.concealmentEvents = stats.concealmentEvents;
|
|
@@ -1250,6 +1297,16 @@ var ObservedMediaSource = class {
|
|
|
1250
1297
|
frames;
|
|
1251
1298
|
framesPerSecond;
|
|
1252
1299
|
attachments;
|
|
1300
|
+
/** Increase of `totalAudioEnergy` in this tick (audio only). */
|
|
1301
|
+
deltaTotalAudioEnergy = 0;
|
|
1302
|
+
/** Increase of `totalSamplesDuration` in this tick, in seconds (audio only). */
|
|
1303
|
+
deltaTotalSamplesDuration = 0;
|
|
1304
|
+
/**
|
|
1305
|
+
* RMS level (0..1) of what the capture device delivered over the tick, after the browser's own
|
|
1306
|
+
* processing (AEC, NS, AGC). Chrome and Safari report the inputs; Firefox does not, and there the
|
|
1307
|
+
* publisher's level has to come from its receivers — see `ObservedOutboundTrack.rmsAudioLevel`.
|
|
1308
|
+
*/
|
|
1309
|
+
rmsAudioLevel;
|
|
1253
1310
|
get visited() {
|
|
1254
1311
|
const visited = this._visited;
|
|
1255
1312
|
this._visited = false;
|
|
@@ -1263,6 +1320,14 @@ var ObservedMediaSource = class {
|
|
|
1263
1320
|
}
|
|
1264
1321
|
update(stats) {
|
|
1265
1322
|
this._visited = true;
|
|
1323
|
+
this.deltaTotalAudioEnergy = 0;
|
|
1324
|
+
this.deltaTotalSamplesDuration = 0;
|
|
1325
|
+
this.rmsAudioLevel = void 0;
|
|
1326
|
+
if (this.timestamp < stats.timestamp) {
|
|
1327
|
+
this.deltaTotalAudioEnergy = counterDelta(this.totalAudioEnergy, stats.totalAudioEnergy);
|
|
1328
|
+
this.deltaTotalSamplesDuration = counterDelta(this.totalSamplesDuration, stats.totalSamplesDuration);
|
|
1329
|
+
this.rmsAudioLevel = rmsAudioLevel(this.deltaTotalAudioEnergy, this.deltaTotalSamplesDuration);
|
|
1330
|
+
}
|
|
1266
1331
|
this.timestamp = stats.timestamp;
|
|
1267
1332
|
this.trackIdentifier = stats.trackIdentifier;
|
|
1268
1333
|
this.audioLevel = stats.audioLevel;
|
|
@@ -1661,6 +1726,20 @@ var ObservedOutboundTrack = class {
|
|
|
1661
1726
|
remoteFractionLost;
|
|
1662
1727
|
remoteRttInMs;
|
|
1663
1728
|
qualityLimitationReason;
|
|
1729
|
+
/**
|
|
1730
|
+
* This audio track's RMS level (0..1) over the latest tick — how loud the publisher was sending.
|
|
1731
|
+
* Taken from the publisher's own `media-source` when it reports one (Chrome, Safari), otherwise
|
|
1732
|
+
* the median of what its receivers heard ({@link receivedRmsAudioLevel}), which is what makes it
|
|
1733
|
+
* available for Firefox publishers too. `undefined` for video, or when neither is known.
|
|
1734
|
+
* {@link rmsAudioLevelSource} says which one it is.
|
|
1735
|
+
*/
|
|
1736
|
+
rmsAudioLevel;
|
|
1737
|
+
rmsAudioLevelSource;
|
|
1738
|
+
/**
|
|
1739
|
+
* Median RMS level (0..1) of this track across the receivers that played it out, each over its own
|
|
1740
|
+
* latest tick. Needs a `RemoteTrackResolver`: without links there are no receivers to ask.
|
|
1741
|
+
*/
|
|
1742
|
+
receivedRmsAudioLevel;
|
|
1664
1743
|
get score() {
|
|
1665
1744
|
return this.calculatedScore.value;
|
|
1666
1745
|
}
|
|
@@ -1681,6 +1760,10 @@ var ObservedOutboundTrack = class {
|
|
|
1681
1760
|
getMediaSource() {
|
|
1682
1761
|
return this._mediaSource;
|
|
1683
1762
|
}
|
|
1763
|
+
/** The capture device behind this track, when the client sent `AUDIO_INPUT_DEVICE` metadata for it. */
|
|
1764
|
+
get audioInputDevice() {
|
|
1765
|
+
return this._peerConnection.client.audioInputDevices.get(this.id);
|
|
1766
|
+
}
|
|
1684
1767
|
update(stats) {
|
|
1685
1768
|
this._visited = true;
|
|
1686
1769
|
this.timestamp = stats.timestamp;
|
|
@@ -1712,8 +1795,30 @@ var ObservedOutboundTrack = class {
|
|
|
1712
1795
|
if (this.muted !== true && this.deltaPacketsSent === 0) reasons.push("no-packets-sent");
|
|
1713
1796
|
this.degradedReasons = reasons.length === 0 ? void 0 : reasons;
|
|
1714
1797
|
this.receivingDistribution = this.createReceivingDistribution();
|
|
1798
|
+
this._updateAudioLevel();
|
|
1715
1799
|
this.detectors.update();
|
|
1716
1800
|
}
|
|
1801
|
+
_updateAudioLevel() {
|
|
1802
|
+
this.rmsAudioLevel = void 0;
|
|
1803
|
+
this.rmsAudioLevelSource = void 0;
|
|
1804
|
+
this.receivedRmsAudioLevel = void 0;
|
|
1805
|
+
if (this.kind !== "audio") return;
|
|
1806
|
+
if (0 < this.remoteInboundTracks.size) {
|
|
1807
|
+
const levels = [];
|
|
1808
|
+
for (const inboundTrack of this.remoteInboundTracks) {
|
|
1809
|
+
pushFinite(levels, inboundTrack.getInboundRtp()?.rmsAudioLevel);
|
|
1810
|
+
}
|
|
1811
|
+
this.receivedRmsAudioLevel = median(levels);
|
|
1812
|
+
}
|
|
1813
|
+
const sourceLevel = this._mediaSource?.rmsAudioLevel;
|
|
1814
|
+
if (sourceLevel !== void 0) {
|
|
1815
|
+
this.rmsAudioLevel = sourceLevel;
|
|
1816
|
+
this.rmsAudioLevelSource = "media-source";
|
|
1817
|
+
} else if (this.receivedRmsAudioLevel !== void 0) {
|
|
1818
|
+
this.rmsAudioLevel = this.receivedRmsAudioLevel;
|
|
1819
|
+
this.rmsAudioLevelSource = "receivers";
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1717
1822
|
createReceivingDistribution() {
|
|
1718
1823
|
if (this.remoteInboundTracks.size === 0) return void 0;
|
|
1719
1824
|
let freezeAffected = 0;
|
|
@@ -1776,7 +1881,7 @@ function publicAddressOfLocalCandidate(candidate) {
|
|
|
1776
1881
|
if (isPublicIpAddress(candidate.relatedAddress)) return candidate.relatedAddress;
|
|
1777
1882
|
return void 0;
|
|
1778
1883
|
}
|
|
1779
|
-
var ObservedPeerConnection = class extends
|
|
1884
|
+
var ObservedPeerConnection = class extends import_node_events.EventEmitter {
|
|
1780
1885
|
constructor(peerConnectionId, client) {
|
|
1781
1886
|
super();
|
|
1782
1887
|
this.peerConnectionId = peerConnectionId;
|
|
@@ -2753,6 +2858,7 @@ var ClientMetaTypes = /* @__PURE__ */ ((ClientMetaTypes2) => {
|
|
|
2753
2858
|
ClientMetaTypes2["MEDIA_DEVICES_SUPPORTED_CONSTRAINTS"] = "MEDIA_DEVICES_SUPPORTED_CONSTRAINTS";
|
|
2754
2859
|
ClientMetaTypes2["USER_MEDIA_ERROR"] = "USER_MEDIA_ERROR";
|
|
2755
2860
|
ClientMetaTypes2["LOCAL_SDP"] = "LOCAL_SDP";
|
|
2861
|
+
ClientMetaTypes2["AUDIO_INPUT_DEVICE"] = "AUDIO_INPUT_DEVICE";
|
|
2756
2862
|
ClientMetaTypes2["OPERATION_SYSTEM"] = "OPERATION_SYSTEM";
|
|
2757
2863
|
ClientMetaTypes2["ENGINE"] = "ENGINE";
|
|
2758
2864
|
ClientMetaTypes2["PLATFORM"] = "PLATFORM";
|
|
@@ -2771,7 +2877,11 @@ function baseIssueType(type) {
|
|
|
2771
2877
|
|
|
2772
2878
|
// src/ObservedClient.ts
|
|
2773
2879
|
var logger2 = createLogger("ObservedClient");
|
|
2774
|
-
var
|
|
2880
|
+
var RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS = 20;
|
|
2881
|
+
var RTT_HISTOGRAM_NUMBER_OF_BUCKETS = 50;
|
|
2882
|
+
var SCORE_HISTOGRAM_BUCKET_WIDTH = 1;
|
|
2883
|
+
var SCORE_HISTOGRAM_NUMBER_OF_BUCKETS = 5;
|
|
2884
|
+
var ObservedClient = class extends import_node_events2.EventEmitter {
|
|
2775
2885
|
constructor(settings, call, activeIssues) {
|
|
2776
2886
|
super();
|
|
2777
2887
|
this.call = call;
|
|
@@ -2835,10 +2945,6 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
2835
2945
|
deltaOutboundPacketsSent = 0;
|
|
2836
2946
|
deltaTransportSentBytes = 0;
|
|
2837
2947
|
deltaTransportReceivedBytes = 0;
|
|
2838
|
-
deltaRttLt50Measurements = 0;
|
|
2839
|
-
deltaRttLt150Measurements = 0;
|
|
2840
|
-
deltaRttLt300Measurements = 0;
|
|
2841
|
-
deltaRttGtOrEq300Measurements = 0;
|
|
2842
2948
|
currentMaxRttInMs;
|
|
2843
2949
|
currentMinRttInMs;
|
|
2844
2950
|
currentAvgRttInMs;
|
|
@@ -2854,8 +2960,73 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
2854
2960
|
deltaNumberOfIssues = 0;
|
|
2855
2961
|
totalScoreSum = 0;
|
|
2856
2962
|
numberOfScoreMeasurements = 0;
|
|
2857
|
-
|
|
2963
|
+
minScore;
|
|
2964
|
+
maxScore;
|
|
2965
|
+
/**
|
|
2966
|
+
* Score measurements (one per accepted sample with a score) counted into
|
|
2967
|
+
* `SCORE_HISTOGRAM_NUMBER_OF_BUCKETS` buckets of `SCORE_HISTOGRAM_BUCKET_WIDTH`. Histograms of
|
|
2968
|
+
* several clients can be added bucket by bucket to get the distribution of a call.
|
|
2969
|
+
*/
|
|
2970
|
+
scoreHistogram = new Array(SCORE_HISTOGRAM_NUMBER_OF_BUCKETS).fill(0);
|
|
2971
|
+
/** Issues raised over the session, counted as in `issueHistogram`. */
|
|
2972
|
+
totalNumberOfIssues = 0;
|
|
2973
|
+
/**
|
|
2974
|
+
* Number of times each issue type was raised over the session, keyed by the base type (without
|
|
2975
|
+
* the `-resolved` suffix): `client.issueHistogram['loopback-audio-input']`. A resolution entry is
|
|
2976
|
+
* not counted, and neither is a re-raise of a keyed issue that is still open, so a stateful issue
|
|
2977
|
+
* reported on every tick counts once per open interval. A type never raised is `undefined`.
|
|
2978
|
+
*/
|
|
2979
|
+
issueHistogram = /* @__PURE__ */ Object.create(null);
|
|
2980
|
+
// Session totals. Unlike the `delta*` and `current*` fields they are never reset, and unlike the
|
|
2981
|
+
// `total*` fields of `ObservedPeerConnection` they survive a peer connection being closed.
|
|
2982
|
+
// Every RTT measurement is one peer connection's `currentRttInMs` in one accepted sample.
|
|
2983
|
+
totalRttSumInMs = 0;
|
|
2984
|
+
numberOfRttMeasurements = 0;
|
|
2985
|
+
maxRttInMs;
|
|
2986
|
+
minRttInMs;
|
|
2987
|
+
/**
|
|
2988
|
+
* RTT measurements counted into fixed buckets of `RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS`; the last bucket
|
|
2989
|
+
* holds every measurement of 980 ms and above. Constant memory, so percentiles can be estimated
|
|
2990
|
+
* over the whole session (see `rttSummary`) without keeping every measurement. Histograms
|
|
2991
|
+
* of several clients can be added bucket by bucket to get the distribution of a call.
|
|
2992
|
+
*/
|
|
2993
|
+
rttHistogram = new Array(RTT_HISTOGRAM_NUMBER_OF_BUCKETS).fill(0);
|
|
2994
|
+
_rttSummary;
|
|
2995
|
+
_scoreSummary;
|
|
2996
|
+
totalInboundPacketsLost = 0;
|
|
2997
|
+
totalInboundPacketsReceived = 0;
|
|
2998
|
+
totalOutboundPacketsSent = 0;
|
|
2999
|
+
totalReceivedAudioBytes = 0;
|
|
3000
|
+
totalReceivedVideoBytes = 0;
|
|
3001
|
+
totalSentAudioBytes = 0;
|
|
3002
|
+
totalSentVideoBytes = 0;
|
|
3003
|
+
totalTransportReceivedBytes = 0;
|
|
3004
|
+
totalTransportSentBytes = 0;
|
|
3005
|
+
totalDataChannelBytesReceived = 0;
|
|
3006
|
+
totalDataChannelBytesSent = 0;
|
|
3007
|
+
totalDataChannelMessagesReceived = 0;
|
|
3008
|
+
totalDataChannelMessagesSent = 0;
|
|
3009
|
+
/** Highest `sendingAudioBitrate + sendingVideoBitrate` of any sample, in bits per second. */
|
|
3010
|
+
peakSendingBitrate = 0;
|
|
3011
|
+
/** Highest `receivingAudioBitrate + receivingVideoBitrate` of any sample, in bits per second. */
|
|
3012
|
+
peakReceivingBitrate = 0;
|
|
3013
|
+
// Samples by the transport path of the selected ICE candidate pairs. A sample whose peer
|
|
3014
|
+
// connections use different paths is counted under each of them, so more than one non-zero
|
|
3015
|
+
// counter means the path changed or was mixed. A sample with no selected pair counts as unknown.
|
|
3016
|
+
numberOfSamplesOverUdp = 0;
|
|
3017
|
+
numberOfSamplesOverTcp = 0;
|
|
3018
|
+
numberOfSamplesOverTurnUdp = 0;
|
|
3019
|
+
numberOfSamplesOverTurnTcp = 0;
|
|
3020
|
+
numberOfSamplesWithUnknownPath = 0;
|
|
3021
|
+
numberOfPublicAddressChanges = 0;
|
|
2858
3022
|
mediaDevices = [];
|
|
3023
|
+
/**
|
|
3024
|
+
* The capture device behind each outbound audio track, keyed by track id, from the client's
|
|
3025
|
+
* `AUDIO_INPUT_DEVICE` metadata. Kept for the client's lifetime (a device switch is a new track
|
|
3026
|
+
* id), so a finding raised after a track ended can still name its device. Read it per track via
|
|
3027
|
+
* `ObservedOutboundTrack.audioInputDevice`.
|
|
3028
|
+
*/
|
|
3029
|
+
audioInputDevices = /* @__PURE__ */ new Map();
|
|
2859
3030
|
/**
|
|
2860
3031
|
* The client's currently **open** stateful issues, keyed by `ClientIssue.key` — the server-side
|
|
2861
3032
|
* mirror of the client monitor's own active-issue map (client-monitor-js >= 4.6.0).
|
|
@@ -2868,9 +3039,7 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
2868
3039
|
activeIssues;
|
|
2869
3040
|
_pendingInjections = {};
|
|
2870
3041
|
_activeSample;
|
|
2871
|
-
/** Peer connections the client reported closed (`PEER_CONNECTION_CLOSED`), closed once the current sample is applied. */
|
|
2872
3042
|
_peerConnectionsToClose = /* @__PURE__ */ new Set();
|
|
2873
|
-
/** Ids of peer connections closed by the client; late stats for them are ignored rather than resurrecting them. */
|
|
2874
3043
|
_closedPeerConnectionIds = /* @__PURE__ */ new Set();
|
|
2875
3044
|
closeTimer;
|
|
2876
3045
|
/** The distinct `publicAddress`es currently known by this client's open peer connections. */
|
|
@@ -2887,6 +3056,61 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
2887
3056
|
get score() {
|
|
2888
3057
|
return this.calculatedScore.value;
|
|
2889
3058
|
}
|
|
3059
|
+
/**
|
|
3060
|
+
* Percentiles (error at most one 20 ms bucket; a percentile in the overflow bucket is reported as
|
|
3061
|
+
* `maxRttInMs`) and mean of every RTT measurement of the session. Cached until the next measurement.
|
|
3062
|
+
*/
|
|
3063
|
+
get rttSummary() {
|
|
3064
|
+
if (!this._rttSummary) {
|
|
3065
|
+
const bounds = { min: this.minRttInMs, max: this.maxRttInMs, openEnded: true };
|
|
3066
|
+
const percentile2 = (p) => percentileOfHistogram(this.rttHistogram, RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS, p, bounds);
|
|
3067
|
+
this._rttSummary = Object.freeze({
|
|
3068
|
+
p50: percentile2(0.5),
|
|
3069
|
+
p75: percentile2(0.75),
|
|
3070
|
+
p95: percentile2(0.95),
|
|
3071
|
+
p99: percentile2(0.99),
|
|
3072
|
+
avg: 0 < this.numberOfRttMeasurements ? this.totalRttSumInMs / this.numberOfRttMeasurements : void 0
|
|
3073
|
+
});
|
|
3074
|
+
}
|
|
3075
|
+
return this._rttSummary;
|
|
3076
|
+
}
|
|
3077
|
+
/**
|
|
3078
|
+
* Percentiles (error at most one score point) and mean of every score of the session. Cached until
|
|
3079
|
+
* the next score.
|
|
3080
|
+
*/
|
|
3081
|
+
get scoreSummary() {
|
|
3082
|
+
if (!this._scoreSummary) {
|
|
3083
|
+
const bounds = { min: this.minScore, max: this.maxScore };
|
|
3084
|
+
const percentile2 = (p) => percentileOfHistogram(this.scoreHistogram, SCORE_HISTOGRAM_BUCKET_WIDTH, p, bounds);
|
|
3085
|
+
this._scoreSummary = Object.freeze({
|
|
3086
|
+
p5: percentile2(0.05),
|
|
3087
|
+
p25: percentile2(0.25),
|
|
3088
|
+
p50: percentile2(0.5),
|
|
3089
|
+
p75: percentile2(0.75),
|
|
3090
|
+
p95: percentile2(0.95),
|
|
3091
|
+
avg: 0 < this.numberOfScoreMeasurements ? this.totalScoreSum / this.numberOfScoreMeasurements : void 0
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
return this._scoreSummary;
|
|
3095
|
+
}
|
|
3096
|
+
_addScoreMeasurement(score) {
|
|
3097
|
+
this._scoreSummary = void 0;
|
|
3098
|
+
this.totalScoreSum += score;
|
|
3099
|
+
++this.numberOfScoreMeasurements;
|
|
3100
|
+
if (this.maxScore === void 0 || this.maxScore < score) this.maxScore = score;
|
|
3101
|
+
if (this.minScore === void 0 || score < this.minScore) this.minScore = score;
|
|
3102
|
+
const bucket = Math.min(Math.max(Math.floor(score / SCORE_HISTOGRAM_BUCKET_WIDTH), 0), SCORE_HISTOGRAM_NUMBER_OF_BUCKETS - 1);
|
|
3103
|
+
++this.scoreHistogram[bucket];
|
|
3104
|
+
}
|
|
3105
|
+
_addRttMeasurement(rttInMs) {
|
|
3106
|
+
this._rttSummary = void 0;
|
|
3107
|
+
this.totalRttSumInMs += rttInMs;
|
|
3108
|
+
++this.numberOfRttMeasurements;
|
|
3109
|
+
if (this.maxRttInMs === void 0 || this.maxRttInMs < rttInMs) this.maxRttInMs = rttInMs;
|
|
3110
|
+
if (this.minRttInMs === void 0 || rttInMs < this.minRttInMs) this.minRttInMs = rttInMs;
|
|
3111
|
+
const bucket = Math.min(Math.max(Math.floor(rttInMs / RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS), 0), RTT_HISTOGRAM_NUMBER_OF_BUCKETS - 1);
|
|
3112
|
+
++this.rttHistogram[bucket];
|
|
3113
|
+
}
|
|
2890
3114
|
close() {
|
|
2891
3115
|
if (this.closed) return;
|
|
2892
3116
|
if (this.closeTimer) {
|
|
@@ -2930,6 +3154,10 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
2930
3154
|
const elapsedInSeconds = elapsedInMs / 1e3;
|
|
2931
3155
|
let sumOfRtts = 0;
|
|
2932
3156
|
let numberOfRttMeasurements = 0;
|
|
3157
|
+
let overUdp = false;
|
|
3158
|
+
let overTcp = false;
|
|
3159
|
+
let overTurnUdp = false;
|
|
3160
|
+
let overTurnTcp = false;
|
|
2933
3161
|
++this.acceptedSamples;
|
|
2934
3162
|
this.availableIncomingBitrate = 0;
|
|
2935
3163
|
this.availableOutgoingBitrate = 0;
|
|
@@ -2946,10 +3174,6 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
2946
3174
|
this.deltaSentVideoBytes = 0;
|
|
2947
3175
|
this.deltaTransportReceivedBytes = 0;
|
|
2948
3176
|
this.deltaTransportSentBytes = 0;
|
|
2949
|
-
this.deltaRttLt50Measurements = 0;
|
|
2950
|
-
this.deltaRttLt150Measurements = 0;
|
|
2951
|
-
this.deltaRttLt300Measurements = 0;
|
|
2952
|
-
this.deltaRttGtOrEq300Measurements = 0;
|
|
2953
3177
|
this.deltaNumberOfIssues = 0;
|
|
2954
3178
|
this.numberOfDataChannels = 0;
|
|
2955
3179
|
this.numberOfInboundRtpStreams = 0;
|
|
@@ -3021,6 +3245,18 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3021
3245
|
if (observedPeerConnection.usingTCP) {
|
|
3022
3246
|
this.usingTCP = true;
|
|
3023
3247
|
}
|
|
3248
|
+
for (const selectedPair of observedPeerConnection.selectedIceCandidatePairs) {
|
|
3249
|
+
const localCandidate = selectedPair.getLocalCandidate();
|
|
3250
|
+
if (!localCandidate) continue;
|
|
3251
|
+
if (localCandidate.candidateType === "relay") {
|
|
3252
|
+
if (localCandidate.relayProtocol === "tcp" || localCandidate.relayProtocol === "tls") overTurnTcp = true;
|
|
3253
|
+
else overTurnUdp = true;
|
|
3254
|
+
} else if (localCandidate.protocol === "tcp") {
|
|
3255
|
+
overTcp = true;
|
|
3256
|
+
} else {
|
|
3257
|
+
overUdp = true;
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3024
3260
|
if (observedPeerConnection.currentRttInMs) {
|
|
3025
3261
|
if (this.currentMinRttInMs === void 0 || observedPeerConnection.currentRttInMs < this.currentMinRttInMs) {
|
|
3026
3262
|
this.currentMinRttInMs = observedPeerConnection.currentRttInMs;
|
|
@@ -3028,17 +3264,9 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3028
3264
|
if (this.currentMaxRttInMs === void 0 || observedPeerConnection.currentRttInMs > this.currentMaxRttInMs) {
|
|
3029
3265
|
this.currentMaxRttInMs = observedPeerConnection.currentRttInMs;
|
|
3030
3266
|
}
|
|
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
3267
|
sumOfRtts += observedPeerConnection.currentRttInMs;
|
|
3041
3268
|
++numberOfRttMeasurements;
|
|
3269
|
+
this._addRttMeasurement(observedPeerConnection.currentRttInMs);
|
|
3042
3270
|
}
|
|
3043
3271
|
}
|
|
3044
3272
|
for (const clientEvent of clientEventsPostBuffer) {
|
|
@@ -3048,6 +3276,7 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3048
3276
|
if (latestPublicAddress !== void 0 && latestPublicAddress !== this.publicAddress) {
|
|
3049
3277
|
const previousPublicAddress = this.publicAddress;
|
|
3050
3278
|
this.publicAddress = latestPublicAddress;
|
|
3279
|
+
if (previousPublicAddress !== void 0) ++this.numberOfPublicAddressChanges;
|
|
3051
3280
|
this._notify("client-public-address-changed", { ...this.eventScope, publicAddress: latestPublicAddress, previousPublicAddress });
|
|
3052
3281
|
}
|
|
3053
3282
|
this.attachments = sample.attachments;
|
|
@@ -3063,15 +3292,34 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3063
3292
|
this.sendingVideoBitrate = 0;
|
|
3064
3293
|
}
|
|
3065
3294
|
this.currentAvgRttInMs = 0 < numberOfRttMeasurements ? sumOfRtts / numberOfRttMeasurements : void 0;
|
|
3295
|
+
this.peakSendingBitrate = Math.max(this.peakSendingBitrate, this.sendingAudioBitrate + this.sendingVideoBitrate);
|
|
3296
|
+
this.peakReceivingBitrate = Math.max(this.peakReceivingBitrate, this.receivingAudioBitrate + this.receivingVideoBitrate);
|
|
3297
|
+
this.totalInboundPacketsLost += this.deltaInboundPacketsLost;
|
|
3298
|
+
this.totalInboundPacketsReceived += this.deltaInboundPacketsReceived;
|
|
3299
|
+
this.totalOutboundPacketsSent += this.deltaOutboundPacketsSent;
|
|
3300
|
+
this.totalReceivedAudioBytes += this.deltaReceivedAudioBytes;
|
|
3301
|
+
this.totalReceivedVideoBytes += this.deltaReceivedVideoBytes;
|
|
3302
|
+
this.totalSentAudioBytes += this.deltaSentAudioBytes;
|
|
3303
|
+
this.totalSentVideoBytes += this.deltaSentVideoBytes;
|
|
3304
|
+
this.totalTransportReceivedBytes += this.deltaTransportReceivedBytes;
|
|
3305
|
+
this.totalTransportSentBytes += this.deltaTransportSentBytes;
|
|
3306
|
+
this.totalDataChannelBytesReceived += this.deltaDataChannelBytesReceived;
|
|
3307
|
+
this.totalDataChannelBytesSent += this.deltaDataChannelBytesSent;
|
|
3308
|
+
this.totalDataChannelMessagesReceived += this.deltaDataChannelMessagesReceived;
|
|
3309
|
+
this.totalDataChannelMessagesSent += this.deltaDataChannelMessagesSent;
|
|
3310
|
+
if (overUdp) ++this.numberOfSamplesOverUdp;
|
|
3311
|
+
if (overTcp) ++this.numberOfSamplesOverTcp;
|
|
3312
|
+
if (overTurnUdp) ++this.numberOfSamplesOverTurnUdp;
|
|
3313
|
+
if (overTurnTcp) ++this.numberOfSamplesOverTurnTcp;
|
|
3314
|
+
if (!overUdp && !overTcp && !overTurnUdp && !overTurnTcp) ++this.numberOfSamplesWithUnknownPath;
|
|
3066
3315
|
this.calculatedScore.value = sample.score;
|
|
3067
3316
|
this.lastSampleTimestamp = sample.timestamp;
|
|
3068
3317
|
const elapsedTimeInMs = now - this.updated;
|
|
3069
3318
|
this.emit("update", sample, elapsedTimeInMs, context);
|
|
3070
3319
|
this._notify("client-updated", { ...this.eventScope, sample, elapsedTimeInMs, context });
|
|
3071
3320
|
this.updated = now;
|
|
3072
|
-
if (this.calculatedScore.value) {
|
|
3073
|
-
this.
|
|
3074
|
-
++this.numberOfScoreMeasurements;
|
|
3321
|
+
if (typeof this.calculatedScore.value === "number" && Number.isFinite(this.calculatedScore.value)) {
|
|
3322
|
+
this._addScoreMeasurement(this.calculatedScore.value);
|
|
3075
3323
|
}
|
|
3076
3324
|
if (this.settings.closeClientIfIdleForMs !== void 0) {
|
|
3077
3325
|
this.closeTimer = setTimeout(() => {
|
|
@@ -3162,6 +3410,13 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3162
3410
|
this.operationSystem = parseJsonAs(metadata.payload);
|
|
3163
3411
|
break;
|
|
3164
3412
|
}
|
|
3413
|
+
case "AUDIO_INPUT_DEVICE" /* AUDIO_INPUT_DEVICE */: {
|
|
3414
|
+
const device = parseJsonAs(metadata.payload);
|
|
3415
|
+
if (typeof device?.trackId === "string" && typeof device.label === "string") {
|
|
3416
|
+
this.audioInputDevices.set(device.trackId, device);
|
|
3417
|
+
}
|
|
3418
|
+
break;
|
|
3419
|
+
}
|
|
3165
3420
|
}
|
|
3166
3421
|
this._notify("client-metadata", { ...this.eventScope, metaData: metadata });
|
|
3167
3422
|
}
|
|
@@ -3182,6 +3437,11 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
3182
3437
|
if (isClientIssueResolutionEntry(issue)) {
|
|
3183
3438
|
return this._resolveIssue(issue);
|
|
3184
3439
|
}
|
|
3440
|
+
if (!issue.key || !this.activeIssues.get(issue.key)) {
|
|
3441
|
+
const type = baseIssueType(issue.type);
|
|
3442
|
+
this.issueHistogram[type] = (this.issueHistogram[type] ?? 0) + 1;
|
|
3443
|
+
++this.totalNumberOfIssues;
|
|
3444
|
+
}
|
|
3185
3445
|
const now = Date.now();
|
|
3186
3446
|
if (issue.key) {
|
|
3187
3447
|
const payload = parseJsonObject(issue.payload);
|
|
@@ -4453,9 +4713,172 @@ var PublisherFaultCorroborationDetector = class _PublisherFaultCorroborationDete
|
|
|
4453
4713
|
}
|
|
4454
4714
|
};
|
|
4455
4715
|
|
|
4716
|
+
// src/detectors/AudioEchoDetector.ts
|
|
4717
|
+
var AudioEchoTypes = {
|
|
4718
|
+
/**
|
|
4719
|
+
* One participant's outgoing audio follows another participant's voice: it rises when they speak,
|
|
4720
|
+
* in proportion to how loud they are. The echoing participant is sending the far end's playout
|
|
4721
|
+
* back into the call — speakers into an open mic, or a loopback capture device.
|
|
4722
|
+
*/
|
|
4723
|
+
audioEchoSuspected: "AUDIO_ECHO_SUSPECTED"
|
|
4724
|
+
};
|
|
4725
|
+
var DIRECTION_MARGIN = 0.1;
|
|
4726
|
+
var mean = (values) => values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
4727
|
+
var LEVEL_FLOOR = 1e-5;
|
|
4728
|
+
var toDb = (level) => 20 * Math.log10(Math.max(level, LEVEL_FLOOR));
|
|
4729
|
+
var AudioEchoDetector = class _AudioEchoDetector {
|
|
4730
|
+
constructor(call, config = {}) {
|
|
4731
|
+
this.call = call;
|
|
4732
|
+
this._config = {
|
|
4733
|
+
bucketMs: 5e3,
|
|
4734
|
+
windowBuckets: 24,
|
|
4735
|
+
activeRmsLevel: 0.01,
|
|
4736
|
+
minSourceActiveBuckets: 8,
|
|
4737
|
+
minFollowRatio: 0.8,
|
|
4738
|
+
minCorrelation: 0.6,
|
|
4739
|
+
cooldownMs: 12e4,
|
|
4740
|
+
loopbackAudioInputIssueType: "loopback-audio-input",
|
|
4741
|
+
...config
|
|
4742
|
+
};
|
|
4743
|
+
}
|
|
4744
|
+
call;
|
|
4745
|
+
static NAME = "audio-echo-detector";
|
|
4746
|
+
name = _AudioEchoDetector.NAME;
|
|
4747
|
+
_config;
|
|
4748
|
+
_series = /* @__PURE__ */ new Map();
|
|
4749
|
+
_lastRaisedAt = /* @__PURE__ */ new Map();
|
|
4750
|
+
_lastEvaluatedBucket;
|
|
4751
|
+
close() {
|
|
4752
|
+
this._series.clear();
|
|
4753
|
+
this._lastRaisedAt.clear();
|
|
4754
|
+
}
|
|
4755
|
+
update() {
|
|
4756
|
+
const now = Date.now();
|
|
4757
|
+
const bucket = Math.floor(now / this._config.bucketMs);
|
|
4758
|
+
if (this._lastEvaluatedBucket === bucket) return;
|
|
4759
|
+
const closedBucket = bucket - 1;
|
|
4760
|
+
this._lastEvaluatedBucket = bucket;
|
|
4761
|
+
this._collect(now, closedBucket);
|
|
4762
|
+
this._evaluate(now, bucket);
|
|
4763
|
+
}
|
|
4764
|
+
_collect(now, bucket) {
|
|
4765
|
+
for (const client of this.call.observedClients.values()) {
|
|
4766
|
+
for (const peerConnection of client.observedPeerConnections.values()) {
|
|
4767
|
+
for (const track of peerConnection.observedOutboundTracks.values()) {
|
|
4768
|
+
if (track.kind !== "audio") continue;
|
|
4769
|
+
let series = this._series.get(track.id);
|
|
4770
|
+
if (!series) {
|
|
4771
|
+
series = { track, clientId: client.clientId, lastTimestamp: -1, samples: [], lastSeenAt: now };
|
|
4772
|
+
this._series.set(track.id, series);
|
|
4773
|
+
}
|
|
4774
|
+
series.lastSeenAt = now;
|
|
4775
|
+
if (series.lastTimestamp === track.timestamp) continue;
|
|
4776
|
+
series.lastTimestamp = track.timestamp;
|
|
4777
|
+
const level = track.rmsAudioLevel;
|
|
4778
|
+
if (level === void 0) continue;
|
|
4779
|
+
series.samples.push({ bucket, level });
|
|
4780
|
+
const oldest = bucket - this._config.windowBuckets;
|
|
4781
|
+
while (series.samples.length && series.samples[0].bucket <= oldest) series.samples.shift();
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4785
|
+
for (const [trackId, series] of this._series) {
|
|
4786
|
+
if (series.lastSeenAt !== now) this._series.delete(trackId);
|
|
4787
|
+
}
|
|
4788
|
+
}
|
|
4789
|
+
_evaluate(now, currentBucket) {
|
|
4790
|
+
if (this._series.size < 2) return;
|
|
4791
|
+
const tops = /* @__PURE__ */ new Map();
|
|
4792
|
+
for (const [trackId, series] of this._series) {
|
|
4793
|
+
for (const { bucket, level } of series.samples) {
|
|
4794
|
+
if (currentBucket <= bucket) continue;
|
|
4795
|
+
const top = tops.get(bucket);
|
|
4796
|
+
const entry = { id: trackId, level };
|
|
4797
|
+
if (!top) tops.set(bucket, [entry, void 0]);
|
|
4798
|
+
else if (top[0].level < level) tops.set(bucket, [entry, top[0]]);
|
|
4799
|
+
else if (!top[1] || top[1].level < level) top[1] = entry;
|
|
4800
|
+
}
|
|
4801
|
+
}
|
|
4802
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
4803
|
+
for (const [trackId, series] of this._series) {
|
|
4804
|
+
const suspectDb = [];
|
|
4805
|
+
const sourceDb = [];
|
|
4806
|
+
const sourceCounts = /* @__PURE__ */ new Map();
|
|
4807
|
+
let followed = 0;
|
|
4808
|
+
for (const { bucket, level } of series.samples) {
|
|
4809
|
+
const top = tops.get(bucket);
|
|
4810
|
+
if (!top) continue;
|
|
4811
|
+
const source = top[0].id === trackId ? top[1] : top[0];
|
|
4812
|
+
if (!source || source.level < this._config.activeRmsLevel) continue;
|
|
4813
|
+
if (this._config.activeRmsLevel <= level) followed += 1;
|
|
4814
|
+
suspectDb.push(toDb(level));
|
|
4815
|
+
sourceDb.push(toDb(source.level));
|
|
4816
|
+
sourceCounts.set(source.id, (sourceCounts.get(source.id) ?? 0) + 1);
|
|
4817
|
+
}
|
|
4818
|
+
const sourceActiveBuckets = sourceDb.length;
|
|
4819
|
+
if (sourceActiveBuckets < this._config.minSourceActiveBuckets) continue;
|
|
4820
|
+
const followRatio = followed / sourceActiveBuckets;
|
|
4821
|
+
if (followRatio < this._config.minFollowRatio) continue;
|
|
4822
|
+
const levelCorrelation = correlation(suspectDb, sourceDb);
|
|
4823
|
+
if (levelCorrelation < this._config.minCorrelation) continue;
|
|
4824
|
+
const [sourceTrackId] = [...sourceCounts].reduce((a, b) => a[1] < b[1] ? b : a);
|
|
4825
|
+
const client = this.call.observedClients.get(series.clientId);
|
|
4826
|
+
candidates.set(trackId, {
|
|
4827
|
+
trackId,
|
|
4828
|
+
series,
|
|
4829
|
+
sourceTrackId,
|
|
4830
|
+
followRatio,
|
|
4831
|
+
levelCorrelation,
|
|
4832
|
+
sourceActiveBuckets,
|
|
4833
|
+
// How much quieter than its source the suspect is, in dB. An echo is at most as loud as
|
|
4834
|
+
// what it echoes, give or take AGC.
|
|
4835
|
+
relativeLevelDb: mean(suspectDb) - mean(sourceDb),
|
|
4836
|
+
loopbackAudioInput: client?.activeIssues.get(`${this._config.loopbackAudioInputIssueType}-track-${trackId}`) !== void 0
|
|
4837
|
+
});
|
|
4838
|
+
}
|
|
4839
|
+
for (const candidate of candidates.values()) {
|
|
4840
|
+
const reverse = candidates.get(candidate.sourceTrackId);
|
|
4841
|
+
let ambiguousDirection = false;
|
|
4842
|
+
if (reverse && reverse.sourceTrackId === candidate.trackId) {
|
|
4843
|
+
if (candidate.loopbackAudioInput !== reverse.loopbackAudioInput) {
|
|
4844
|
+
if (!candidate.loopbackAudioInput) continue;
|
|
4845
|
+
} else if (DIRECTION_MARGIN < Math.abs(candidate.followRatio - reverse.followRatio)) {
|
|
4846
|
+
if (candidate.followRatio < reverse.followRatio) continue;
|
|
4847
|
+
} else {
|
|
4848
|
+
ambiguousDirection = true;
|
|
4849
|
+
if (reverse.relativeLevelDb < candidate.relativeLevelDb) continue;
|
|
4850
|
+
if (reverse.relativeLevelDb === candidate.relativeLevelDb && reverse.trackId < candidate.trackId) continue;
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
if (now - (this._lastRaisedAt.get(candidate.trackId) ?? -Infinity) < this._config.cooldownMs) continue;
|
|
4854
|
+
this._lastRaisedAt.set(candidate.trackId, now);
|
|
4855
|
+
const { series } = candidate;
|
|
4856
|
+
this.call.addIssue({
|
|
4857
|
+
type: AudioEchoTypes.audioEchoSuspected,
|
|
4858
|
+
timestamp: now,
|
|
4859
|
+
payload: {
|
|
4860
|
+
trackId: candidate.trackId,
|
|
4861
|
+
clientId: series.clientId,
|
|
4862
|
+
sourceTrackId: candidate.sourceTrackId,
|
|
4863
|
+
sourceClientId: this._series.get(candidate.sourceTrackId)?.clientId,
|
|
4864
|
+
followRatio: candidate.followRatio,
|
|
4865
|
+
levelCorrelation: candidate.levelCorrelation,
|
|
4866
|
+
relativeLevelDb: candidate.relativeLevelDb,
|
|
4867
|
+
sourceActiveBuckets: candidate.sourceActiveBuckets,
|
|
4868
|
+
bucketMs: this._config.bucketMs,
|
|
4869
|
+
ambiguousDirection,
|
|
4870
|
+
levelSource: series.track.rmsAudioLevelSource,
|
|
4871
|
+
audioInputLabel: series.track.audioInputDevice?.label,
|
|
4872
|
+
loopbackAudioInput: candidate.loopbackAudioInput
|
|
4873
|
+
}
|
|
4874
|
+
});
|
|
4875
|
+
}
|
|
4876
|
+
}
|
|
4877
|
+
};
|
|
4878
|
+
|
|
4456
4879
|
// src/ObservedCall.ts
|
|
4457
4880
|
var logger4 = createLogger("ObservedCall");
|
|
4458
|
-
var ObservedCall = class extends
|
|
4881
|
+
var ObservedCall = class extends import_node_events3.EventEmitter {
|
|
4459
4882
|
constructor(settings, observer, activeIssuesRegistry) {
|
|
4460
4883
|
super();
|
|
4461
4884
|
this.observer = observer;
|
|
@@ -4511,10 +4934,6 @@ var ObservedCall = class extends import_events3.EventEmitter {
|
|
|
4511
4934
|
numberOfDataChannels = 0;
|
|
4512
4935
|
maxNumberOfClients = 0;
|
|
4513
4936
|
deltaNumberOfIssues = 0;
|
|
4514
|
-
// public deltaRttLt50Measurements = 0;
|
|
4515
|
-
// public deltaRttLt150Measurements = 0;
|
|
4516
|
-
// public deltaRttLt300Measurements = 0;
|
|
4517
|
-
// public deltaRttGtOrEq300Measurements = 0;
|
|
4518
4937
|
appData;
|
|
4519
4938
|
closed = false;
|
|
4520
4939
|
startedAt;
|
|
@@ -4560,6 +4979,10 @@ var ObservedCall = class extends import_events3.EventEmitter {
|
|
|
4560
4979
|
detector = new PublisherFaultCorroborationDetector(this, config);
|
|
4561
4980
|
break;
|
|
4562
4981
|
}
|
|
4982
|
+
case AudioEchoDetector.NAME: {
|
|
4983
|
+
detector = new AudioEchoDetector(this, config);
|
|
4984
|
+
break;
|
|
4985
|
+
}
|
|
4563
4986
|
default: {
|
|
4564
4987
|
logger4.warn("Unknown detector name %s; skipping", name);
|
|
4565
4988
|
return this;
|
|
@@ -4759,10 +5182,10 @@ var ObservedCall = class extends import_events3.EventEmitter {
|
|
|
4759
5182
|
};
|
|
4760
5183
|
|
|
4761
5184
|
// src/Observer.ts
|
|
4762
|
-
var
|
|
5185
|
+
var import_node_events6 = require("events");
|
|
4763
5186
|
|
|
4764
5187
|
// src/ObservedTURN.ts
|
|
4765
|
-
var
|
|
5188
|
+
var import_node_events4 = require("events");
|
|
4766
5189
|
|
|
4767
5190
|
// src/ObservedTurnServer.ts
|
|
4768
5191
|
var ObservedTurnServer = class {
|
|
@@ -4823,7 +5246,7 @@ var ObservedTurnServer = class {
|
|
|
4823
5246
|
|
|
4824
5247
|
// src/ObservedTURN.ts
|
|
4825
5248
|
var logger5 = createLogger("ObservedTURN");
|
|
4826
|
-
var ObservedTURN = class extends
|
|
5249
|
+
var ObservedTURN = class extends import_node_events4.EventEmitter {
|
|
4827
5250
|
totalBytesSent = 0;
|
|
4828
5251
|
totalBytesReceived = 0;
|
|
4829
5252
|
totalPacketsSent = 0;
|
|
@@ -4933,9 +5356,9 @@ var MiddlewareProcessor = class {
|
|
|
4933
5356
|
};
|
|
4934
5357
|
|
|
4935
5358
|
// src/ObservedMediasoupRouter.ts
|
|
4936
|
-
var
|
|
5359
|
+
var import_node_events5 = require("events");
|
|
4937
5360
|
var logger6 = createLogger("ObservedMediasoupRouter");
|
|
4938
|
-
var ObservedMediasoupRouter = class extends
|
|
5361
|
+
var ObservedMediasoupRouter = class extends import_node_events5.EventEmitter {
|
|
4939
5362
|
router;
|
|
4940
5363
|
appData;
|
|
4941
5364
|
sample;
|
|
@@ -6782,7 +7205,7 @@ function mediaKindOf(mimeType) {
|
|
|
6782
7205
|
|
|
6783
7206
|
// src/Observer.ts
|
|
6784
7207
|
var logger10 = createLogger("Observer");
|
|
6785
|
-
var Observer = class extends
|
|
7208
|
+
var Observer = class extends import_node_events6.EventEmitter {
|
|
6786
7209
|
observedTURN = new ObservedTURN();
|
|
6787
7210
|
observedCalls = /* @__PURE__ */ new Map();
|
|
6788
7211
|
observedMediasoupRouters = /* @__PURE__ */ new Map();
|
|
@@ -7404,8 +7827,8 @@ var TrendTester = class {
|
|
|
7404
7827
|
};
|
|
7405
7828
|
|
|
7406
7829
|
// src/sinks/ClientSampleSink.ts
|
|
7407
|
-
var
|
|
7408
|
-
var ClientSampleSink = class extends
|
|
7830
|
+
var import_node_events7 = require("events");
|
|
7831
|
+
var ClientSampleSink = class extends import_node_events7.EventEmitter {
|
|
7409
7832
|
};
|
|
7410
7833
|
|
|
7411
7834
|
// src/sinks/JsonlFileSink.ts
|
|
@@ -7641,6 +8064,8 @@ function createP2pRemoteTrackResolverFactory() {
|
|
|
7641
8064
|
// Annotate the CommonJS export names for ESM import in node:
|
|
7642
8065
|
0 && (module.exports = {
|
|
7643
8066
|
ActiveIssuesRegistry,
|
|
8067
|
+
AudioEchoDetector,
|
|
8068
|
+
AudioEchoTypes,
|
|
7644
8069
|
CODEC_MISMATCH_ISSUE,
|
|
7645
8070
|
CallConcurrentIssueDetector,
|
|
7646
8071
|
CallConcurrentIssueTypes,
|
|
@@ -7688,8 +8113,12 @@ function createP2pRemoteTrackResolverFactory() {
|
|
|
7688
8113
|
PublisherFaultCorroborationDetector,
|
|
7689
8114
|
PublisherFaultTypes,
|
|
7690
8115
|
RESOLVED_ISSUE_SUFFIX,
|
|
8116
|
+
RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS,
|
|
8117
|
+
RTT_HISTOGRAM_NUMBER_OF_BUCKETS,
|
|
7691
8118
|
RemoteTrackResolver,
|
|
7692
8119
|
RemoteTrackResolverValidator,
|
|
8120
|
+
SCORE_HISTOGRAM_BUCKET_WIDTH,
|
|
8121
|
+
SCORE_HISTOGRAM_NUMBER_OF_BUCKETS,
|
|
7693
8122
|
SfuCongestionDetector,
|
|
7694
8123
|
SimulcastReceiverValidator,
|
|
7695
8124
|
SlidingWindow,
|
|
@@ -7727,6 +8156,7 @@ function createP2pRemoteTrackResolverFactory() {
|
|
|
7727
8156
|
medianAbsoluteDeviation,
|
|
7728
8157
|
pageHinkley,
|
|
7729
8158
|
percentile,
|
|
8159
|
+
percentileOfHistogram,
|
|
7730
8160
|
percentileOfSorted,
|
|
7731
8161
|
robustZScore,
|
|
7732
8162
|
schemaVersion,
|