@stream-io/video-client 0.3.12 → 0.3.14

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.browser.es.js +614 -391
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +613 -390
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +614 -391
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +6 -1
  9. package/dist/src/devices/CameraManager.d.ts +1 -2
  10. package/dist/src/devices/InputMediaDeviceManager.d.ts +10 -3
  11. package/dist/src/devices/MicrophoneManager.d.ts +1 -2
  12. package/dist/src/devices/SpeakerManager.d.ts +28 -0
  13. package/dist/src/devices/SpeakerState.d.ts +64 -0
  14. package/dist/src/devices/__tests__/SpeakerManager.test.d.ts +1 -0
  15. package/dist/src/rtc/Publisher.d.ts +6 -0
  16. package/dist/src/types.d.ts +2 -0
  17. package/dist/version.d.ts +1 -1
  18. package/package.json +1 -1
  19. package/src/Call.ts +91 -6
  20. package/src/StreamSfuClient.ts +4 -0
  21. package/src/coordinator/connection/location.ts +2 -2
  22. package/src/devices/CameraManager.ts +7 -8
  23. package/src/devices/InputMediaDeviceManager.ts +58 -12
  24. package/src/devices/MicrophoneManager.ts +3 -8
  25. package/src/devices/SpeakerManager.ts +50 -0
  26. package/src/devices/SpeakerState.ts +90 -0
  27. package/src/devices/__tests__/InputMediaDeviceManager.test.ts +5 -12
  28. package/src/devices/__tests__/SpeakerManager.test.ts +66 -0
  29. package/src/devices/__tests__/mocks.ts +4 -0
  30. package/src/helpers/DynascaleManager.ts +25 -7
  31. package/src/helpers/__tests__/DynascaleManager.test.ts +33 -2
  32. package/src/rtc/Publisher.ts +35 -7
  33. package/src/rtc/__tests__/Publisher.test.ts +1 -0
  34. package/src/rtc/codecs.ts +0 -2
  35. package/src/types.ts +2 -0
@@ -7,7 +7,9 @@ import { DynascaleManager } from './helpers/DynascaleManager';
7
7
  import { PermissionsContext } from './permissions';
8
8
  import { StreamClient } from './coordinator/connection/client';
9
9
  import { CallEventHandler, CallEventTypes, EventTypes, Logger } from './coordinator/connection/types';
10
- import { CameraManager, MicrophoneManager } from './devices';
10
+ import { CameraManager } from './devices/CameraManager';
11
+ import { MicrophoneManager } from './devices/MicrophoneManager';
12
+ import { SpeakerManager } from './devices/SpeakerManager';
11
13
  /**
12
14
  * An object representation of a `Call`.
13
15
  */
