@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/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 import_events3 = require("events");
179
+ var import_node_events3 = require("events");
173
180
 
174
181
  // src/ObservedClient.ts
175
- var import_events2 = require("events");
182
+ var import_node_events2 = require("events");
176
183
 
177
184
  // src/ObservedPeerConnection.ts
178
- var import_events = require("events");
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 mean = sum / (i + 1);
266
- cumulative += values[i] - mean - delta;
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 import_events.EventEmitter {
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 ObservedClient = class extends import_events2.EventEmitter {
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
- // public totalNumberOfIssues = 0;
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.totalScoreSum += this.calculatedScore.value;
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 import_events3.EventEmitter {
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 import_events6 = require("events");
5185
+ var import_node_events6 = require("events");
4763
5186
 
4764
5187
  // src/ObservedTURN.ts
4765
- var import_events4 = require("events");
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 import_events4.EventEmitter {
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 import_events5 = require("events");
5359
+ var import_node_events5 = require("events");
4937
5360
  var logger6 = createLogger("ObservedMediasoupRouter");
4938
- var ObservedMediasoupRouter = class extends import_events5.EventEmitter {
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 import_events6.EventEmitter {
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 import_events7 = require("events");
7408
- var ClientSampleSink = class extends import_events7.EventEmitter {
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,