@stream-io/video-client 0.7.9 → 0.7.11

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.
@@ -23,6 +23,6 @@ export declare class CameraManagerState extends InputMediaDeviceManagerState {
23
23
  /**
24
24
  * @internal
25
25
  */
26
- setMediaStream(stream: MediaStream | undefined): void;
26
+ setMediaStream(stream: MediaStream | undefined, rootStream: MediaStream | undefined): void;
27
27
  protected getDeviceIdFromStream(stream: MediaStream): string | undefined;
28
28
  }
@@ -1,8 +1,9 @@
1
1
  import { BehaviorSubject, Observable } from 'rxjs';
2
2
  import { RxUtils } from '../store';
3
3
  export type InputDeviceStatus = 'enabled' | 'disabled' | undefined;
4
+ export type TrackDisableMode = 'stop-tracks' | 'disable-tracks';
4
5
  export declare abstract class InputMediaDeviceManagerState<C = MediaTrackConstraints> {
5
- readonly disableMode: 'stop-tracks' | 'disable-tracks';
6
+ readonly disableMode: TrackDisableMode;
6
7
  private readonly permissionName;
7
8
  protected statusSubject: BehaviorSubject<InputDeviceStatus>;
8
9
  protected mediaStreamSubject: BehaviorSubject<MediaStream | undefined>;
@@ -41,7 +42,7 @@ export declare abstract class InputMediaDeviceManagerState<C = MediaTrackConstra
41
42
  * @param permissionName the permission name to use for querying.
42
43
  * `undefined` means no permission is required.
43
44
  */
44
- constructor(disableMode?: 'stop-tracks' | 'disable-tracks', permissionName?: PermissionName | undefined);
45
+ constructor(disableMode?: TrackDisableMode, permissionName?: PermissionName | undefined);
45
46
  /**
46
47
  * The device status
47
48
  */
@@ -67,10 +68,14 @@ export declare abstract class InputMediaDeviceManagerState<C = MediaTrackConstra
67
68
  */
68
69
  setStatus(status: InputDeviceStatus): void;
69
70
  /**
71
+ * Updates the `mediaStream` state variable.
72
+ *
70
73
  * @internal
71
74
  * @param stream the stream to set.
75
+ * @param rootStream the root stream, applicable when filters are used
76
+ * as this is the stream that holds the actual deviceId information.
72
77
  */
73
- setMediaStream(stream: MediaStream | undefined): void;
78
+ setMediaStream(stream: MediaStream | undefined, rootStream: MediaStream | undefined): void;
74
79
  /**
75
80
  * @internal
76
81
  * @param deviceId the device id to set.
@@ -3,13 +3,15 @@ import type { INoiseCancellation } from '@stream-io/audio-filters-web';
3
3
  import { Call } from '../Call';
4
4
  import { InputMediaDeviceManager } from './InputMediaDeviceManager';
5
5
  import { MicrophoneManagerState } from './MicrophoneManagerState';
6
+ import { TrackDisableMode } from './InputMediaDeviceManagerState';
6
7
  export declare class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManagerState> {
8
+ private speakingWhileMutedNotificationEnabled;
7
9
  private soundDetectorCleanup?;
8
10
  private rnSpeechDetector;
9
11
  private noiseCancellation;
10
12
  private noiseCancellationChangeUnsubscribe;
11
13
  private noiseCancellationRegistration?;
12
- constructor(call: Call);
14
+ constructor(call: Call, disableMode?: TrackDisableMode);
13
15
  /**
14
16
  * Enables noise cancellation for the microphone.
15
17
  *
@@ -23,6 +25,14 @@ export declare class MicrophoneManager extends InputMediaDeviceManager<Microphon
23
25
  * Note: not supported in React Native.
24
26
  */
25
27
  disableNoiseCancellation(): Promise<void>;
28
+ /**
29
+ * Enables speaking while muted notification.
30
+ */
31
+ enableSpeakingWhileMutedNotification(): Promise<void>;
32
+ /**
33
+ * Disables speaking while muted notification.
34
+ */
35
+ disableSpeakingWhileMutedNotification(): Promise<void>;
26
36
  protected getDevices(): Observable<MediaDeviceInfo[]>;
27
37
  protected getStream(constraints: MediaTrackConstraints): Promise<MediaStream>;
28
38
  protected publishStream(stream: MediaStream): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { Observable } from 'rxjs';
2
- import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
2
+ import { InputMediaDeviceManagerState, TrackDisableMode } from './InputMediaDeviceManagerState';
3
3
  export declare class MicrophoneManagerState extends InputMediaDeviceManagerState {
4
4
  private speakingWhileMutedSubject;
5
5
  /**
@@ -8,7 +8,7 @@ export declare class MicrophoneManagerState extends InputMediaDeviceManagerState
8
8
  * This feature is not available in the React Native SDK.
9
9
  */
10
10
  speakingWhileMuted$: Observable<boolean>;
11
- constructor();
11
+ constructor(disableMode: TrackDisableMode);
12
12
  /**
13
13
  * `true` if the user's microphone is muted but they'are speaking.
14
14
  *
@@ -5,7 +5,6 @@ import { CallStatsReport } from '../stats';
5
5
  import { CallIngressResponse, CallResponse, CallSessionResponse, CallSettingsResponse, EgressResponse, MemberResponse, OwnCapability, ThumbnailResponse, UserResponse, WSEvent } from '../gen/coordinator';
6
6
  import { Pin } from '../gen/video/sfu/models/models';
7
7
  import { Comparator } from '../sorting';
8
- import { Logger } from '../coordinator/connection/types';
9
8
  /**
10
9
  * Represents the state of the current call.
11
10
  */
@@ -211,7 +210,7 @@ export declare class CallState {
211
210
  * Will provide the thumbnails of this call.
212
211
  */
213
212
  thumbnails$: Observable<ThumbnailResponse | undefined>;
214
- readonly logger: Logger;
213
+ readonly logger: import("../..").Logger;
215
214
  /**
216
215
  * A list of comparators that are used to sort the participants.
217
216
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "0.7.9",
3
+ "version": "0.7.11",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.es.js",
@@ -45,8 +45,11 @@ export class CameraManagerState extends InputMediaDeviceManagerState {
45
45
  /**
46
46
  * @internal
47
47
  */
48
- setMediaStream(stream: MediaStream | undefined): void {
49
- super.setMediaStream(stream);
48
+ setMediaStream(
49
+ stream: MediaStream | undefined,
50
+ rootStream: MediaStream | undefined,
51
+ ): void {
52
+ super.setMediaStream(stream, rootStream);
50
53
  if (stream) {
51
54
  // RN getSettings() doesn't return facingMode, so we don't verify camera direction
52
55
  const direction = isReactNative()
@@ -59,8 +62,7 @@ export class CameraManagerState extends InputMediaDeviceManagerState {
59
62
  }
60
63
 
61
64
  protected getDeviceIdFromStream(stream: MediaStream): string | undefined {
62
- return stream.getVideoTracks()[0]?.getSettings().deviceId as
63
- | string
64
- | undefined;
65
+ const [track] = stream.getVideoTracks();
66
+ return track?.getSettings().deviceId;
65
67
  }
66
68
  }
@@ -209,7 +209,7 @@ export abstract class InputMediaDeviceManager<
209
209
  // @ts-expect-error called to dispose the stream in RN
210
210
  this.state.mediaStream.release();
211
211
  }
212
- this.state.setMediaStream(undefined);
212
+ this.state.setMediaStream(undefined, undefined);
213
213
  }
214
214
  }
215
215
 
@@ -245,6 +245,7 @@ export abstract class InputMediaDeviceManager<
245
245
  protected async unmuteStream() {
246
246
  this.logger('debug', 'Starting stream');
247
247
  let stream: MediaStream;
248
+ let rootStream: Promise<MediaStream> | undefined;
248
249
  if (
249
250
  this.state.mediaStream &&
250
251
  this.getTracks().every((t) => t.readyState === 'live')
@@ -315,17 +316,20 @@ export abstract class InputMediaDeviceManager<
315
316
  return filterStream;
316
317
  };
317
318
 
319
+ // the rootStream represents the stream coming from the actual device
320
+ // e.g. camera or microphone stream
321
+ rootStream = this.getStream(constraints as C);
318
322
  // we publish the last MediaStream of the chain
319
323
  stream = await this.filters.reduce(
320
324
  (parent, filter) => parent.then(filter).then(chainWith(parent)),
321
- this.getStream(constraints as C),
325
+ rootStream,
322
326
  );
323
327
  }
324
328
  if (this.call.state.callingState === CallingState.JOINED) {
325
329
  await this.publishStream(stream);
326
330
  }
327
331
  if (this.state.mediaStream !== stream) {
328
- this.state.setMediaStream(stream);
332
+ this.state.setMediaStream(stream, await rootStream);
329
333
  this.getTracks().forEach((track) => {
330
334
  track.addEventListener('ended', async () => {
331
335
  if (this.enablePromise) {
@@ -9,6 +9,7 @@ import { RxUtils } from '../store';
9
9
  import { getLogger } from '../logger';
10
10
 
11
11
  export type InputDeviceStatus = 'enabled' | 'disabled' | undefined;
12
+ export type TrackDisableMode = 'stop-tracks' | 'disable-tracks';
12
13
 
13
14
  export abstract class InputMediaDeviceManagerState<C = MediaTrackConstraints> {
14
15
  protected statusSubject = new BehaviorSubject<InputDeviceStatus>(undefined);
@@ -102,9 +103,7 @@ export abstract class InputMediaDeviceManagerState<C = MediaTrackConstraints> {
102
103
  * `undefined` means no permission is required.
103
104
  */
104
105
  constructor(
105
- public readonly disableMode:
106
- | 'stop-tracks'
107
- | 'disable-tracks' = 'stop-tracks',
106
+ public readonly disableMode: TrackDisableMode = 'stop-tracks',
108
107
  private readonly permissionName: PermissionName | undefined = undefined,
109
108
  ) {}
110
109
 
@@ -146,13 +145,20 @@ export abstract class InputMediaDeviceManagerState<C = MediaTrackConstraints> {
146
145
  }
147
146
 
148
147
  /**
148
+ * Updates the `mediaStream` state variable.
149
+ *
149
150
  * @internal
150
151
  * @param stream the stream to set.
152
+ * @param rootStream the root stream, applicable when filters are used
153
+ * as this is the stream that holds the actual deviceId information.
151
154
  */
152
- setMediaStream(stream: MediaStream | undefined) {
155
+ setMediaStream(
156
+ stream: MediaStream | undefined,
157
+ rootStream: MediaStream | undefined,
158
+ ) {
153
159
  this.setCurrentValue(this.mediaStreamSubject, stream);
154
- if (stream) {
155
- this.setDevice(this.getDeviceIdFromStream(stream));
160
+ if (rootStream) {
161
+ this.setDevice(this.getDeviceIdFromStream(rootStream));
156
162
  }
157
163
  }
158
164
 
@@ -3,6 +3,7 @@ import type { INoiseCancellation } from '@stream-io/audio-filters-web';
3
3
  import { Call } from '../Call';
4
4
  import { InputMediaDeviceManager } from './InputMediaDeviceManager';
5
5
  import { MicrophoneManagerState } from './MicrophoneManagerState';
6
+ import { TrackDisableMode } from './InputMediaDeviceManagerState';
6
7
  import { getAudioDevices, getAudioStream } from './devices';
7
8
  import { TrackType } from '../gen/video/sfu/models/models';
8
9
  import { createSoundDetector } from '../helpers/sound-detector';
@@ -16,37 +17,48 @@ import { createSubscription } from '../store/rxUtils';
16
17
  import { RNSpeechDetector } from '../helpers/RNSpeechDetector';
17
18
 
18
19
  export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManagerState> {
20
+ private speakingWhileMutedNotificationEnabled = true;
19
21
  private soundDetectorCleanup?: Function;
20
22
  private rnSpeechDetector: RNSpeechDetector | undefined;
21
23
  private noiseCancellation: INoiseCancellation | undefined;
22
24
  private noiseCancellationChangeUnsubscribe: (() => void) | undefined;
23
25
  private noiseCancellationRegistration?: Promise<() => Promise<void>>;
24
26
 
25
- constructor(call: Call) {
26
- super(call, new MicrophoneManagerState(), TrackType.AUDIO);
27
-
28
- combineLatest([
29
- this.call.state.callingState$,
30
- this.call.state.ownCapabilities$,
31
- this.state.selectedDevice$,
32
- this.state.status$,
33
- ]).subscribe(async ([callingState, ownCapabilities, deviceId, status]) => {
34
- if (callingState !== CallingState.JOINED) {
35
- if (callingState === CallingState.LEFT) {
36
- await this.stopSpeakingWhileMutedDetection();
37
- }
38
- return;
39
- }
40
- if (ownCapabilities.includes(OwnCapability.SEND_AUDIO)) {
41
- if (status === 'disabled') {
42
- await this.startSpeakingWhileMutedDetection(deviceId);
43
- } else {
44
- await this.stopSpeakingWhileMutedDetection();
45
- }
46
- } else {
47
- await this.stopSpeakingWhileMutedDetection();
48
- }
49
- });
27
+ constructor(
28
+ call: Call,
29
+ disableMode: TrackDisableMode = isReactNative()
30
+ ? 'disable-tracks'
31
+ : 'stop-tracks',
32
+ ) {
33
+ super(call, new MicrophoneManagerState(disableMode), TrackType.AUDIO);
34
+
35
+ this.subscriptions.push(
36
+ createSubscription(
37
+ combineLatest([
38
+ this.call.state.callingState$,
39
+ this.call.state.ownCapabilities$,
40
+ this.state.selectedDevice$,
41
+ this.state.status$,
42
+ ]),
43
+ async ([callingState, ownCapabilities, deviceId, status]) => {
44
+ if (callingState === CallingState.LEFT) {
45
+ await this.stopSpeakingWhileMutedDetection();
46
+ }
47
+ if (callingState !== CallingState.JOINED) return;
48
+ if (!this.speakingWhileMutedNotificationEnabled) return;
49
+
50
+ if (ownCapabilities.includes(OwnCapability.SEND_AUDIO)) {
51
+ if (status === 'disabled') {
52
+ await this.startSpeakingWhileMutedDetection(deviceId);
53
+ } else {
54
+ await this.stopSpeakingWhileMutedDetection();
55
+ }
56
+ } else {
57
+ await this.stopSpeakingWhileMutedDetection();
58
+ }
59
+ },
60
+ ),
61
+ );
50
62
 
51
63
  this.subscriptions.push(
52
64
  createSubscription(this.call.state.callingState$, (callingState) => {
@@ -163,6 +175,24 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
163
175
  await this.call.notifyNoiseCancellationStopped();
164
176
  }
165
177
 
178
+ /**
179
+ * Enables speaking while muted notification.
180
+ */
181
+ async enableSpeakingWhileMutedNotification() {
182
+ this.speakingWhileMutedNotificationEnabled = true;
183
+ if (this.state.status === 'disabled') {
184
+ await this.startSpeakingWhileMutedDetection(this.state.selectedDevice);
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Disables speaking while muted notification.
190
+ */
191
+ async disableSpeakingWhileMutedNotification() {
192
+ this.speakingWhileMutedNotificationEnabled = false;
193
+ await this.stopSpeakingWhileMutedDetection();
194
+ }
195
+
166
196
  protected getDevices(): Observable<MediaDeviceInfo[]> {
167
197
  return getAudioDevices();
168
198
  }
@@ -208,9 +238,7 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
208
238
  }
209
239
 
210
240
  private async stopSpeakingWhileMutedDetection() {
211
- if (!this.soundDetectorCleanup) {
212
- return;
213
- }
241
+ if (!this.soundDetectorCleanup) return;
214
242
  this.state.setSpeakingWhileMuted(false);
215
243
  try {
216
244
  await this.soundDetectorCleanup();
@@ -1,5 +1,8 @@
1
1
  import { BehaviorSubject, distinctUntilChanged, Observable } from 'rxjs';
2
- import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
2
+ import {
3
+ InputMediaDeviceManagerState,
4
+ TrackDisableMode,
5
+ } from './InputMediaDeviceManagerState';
3
6
 
4
7
  export class MicrophoneManagerState extends InputMediaDeviceManagerState {
5
8
  private speakingWhileMutedSubject = new BehaviorSubject<boolean>(false);
@@ -11,9 +14,9 @@ export class MicrophoneManagerState extends InputMediaDeviceManagerState {
11
14
  */
12
15
  speakingWhileMuted$: Observable<boolean>;
13
16
 
14
- constructor() {
17
+ constructor(disableMode: TrackDisableMode) {
15
18
  super(
16
- 'disable-tracks',
19
+ disableMode,
17
20
  // `microphone` is not in the W3C standard yet,
18
21
  // but it's supported by Chrome and Safari.
19
22
  'microphone' as PermissionName,
@@ -41,8 +44,7 @@ export class MicrophoneManagerState extends InputMediaDeviceManagerState {
41
44
  }
42
45
 
43
46
  protected getDeviceIdFromStream(stream: MediaStream): string | undefined {
44
- return stream.getAudioTracks()[0]?.getSettings().deviceId as
45
- | string
46
- | undefined;
47
+ const [track] = stream.getAudioTracks();
48
+ return track?.getSettings().deviceId;
47
49
  }
48
50
  }
@@ -76,6 +76,7 @@ describe('MicrophoneManager', () => {
76
76
  streamClient: new StreamClient('abc123'),
77
77
  clientStore: new StreamVideoWriteableStateStore(),
78
78
  }),
79
+ 'disable-tracks',
79
80
  );
80
81
  });
81
82
  it('list devices', () => {
@@ -147,65 +148,88 @@ describe('MicrophoneManager', () => {
147
148
  expect(manager.state.mediaStream).toBeUndefined();
148
149
  });
149
150
 
150
- it(`should start sound detection if mic is disabled`, async () => {
151
- await manager.enable();
152
- // @ts-expect-error
153
- vi.spyOn(manager, 'startSpeakingWhileMutedDetection');
154
- await manager.disable();
151
+ describe('Speaking While Muted', () => {
152
+ it(`should start sound detection if mic is disabled`, async () => {
153
+ await manager.enable();
154
+ // @ts-expect-error
155
+ vi.spyOn(manager, 'startSpeakingWhileMutedDetection');
156
+ await manager.disable();
155
157
 
156
- expect(manager['startSpeakingWhileMutedDetection']).toHaveBeenCalled();
157
- });
158
+ expect(manager['startSpeakingWhileMutedDetection']).toHaveBeenCalled();
159
+ });
158
160
 
159
- it(`should stop sound detection if mic is enabled`, async () => {
160
- manager.state.setSpeakingWhileMuted(true);
161
- manager['soundDetectorCleanup'] = () => {};
161
+ it(`should stop sound detection if mic is enabled`, async () => {
162
+ manager.state.setSpeakingWhileMuted(true);
163
+ manager['soundDetectorCleanup'] = () => {};
162
164
 
163
- await manager.enable();
165
+ await manager.enable();
164
166
 
165
- expect(manager.state.speakingWhileMuted).toBe(false);
166
- });
167
-
168
- it('should update speaking while muted state', async () => {
169
- const mock = createSoundDetector as Mock;
170
- let handler: SoundStateChangeHandler;
171
- mock.mockImplementation((_: MediaStream, h: SoundStateChangeHandler) => {
172
- handler = h;
167
+ expect(manager.state.speakingWhileMuted).toBe(false);
173
168
  });
174
- await manager['startSpeakingWhileMutedDetection']();
175
169
 
176
- expect(manager.state.speakingWhileMuted).toBe(false);
170
+ it('should update speaking while muted state', async () => {
171
+ const mock = createSoundDetector as Mock;
172
+ let handler: SoundStateChangeHandler;
173
+ mock.mockImplementation((_: MediaStream, h: SoundStateChangeHandler) => {
174
+ handler = h;
175
+ });
176
+ await manager['startSpeakingWhileMutedDetection']();
177
177
 
178
- handler!({ isSoundDetected: true, audioLevel: 2 });
178
+ expect(manager.state.speakingWhileMuted).toBe(false);
179
179
 
180
- expect(manager.state.speakingWhileMuted).toBe(true);
180
+ handler!({ isSoundDetected: true, audioLevel: 2 });
181
181
 
182
- handler!({ isSoundDetected: false, audioLevel: 0 });
182
+ expect(manager.state.speakingWhileMuted).toBe(true);
183
183
 
184
- expect(manager.state.speakingWhileMuted).toBe(false);
185
- });
184
+ handler!({ isSoundDetected: false, audioLevel: 0 });
186
185
 
187
- it('should stop speaking while muted notifications if user loses permission to send audio', async () => {
188
- await manager.enable();
189
- await manager.disable();
186
+ expect(manager.state.speakingWhileMuted).toBe(false);
187
+ });
190
188
 
191
- // @ts-expect-error
192
- vi.spyOn(manager, 'stopSpeakingWhileMutedDetection');
193
- manager['call'].state.setOwnCapabilities([]);
189
+ it('should stop speaking while muted notifications if user loses permission to send audio', async () => {
190
+ await manager.enable();
191
+ await manager.disable();
194
192
 
195
- expect(manager['stopSpeakingWhileMutedDetection']).toHaveBeenCalled();
196
- });
193
+ // @ts-expect-error
194
+ vi.spyOn(manager, 'stopSpeakingWhileMutedDetection');
195
+ manager['call'].state.setOwnCapabilities([]);
197
196
 
198
- it('should start speaking while muted notifications if user gains permission to send audio', async () => {
199
- await manager.enable();
200
- await manager.disable();
197
+ expect(manager['stopSpeakingWhileMutedDetection']).toHaveBeenCalled();
198
+ });
199
+
200
+ it('should start speaking while muted notifications if user gains permission to send audio', async () => {
201
+ await manager.enable();
202
+ await manager.disable();
201
203
 
202
- manager['call'].state.setOwnCapabilities([]);
204
+ manager['call'].state.setOwnCapabilities([]);
203
205
 
204
- // @ts-expect-error
205
- vi.spyOn(manager, 'stopSpeakingWhileMutedDetection');
206
- manager['call'].state.setOwnCapabilities([OwnCapability.SEND_AUDIO]);
206
+ // @ts-expect-error
207
+ vi.spyOn(manager, 'stopSpeakingWhileMutedDetection');
208
+ manager['call'].state.setOwnCapabilities([OwnCapability.SEND_AUDIO]);
207
209
 
208
- expect(manager['stopSpeakingWhileMutedDetection']).toHaveBeenCalled();
210
+ expect(manager['stopSpeakingWhileMutedDetection']).toHaveBeenCalled();
211
+ });
212
+
213
+ it(`disables speaking while muted notifications`, async () => {
214
+ // @ts-expect-error - private api
215
+ vi.spyOn(manager, 'startSpeakingWhileMutedDetection');
216
+ // @ts-expect-error - private api
217
+ vi.spyOn(manager, 'stopSpeakingWhileMutedDetection');
218
+ await manager.disableSpeakingWhileMutedNotification();
219
+ await manager.disable();
220
+ expect(manager['stopSpeakingWhileMutedDetection']).toHaveBeenCalled();
221
+ expect(
222
+ manager['startSpeakingWhileMutedDetection'],
223
+ ).not.toHaveBeenCalled();
224
+ });
225
+
226
+ it(`enables speaking while muted notifications`, async () => {
227
+ // @ts-expect-error - private api
228
+ vi.spyOn(manager, 'startSpeakingWhileMutedDetection');
229
+ await manager.enableSpeakingWhileMutedNotification();
230
+ await manager.disable();
231
+ expect(manager['startSpeakingWhileMutedDetection']).toHaveBeenCalled();
232
+ });
209
233
  });
210
234
 
211
235
  describe('Noise Cancellation', () => {
@@ -1,18 +1,6 @@
1
1
  import { BaseStats } from '../stats';
2
2
  import { SoundStateChangeHandler } from './sound-detector';
3
-
4
- /**
5
- * Flatten the stats report into an array of stats objects.
6
- *
7
- * @param report the report to flatten.
8
- */
9
- const flatten = (report: RTCStatsReport) => {
10
- const stats: RTCStats[] = [];
11
- report.forEach((s) => {
12
- stats.push(s);
13
- });
14
- return stats;
15
- };
3
+ import { flatten } from '../stats/utils';
16
4
 
17
5
  const AUDIO_LEVEL_THRESHOLD = 0.2;
18
6