@@ -45,6 +47,7 @@ export declare class Call {
45
47
  * The DynascaleManager instance.
46
48
  */
47
49
  readonly dynascaleManager: DynascaleManager;
50
+ readonly speaker: SpeakerManager;
48
51
  /**
49
52
  * Flag telling whether this call is a "ringing" call.
50
53
  */
@@ -251,6 +254,8 @@ export declare class Call {
251
254
  *
252
255
  *
253
256
  * @param deviceId the selected device, `undefined` means the user wants to use the system's default audio output
257
+ *
258
+ * @deprecated use `call.speaker` instead
254
259
  */
255
260
  setAudioOutputDevice: (deviceId?: string) => void;
256
261
  /**
@@ -28,6 +28,5 @@ export declare class CameraManager extends InputMediaDeviceManager<CameraManager
28
28
  protected getStream(constraints: MediaTrackConstraints): Promise<MediaStream>;
29
29
  protected publishStream(stream: MediaStream): Promise<void>;
30
30
  protected stopPublishStream(stopTracks: boolean): Promise<void>;
31
- protected muteTracks(): void;
32
- protected unmuteTracks(): void;
31
+ protected getTrack(): MediaStreamTrack | undefined;
33
32
  }
@@ -1,9 +1,12 @@
1
1
  import { Observable } from 'rxjs';
2
2
  import { Call } from '../Call';
3
3
  import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
4
+ import { Logger } from '../coordinator/connection/types';
5
+ import { TrackType } from '../gen/video/sfu/models/models';
4
6
  export declare abstract class InputMediaDeviceManager<T extends InputMediaDeviceManagerState> {
5
7
  protected readonly call: Call;
6
8
  readonly state: T;
9
+ protected readonly trackType: TrackType;
7
10
  /**
8
11
  * @internal
9
12
  */
@@ -12,7 +15,8 @@ export declare abstract class InputMediaDeviceManager<T extends InputMediaDevice
12
15
  * @internal
13
16
  */
14
17
  disablePromise?: Promise<void>;
15
- constructor(call: Call, state: T);
18
+ logger: Logger;
19
+ constructor(call: Call, state: T, trackType: TrackType);
16
20
  /**
17
21
  * Lists the available audio/video devices
18
22
  *
@@ -54,8 +58,11 @@ export declare abstract class InputMediaDeviceManager<T extends InputMediaDevice
54
58
  protected abstract getStream(constraints: MediaTrackConstraints): Promise<MediaStream>;
55
59
  protected abstract publishStream(stream: MediaStream): Promise<void>;
56
60
  protected abstract stopPublishStream(stopTracks: boolean): Promise<void>;
57
- protected abstract muteTracks(): void;
58
- protected abstract unmuteTracks(): void;
61
+ protected abstract getTrack(): undefined | MediaStreamTrack;
59
62
  private muteStream;
63
+ private muteTrack;
64
+ private unmuteTrack;
65
+ private stopTrack;
66
+ private muteLocalStream;
60
67
  private unmuteStream;
61
68
  }
@@ -8,6 +8,5 @@ export declare class MicrophoneManager extends InputMediaDeviceManager<Microphon
8
8
  protected getStream(constraints: MediaTrackConstraints): Promise<MediaStream>;
9
9
  protected publishStream(stream: MediaStream): Promise<void>;
10
10
  protected stopPublishStream(stopTracks: boolean): Promise<void>;
11
- protected muteTracks(): void;
12
- protected unmuteTracks(): void;
11
+ protected getTrack(): MediaStreamTrack | undefined;
13
12
  }
@@ -0,0 +1,28 @@
1
+ import { SpeakerState } from './SpeakerState';
2
+ export declare class SpeakerManager {
3
+ readonly state: SpeakerState;
4
+ constructor();
5
+ /**
6
+ * Lists the available audio output devices
7
+ *
8
+ * Note: It prompts the user for a permission to use devices (if not already granted)
9
+ *
10
+ * @returns an Observable that will be updated if a device is connected or disconnected
11
+ */
12
+ listDevices(): import("rxjs").Observable<MediaDeviceInfo[]>;
13
+ /**
14
+ * Select device
15
+ *
16
+ * Note: this method is not supported in React Native
17
+ *
18
+ * @param deviceId empty string means the system default
19
+ */
20
+ select(deviceId: string): void;
21
+ /**
22
+ * Set the volume of the audio elements
23
+ * @param volume a number between 0 and 1
24
+ *
25
+ * Note: this method is not supported in React Native
26
+ */
27
+ setVolume(volume: number): void;
28
+ }
@@ -0,0 +1,64 @@
1
+ import { BehaviorSubject, Observable } from 'rxjs';
2
+ import { RxUtils } from '../store';
3
+ export declare class SpeakerState {
4
+ protected selectedDeviceSubject: BehaviorSubject<string>;
5
+ protected volumeSubject: BehaviorSubject<number>;
6
+ /**
7
+ * [Tells if the browser supports audio output change on 'audio' elements](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId).
8
+ */
9
+ readonly isDeviceSelectionSupported: boolean;
10
+ /**
11
+ * An Observable that emits the currently selected device
12
+ *
13
+ * Note: this feature is not supported in React Native
14
+ */
15
+ selectedDevice$: Observable<string>;
16
+ /**
17
+ * An Observable that emits the currently selected volume
18
+ *
19
+ * Note: this feature is not supported in React Native
20
+ */
21
+ volume$: Observable<number>;
22
+ constructor();
23
+ /**
24
+ * The currently selected device
25
+ *
26
+ * Note: this feature is not supported in React Native
27
+ */
28
+ get selectedDevice(): string;
29
+ /**
30
+ * The currently selected volume
31
+ *
32
+ * Note: this feature is not supported in React Native
33
+ */
34
+ get volume(): number;
35
+ /**
36
+ * Gets the current value of an observable, or undefined if the observable has
37
+ * not emitted a value yet.
38
+ *
39
+ * @param observable$ the observable to get the value from.
40
+ */
41
+ getCurrentValue: <T>(observable$: Observable<T>) => T;
42
+ /**
43
+ * @internal
44
+ * @param deviceId
45
+ */
46
+ setDevice(deviceId: string): void;
47
+ /**
48
+ * @internal
49
+ * @param volume
50
+ */
51
+ setVolume(volume: number): void;
52
+ /**
53
+ * Updates the value of the provided Subject.
54
+ * An `update` can either be a new value or a function which takes
55
+ * the current value and returns a new value.
56
+ *
57
+ * @internal
58
+ *
59
+ * @param subject the subject to update.
60
+ * @param update the update to apply to the subject.
61
+ * @return the updated value.
62
+ */
63
+ protected setCurrentValue: <T>(subject: import("rxjs").Subject<T>, update: RxUtils.Patch<T>) => T;
64
+ }
@@ -85,6 +85,12 @@ export declare class Publisher {
85
85
  * @param trackType the track type to check.
86
86
  */
87
87
  isPublishing: (trackType: TrackType) => boolean;
88
+ /**
89
+ * Returns true if the given track type is currently live
90
+ *
91
+ * @param trackType the track type to check.
92
+ */
93
+ isLive: (trackType: TrackType) => boolean;
88
94
  private notifyTrackMuteStateChanged;
89
95
  /**
90
96
  * Stops publishing all tracks and stop all tracks.
@@ -77,6 +77,8 @@ export interface StreamVideoLocalParticipant extends StreamVideoParticipant {
77
77
  * The device ID of the currently selected audio output device of the local participant (returned by the [MediaDevices API](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia))
78
78
  *
79
79
  * If the value is not defined, the user hasn't selected any device (in these cases the default system audio output could be used)
80
+ *
81
+ * @deprecated use call.speaker.state.selectedDevice
80
82
  */
81
83
  audioOutputDeviceId?: string;
82
84
  }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "0.3.12";
1
+ export declare const version = "0.3.14";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.es.js",
package/src/Call.ts CHANGED
@@ -116,7 +116,10 @@ import {
116
116
  } from './coordinator/connection/types';
117
117
  import { getClientDetails, getSdkInfo } from './client-details';
118
118
  import { getLogger } from './logger';
119
- import { CameraDirection, CameraManager, MicrophoneManager } from './devices';
119
+ import { CameraManager } from './devices/CameraManager';
120
+ import { MicrophoneManager } from './devices/MicrophoneManager';
121
+ import { CameraDirection } from './devices/CameraManagerState';
122
+ import { SpeakerManager } from './devices/SpeakerManager';
120
123
 
121
124
  /**
122
125
  * An object representation of a `Call`.
@@ -163,6 +166,11 @@ export class Call {
163
166
  */
164
167
  readonly dynascaleManager = new DynascaleManager(this);
165
168
 
169
+ /*
170
+ * Device manager for the speaker
171
+ */
172
+ readonly speaker: SpeakerManager;
173
+
166
174
  /**
167
175
  * Flag telling whether this call is a "ringing" call.
168
176
  */
@@ -269,6 +277,38 @@ export class Call {
269
277
 
270
278
  this.camera = new CameraManager(this);
271
279
  this.microphone = new MicrophoneManager(this);
280
+
281
+ this.state.localParticipant$.subscribe(async (p) => {
282
+ // Mute via device manager
283
+ // If integrator doesn't use device manager, we mute using stopPublish
284
+ if (
285
+ !p?.publishedTracks.includes(TrackType.VIDEO) &&
286
+ this.publisher?.isPublishing(TrackType.VIDEO)
287
+ ) {
288
+ this.logger(
289
+ 'info',
290
+ `Local participant's video track is muted remotely`,
291
+ );
292
+ await this.camera.disable();
293
+ if (this.publisher.isPublishing(TrackType.VIDEO)) {
294
+ this.stopPublish(TrackType.VIDEO);
295
+ }
296
+ }
297
+ if (
298
+ !p?.publishedTracks.includes(TrackType.AUDIO) &&
299
+ this.publisher?.isPublishing(TrackType.AUDIO)
300
+ ) {
301
+ this.logger(
302
+ 'info',
303
+ `Local participant's audio track is muted remotely`,
304
+ );
305
+ await this.microphone.disable();
306
+ if (this.publisher.isPublishing(TrackType.AUDIO)) {
307
+ this.stopPublish(TrackType.AUDIO);
308
+ }
309
+ }
310
+ });
311
+ this.speaker = new SpeakerManager();
272
312
  }
273
313
 
274
314
  private registerEffects() {
@@ -300,10 +340,50 @@ export class Call {
300
340
  const hasPermission = this.permissionsContext.hasPermission(
301
341
  permission as OwnCapability,
302
342
  );
303
- if (!hasPermission && this.publisher.isPublishing(trackType)) {
304
- this.stopPublish(trackType).catch((err) => {
305
- this.logger('error', `Error stopping publish ${trackType}`, err);
306
- });
343
+ if (
344
+ !hasPermission &&
345
+ (this.publisher.isPublishing(trackType) ||
346
+ this.publisher.isLive(trackType))
347
+ ) {
348
+ // Stop tracks, then notify device manager
349
+ this.stopPublish(trackType)
350
+ .catch((err) => {
351
+ this.logger(
352
+ 'error',
353
+ `Error stopping publish ${trackType}`,
354
+ err,
355
+ );
356
+ })
357
+ .then(() => {
358
+ if (
359
+ trackType === TrackType.VIDEO &&
360
+ this.camera.state.status === 'enabled'
361
+ ) {
362
+ this.camera
363
+ .disable()
364
+ .catch((err) =>
365
+ this.logger(
366
+ 'error',
367
+ `Error disabling camera after pemission revoked`,
368
+ err,
369
+ ),
370
+ );
371
+ }
372
+ if (
373
+ trackType === TrackType.AUDIO &&
374
+ this.microphone.state.status === 'enabled'
375
+ ) {
376
+ this.microphone
377
+ .disable()
378
+ .catch((err) =>
379
+ this.logger(
380
+ 'error',
381
+ `Error disabling microphone after pemission revoked`,
382
+ err,
383
+ ),
384
+ );
385
+ }
386
+ });
307
387
  }
308
388
  }
309
389
  }),
@@ -1103,7 +1183,10 @@ export class Call {
1103
1183
  * @param stopTrack if `true` the track will be stopped, else it will be just disabled
1104
1184
  */
1105
1185
  stopPublish = async (trackType: TrackType, stopTrack: boolean = true) => {
1106
- this.logger('info', `stopPublish ${TrackType[trackType]}`);
1186
+ this.logger(
1187
+ 'info',
1188
+ `stopPublish ${TrackType[trackType]}, stop tracks: ${stopTrack}`,
1189
+ );
1107
1190
  await this.publisher?.unpublishStream(trackType, stopTrack);
1108
1191
  };
1109
1192
 
@@ -1230,6 +1313,8 @@ export class Call {
1230
1313
  *
1231
1314
  *
1232
1315
  * @param deviceId the selected device, `undefined` means the user wants to use the system's default audio output
1316
+ *
1317
+ * @deprecated use `call.speaker` instead
1233
1318
  */
1234
1319
  setAudioOutputDevice = (deviceId?: string) => {
1235
1320
  if (!this.sfuClient) return;
@@ -396,5 +396,9 @@ const retryable = async <I extends object, O extends SfuResponseWithError>(
396
396
  retryAttempt < MAX_RETRIES
397
397
  );
398
398
 
399
+ if (rpcCallResult.response.error) {
400
+ throw rpcCallResult.response.error;
401
+ }
402
+
399
403
  return rpcCallResult;
400
404
  };
@@ -5,7 +5,7 @@ const HINT_URL = `https://hint.stream-io-video.com/`;
5
5
 
6
6
  export const getLocationHint = async (
7
7
  hintUrl: string = HINT_URL,
8
- timeout: number = 1500,
8
+ timeout: number = 2000,
9
9
  ) => {
10
10
  const abortController = new AbortController();
11
11
  const timeoutId = setTimeout(() => abortController.abort(), timeout);
@@ -18,7 +18,7 @@ export const getLocationHint = async (
18
18
  logger('debug', `Location header: ${awsPop}`);
19
19
  return awsPop.substring(0, 3); // AMS1-P2 -> AMS
20
20
  } catch (e) {
21
- logger('error', `Failed to get location hint from ${HINT_URL}`, e);
21
+ logger('warn', `Failed to get location hint from ${HINT_URL}`, e);
22
22
  return 'ERR';
23
23
  } finally {
24
24
  clearTimeout(timeoutId);
@@ -12,7 +12,7 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
12
12
  };
13
13
 
14
14
  constructor(call: Call) {
15
- super(call, new CameraManagerState());
15
+ super(call, new CameraManagerState(), TrackType.VIDEO);
16
16
  }
17
17
 
18
18
  /**
@@ -59,6 +59,10 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
59
59
  height !== this.targetResolution.height
60
60
  )
61
61
  await this.applySettingsToStream();
62
+ this.logger(
63
+ 'debug',
64
+ `${width}x${height} target resolution applied to media stream`,
65
+ );
62
66
  }
63
67
  }
64
68
 
@@ -85,12 +89,7 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
85
89
  return this.call.stopPublish(TrackType.VIDEO, stopTracks);
86
90
  }
87
91
 
88
- protected muteTracks(): void {
89
- this.state.mediaStream
90
- ?.getVideoTracks()
91
- .forEach((t) => (t.enabled = false));
92
- }
93
- protected unmuteTracks(): void {
94
- this.state.mediaStream?.getVideoTracks().forEach((t) => (t.enabled = true));
92
+ protected getTrack() {
93
+ return this.state.mediaStream?.getVideoTracks()[0];
95
94
  }
96
95
  }
@@ -2,8 +2,10 @@ import { Observable } from 'rxjs';
2
2
  import { Call } from '../Call';
3
3
  import { CallingState } from '../store';
4
4
  import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
5
- import { disposeOfMediaStream } from './devices';
6
5
  import { isReactNative } from '../helpers/platforms';
6
+ import { Logger } from '../coordinator/connection/types';
7
+ import { getLogger } from '../logger';
8
+ import { TrackType } from '../gen/video/sfu/models/models';
7
9
 
8
10
  export abstract class InputMediaDeviceManager<
9
11
  T extends InputMediaDeviceManagerState,
@@ -16,7 +18,15 @@ export abstract class InputMediaDeviceManager<
16
18
  * @internal
17
19
  */
18
20
  disablePromise?: Promise<void>;
19
- constructor(protected readonly call: Call, public readonly state: T) {}
21
+ logger: Logger;
22
+
23
+ constructor(
24
+ protected readonly call: Call,
25
+ public readonly state: T,
26
+ protected readonly trackType: TrackType,
27
+ ) {
28
+ this.logger = getLogger([`${TrackType[trackType].toLowerCase()} manager`]);
29
+ }
20
30
 
21
31
  /**
22
32
  * Lists the available audio/video devices
@@ -129,32 +139,68 @@ export abstract class InputMediaDeviceManager<
129
139
 
130
140
  protected abstract stopPublishStream(stopTracks: boolean): Promise<void>;
131
141
 
132
- protected abstract muteTracks(): void;
133
-
134
- protected abstract unmuteTracks(): void;
142
+ protected abstract getTrack(): undefined | MediaStreamTrack;
135
143
 
136
144
  private async muteStream(stopTracks: boolean = true) {
137
145
  if (!this.state.mediaStream) {
138
146
  return;
139
147
  }
148
+ this.logger('debug', `${stopTracks ? 'Stopping' : 'Disabling'} stream`);
140
149
  if (this.call.state.callingState === CallingState.JOINED) {
141
150
  await this.stopPublishStream(stopTracks);
142
- } else if (this.state.mediaStream) {
143
- stopTracks
144
- ? disposeOfMediaStream(this.state.mediaStream)
145
- : this.muteTracks();
146
151
  }
147
- if (stopTracks) {
152
+ this.muteLocalStream(stopTracks);
153
+ if (this.getTrack()?.readyState === 'ended') {
154
+ // @ts-expect-error release() is present in react-native-webrtc and must be called to dispose the stream
155
+ if (typeof this.state.mediaStream.release === 'function') {
156
+ // @ts-expect-error
157
+ this.state.mediaStream.release();
158
+ }
148
159
  this.state.setMediaStream(undefined);
149
160
  }
150
161
  }
151
162
 
163
+ private muteTrack() {
164
+ const track = this.getTrack();
165
+ if (!track || !track.enabled) {
166
+ return;
167
+ }
168
+ track.enabled = false;
169
+ }
170
+
171
+ private unmuteTrack() {
172
+ const track = this.getTrack();
173
+ if (!track || track.enabled) {
174
+ return;
175
+ }
176
+ track.enabled = true;
177
+ }
178
+
179
+ private stopTrack() {
180
+ const track = this.getTrack();
181
+ if (!track || track.readyState === 'ended') {
182
+ return;
183
+ }
184
+ track.stop();
185
+ }
186
+
187
+ private muteLocalStream(stopTracks: boolean) {
188
+ if (!this.state.mediaStream) {
189
+ return;
190
+ }
191
+ stopTracks ? this.stopTrack() : this.muteTrack();
192
+ }
193
+
152
194
  private async unmuteStream() {
195
+ this.logger('debug', 'Starting stream');
153
196
  let stream: MediaStream;
154
- if (this.state.mediaStream) {
197
+ if (this.state.mediaStream && this.getTrack()?.readyState === 'live') {
155
198
  stream = this.state.mediaStream;
156
- this.unmuteTracks();
199
+ this.unmuteTrack();
157
200
  } else {
201
+ if (this.state.mediaStream) {
202
+ this.stopTrack();
203
+ }
158
204
  const constraints = { deviceId: this.state.selectedDevice };
159
205
  stream = await this.getStream(constraints);
160
206
  }
@@ -7,7 +7,7 @@ import { TrackType } from '../gen/video/sfu/models/models';
7
7
 
8
8
  export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManagerState> {
9
9
  constructor(call: Call) {
10
- super(call, new MicrophoneManagerState());
10
+ super(call, new MicrophoneManagerState(), TrackType.AUDIO);
11
11
  }
12
12
 
13
13
  protected getDevices(): Observable<MediaDeviceInfo[]> {
@@ -25,12 +25,7 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
25
25
  return this.call.stopPublish(TrackType.AUDIO, stopTracks);
26
26
  }
27
27
 
28
- protected muteTracks(): void {
29
- this.state.mediaStream
30
- ?.getAudioTracks()
31
- .forEach((t) => (t.enabled = false));
32
- }
33
- protected unmuteTracks(): void {
34
- this.state.mediaStream?.getAudioTracks().forEach((t) => (t.enabled = true));
28
+ protected getTrack() {
29
+ return this.state.mediaStream?.getAudioTracks()[0];
35
30
  }
36
31
  }
@@ -0,0 +1,50 @@
1
+ import { isReactNative } from '../helpers/platforms';
2
+ import { SpeakerState } from './SpeakerState';
3
+ import { getAudioOutputDevices } from './devices';
4
+
5
+ export class SpeakerManager {
6
+ public readonly state = new SpeakerState();
7
+
8
+ constructor() {}
9
+
10
+ /**
11
+ * Lists the available audio output devices
12
+ *
13
+ * Note: It prompts the user for a permission to use devices (if not already granted)
14
+ *
15
+ * @returns an Observable that will be updated if a device is connected or disconnected
16
+ */
17
+ listDevices() {
18
+ return getAudioOutputDevices();
19
+ }
20
+
21
+ /**
22
+ * Select device
23
+ *
24
+ * Note: this method is not supported in React Native
25
+ *
26
+ * @param deviceId empty string means the system default
27
+ */
28
+ select(deviceId: string) {
29
+ if (isReactNative()) {
30
+ throw new Error('This feature is not supported in React Native');
31
+ }
32
+ this.state.setDevice(deviceId);
33
+ }
34
+
35
+ /**
36
+ * Set the volume of the audio elements
37
+ * @param volume a number between 0 and 1
38
+ *
39
+ * Note: this method is not supported in React Native
40
+ */
41
+ setVolume(volume: number) {
42
+ if (isReactNative()) {
43
+ throw new Error('This feature is not supported in React Native');
44
+ }
45
+ if (volume && (volume < 0 || volume > 1)) {
46
+ throw new Error('Volume must be between 0 and 1');
47
+ }
48
+ this.state.setVolume(volume);
49
+ }
50
+ }