@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.mjs
CHANGED
|
@@ -103,6 +103,10 @@ function counterDelta(previous, current) {
|
|
|
103
103
|
if (current < previous) return 0;
|
|
104
104
|
return current - previous;
|
|
105
105
|
}
|
|
106
|
+
function rmsAudioLevel(deltaTotalAudioEnergy, deltaTotalSamplesDuration) {
|
|
107
|
+
if (!(0 < deltaTotalSamplesDuration)) return void 0;
|
|
108
|
+
return Math.sqrt(Math.max(0, deltaTotalAudioEnergy) / deltaTotalSamplesDuration);
|
|
109
|
+
}
|
|
106
110
|
function correlation(xs, ys) {
|
|
107
111
|
const n = Math.min(xs.length, ys.length);
|
|
108
112
|
if (n < 2) return 0;
|
|
@@ -135,8 +139,8 @@ function pageHinkley(values, delta = 0, lambda = 50) {
|
|
|
135
139
|
let runningMinimum = 0;
|
|
136
140
|
for (let i = 0; i < values.length; i++) {
|
|
137
141
|
sum += values[i];
|
|
138
|
-
const
|
|
139
|
-
cumulative += values[i] -
|
|
142
|
+
const mean2 = sum / (i + 1);
|
|
143
|
+
cumulative += values[i] - mean2 - delta;
|
|
140
144
|
if (cumulative < runningMinimum) runningMinimum = cumulative;
|
|
141
145
|
const ph = cumulative - runningMinimum;
|
|
142
146
|
statistic.push(ph);
|
|
@@ -188,6 +192,24 @@ function erf(x) {
|
|
|
188
192
|
const y = 1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-(ax * ax));
|
|
189
193
|
return sign * y;
|
|
190
194
|
}
|
|
195
|
+
function percentileOfHistogram(histogram, bucketWidth, p, options = {}) {
|
|
196
|
+
let total = 0;
|
|
197
|
+
for (const count of histogram) total += count;
|
|
198
|
+
if (total < 1) return void 0;
|
|
199
|
+
const rank = Math.min(Math.max(p, 0), 1) * total;
|
|
200
|
+
let cumulative = 0;
|
|
201
|
+
for (let index = 0; index < histogram.length; ++index) {
|
|
202
|
+
const count = histogram[index];
|
|
203
|
+
if (count < 1 || cumulative + count < rank) {
|
|
204
|
+
cumulative += count;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (options.openEnded && index === histogram.length - 1 && options.max !== void 0) return options.max;
|
|
208
|
+
const estimate = index * bucketWidth + (rank - cumulative) / count * bucketWidth;
|
|
209
|
+
return Math.min(Math.max(estimate, options.min ?? estimate), options.max ?? estimate);
|
|
210
|
+
}
|
|
211
|
+
return options.max;
|
|
212
|
+
}
|
|
191
213
|
|
|
192
214
|
// src/ObservedInboundRtp.ts
|
|
193
215
|
var ObservedInboundRtp = class {
|
|
@@ -298,6 +320,15 @@ var ObservedInboundRtp = class {
|
|
|
298
320
|
deltaFecPacketsReceived = 0;
|
|
299
321
|
deltaFecPacketsDiscarded = 0;
|
|
300
322
|
deltaPausesDuration = 0;
|
|
323
|
+
deltaTotalAudioEnergy = 0;
|
|
324
|
+
deltaTotalSamplesDuration = 0;
|
|
325
|
+
/**
|
|
326
|
+
* RMS level (0..1) of the audio played out from this stream over the tick, from the two deltas
|
|
327
|
+
* above. This is the far end's outgoing level **as this receiver heard it**, which is the only
|
|
328
|
+
* send-side level available for publishers on browsers that do not report `media-source` audio
|
|
329
|
+
* levels (Firefox). `undefined` for video and for a tick with no audio accounted for.
|
|
330
|
+
*/
|
|
331
|
+
rmsAudioLevel;
|
|
301
332
|
/**
|
|
302
333
|
* Mean jitter-buffer delay for the frames/samples emitted in this tick (seconds), derived from
|
|
303
334
|
* the cumulative `jitterBufferDelay` / `jitterBufferEmittedCount` pair — the only correct way to
|
|
@@ -383,6 +414,9 @@ var ObservedInboundRtp = class {
|
|
|
383
414
|
this.jitterBufferDelayInMs = void 0;
|
|
384
415
|
this.concealmentRatio = void 0;
|
|
385
416
|
this.framesDroppedRatio = void 0;
|
|
417
|
+
this.deltaTotalAudioEnergy = 0;
|
|
418
|
+
this.deltaTotalSamplesDuration = 0;
|
|
419
|
+
this.rmsAudioLevel = void 0;
|
|
386
420
|
const elapsedTimeInMs = stats.timestamp - this.timestamp;
|
|
387
421
|
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
422
|
if (0 < elapsedTimeInMs && !this.counterResetBoundary) {
|
|
@@ -412,6 +446,9 @@ var ObservedInboundRtp = class {
|
|
|
412
446
|
this.deltaRetransmittedPacketsReceived = counterDelta(this.retransmittedPacketsReceived, stats.retransmittedPacketsReceived);
|
|
413
447
|
this.deltaFecPacketsReceived = counterDelta(this.fecPacketsReceived, stats.fecPacketsReceived);
|
|
414
448
|
this.deltaFecPacketsDiscarded = counterDelta(this.fecPacketsDiscarded, stats.fecPacketsDiscarded);
|
|
449
|
+
this.deltaTotalAudioEnergy = counterDelta(this.totalAudioEnergy, stats.totalAudioEnergy);
|
|
450
|
+
this.deltaTotalSamplesDuration = counterDelta(this.totalSamplesDuration, stats.totalSamplesDuration);
|
|
451
|
+
this.rmsAudioLevel = rmsAudioLevel(this.deltaTotalAudioEnergy, this.deltaTotalSamplesDuration);
|
|
415
452
|
this.bitrate = this.deltaBytesReceived * 8 / elapsedInSeconds;
|
|
416
453
|
const pixels = (stats.frameWidth ?? 0) * (stats.frameHeight ?? 0);
|
|
417
454
|
if (0 < pixels && 0 < this.deltaFramesReceived) {
|
|
@@ -469,6 +506,9 @@ var ObservedInboundRtp = class {
|
|
|
469
506
|
this.jitterBufferEmittedCount = stats.jitterBufferEmittedCount;
|
|
470
507
|
this.jitterBufferMinimumDelay = stats.jitterBufferMinimumDelay;
|
|
471
508
|
this.totalSamplesReceived = stats.totalSamplesReceived;
|
|
509
|
+
this.audioLevel = stats.audioLevel;
|
|
510
|
+
this.totalAudioEnergy = stats.totalAudioEnergy;
|
|
511
|
+
this.totalSamplesDuration = stats.totalSamplesDuration;
|
|
472
512
|
this.concealedSamples = stats.concealedSamples;
|
|
473
513
|
this.silentConcealedSamples = stats.silentConcealedSamples;
|
|
474
514
|
this.concealmentEvents = stats.concealmentEvents;
|
|
@@ -1123,6 +1163,16 @@ var ObservedMediaSource = class {
|
|
|
1123
1163
|
frames;
|
|
1124
1164
|
framesPerSecond;
|
|
1125
1165
|
attachments;
|
|
1166
|
+
/** Increase of `totalAudioEnergy` in this tick (audio only). */
|
|
1167
|
+
deltaTotalAudioEnergy = 0;
|
|
1168
|
+
/** Increase of `totalSamplesDuration` in this tick, in seconds (audio only). */
|
|
1169
|
+
deltaTotalSamplesDuration = 0;
|
|
1170
|
+
/**
|
|
1171
|
+
* RMS level (0..1) of what the capture device delivered over the tick, after the browser's own
|
|
1172
|
+
* processing (AEC, NS, AGC). Chrome and Safari report the inputs; Firefox does not, and there the
|
|
1173
|
+
* publisher's level has to come from its receivers — see `ObservedOutboundTrack.rmsAudioLevel`.
|
|
1174
|
+
*/
|
|
1175
|
+
rmsAudioLevel;
|
|
1126
1176
|
get visited() {
|
|
1127
1177
|
const visited = this._visited;
|
|
1128
1178
|
this._visited = false;
|
|
@@ -1136,6 +1186,14 @@ var ObservedMediaSource = class {
|
|
|
1136
1186
|
}
|
|
1137
1187
|
update(stats) {
|
|
1138
1188
|
this._visited = true;
|
|
1189
|
+
this.deltaTotalAudioEnergy = 0;
|
|
1190
|
+
this.deltaTotalSamplesDuration = 0;
|
|
1191
|
+
this.rmsAudioLevel = void 0;
|
|
1192
|
+
if (this.timestamp < stats.timestamp) {
|
|
1193
|
+
this.deltaTotalAudioEnergy = counterDelta(this.totalAudioEnergy, stats.totalAudioEnergy);
|
|
1194
|
+
this.deltaTotalSamplesDuration = counterDelta(this.totalSamplesDuration, stats.totalSamplesDuration);
|
|
1195
|
+
this.rmsAudioLevel = rmsAudioLevel(this.deltaTotalAudioEnergy, this.deltaTotalSamplesDuration);
|
|
1196
|
+
}
|
|
1139
1197
|
this.timestamp = stats.timestamp;
|
|
1140
1198
|
this.trackIdentifier = stats.trackIdentifier;
|
|
1141
1199
|
this.audioLevel = stats.audioLevel;
|
|
@@ -1534,6 +1592,20 @@ var ObservedOutboundTrack = class {
|
|
|
1534
1592
|
remoteFractionLost;
|
|
1535
1593
|
remoteRttInMs;
|
|
1536
1594
|
qualityLimitationReason;
|
|
1595
|
+
/**
|
|
1596
|
+
* This audio track's RMS level (0..1) over the latest tick — how loud the publisher was sending.
|
|
1597
|
+
* Taken from the publisher's own `media-source` when it reports one (Chrome, Safari), otherwise
|
|
1598
|
+
* the median of what its receivers heard ({@link receivedRmsAudioLevel}), which is what makes it
|
|
1599
|
+
* available for Firefox publishers too. `undefined` for video, or when neither is known.
|
|
1600
|
+
* {@link rmsAudioLevelSource} says which one it is.
|
|
1601
|
+
*/
|
|
1602
|
+
rmsAudioLevel;
|
|
1603
|
+
rmsAudioLevelSource;
|
|
1604
|
+
/**
|
|
1605
|
+
* Median RMS level (0..1) of this track across the receivers that played it out, each over its own
|
|
1606
|
+
* latest tick. Needs a `RemoteTrackResolver`: without links there are no receivers to ask.
|
|
1607
|
+
*/
|
|
1608
|
+
receivedRmsAudioLevel;
|
|
1537
1609
|
get score() {
|
|
1538
1610
|
return this.calculatedScore.value;
|
|
1539
1611
|
}
|
|
@@ -1554,6 +1626,10 @@ var ObservedOutboundTrack = class {
|
|
|
1554
1626
|
getMediaSource() {
|
|
1555
1627
|
return this._mediaSource;
|
|
1556
1628
|
}
|
|
1629
|
+
/** The capture device behind this track, when the client sent `AUDIO_INPUT_DEVICE` metadata for it. */
|
|
1630
|
+
get audioInputDevice() {
|
|
1631
|
+
return this._peerConnection.client.audioInputDevices.get(this.id);
|
|
1632
|
+
}
|
|
1557
1633
|
update(stats) {
|
|
1558
1634
|
this._visited = true;
|
|
1559
1635
|
this.timestamp = stats.timestamp;
|
|
@@ -1585,8 +1661,30 @@ var ObservedOutboundTrack = class {
|
|
|
1585
1661
|
if (this.muted !== true && this.deltaPacketsSent === 0) reasons.push("no-packets-sent");
|
|
1586
1662
|
this.degradedReasons = reasons.length === 0 ? void 0 : reasons;
|
|
1587
1663
|
this.receivingDistribution = this.createReceivingDistribution();
|
|
1664
|
+
this._updateAudioLevel();
|
|
1588
1665
|
this.detectors.update();
|
|
1589
1666
|
}
|
|
1667
|
+
_updateAudioLevel() {
|
|
1668
|
+
this.rmsAudioLevel = void 0;
|
|
1669
|
+
this.rmsAudioLevelSource = void 0;
|
|
1670
|
+
this.receivedRmsAudioLevel = void 0;
|
|
1671
|
+
if (this.kind !== "audio") return;
|
|
1672
|
+
if (0 < this.remoteInboundTracks.size) {
|
|
1673
|
+
const levels = [];
|
|
1674
|
+
for (const inboundTrack of this.remoteInboundTracks) {
|
|
1675
|
+
pushFinite(levels, inboundTrack.getInboundRtp()?.rmsAudioLevel);
|
|
1676
|
+
}
|
|
1677
|
+
this.receivedRmsAudioLevel = median(levels);
|
|
1678
|
+
}
|
|
1679
|
+
const sourceLevel = this._mediaSource?.rmsAudioLevel;
|
|
1680
|
+
if (sourceLevel !== void 0) {
|
|
1681
|
+
this.rmsAudioLevel = sourceLevel;
|
|
1682
|
+
this.rmsAudioLevelSource = "media-source";
|
|
1683
|
+
} else if (this.receivedRmsAudioLevel !== void 0) {
|
|
1684
|
+
this.rmsAudioLevel = this.receivedRmsAudioLevel;
|
|
1685
|
+
this.rmsAudioLevelSource = "receivers";
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1590
1688
|
createReceivingDistribution() {
|
|
1591
1689
|
if (this.remoteInboundTracks.size === 0) return void 0;
|
|
1592
1690
|
let freezeAffected = 0;
|
|
@@ -2626,6 +2724,7 @@ var ClientMetaTypes = /* @__PURE__ */ ((ClientMetaTypes2) => {
|
|
|
2626
2724
|
ClientMetaTypes2["MEDIA_DEVICES_SUPPORTED_CONSTRAINTS"] = "MEDIA_DEVICES_SUPPORTED_CONSTRAINTS";
|
|
2627
2725
|
ClientMetaTypes2["USER_MEDIA_ERROR"] = "USER_MEDIA_ERROR";
|
|
2628
2726
|
ClientMetaTypes2["LOCAL_SDP"] = "LOCAL_SDP";
|
|
2727
|
+
ClientMetaTypes2["AUDIO_INPUT_DEVICE"] = "AUDIO_INPUT_DEVICE";
|
|
2629
2728
|
ClientMetaTypes2["OPERATION_SYSTEM"] = "OPERATION_SYSTEM";
|
|
2630
2729
|
ClientMetaTypes2["ENGINE"] = "ENGINE";
|
|
2631
2730
|
ClientMetaTypes2["PLATFORM"] = "PLATFORM";
|
|
@@ -2644,6 +2743,10 @@ function baseIssueType(type) {
|
|
|
2644
2743
|
|
|
2645
2744
|
// src/ObservedClient.ts
|
|
2646
2745
|
var logger2 = createLogger("ObservedClient");
|
|
2746
|
+
var RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS = 20;
|
|
2747
|
+
var RTT_HISTOGRAM_NUMBER_OF_BUCKETS = 50;
|
|
2748
|
+
var SCORE_HISTOGRAM_BUCKET_WIDTH = 1;
|
|
2749
|
+
var SCORE_HISTOGRAM_NUMBER_OF_BUCKETS = 5;
|
|
2647
2750
|
var ObservedClient = class extends EventEmitter2 {
|
|
2648
2751
|
constructor(settings, call, activeIssues) {
|
|
2649
2752
|
super();
|
|
@@ -2708,10 +2811,6 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2708
2811
|
deltaOutboundPacketsSent = 0;
|
|
2709
2812
|
deltaTransportSentBytes = 0;
|
|
2710
2813
|
deltaTransportReceivedBytes = 0;
|
|
2711
|
-
deltaRttLt50Measurements = 0;
|
|
2712
|
-
deltaRttLt150Measurements = 0;
|
|
2713
|
-
deltaRttLt300Measurements = 0;
|
|
2714
|
-
deltaRttGtOrEq300Measurements = 0;
|
|
2715
2814
|
currentMaxRttInMs;
|
|
2716
2815
|
currentMinRttInMs;
|
|
2717
2816
|
currentAvgRttInMs;
|
|
@@ -2727,8 +2826,73 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2727
2826
|
deltaNumberOfIssues = 0;
|
|
2728
2827
|
totalScoreSum = 0;
|
|
2729
2828
|
numberOfScoreMeasurements = 0;
|
|
2730
|
-
|
|
2829
|
+
minScore;
|
|
2830
|
+
maxScore;
|
|
2831
|
+
/**
|
|
2832
|
+
* Score measurements (one per accepted sample with a score) counted into
|
|
2833
|
+
* `SCORE_HISTOGRAM_NUMBER_OF_BUCKETS` buckets of `SCORE_HISTOGRAM_BUCKET_WIDTH`. Histograms of
|
|
2834
|
+
* several clients can be added bucket by bucket to get the distribution of a call.
|
|
2835
|
+
*/
|
|
2836
|
+
scoreHistogram = new Array(SCORE_HISTOGRAM_NUMBER_OF_BUCKETS).fill(0);
|
|
2837
|
+
/** Issues raised over the session, counted as in `issueHistogram`. */
|
|
2838
|
+
totalNumberOfIssues = 0;
|
|
2839
|
+
/**
|
|
2840
|
+
* Number of times each issue type was raised over the session, keyed by the base type (without
|
|
2841
|
+
* the `-resolved` suffix): `client.issueHistogram['loopback-audio-input']`. A resolution entry is
|
|
2842
|
+
* not counted, and neither is a re-raise of a keyed issue that is still open, so a stateful issue
|
|
2843
|
+
* reported on every tick counts once per open interval. A type never raised is `undefined`.
|
|
2844
|
+
*/
|
|
2845
|
+
issueHistogram = /* @__PURE__ */ Object.create(null);
|
|
2846
|
+
// Session totals. Unlike the `delta*` and `current*` fields they are never reset, and unlike the
|
|
2847
|
+
// `total*` fields of `ObservedPeerConnection` they survive a peer connection being closed.
|
|
2848
|
+
// Every RTT measurement is one peer connection's `currentRttInMs` in one accepted sample.
|
|
2849
|
+
totalRttSumInMs = 0;
|
|
2850
|
+
numberOfRttMeasurements = 0;
|
|
2851
|
+
maxRttInMs;
|
|
2852
|
+
minRttInMs;
|
|
2853
|
+
/**
|
|
2854
|
+
* RTT measurements counted into fixed buckets of `RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS`; the last bucket
|
|
2855
|
+
* holds every measurement of 980 ms and above. Constant memory, so percentiles can be estimated
|
|
2856
|
+
* over the whole session (see `rttSummary`) without keeping every measurement. Histograms
|
|
2857
|
+
* of several clients can be added bucket by bucket to get the distribution of a call.
|
|
2858
|
+
*/
|
|
2859
|
+
rttHistogram = new Array(RTT_HISTOGRAM_NUMBER_OF_BUCKETS).fill(0);
|
|
2860
|
+
_rttSummary;
|
|
2861
|
+
_scoreSummary;
|
|
2862
|
+
totalInboundPacketsLost = 0;
|
|
2863
|
+
totalInboundPacketsReceived = 0;
|
|
2864
|
+
totalOutboundPacketsSent = 0;
|
|
2865
|
+
totalReceivedAudioBytes = 0;
|
|
2866
|
+
totalReceivedVideoBytes = 0;
|
|
2867
|
+
totalSentAudioBytes = 0;
|
|
2868
|
+
totalSentVideoBytes = 0;
|
|
2869
|
+
totalTransportReceivedBytes = 0;
|
|
2870
|
+
totalTransportSentBytes = 0;
|
|
2871
|
+
totalDataChannelBytesReceived = 0;
|
|
2872
|
+
totalDataChannelBytesSent = 0;
|
|
2873
|
+
totalDataChannelMessagesReceived = 0;
|
|
2874
|
+
totalDataChannelMessagesSent = 0;
|
|
2875
|
+
/** Highest `sendingAudioBitrate + sendingVideoBitrate` of any sample, in bits per second. */
|
|
2876
|
+
peakSendingBitrate = 0;
|
|
2877
|
+
/** Highest `receivingAudioBitrate + receivingVideoBitrate` of any sample, in bits per second. */
|
|
2878
|
+
peakReceivingBitrate = 0;
|
|
2879
|
+
// Samples by the transport path of the selected ICE candidate pairs. A sample whose peer
|
|
2880
|
+
// connections use different paths is counted under each of them, so more than one non-zero
|
|
2881
|
+
// counter means the path changed or was mixed. A sample with no selected pair counts as unknown.
|
|
2882
|
+
numberOfSamplesOverUdp = 0;
|
|
2883
|
+
numberOfSamplesOverTcp = 0;
|
|
2884
|
+
numberOfSamplesOverTurnUdp = 0;
|
|
2885
|
+
numberOfSamplesOverTurnTcp = 0;
|
|
2886
|
+
numberOfSamplesWithUnknownPath = 0;
|
|
2887
|
+
numberOfPublicAddressChanges = 0;
|
|
2731
2888
|
mediaDevices = [];
|
|
2889
|
+
/**
|
|
2890
|
+
* The capture device behind each outbound audio track, keyed by track id, from the client's
|
|
2891
|
+
* `AUDIO_INPUT_DEVICE` metadata. Kept for the client's lifetime (a device switch is a new track
|
|
2892
|
+
* id), so a finding raised after a track ended can still name its device. Read it per track via
|
|
2893
|
+
* `ObservedOutboundTrack.audioInputDevice`.
|
|
2894
|
+
*/
|
|
2895
|
+
audioInputDevices = /* @__PURE__ */ new Map();
|
|
2732
2896
|
/**
|
|
2733
2897
|
* The client's currently **open** stateful issues, keyed by `ClientIssue.key` — the server-side
|
|
2734
2898
|
* mirror of the client monitor's own active-issue map (client-monitor-js >= 4.6.0).
|
|
@@ -2741,9 +2905,7 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2741
2905
|
activeIssues;
|
|
2742
2906
|
_pendingInjections = {};
|
|
2743
2907
|
_activeSample;
|
|
2744
|
-
/** Peer connections the client reported closed (`PEER_CONNECTION_CLOSED`), closed once the current sample is applied. */
|
|
2745
2908
|
_peerConnectionsToClose = /* @__PURE__ */ new Set();
|
|
2746
|
-
/** Ids of peer connections closed by the client; late stats for them are ignored rather than resurrecting them. */
|
|
2747
2909
|
_closedPeerConnectionIds = /* @__PURE__ */ new Set();
|
|
2748
2910
|
closeTimer;
|
|
2749
2911
|
/** The distinct `publicAddress`es currently known by this client's open peer connections. */
|
|
@@ -2760,6 +2922,61 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2760
2922
|
get score() {
|
|
2761
2923
|
return this.calculatedScore.value;
|
|
2762
2924
|
}
|
|
2925
|
+
/**
|
|
2926
|
+
* Percentiles (error at most one 20 ms bucket; a percentile in the overflow bucket is reported as
|
|
2927
|
+
* `maxRttInMs`) and mean of every RTT measurement of the session. Cached until the next measurement.
|
|
2928
|
+
*/
|
|
2929
|
+
get rttSummary() {
|
|
2930
|
+
if (!this._rttSummary) {
|
|
2931
|
+
const bounds = { min: this.minRttInMs, max: this.maxRttInMs, openEnded: true };
|
|
2932
|
+
const percentile2 = (p) => percentileOfHistogram(this.rttHistogram, RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS, p, bounds);
|
|
2933
|
+
this._rttSummary = Object.freeze({
|
|
2934
|
+
p50: percentile2(0.5),
|
|
2935
|
+
p75: percentile2(0.75),
|
|
2936
|
+
p95: percentile2(0.95),
|
|
2937
|
+
p99: percentile2(0.99),
|
|
2938
|
+
avg: 0 < this.numberOfRttMeasurements ? this.totalRttSumInMs / this.numberOfRttMeasurements : void 0
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
return this._rttSummary;
|
|
2942
|
+
}
|
|
2943
|
+
/**
|
|
2944
|
+
* Percentiles (error at most one score point) and mean of every score of the session. Cached until
|
|
2945
|
+
* the next score.
|
|
2946
|
+
*/
|
|
2947
|
+
get scoreSummary() {
|
|
2948
|
+
if (!this._scoreSummary) {
|
|
2949
|
+
const bounds = { min: this.minScore, max: this.maxScore };
|
|
2950
|
+
const percentile2 = (p) => percentileOfHistogram(this.scoreHistogram, SCORE_HISTOGRAM_BUCKET_WIDTH, p, bounds);
|
|
2951
|
+
this._scoreSummary = Object.freeze({
|
|
2952
|
+
p5: percentile2(0.05),
|
|
2953
|
+
p25: percentile2(0.25),
|
|
2954
|
+
p50: percentile2(0.5),
|
|
2955
|
+
p75: percentile2(0.75),
|
|
2956
|
+
p95: percentile2(0.95),
|
|
2957
|
+
avg: 0 < this.numberOfScoreMeasurements ? this.totalScoreSum / this.numberOfScoreMeasurements : void 0
|
|
2958
|
+
});
|
|
2959
|
+
}
|
|
2960
|
+
return this._scoreSummary;
|
|
2961
|
+
}
|
|
2962
|
+
_addScoreMeasurement(score) {
|
|
2963
|
+
this._scoreSummary = void 0;
|
|
2964
|
+
this.totalScoreSum += score;
|
|
2965
|
+
++this.numberOfScoreMeasurements;
|
|
2966
|
+
if (this.maxScore === void 0 || this.maxScore < score) this.maxScore = score;
|
|
2967
|
+
if (this.minScore === void 0 || score < this.minScore) this.minScore = score;
|
|
2968
|
+
const bucket = Math.min(Math.max(Math.floor(score / SCORE_HISTOGRAM_BUCKET_WIDTH), 0), SCORE_HISTOGRAM_NUMBER_OF_BUCKETS - 1);
|
|
2969
|
+
++this.scoreHistogram[bucket];
|
|
2970
|
+
}
|
|
2971
|
+
_addRttMeasurement(rttInMs) {
|
|
2972
|
+
this._rttSummary = void 0;
|
|
2973
|
+
this.totalRttSumInMs += rttInMs;
|
|
2974
|
+
++this.numberOfRttMeasurements;
|
|
2975
|
+
if (this.maxRttInMs === void 0 || this.maxRttInMs < rttInMs) this.maxRttInMs = rttInMs;
|
|
2976
|
+
if (this.minRttInMs === void 0 || rttInMs < this.minRttInMs) this.minRttInMs = rttInMs;
|
|
2977
|
+
const bucket = Math.min(Math.max(Math.floor(rttInMs / RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS), 0), RTT_HISTOGRAM_NUMBER_OF_BUCKETS - 1);
|
|
2978
|
+
++this.rttHistogram[bucket];
|
|
2979
|
+
}
|
|
2763
2980
|
close() {
|
|
2764
2981
|
if (this.closed) return;
|
|
2765
2982
|
if (this.closeTimer) {
|
|
@@ -2803,6 +3020,10 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2803
3020
|
const elapsedInSeconds = elapsedInMs / 1e3;
|
|
2804
3021
|
let sumOfRtts = 0;
|
|
2805
3022
|
let numberOfRttMeasurements = 0;
|
|
3023
|
+
let overUdp = false;
|
|
3024
|
+
let overTcp = false;
|
|
3025
|
+
let overTurnUdp = false;
|
|
3026
|
+
let overTurnTcp = false;
|
|
2806
3027
|
++this.acceptedSamples;
|
|
2807
3028
|
this.availableIncomingBitrate = 0;
|
|
2808
3029
|
this.availableOutgoingBitrate = 0;
|
|
@@ -2819,10 +3040,6 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2819
3040
|
this.deltaSentVideoBytes = 0;
|
|
2820
3041
|
this.deltaTransportReceivedBytes = 0;
|
|
2821
3042
|
this.deltaTransportSentBytes = 0;
|
|
2822
|
-
this.deltaRttLt50Measurements = 0;
|
|
2823
|
-
this.deltaRttLt150Measurements = 0;
|
|
2824
|
-
this.deltaRttLt300Measurements = 0;
|
|
2825
|
-
this.deltaRttGtOrEq300Measurements = 0;
|
|
2826
3043
|
this.deltaNumberOfIssues = 0;
|
|
2827
3044
|
this.numberOfDataChannels = 0;
|
|
2828
3045
|
this.numberOfInboundRtpStreams = 0;
|
|
@@ -2894,6 +3111,18 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2894
3111
|
if (observedPeerConnection.usingTCP) {
|
|
2895
3112
|
this.usingTCP = true;
|
|
2896
3113
|
}
|
|
3114
|
+
for (const selectedPair of observedPeerConnection.selectedIceCandidatePairs) {
|
|
3115
|
+
const localCandidate = selectedPair.getLocalCandidate();
|
|
3116
|
+
if (!localCandidate) continue;
|
|
3117
|
+
if (localCandidate.candidateType === "relay") {
|
|
3118
|
+
if (localCandidate.relayProtocol === "tcp" || localCandidate.relayProtocol === "tls") overTurnTcp = true;
|
|
3119
|
+
else overTurnUdp = true;
|
|
3120
|
+
} else if (localCandidate.protocol === "tcp") {
|
|
3121
|
+
overTcp = true;
|
|
3122
|
+
} else {
|
|
3123
|
+
overUdp = true;
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
2897
3126
|
if (observedPeerConnection.currentRttInMs) {
|
|
2898
3127
|
if (this.currentMinRttInMs === void 0 || observedPeerConnection.currentRttInMs < this.currentMinRttInMs) {
|
|
2899
3128
|
this.currentMinRttInMs = observedPeerConnection.currentRttInMs;
|
|
@@ -2901,17 +3130,9 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2901
3130
|
if (this.currentMaxRttInMs === void 0 || observedPeerConnection.currentRttInMs > this.currentMaxRttInMs) {
|
|
2902
3131
|
this.currentMaxRttInMs = observedPeerConnection.currentRttInMs;
|
|
2903
3132
|
}
|
|
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
3133
|
sumOfRtts += observedPeerConnection.currentRttInMs;
|
|
2914
3134
|
++numberOfRttMeasurements;
|
|
3135
|
+
this._addRttMeasurement(observedPeerConnection.currentRttInMs);
|
|
2915
3136
|
}
|
|
2916
3137
|
}
|
|
2917
3138
|
for (const clientEvent of clientEventsPostBuffer) {
|
|
@@ -2921,6 +3142,7 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2921
3142
|
if (latestPublicAddress !== void 0 && latestPublicAddress !== this.publicAddress) {
|
|
2922
3143
|
const previousPublicAddress = this.publicAddress;
|
|
2923
3144
|
this.publicAddress = latestPublicAddress;
|
|
3145
|
+
if (previousPublicAddress !== void 0) ++this.numberOfPublicAddressChanges;
|
|
2924
3146
|
this._notify("client-public-address-changed", { ...this.eventScope, publicAddress: latestPublicAddress, previousPublicAddress });
|
|
2925
3147
|
}
|
|
2926
3148
|
this.attachments = sample.attachments;
|
|
@@ -2936,15 +3158,34 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2936
3158
|
this.sendingVideoBitrate = 0;
|
|
2937
3159
|
}
|
|
2938
3160
|
this.currentAvgRttInMs = 0 < numberOfRttMeasurements ? sumOfRtts / numberOfRttMeasurements : void 0;
|
|
3161
|
+
this.peakSendingBitrate = Math.max(this.peakSendingBitrate, this.sendingAudioBitrate + this.sendingVideoBitrate);
|
|
3162
|
+
this.peakReceivingBitrate = Math.max(this.peakReceivingBitrate, this.receivingAudioBitrate + this.receivingVideoBitrate);
|
|
3163
|
+
this.totalInboundPacketsLost += this.deltaInboundPacketsLost;
|
|
3164
|
+
this.totalInboundPacketsReceived += this.deltaInboundPacketsReceived;
|
|
3165
|
+
this.totalOutboundPacketsSent += this.deltaOutboundPacketsSent;
|
|
3166
|
+
this.totalReceivedAudioBytes += this.deltaReceivedAudioBytes;
|
|
3167
|
+
this.totalReceivedVideoBytes += this.deltaReceivedVideoBytes;
|
|
3168
|
+
this.totalSentAudioBytes += this.deltaSentAudioBytes;
|
|
3169
|
+
this.totalSentVideoBytes += this.deltaSentVideoBytes;
|
|
3170
|
+
this.totalTransportReceivedBytes += this.deltaTransportReceivedBytes;
|
|
3171
|
+
this.totalTransportSentBytes += this.deltaTransportSentBytes;
|
|
3172
|
+
this.totalDataChannelBytesReceived += this.deltaDataChannelBytesReceived;
|
|
3173
|
+
this.totalDataChannelBytesSent += this.deltaDataChannelBytesSent;
|
|
3174
|
+
this.totalDataChannelMessagesReceived += this.deltaDataChannelMessagesReceived;
|
|
3175
|
+
this.totalDataChannelMessagesSent += this.deltaDataChannelMessagesSent;
|
|
3176
|
+
if (overUdp) ++this.numberOfSamplesOverUdp;
|
|
3177
|
+
if (overTcp) ++this.numberOfSamplesOverTcp;
|
|
3178
|
+
if (overTurnUdp) ++this.numberOfSamplesOverTurnUdp;
|
|
3179
|
+
if (overTurnTcp) ++this.numberOfSamplesOverTurnTcp;
|
|
3180
|
+
if (!overUdp && !overTcp && !overTurnUdp && !overTurnTcp) ++this.numberOfSamplesWithUnknownPath;
|
|
2939
3181
|
this.calculatedScore.value = sample.score;
|
|
2940
3182
|
this.lastSampleTimestamp = sample.timestamp;
|
|
2941
3183
|
const elapsedTimeInMs = now - this.updated;
|
|
2942
3184
|
this.emit("update", sample, elapsedTimeInMs, context);
|
|
2943
3185
|
this._notify("client-updated", { ...this.eventScope, sample, elapsedTimeInMs, context });
|
|
2944
3186
|
this.updated = now;
|
|
2945
|
-
if (this.calculatedScore.value) {
|
|
2946
|
-
this.
|
|
2947
|
-
++this.numberOfScoreMeasurements;
|
|
3187
|
+
if (typeof this.calculatedScore.value === "number" && Number.isFinite(this.calculatedScore.value)) {
|
|
3188
|
+
this._addScoreMeasurement(this.calculatedScore.value);
|
|
2948
3189
|
}
|
|
2949
3190
|
if (this.settings.closeClientIfIdleForMs !== void 0) {
|
|
2950
3191
|
this.closeTimer = setTimeout(() => {
|
|
@@ -3035,6 +3276,13 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3035
3276
|
this.operationSystem = parseJsonAs(metadata.payload);
|
|
3036
3277
|
break;
|
|
3037
3278
|
}
|
|
3279
|
+
case "AUDIO_INPUT_DEVICE" /* AUDIO_INPUT_DEVICE */: {
|
|
3280
|
+
const device = parseJsonAs(metadata.payload);
|
|
3281
|
+
if (typeof device?.trackId === "string" && typeof device.label === "string") {
|
|
3282
|
+
this.audioInputDevices.set(device.trackId, device);
|
|
3283
|
+
}
|
|
3284
|
+
break;
|
|
3285
|
+
}
|
|
3038
3286
|
}
|
|
3039
3287
|
this._notify("client-metadata", { ...this.eventScope, metaData: metadata });
|
|
3040
3288
|
}
|
|
@@ -3055,6 +3303,11 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
3055
3303
|
if (isClientIssueResolutionEntry(issue)) {
|
|
3056
3304
|
return this._resolveIssue(issue);
|
|
3057
3305
|
}
|
|
3306
|
+
if (!issue.key || !this.activeIssues.get(issue.key)) {
|
|
3307
|
+
const type = baseIssueType(issue.type);
|
|
3308
|
+
this.issueHistogram[type] = (this.issueHistogram[type] ?? 0) + 1;
|
|
3309
|
+
++this.totalNumberOfIssues;
|
|
3310
|
+
}
|
|
3058
3311
|
const now = Date.now();
|
|
3059
3312
|
if (issue.key) {
|
|
3060
3313
|
const payload = parseJsonObject(issue.payload);
|
|
@@ -4326,6 +4579,169 @@ var PublisherFaultCorroborationDetector = class _PublisherFaultCorroborationDete
|
|
|
4326
4579
|
}
|
|
4327
4580
|
};
|
|
4328
4581
|
|
|
4582
|
+
// src/detectors/AudioEchoDetector.ts
|
|
4583
|
+
var AudioEchoTypes = {
|
|
4584
|
+
/**
|
|
4585
|
+
* One participant's outgoing audio follows another participant's voice: it rises when they speak,
|
|
4586
|
+
* in proportion to how loud they are. The echoing participant is sending the far end's playout
|
|
4587
|
+
* back into the call — speakers into an open mic, or a loopback capture device.
|
|
4588
|
+
*/
|
|
4589
|
+
audioEchoSuspected: "AUDIO_ECHO_SUSPECTED"
|
|
4590
|
+
};
|
|
4591
|
+
var DIRECTION_MARGIN = 0.1;
|
|
4592
|
+
var mean = (values) => values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
4593
|
+
var LEVEL_FLOOR = 1e-5;
|
|
4594
|
+
var toDb = (level) => 20 * Math.log10(Math.max(level, LEVEL_FLOOR));
|
|
4595
|
+
var AudioEchoDetector = class _AudioEchoDetector {
|
|
4596
|
+
constructor(call, config = {}) {
|
|
4597
|
+
this.call = call;
|
|
4598
|
+
this._config = {
|
|
4599
|
+
bucketMs: 5e3,
|
|
4600
|
+
windowBuckets: 24,
|
|
4601
|
+
activeRmsLevel: 0.01,
|
|
4602
|
+
minSourceActiveBuckets: 8,
|
|
4603
|
+
minFollowRatio: 0.8,
|
|
4604
|
+
minCorrelation: 0.6,
|
|
4605
|
+
cooldownMs: 12e4,
|
|
4606
|
+
loopbackAudioInputIssueType: "loopback-audio-input",
|
|
4607
|
+
...config
|
|
4608
|
+
};
|
|
4609
|
+
}
|
|
4610
|
+
call;
|
|
4611
|
+
static NAME = "audio-echo-detector";
|
|
4612
|
+
name = _AudioEchoDetector.NAME;
|
|
4613
|
+
_config;
|
|
4614
|
+
_series = /* @__PURE__ */ new Map();
|
|
4615
|
+
_lastRaisedAt = /* @__PURE__ */ new Map();
|
|
4616
|
+
_lastEvaluatedBucket;
|
|
4617
|
+
close() {
|
|
4618
|
+
this._series.clear();
|
|
4619
|
+
this._lastRaisedAt.clear();
|
|
4620
|
+
}
|
|
4621
|
+
update() {
|
|
4622
|
+
const now = Date.now();
|
|
4623
|
+
const bucket = Math.floor(now / this._config.bucketMs);
|
|
4624
|
+
if (this._lastEvaluatedBucket === bucket) return;
|
|
4625
|
+
const closedBucket = bucket - 1;
|
|
4626
|
+
this._lastEvaluatedBucket = bucket;
|
|
4627
|
+
this._collect(now, closedBucket);
|
|
4628
|
+
this._evaluate(now, bucket);
|
|
4629
|
+
}
|
|
4630
|
+
_collect(now, bucket) {
|
|
4631
|
+
for (const client of this.call.observedClients.values()) {
|
|
4632
|
+
for (const peerConnection of client.observedPeerConnections.values()) {
|
|
4633
|
+
for (const track of peerConnection.observedOutboundTracks.values()) {
|
|
4634
|
+
if (track.kind !== "audio") continue;
|
|
4635
|
+
let series = this._series.get(track.id);
|
|
4636
|
+
if (!series) {
|
|
4637
|
+
series = { track, clientId: client.clientId, lastTimestamp: -1, samples: [], lastSeenAt: now };
|
|
4638
|
+
this._series.set(track.id, series);
|
|
4639
|
+
}
|
|
4640
|
+
series.lastSeenAt = now;
|
|
4641
|
+
if (series.lastTimestamp === track.timestamp) continue;
|
|
4642
|
+
series.lastTimestamp = track.timestamp;
|
|
4643
|
+
const level = track.rmsAudioLevel;
|
|
4644
|
+
if (level === void 0) continue;
|
|
4645
|
+
series.samples.push({ bucket, level });
|
|
4646
|
+
const oldest = bucket - this._config.windowBuckets;
|
|
4647
|
+
while (series.samples.length && series.samples[0].bucket <= oldest) series.samples.shift();
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
for (const [trackId, series] of this._series) {
|
|
4652
|
+
if (series.lastSeenAt !== now) this._series.delete(trackId);
|
|
4653
|
+
}
|
|
4654
|
+
}
|
|
4655
|
+
_evaluate(now, currentBucket) {
|
|
4656
|
+
if (this._series.size < 2) return;
|
|
4657
|
+
const tops = /* @__PURE__ */ new Map();
|
|
4658
|
+
for (const [trackId, series] of this._series) {
|
|
4659
|
+
for (const { bucket, level } of series.samples) {
|
|
4660
|
+
if (currentBucket <= bucket) continue;
|
|
4661
|
+
const top = tops.get(bucket);
|
|
4662
|
+
const entry = { id: trackId, level };
|
|
4663
|
+
if (!top) tops.set(bucket, [entry, void 0]);
|
|
4664
|
+
else if (top[0].level < level) tops.set(bucket, [entry, top[0]]);
|
|
4665
|
+
else if (!top[1] || top[1].level < level) top[1] = entry;
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
4669
|
+
for (const [trackId, series] of this._series) {
|
|
4670
|
+
const suspectDb = [];
|
|
4671
|
+
const sourceDb = [];
|
|
4672
|
+
const sourceCounts = /* @__PURE__ */ new Map();
|
|
4673
|
+
let followed = 0;
|
|
4674
|
+
for (const { bucket, level } of series.samples) {
|
|
4675
|
+
const top = tops.get(bucket);
|
|
4676
|
+
if (!top) continue;
|
|
4677
|
+
const source = top[0].id === trackId ? top[1] : top[0];
|
|
4678
|
+
if (!source || source.level < this._config.activeRmsLevel) continue;
|
|
4679
|
+
if (this._config.activeRmsLevel <= level) followed += 1;
|
|
4680
|
+
suspectDb.push(toDb(level));
|
|
4681
|
+
sourceDb.push(toDb(source.level));
|
|
4682
|
+
sourceCounts.set(source.id, (sourceCounts.get(source.id) ?? 0) + 1);
|
|
4683
|
+
}
|
|
4684
|
+
const sourceActiveBuckets = sourceDb.length;
|
|
4685
|
+
if (sourceActiveBuckets < this._config.minSourceActiveBuckets) continue;
|
|
4686
|
+
const followRatio = followed / sourceActiveBuckets;
|
|
4687
|
+
if (followRatio < this._config.minFollowRatio) continue;
|
|
4688
|
+
const levelCorrelation = correlation(suspectDb, sourceDb);
|
|
4689
|
+
if (levelCorrelation < this._config.minCorrelation) continue;
|
|
4690
|
+
const [sourceTrackId] = [...sourceCounts].reduce((a, b) => a[1] < b[1] ? b : a);
|
|
4691
|
+
const client = this.call.observedClients.get(series.clientId);
|
|
4692
|
+
candidates.set(trackId, {
|
|
4693
|
+
trackId,
|
|
4694
|
+
series,
|
|
4695
|
+
sourceTrackId,
|
|
4696
|
+
followRatio,
|
|
4697
|
+
levelCorrelation,
|
|
4698
|
+
sourceActiveBuckets,
|
|
4699
|
+
// How much quieter than its source the suspect is, in dB. An echo is at most as loud as
|
|
4700
|
+
// what it echoes, give or take AGC.
|
|
4701
|
+
relativeLevelDb: mean(suspectDb) - mean(sourceDb),
|
|
4702
|
+
loopbackAudioInput: client?.activeIssues.get(`${this._config.loopbackAudioInputIssueType}-track-${trackId}`) !== void 0
|
|
4703
|
+
});
|
|
4704
|
+
}
|
|
4705
|
+
for (const candidate of candidates.values()) {
|
|
4706
|
+
const reverse = candidates.get(candidate.sourceTrackId);
|
|
4707
|
+
let ambiguousDirection = false;
|
|
4708
|
+
if (reverse && reverse.sourceTrackId === candidate.trackId) {
|
|
4709
|
+
if (candidate.loopbackAudioInput !== reverse.loopbackAudioInput) {
|
|
4710
|
+
if (!candidate.loopbackAudioInput) continue;
|
|
4711
|
+
} else if (DIRECTION_MARGIN < Math.abs(candidate.followRatio - reverse.followRatio)) {
|
|
4712
|
+
if (candidate.followRatio < reverse.followRatio) continue;
|
|
4713
|
+
} else {
|
|
4714
|
+
ambiguousDirection = true;
|
|
4715
|
+
if (reverse.relativeLevelDb < candidate.relativeLevelDb) continue;
|
|
4716
|
+
if (reverse.relativeLevelDb === candidate.relativeLevelDb && reverse.trackId < candidate.trackId) continue;
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
if (now - (this._lastRaisedAt.get(candidate.trackId) ?? -Infinity) < this._config.cooldownMs) continue;
|
|
4720
|
+
this._lastRaisedAt.set(candidate.trackId, now);
|
|
4721
|
+
const { series } = candidate;
|
|
4722
|
+
this.call.addIssue({
|
|
4723
|
+
type: AudioEchoTypes.audioEchoSuspected,
|
|
4724
|
+
timestamp: now,
|
|
4725
|
+
payload: {
|
|
4726
|
+
trackId: candidate.trackId,
|
|
4727
|
+
clientId: series.clientId,
|
|
4728
|
+
sourceTrackId: candidate.sourceTrackId,
|
|
4729
|
+
sourceClientId: this._series.get(candidate.sourceTrackId)?.clientId,
|
|
4730
|
+
followRatio: candidate.followRatio,
|
|
4731
|
+
levelCorrelation: candidate.levelCorrelation,
|
|
4732
|
+
relativeLevelDb: candidate.relativeLevelDb,
|
|
4733
|
+
sourceActiveBuckets: candidate.sourceActiveBuckets,
|
|
4734
|
+
bucketMs: this._config.bucketMs,
|
|
4735
|
+
ambiguousDirection,
|
|
4736
|
+
levelSource: series.track.rmsAudioLevelSource,
|
|
4737
|
+
audioInputLabel: series.track.audioInputDevice?.label,
|
|
4738
|
+
loopbackAudioInput: candidate.loopbackAudioInput
|
|
4739
|
+
}
|
|
4740
|
+
});
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
};
|
|
4744
|
+
|
|
4329
4745
|
// src/ObservedCall.ts
|
|
4330
4746
|
var logger4 = createLogger("ObservedCall");
|
|
4331
4747
|
var ObservedCall = class extends EventEmitter3 {
|
|
@@ -4384,10 +4800,6 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4384
4800
|
numberOfDataChannels = 0;
|
|
4385
4801
|
maxNumberOfClients = 0;
|
|
4386
4802
|
deltaNumberOfIssues = 0;
|
|
4387
|
-
// public deltaRttLt50Measurements = 0;
|
|
4388
|
-
// public deltaRttLt150Measurements = 0;
|
|
4389
|
-
// public deltaRttLt300Measurements = 0;
|
|
4390
|
-
// public deltaRttGtOrEq300Measurements = 0;
|
|
4391
4803
|
appData;
|
|
4392
4804
|
closed = false;
|
|
4393
4805
|
startedAt;
|
|
@@ -4433,6 +4845,10 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4433
4845
|
detector = new PublisherFaultCorroborationDetector(this, config);
|
|
4434
4846
|
break;
|
|
4435
4847
|
}
|
|
4848
|
+
case AudioEchoDetector.NAME: {
|
|
4849
|
+
detector = new AudioEchoDetector(this, config);
|
|
4850
|
+
break;
|
|
4851
|
+
}
|
|
4436
4852
|
default: {
|
|
4437
4853
|
logger4.warn("Unknown detector name %s; skipping", name);
|
|
4438
4854
|
return this;
|
|
@@ -7513,6 +7929,8 @@ function createP2pRemoteTrackResolverFactory() {
|
|
|
7513
7929
|
}
|
|
7514
7930
|
export {
|
|
7515
7931
|
ActiveIssuesRegistry,
|
|
7932
|
+
AudioEchoDetector,
|
|
7933
|
+
AudioEchoTypes,
|
|
7516
7934
|
CODEC_MISMATCH_ISSUE,
|
|
7517
7935
|
CallConcurrentIssueDetector,
|
|
7518
7936
|
CallConcurrentIssueTypes,
|
|
@@ -7560,8 +7978,12 @@ export {
|
|
|
7560
7978
|
PublisherFaultCorroborationDetector,
|
|
7561
7979
|
PublisherFaultTypes,
|
|
7562
7980
|
RESOLVED_ISSUE_SUFFIX,
|
|
7981
|
+
RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS,
|
|
7982
|
+
RTT_HISTOGRAM_NUMBER_OF_BUCKETS,
|
|
7563
7983
|
RemoteTrackResolver,
|
|
7564
7984
|
RemoteTrackResolverValidator,
|
|
7985
|
+
SCORE_HISTOGRAM_BUCKET_WIDTH,
|
|
7986
|
+
SCORE_HISTOGRAM_NUMBER_OF_BUCKETS,
|
|
7565
7987
|
SfuCongestionDetector,
|
|
7566
7988
|
SimulcastReceiverValidator,
|
|
7567
7989
|
SlidingWindow,
|
|
@@ -7599,6 +8021,7 @@ export {
|
|
|
7599
8021
|
medianAbsoluteDeviation,
|
|
7600
8022
|
pageHinkley,
|
|
7601
8023
|
percentile,
|
|
8024
|
+
percentileOfHistogram,
|
|
7602
8025
|
percentileOfSorted,
|
|
7603
8026
|
robustZScore,
|
|
7604
8027
|
schemaVersion,
|