@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 CHANGED
@@ -1126,6 +1126,7 @@ counters** โ€” that is the rule the whole design hangs on:
1126
1126
  | `PublisherFaultCorroborationDetector` | ๐Ÿ”— | call | `CORROBORATED_PUBLISHER_FAULT` |
1127
1127
  | `TrackDeliveryMismatchDetector` | ๐Ÿ”— | call | `PUBLISHED_TRACK_NOT_DELIVERED`, `RECEIVER_TRACK_NOT_DELIVERED`, `PUBLISHER_TRACK_DRY` |
1128
1128
  | `UnconsumedTrackDetector` | ๐Ÿ”— | call | `UNCONSUMED_PUBLISHED_TRACK` |
1129
+ | `AudioEchoDetector` | ๐Ÿ”—ยน | call | `AUDIO_ECHO_SUSPECTED` |
1129
1130
  | `ClientPopulationIssueDetector` | | **observer** | `CLIENT_POPULATION_ISSUE` |
1130
1131
  | `SfuCongestionDetector` | | **observer** | `sfu-congestion` |
1131
1132
  | `TurnServerHealthDetector` | | **observer** | `TURN_SERVER_DEGRADED` |
@@ -1168,6 +1169,16 @@ What each adds that no endpoint can know:
1168
1169
  from the **same SFU** โ€” the finding's meaning is "these clients share only that server".
1169
1170
  - **`TrackDeliveryMismatchDetector`** โ€” *are the two ends of a track disagreeing?*
1170
1171
  - **`UnconsumedTrackDetector`** โ€” *is anyone actually subscribed?* (reads the resolver's silence)
1172
+ - **`AudioEchoDetector`** โ€” *is someone sending the others' voices back into the call?* Compares
1173
+ each outbound audio track's per-tick level (`ObservedOutboundTrack.rmsAudioLevel`) with the
1174
+ loudest other participant's, bucket by bucket. An echo is active whenever the source speaks, and
1175
+ louder when the source is louder; turn-taking fails the first test and cross-talk the second.
1176
+ When both tracks of a pair pass, it reports the one with the client's `loopback-audio-input`
1177
+ issue open, else the one that follows more reliably, else the quieter one (`ambiguousDirection`).
1178
+ Works at the clients' sampling granularity, so it catches sustained echo (a `Monitor of โ€ฆ`
1179
+ loopback device, speakers into an open mic) rather than faint echo.
1180
+ ยน The resolver is needed only for publishers whose browser reports no media-source audio level
1181
+ (Firefox): their level is then read from what their receivers heard.
1171
1182
  - **`TurnServerHealthDetector`** โ€” *does trouble cluster on one relay?*
1172
1183
  - **`TurnServerOutageDetector`** โ€” covers the case the health detector structurally cannot. The
1173
1184
  health detector groups clients by the server relaying them and asks how many report issues โ€” it
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { EventEmitter } from 'events';
1
+ import { EventEmitter } from 'node:events';
2
2
  import { types } from 'mediasoup';
3
3
 
4
4
  declare const schemaVersion = "3.7.0";
@@ -1479,6 +1479,16 @@ declare class ObservedMediaSource implements MediaSourceStats {
1479
1479
  frames?: number | undefined;
1480
1480
  framesPerSecond?: number | undefined;
1481
1481
  attachments?: Record<string, unknown> | undefined;
1482
+ /** Increase of `totalAudioEnergy` in this tick (audio only). */
1483
+ deltaTotalAudioEnergy: number;
1484
+ /** Increase of `totalSamplesDuration` in this tick, in seconds (audio only). */
1485
+ deltaTotalSamplesDuration: number;
1486
+ /**
1487
+ * RMS level (0..1) of what the capture device delivered over the tick, after the browser's own
1488
+ * processing (AEC, NS, AGC). Chrome and Safari report the inputs; Firefox does not, and there the
1489
+ * publisher's level has to come from its receivers โ€” see `ObservedOutboundTrack.rmsAudioLevel`.
1490
+ */
1491
+ rmsAudioLevel?: number;
1482
1492
  constructor(timestamp: number, id: string, kind: MediaKind, _peerConnection: ObservedPeerConnection);
1483
1493
  get visited(): boolean;
1484
1494
  getPeerConnection(): ObservedPeerConnection;
@@ -1932,6 +1942,65 @@ declare function mannKendall(values: number[], alpha?: number): MannKendallResul
1932
1942
  * recomputing every pair from scratch) doesn't have to reimplement the normal approximation.
1933
1943
  */
1934
1944
  declare function mannKendallVerdict(s: number, variance: number, alpha?: number): Pick<MannKendallResult, 'z' | 'pValue' | 'trend'>;
1945
+ /**
1946
+ * Estimates the p-th percentile (0..1) of the values counted in `histogram`, whose bucket `i`
1947
+ * holds the values in `[i * bucketWidth, (i + 1) * bucketWidth)`. Interpolates linearly inside
1948
+ * the bucket the rank falls in, so the error is at most one bucket width. With `openEnded`, the
1949
+ * last bucket holds every value above its lower bound, and a percentile falling in it is reported
1950
+ * as `max`. The estimate is clamped to `[min, max]` when given. `undefined` for an empty histogram.
1951
+ */
1952
+ declare function percentileOfHistogram(histogram: readonly number[], bucketWidth: number, p: number, options?: {
1953
+ min?: number;
1954
+ max?: number;
1955
+ openEnded?: boolean;
1956
+ }): number | undefined;
1957
+
1958
+ declare enum ClientMetaTypes {
1959
+ MEDIA_CONSTRAINT = "MEDIA_CONSTRAINT",
1960
+ MEDIA_DEVICE = "MEDIA_DEVICE",
1961
+ MEDIA_DEVICES_SUPPORTED_CONSTRAINTS = "MEDIA_DEVICES_SUPPORTED_CONSTRAINTS",
1962
+ USER_MEDIA_ERROR = "USER_MEDIA_ERROR",
1963
+ LOCAL_SDP = "LOCAL_SDP",
1964
+ AUDIO_INPUT_DEVICE = "AUDIO_INPUT_DEVICE",
1965
+ OPERATION_SYSTEM = "OPERATION_SYSTEM",
1966
+ ENGINE = "ENGINE",
1967
+ PLATFORM = "PLATFORM",
1968
+ BROWSER = "BROWSER"
1969
+ }
1970
+ type MediaDeviceInfo = {
1971
+ deviceId: string;
1972
+ label: string;
1973
+ kind: string;
1974
+ groupId: string;
1975
+ };
1976
+ /**
1977
+ * Payload of `AUDIO_INPUT_DEVICE`: which device an outbound audio track captures from. Sent by the
1978
+ * client once per track, when it is first monitored.
1979
+ */
1980
+ type AudioInputDevice = {
1981
+ peerConnectionId: string;
1982
+ trackId: string;
1983
+ /** `MediaStreamTrack.label`; empty when capture permission was not yet granted. */
1984
+ label: string;
1985
+ deviceId?: string;
1986
+ groupId?: string;
1987
+ };
1988
+ type Browser = {
1989
+ name: string;
1990
+ version: string;
1991
+ };
1992
+ type Platform = {
1993
+ name: string;
1994
+ version: string;
1995
+ };
1996
+ type Engine = {
1997
+ name: string;
1998
+ version: string;
1999
+ };
2000
+ type OperationSystem = {
2001
+ name: string;
2002
+ version: string;
2003
+ };
1935
2004
 
1936
2005
  /** The per-receiver view the aggregator builds for one subscribed track. */
1937
2006
  type PublishedTrackReceivingDistributionEntry = {
@@ -1983,6 +2052,20 @@ declare class ObservedOutboundTrack implements OutboundTrackSample {
1983
2052
  remoteFractionLost?: number | undefined;
1984
2053
  remoteRttInMs?: number | undefined;
1985
2054
  qualityLimitationReason?: string | undefined;
2055
+ /**
2056
+ * This audio track's RMS level (0..1) over the latest tick โ€” how loud the publisher was sending.
2057
+ * Taken from the publisher's own `media-source` when it reports one (Chrome, Safari), otherwise
2058
+ * the median of what its receivers heard ({@link receivedRmsAudioLevel}), which is what makes it
2059
+ * available for Firefox publishers too. `undefined` for video, or when neither is known.
2060
+ * {@link rmsAudioLevelSource} says which one it is.
2061
+ */
2062
+ rmsAudioLevel?: number;
2063
+ rmsAudioLevelSource?: 'media-source' | 'receivers';
2064
+ /**
2065
+ * Median RMS level (0..1) of this track across the receivers that played it out, each over its own
2066
+ * latest tick. Needs a `RemoteTrackResolver`: without links there are no receivers to ask.
2067
+ */
2068
+ receivedRmsAudioLevel?: number;
1986
2069
  constructor(timestamp: number, id: string, kind: MediaKind, _peerConnection: ObservedPeerConnection, _outboundRtps?: ObservedOutboundRtp[] | undefined, _mediaSource?: ObservedMediaSource | undefined);
1987
2070
  get score(): number | undefined;
1988
2071
  get visited(): boolean;
@@ -1990,7 +2073,10 @@ declare class ObservedOutboundTrack implements OutboundTrackSample {
1990
2073
  getPeerConnection(): ObservedPeerConnection;
1991
2074
  getOutboundRtps(): ObservedOutboundRtp[] | undefined;
1992
2075
  getMediaSource(): ObservedMediaSource | undefined;
2076
+ /** The capture device behind this track, when the client sent `AUDIO_INPUT_DEVICE` metadata for it. */
2077
+ get audioInputDevice(): AudioInputDevice | undefined;
1993
2078
  update(stats: OutboundTrackSample): void;
2079
+ private _updateAudioLevel;
1994
2080
  private createReceivingDistribution;
1995
2081
  }
1996
2082
 
@@ -2146,6 +2232,15 @@ declare class ObservedInboundRtp implements InboundRtpStats {
2146
2232
  deltaFecPacketsReceived: number;
2147
2233
  deltaFecPacketsDiscarded: number;
2148
2234
  deltaPausesDuration: number;
2235
+ deltaTotalAudioEnergy: number;
2236
+ deltaTotalSamplesDuration: number;
2237
+ /**
2238
+ * RMS level (0..1) of the audio played out from this stream over the tick, from the two deltas
2239
+ * above. This is the far end's outgoing level **as this receiver heard it**, which is the only
2240
+ * send-side level available for publishers on browsers that do not report `media-source` audio
2241
+ * levels (Firefox). `undefined` for video and for a tick with no audio accounted for.
2242
+ */
2243
+ rmsAudioLevel?: number;
2149
2244
  /**
2150
2245
  * Mean jitter-buffer delay for the frames/samples emitted in this tick (seconds), derived from
2151
2246
  * the cumulative `jitterBufferDelay` / `jitterBufferEmittedCount` pair โ€” the only correct way to
@@ -2447,40 +2542,6 @@ declare class ObservedPeerConnection extends EventEmitter {
2447
2542
  private _notify;
2448
2543
  }
2449
2544
 
2450
- declare enum ClientMetaTypes {
2451
- MEDIA_CONSTRAINT = "MEDIA_CONSTRAINT",
2452
- MEDIA_DEVICE = "MEDIA_DEVICE",
2453
- MEDIA_DEVICES_SUPPORTED_CONSTRAINTS = "MEDIA_DEVICES_SUPPORTED_CONSTRAINTS",
2454
- USER_MEDIA_ERROR = "USER_MEDIA_ERROR",
2455
- LOCAL_SDP = "LOCAL_SDP",
2456
- OPERATION_SYSTEM = "OPERATION_SYSTEM",
2457
- ENGINE = "ENGINE",
2458
- PLATFORM = "PLATFORM",
2459
- BROWSER = "BROWSER"
2460
- }
2461
- type MediaDeviceInfo = {
2462
- deviceId: string;
2463
- label: string;
2464
- kind: string;
2465
- groupId: string;
2466
- };
2467
- type Browser = {
2468
- name: string;
2469
- version: string;
2470
- };
2471
- type Platform = {
2472
- name: string;
2473
- version: string;
2474
- };
2475
- type Engine = {
2476
- name: string;
2477
- version: string;
2478
- };
2479
- type OperationSystem = {
2480
- name: string;
2481
- version: string;
2482
- };
2483
-
2484
2545
  /**
2485
2546
  * Turning a correlation into a **conclusion**.
2486
2547
  *
@@ -3648,6 +3709,31 @@ declare class ObservedClientIssueRegistry {
3648
3709
  clear(): void;
3649
3710
  }
3650
3711
 
3712
+ /** Width of one `rttHistogram` bucket. */
3713
+ declare const RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS = 20;
3714
+ /** Number of `rttHistogram` buckets: 0โ€“960 ms in 20 ms steps, plus one bucket for 980 ms and above. */
3715
+ declare const RTT_HISTOGRAM_NUMBER_OF_BUCKETS = 50;
3716
+ /** Width of one `scoreHistogram` bucket. Scores are 0..5. */
3717
+ declare const SCORE_HISTOGRAM_BUCKET_WIDTH = 1;
3718
+ /** Number of `scoreHistogram` buckets: [0, 1), [1, 2), [2, 3), [3, 4) and [4, 5]. */
3719
+ declare const SCORE_HISTOGRAM_NUMBER_OF_BUCKETS = 5;
3720
+ /** Session RTT distribution of a client, in milliseconds, estimated from `rttHistogram`. */
3721
+ type RttSummary = {
3722
+ p50: number | undefined;
3723
+ p75: number | undefined;
3724
+ p95: number | undefined;
3725
+ p99: number | undefined;
3726
+ avg: number | undefined;
3727
+ };
3728
+ /** Session score distribution of a client, estimated from `scoreHistogram`. */
3729
+ type ScoreSummary = {
3730
+ p5: number | undefined;
3731
+ p25: number | undefined;
3732
+ p50: number | undefined;
3733
+ p75: number | undefined;
3734
+ p95: number | undefined;
3735
+ avg: number | undefined;
3736
+ };
3651
3737
  type ObservedClientSettings<AppData extends Record<string, unknown> = Record<string, unknown>> = {
3652
3738
  clientId: string;
3653
3739
  appData?: AppData;
@@ -3707,10 +3793,6 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
3707
3793
  deltaOutboundPacketsSent: number;
3708
3794
  deltaTransportSentBytes: number;
3709
3795
  deltaTransportReceivedBytes: number;
3710
- deltaRttLt50Measurements: number;
3711
- deltaRttLt150Measurements: number;
3712
- deltaRttLt300Measurements: number;
3713
- deltaRttGtOrEq300Measurements: number;
3714
3796
  currentMaxRttInMs?: number;
3715
3797
  currentMinRttInMs?: number;
3716
3798
  currentAvgRttInMs?: number;
@@ -3726,7 +3808,67 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
3726
3808
  deltaNumberOfIssues: number;
3727
3809
  totalScoreSum: number;
3728
3810
  numberOfScoreMeasurements: number;
3811
+ minScore?: number;
3812
+ maxScore?: number;
3813
+ /**
3814
+ * Score measurements (one per accepted sample with a score) counted into
3815
+ * `SCORE_HISTOGRAM_NUMBER_OF_BUCKETS` buckets of `SCORE_HISTOGRAM_BUCKET_WIDTH`. Histograms of
3816
+ * several clients can be added bucket by bucket to get the distribution of a call.
3817
+ */
3818
+ readonly scoreHistogram: number[];
3819
+ /** Issues raised over the session, counted as in `issueHistogram`. */
3820
+ totalNumberOfIssues: number;
3821
+ /**
3822
+ * Number of times each issue type was raised over the session, keyed by the base type (without
3823
+ * the `-resolved` suffix): `client.issueHistogram['loopback-audio-input']`. A resolution entry is
3824
+ * not counted, and neither is a re-raise of a keyed issue that is still open, so a stateful issue
3825
+ * reported on every tick counts once per open interval. A type never raised is `undefined`.
3826
+ */
3827
+ readonly issueHistogram: Record<string, number>;
3828
+ totalRttSumInMs: number;
3829
+ numberOfRttMeasurements: number;
3830
+ maxRttInMs?: number;
3831
+ minRttInMs?: number;
3832
+ /**
3833
+ * RTT measurements counted into fixed buckets of `RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS`; the last bucket
3834
+ * holds every measurement of 980 ms and above. Constant memory, so percentiles can be estimated
3835
+ * over the whole session (see `rttSummary`) without keeping every measurement. Histograms
3836
+ * of several clients can be added bucket by bucket to get the distribution of a call.
3837
+ */
3838
+ readonly rttHistogram: number[];
3839
+ private _rttSummary?;
3840
+ private _scoreSummary?;
3841
+ totalInboundPacketsLost: number;
3842
+ totalInboundPacketsReceived: number;
3843
+ totalOutboundPacketsSent: number;
3844
+ totalReceivedAudioBytes: number;
3845
+ totalReceivedVideoBytes: number;
3846
+ totalSentAudioBytes: number;
3847
+ totalSentVideoBytes: number;
3848
+ totalTransportReceivedBytes: number;
3849
+ totalTransportSentBytes: number;
3850
+ totalDataChannelBytesReceived: number;
3851
+ totalDataChannelBytesSent: number;
3852
+ totalDataChannelMessagesReceived: number;
3853
+ totalDataChannelMessagesSent: number;
3854
+ /** Highest `sendingAudioBitrate + sendingVideoBitrate` of any sample, in bits per second. */
3855
+ peakSendingBitrate: number;
3856
+ /** Highest `receivingAudioBitrate + receivingVideoBitrate` of any sample, in bits per second. */
3857
+ peakReceivingBitrate: number;
3858
+ numberOfSamplesOverUdp: number;
3859
+ numberOfSamplesOverTcp: number;
3860
+ numberOfSamplesOverTurnUdp: number;
3861
+ numberOfSamplesOverTurnTcp: number;
3862
+ numberOfSamplesWithUnknownPath: number;
3863
+ numberOfPublicAddressChanges: number;
3729
3864
  readonly mediaDevices: MediaDeviceInfo[];
3865
+ /**
3866
+ * The capture device behind each outbound audio track, keyed by track id, from the client's
3867
+ * `AUDIO_INPUT_DEVICE` metadata. Kept for the client's lifetime (a device switch is a new track
3868
+ * id), so a finding raised after a track ended can still name its device. Read it per track via
3869
+ * `ObservedOutboundTrack.audioInputDevice`.
3870
+ */
3871
+ readonly audioInputDevices: Map<string, AudioInputDevice>;
3730
3872
  /**
3731
3873
  * The client's currently **open** stateful issues, keyed by `ClientIssue.key` โ€” the server-side
3732
3874
  * mirror of the client monitor's own active-issue map (client-monitor-js >= 4.6.0).
@@ -3739,9 +3881,7 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
3739
3881
  readonly activeIssues: ObservedClientIssueRegistry;
3740
3882
  private _pendingInjections;
3741
3883
  private _activeSample?;
3742
- /** Peer connections the client reported closed (`PEER_CONNECTION_CLOSED`), closed once the current sample is applied. */
3743
3884
  private readonly _peerConnectionsToClose;
3744
- /** Ids of peer connections closed by the client; late stats for them are ignored rather than resurrecting them. */
3745
3885
  private readonly _closedPeerConnectionIds;
3746
3886
  private closeTimer?;
3747
3887
  constructor(settings: ObservedClientSettings<AppData>, call: ObservedCall, activeIssues: ObservedClientIssueRegistry);
@@ -3749,6 +3889,18 @@ declare class ObservedClient<AppData extends Record<string, unknown> = Record<st
3749
3889
  get publicAddresses(): string[];
3750
3890
  get numberOfPeerConnections(): number;
3751
3891
  get score(): number | undefined;
3892
+ /**
3893
+ * Percentiles (error at most one 20 ms bucket; a percentile in the overflow bucket is reported as
3894
+ * `maxRttInMs`) and mean of every RTT measurement of the session. Cached until the next measurement.
3895
+ */
3896
+ get rttSummary(): Readonly<RttSummary>;
3897
+ /**
3898
+ * Percentiles (error at most one score point) and mean of every score of the session. Cached until
3899
+ * the next score.
3900
+ */
3901
+ get scoreSummary(): Readonly<ScoreSummary>;
3902
+ private _addScoreMeasurement;
3903
+ private _addRttMeasurement;
3752
3904
  close(): void;
3753
3905
  accept(sample: ClientSample, context?: AcceptContext): void;
3754
3906
  injectMetaData(metaData: ClientMetaData): void;
@@ -5219,6 +5371,105 @@ declare class UnconsumedTrackDetector implements Detector {
5219
5371
  close(): void;
5220
5372
  }
5221
5373
 
5374
+ declare const AudioEchoTypes: {
5375
+ /**
5376
+ * One participant's outgoing audio follows another participant's voice: it rises when they speak,
5377
+ * in proportion to how loud they are. The echoing participant is sending the far end's playout
5378
+ * back into the call โ€” speakers into an open mic, or a loopback capture device.
5379
+ */
5380
+ readonly audioEchoSuspected: "AUDIO_ECHO_SUSPECTED";
5381
+ };
5382
+ type AudioEchoDetectorConfig = {
5383
+ /**
5384
+ * Width of one comparison bucket in ms, on the observer clock. Default `5_000`, the usual
5385
+ * client-monitor sampling period. Each track contributes the level of the last tick seen in the
5386
+ * bucket, so a bucket narrower than the clients' sampling period is mostly empty.
5387
+ */
5388
+ bucketMs: number;
5389
+ /** Buckets kept per track, and so the stretch the verdict is taken over. Default `24` (two minutes at 5 s). */
5390
+ windowBuckets: number;
5391
+ /**
5392
+ * RMS level (0..1) above which a bucket counts as someone speaking. Default `0.01` (-40 dBFS):
5393
+ * speech after AGC sits around 0.03โ€“0.1, a quiet room's noise floor well below 0.003.
5394
+ */
5395
+ activeRmsLevel: number;
5396
+ /**
5397
+ * Buckets in which the loudest *other* participant spoke, required before a track is judged.
5398
+ * Default `8`. With fewer, a couple of overlapping sentences look like an echo.
5399
+ */
5400
+ minSourceActiveBuckets: number;
5401
+ /**
5402
+ * Share of those buckets in which the suspect track was active too. Default `0.8`. On its own this
5403
+ * is not evidence โ€” two people talking over each other in the same 5 s score high โ€” which is why
5404
+ * {@link minCorrelation} has to hold as well.
5405
+ */
5406
+ minFollowRatio: number;
5407
+ /**
5408
+ * Pearson correlation, over those same buckets, between the suspect's level and the loudest other
5409
+ * participant's level, both in dB. Default `0.6`. This is the discriminating signal: an echo is
5410
+ * louder when the source is louder, two independent speakers are not.
5411
+ */
5412
+ minCorrelation: number;
5413
+ /** Re-arm time per suspect track in ms. Default `120_000`. */
5414
+ cooldownMs: number;
5415
+ /**
5416
+ * The client issue type the loopback-device check raises, `'loopback-audio-input'` from
5417
+ * client-monitor-js. Only read, never required: when it is open on the suspect track the finding
5418
+ * says so, because it names the cause.
5419
+ */
5420
+ loopbackAudioInputIssueType: string;
5421
+ };
5422
+ /**
5423
+ * Answers **"is someone sending the others' voices back into the call?"** โ€” the echo one participant
5424
+ * causes and everyone else hears, which no endpoint can see about itself.
5425
+ *
5426
+ * Every outbound audio track's level is taken once per tick: `ObservedOutboundTrack.rmsAudioLevel`,
5427
+ * the publisher's own media-source level where the browser reports it, otherwise the median of what
5428
+ * its receivers heard. Levels are bucketed on the observer clock, and each track is compared with
5429
+ * the **loudest other participant** in the same bucket rather than with every participant in turn,
5430
+ * which keeps a tick linear in the number of publishers.
5431
+ *
5432
+ * A track is suspected when, over the buckets in which someone else was speaking:
5433
+ *
5434
+ * - it was active in at least `minFollowRatio` of them, **and**
5435
+ * - its level in dB correlates with theirs at `minCorrelation` or above.
5436
+ *
5437
+ * Ordinary turn-taking fails the first test (the listener is quiet while the other talks); cross-talk
5438
+ * passes the first and fails the second (two independent voices do not rise and fall together). The
5439
+ * echoing participant's own speech does not hurt: it fills buckets in which the others are quiet,
5440
+ * and those are not scored.
5441
+ *
5442
+ * An echo and its source correlate in both directions, so when the source rarely has the floor to
5443
+ * themselves both tracks pass. Only one is reported: the one with the client's `loopback-audio-input`
5444
+ * issue open, else the one that follows the other more reliably, else the quieter one โ€” flagged
5445
+ * `ambiguousDirection` in that last case.
5446
+ *
5447
+ * The source is reported as the participant most often loudest in those buckets. When the suspect
5448
+ * track also has the client's `loopback-audio-input` issue open, the finding says so: that names
5449
+ * the cause, where the correlation alone only shows the effect.
5450
+ *
5451
+ * **Limits.** The comparison is at the clients' sampling granularity (5 s by default), so it catches
5452
+ * sustained echo โ€” a loopback device, speakers into a laptop mic without AEC โ€” and misses faint or
5453
+ * intermittent echo. Clients sample on their own schedules, so the buckets of two participants are
5454
+ * offset by up to one sampling period; `minSourceActiveBuckets` is what absorbs that. For Firefox
5455
+ * publishers the level comes from receivers, which needs a `RemoteTrackResolver`; without one those
5456
+ * tracks carry no level and are never judged.
5457
+ */
5458
+ declare class AudioEchoDetector implements Detector {
5459
+ private readonly call;
5460
+ static readonly NAME: "audio-echo-detector";
5461
+ readonly name: "audio-echo-detector";
5462
+ private readonly _config;
5463
+ private readonly _series;
5464
+ private readonly _lastRaisedAt;
5465
+ private _lastEvaluatedBucket?;
5466
+ constructor(call: ObservedCall, config?: Partial<AudioEchoDetectorConfig>);
5467
+ close(): void;
5468
+ update(): void;
5469
+ private _collect;
5470
+ private _evaluate;
5471
+ }
5472
+
5222
5473
  /**
5223
5474
  * Detectors that reason **across calls**, created once on the observer.
5224
5475
  *
@@ -5247,6 +5498,7 @@ type AvailableCallScopeDetectorsConfigs = {
5247
5498
  [CallConcurrentIssueDetector.NAME]: CallConcurrentIssueDetectorConfig;
5248
5499
  [IssueFanOutDetector.NAME]: IssueFanOutDetectorConfig;
5249
5500
  [PublisherFaultCorroborationDetector.NAME]: PublisherFaultCorroborationDetectorConfig;
5501
+ [AudioEchoDetector.NAME]: AudioEchoDetectorConfig;
5250
5502
  };
5251
5503
  type AvailableDetectorsConfigs = AvailableObserverScopeDetectorsConfigs | AvailableCallScopeDetectorsConfigs;
5252
5504
  declare class Detectors {
@@ -6292,7 +6544,7 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
6292
6544
  * });
6293
6545
  * ```
6294
6546
  */
6295
- readonly callDetectorConfigs: Map<keyof AvailableCallScopeDetectorsConfigs, Partial<UnconsumedTrackDetectorConfig | TrackDeliveryMismatchDetectorConfig | CallConcurrentIssueDetectorConfig | IssueFanOutDetectorConfig | PublisherFaultCorroborationDetectorConfig>>;
6547
+ readonly callDetectorConfigs: Map<keyof AvailableCallScopeDetectorsConfigs, Partial<UnconsumedTrackDetectorConfig | TrackDeliveryMismatchDetectorConfig | CallConcurrentIssueDetectorConfig | IssueFanOutDetectorConfig | PublisherFaultCorroborationDetectorConfig | AudioEchoDetectorConfig>>;
6296
6548
  /**
6297
6549
  * Owns every call's summary: the resolved `config.callSummary`, the bus subscriptions that keep
6298
6550
  * the summaries current (one per event type, not one per call), and the summaries themselves.
@@ -6839,4 +7091,4 @@ declare function createInMemorySink(samples?: ClientSample[]): InMemorySink;
6839
7091
  declare function createDefaultMediasoupRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
6840
7092
  declare function createP2pRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
6841
7093
 
6842
- export { type AcceptContext, type AcceptMiddleware, type AcceptMiddlewarePayload, type ActiveClientIssue, type ActiveIssueTracker, ActiveIssuesRegistry, type AvailableCallScopeDetectorsConfigs, type AvailableDetectorsConfigs, type AvailableObserverScopeDetectorsConfigs, type AvailableValidatorConfigs, CODEC_MISMATCH_ISSUE, type CalculatedScore, type CallAppDataFactory, CallConcurrentIssueDetector, type CallConcurrentIssueDetectorConfig, type CallConcurrentIssueGroup, CallConcurrentIssueTypes, type CallHealth, CallHealthAggregator, type CallIssue, type CallIssueSpread, type CallScopedEventName, type CallSummary, type CallSummaryClients, CallSummaryCollector, type CallSummaryConfig, type CallSummaryEnricher, type CallSummaryEnrichers, type CallSummaryScores, type CallSummarySection, type CallSummaryTruncation, type CallSummaryTurnServers, type CertificateStats, type ClientAppDataFactory, type ClientEvent, ClientEventTypes, type ClientHealth, type ClientHealthThresholds, type ClientIssue, type ClientLocation, type ClientLocationResolver, type ClientMetaData, ClientMetaTypes, type ClientPopulation, type ClientPopulationAxis, ClientPopulationIssueDetector, type ClientPopulationIssueDetectorConfig, ClientPopulationIssueTypes, type ClientSample, ClientSampleSink, type ClientSampleSinkEvents, type ClientSampleSinkFactory, type CodecConsistencyReportPayload, CodecConsistencyValidator, type CodecConsistencyValidatorConfig, type CodecEvidence, type CodecStats, type CorroboratedPublisherFault, type DataChannelStats, type Detector, Detectors, type ExtensionStat, GEOHASH_CELL_SIZES, type IceCandidatePairStats, type IceCandidateStats, type IceTransportStats, InMemorySink, type InboundRtpStats, type InboundTrackSample, type Issue, type IssueBase, type IssueConclusion, IssueFanOutDetector, type IssueFanOutDetectorConfig, IssueFanOutTypes, type IssueFaultDomain, JsonlFileSink, type JsonlFileSinkFactoryOptions, type JsonlFileSinkOptions, LOWEST_COMMON_DENOMINATOR_ISSUE, type Logger, type MannKendallResult, type MediaKind, type MediaPlayoutStats, type MediaSourceStats, type MediasoupConsumerSample, type MediasoupConsumerSampleEvent, type MediasoupDataConsumerSample, type MediasoupDataProducerSample, type MediasoupDirectTransportSample, type MediasoupDirectTransportSampleEventMap, type MediasoupPipeTransportSample, type MediasoupPipeTransportSampleEventMap, type MediasoupPlainTransportSample, type MediasoupPlainTransportSampleEventMap, type MediasoupProducerSample, type MediasoupProducerSampleEvent, type MediasoupRouterSample, type MediasoupSampleEnricher, type MediasoupTransportSample, type MediasoupWebRtcTransportSample, type MediasoupWebRtcTransportSampleEventMap, type Middleware, MiddlewareProcessor, ObservedCall, type ObservedCallScope, type ObservedCallSettings, ObservedCertificate, ObservedClient, ObservedClientIssueRegistry, type ObservedClientScope, type ObservedClientSettings, ObservedCodec, ObservedDataChannel, ObservedIceCandidate, ObservedIceCandidatePair, ObservedIceTransport, ObservedInboundRtp, ObservedInboundTrack, ObservedMediaPlayout, ObservedMediaSource, ObservedMediasoupRouter, type ObservedMediasoupRouterEvents, type ObservedMediasoupRouterScope, type ObservedMediasoupRouterSettings, ObservedOutboundRtp, ObservedOutboundTrack, ObservedPeerConnection, type ObservedPeerConnectionScope, ObservedPeerConnectionTransport, ObservedRemoteInboundRtp, ObservedRemoteOutboundRtp, ObservedTURN, type ObservedTURNEventMap, ObservedTurnServer, Observer, ObserverConcurrentIssueDetector, type ObserverConcurrentIssueDetectorConfig, type ObserverConcurrentIssueGroup, ObserverConcurrentIssueTypes, type ObserverEventBase, type ObserverEvents, type ObserverIssue, type ObserverIssueSpread, type ObserverLogger, type OutboundRtpStats, type OutboundTrackSample, type PageHinkleyResult, type PeerConnectionSample, type PeerConnectionTransportStats, type PsnrSum, PublisherFaultCorroborationDetector, type PublisherFaultCorroborationDetectorConfig, PublisherFaultTypes, type QualityLimitationDurations, RESOLVED_ISSUE_SUFFIX, type RemoteInboundRtpStats, type RemoteOutboundRtpStats, type RemoteTrackLinkEvidence, RemoteTrackResolver, type RemoteTrackResolverFactory, type RemoteTrackResolverReportPayload, RemoteTrackResolverValidator, type RemoteTrackResolverValidatorConfig, type RemoteTrackResolvers, type ResolvedActiveClientIssue, type RunningValidator, type SampleRejectedReason, type ScoreCalculator, SfuCongestionDetector, type SfuCongestionDetectorBucket, type SfuCongestionDetectorConfig, type SfuCongestionDetectorEvaluation, type SfuCongestionDetectorReport, type SimulcastReceiverEvidence, type SimulcastReceiverReportPayload, SimulcastReceiverValidator, type SimulcastReceiverValidatorConfig, SlidingWindow, type SlidingWindowEntry, type StatsSummary, TrackDeliveryMismatchDetector, type TrackDeliveryMismatchDetectorConfig, TrackDeliveryMismatchTypes, TrendTester, type TrendTesterConfig, type TurnServerHealth, TurnServerHealthDetector, type TurnServerHealthDetectorConfig, TurnServerHealthTypes, TurnServerOutageDetector, type TurnServerOutageDetectorConfig, TurnServerOutageTypes, UNRESOLVED_TRACK_LINKS_ISSUE, UnconsumedTrackDetector, type UnconsumedTrackDetectorConfig, UnconsumedTrackTypes, type ValidationReport, type Validator, type ValidatorName, baseIssueType, concludeCallIssue, concludeObserverIssue, correlation, counterDelta, createCallSummary, createDefaultMediasoupRemoteTrackResolverFactory, createInMemorySink, createJsonlFileSink, createJsonlFileSinkFactory, createLogger, createP2pRemoteTrackResolverFactory, defaultCallSummaryConfig, defaultClientHealthThresholds, geohash, isClientIssueResolutionEntry, isPublicIpAddress, issuePayloadAsString, mannKendall, mannKendallVerdict, median, medianAbsoluteDeviation, pageHinkley, percentile, percentileOfSorted, robustZScore, schemaVersion, setObserverLogger, summarize };
7094
+ export { type AcceptContext, type AcceptMiddleware, type AcceptMiddlewarePayload, type ActiveClientIssue, type ActiveIssueTracker, ActiveIssuesRegistry, AudioEchoDetector, type AudioEchoDetectorConfig, AudioEchoTypes, type AudioInputDevice, type AvailableCallScopeDetectorsConfigs, type AvailableDetectorsConfigs, type AvailableObserverScopeDetectorsConfigs, type AvailableValidatorConfigs, CODEC_MISMATCH_ISSUE, type CalculatedScore, type CallAppDataFactory, CallConcurrentIssueDetector, type CallConcurrentIssueDetectorConfig, type CallConcurrentIssueGroup, CallConcurrentIssueTypes, type CallHealth, CallHealthAggregator, type CallIssue, type CallIssueSpread, type CallScopedEventName, type CallSummary, type CallSummaryClients, CallSummaryCollector, type CallSummaryConfig, type CallSummaryEnricher, type CallSummaryEnrichers, type CallSummaryScores, type CallSummarySection, type CallSummaryTruncation, type CallSummaryTurnServers, type CertificateStats, type ClientAppDataFactory, type ClientEvent, ClientEventTypes, type ClientHealth, type ClientHealthThresholds, type ClientIssue, type ClientLocation, type ClientLocationResolver, type ClientMetaData, ClientMetaTypes, type ClientPopulation, type ClientPopulationAxis, ClientPopulationIssueDetector, type ClientPopulationIssueDetectorConfig, ClientPopulationIssueTypes, type ClientSample, ClientSampleSink, type ClientSampleSinkEvents, type ClientSampleSinkFactory, type CodecConsistencyReportPayload, CodecConsistencyValidator, type CodecConsistencyValidatorConfig, type CodecEvidence, type CodecStats, type CorroboratedPublisherFault, type DataChannelStats, type Detector, Detectors, type ExtensionStat, GEOHASH_CELL_SIZES, type IceCandidatePairStats, type IceCandidateStats, type IceTransportStats, InMemorySink, type InboundRtpStats, type InboundTrackSample, type Issue, type IssueBase, type IssueConclusion, IssueFanOutDetector, type IssueFanOutDetectorConfig, IssueFanOutTypes, type IssueFaultDomain, JsonlFileSink, type JsonlFileSinkFactoryOptions, type JsonlFileSinkOptions, LOWEST_COMMON_DENOMINATOR_ISSUE, type Logger, type MannKendallResult, type MediaKind, type MediaPlayoutStats, type MediaSourceStats, type MediasoupConsumerSample, type MediasoupConsumerSampleEvent, type MediasoupDataConsumerSample, type MediasoupDataProducerSample, type MediasoupDirectTransportSample, type MediasoupDirectTransportSampleEventMap, type MediasoupPipeTransportSample, type MediasoupPipeTransportSampleEventMap, type MediasoupPlainTransportSample, type MediasoupPlainTransportSampleEventMap, type MediasoupProducerSample, type MediasoupProducerSampleEvent, type MediasoupRouterSample, type MediasoupSampleEnricher, type MediasoupTransportSample, type MediasoupWebRtcTransportSample, type MediasoupWebRtcTransportSampleEventMap, type Middleware, MiddlewareProcessor, ObservedCall, type ObservedCallScope, type ObservedCallSettings, ObservedCertificate, ObservedClient, ObservedClientIssueRegistry, type ObservedClientScope, type ObservedClientSettings, ObservedCodec, ObservedDataChannel, ObservedIceCandidate, ObservedIceCandidatePair, ObservedIceTransport, ObservedInboundRtp, ObservedInboundTrack, ObservedMediaPlayout, ObservedMediaSource, ObservedMediasoupRouter, type ObservedMediasoupRouterEvents, type ObservedMediasoupRouterScope, type ObservedMediasoupRouterSettings, ObservedOutboundRtp, ObservedOutboundTrack, ObservedPeerConnection, type ObservedPeerConnectionScope, ObservedPeerConnectionTransport, ObservedRemoteInboundRtp, ObservedRemoteOutboundRtp, ObservedTURN, type ObservedTURNEventMap, ObservedTurnServer, Observer, ObserverConcurrentIssueDetector, type ObserverConcurrentIssueDetectorConfig, type ObserverConcurrentIssueGroup, ObserverConcurrentIssueTypes, type ObserverEventBase, type ObserverEvents, type ObserverIssue, type ObserverIssueSpread, type ObserverLogger, type OutboundRtpStats, type OutboundTrackSample, type PageHinkleyResult, type PeerConnectionSample, type PeerConnectionTransportStats, type PsnrSum, PublisherFaultCorroborationDetector, type PublisherFaultCorroborationDetectorConfig, PublisherFaultTypes, type QualityLimitationDurations, RESOLVED_ISSUE_SUFFIX, RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS, RTT_HISTOGRAM_NUMBER_OF_BUCKETS, type RemoteInboundRtpStats, type RemoteOutboundRtpStats, type RemoteTrackLinkEvidence, RemoteTrackResolver, type RemoteTrackResolverFactory, type RemoteTrackResolverReportPayload, RemoteTrackResolverValidator, type RemoteTrackResolverValidatorConfig, type RemoteTrackResolvers, type ResolvedActiveClientIssue, type RttSummary, type RunningValidator, SCORE_HISTOGRAM_BUCKET_WIDTH, SCORE_HISTOGRAM_NUMBER_OF_BUCKETS, type SampleRejectedReason, type ScoreCalculator, type ScoreSummary, SfuCongestionDetector, type SfuCongestionDetectorBucket, type SfuCongestionDetectorConfig, type SfuCongestionDetectorEvaluation, type SfuCongestionDetectorReport, type SimulcastReceiverEvidence, type SimulcastReceiverReportPayload, SimulcastReceiverValidator, type SimulcastReceiverValidatorConfig, SlidingWindow, type SlidingWindowEntry, type StatsSummary, TrackDeliveryMismatchDetector, type TrackDeliveryMismatchDetectorConfig, TrackDeliveryMismatchTypes, TrendTester, type TrendTesterConfig, type TurnServerHealth, TurnServerHealthDetector, type TurnServerHealthDetectorConfig, TurnServerHealthTypes, TurnServerOutageDetector, type TurnServerOutageDetectorConfig, TurnServerOutageTypes, UNRESOLVED_TRACK_LINKS_ISSUE, UnconsumedTrackDetector, type UnconsumedTrackDetectorConfig, UnconsumedTrackTypes, type ValidationReport, type Validator, type ValidatorName, baseIssueType, concludeCallIssue, concludeObserverIssue, correlation, counterDelta, createCallSummary, createDefaultMediasoupRemoteTrackResolverFactory, createInMemorySink, createJsonlFileSink, createJsonlFileSinkFactory, createLogger, createP2pRemoteTrackResolverFactory, defaultCallSummaryConfig, defaultClientHealthThresholds, geohash, isClientIssueResolutionEntry, isPublicIpAddress, issuePayloadAsString, mannKendall, mannKendallVerdict, median, medianAbsoluteDeviation, pageHinkley, percentile, percentileOfHistogram, percentileOfSorted, robustZScore, schemaVersion, setObserverLogger, summarize };