@stream-io/video-client 0.3.12 → 0.3.13

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.
@@ -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
  /**
@@ -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
+ }
@@ -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.13";
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.13",
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,7 @@ export class Call {
269
277
 
270
278
  this.camera = new CameraManager(this);
271
279
  this.microphone = new MicrophoneManager(this);
280
+ this.speaker = new SpeakerManager();
272
281
  }
273
282
 
274
283
  private registerEffects() {
@@ -1230,6 +1239,8 @@ export class Call {
1230
1239
  *
1231
1240
  *
1232
1241
  * @param deviceId the selected device, `undefined` means the user wants to use the system's default audio output
1242
+ *
1243
+ * @deprecated use `call.speaker` instead
1233
1244
  */
1234
1245
  setAudioOutputDevice = (deviceId?: string) => {
1235
1246
  if (!this.sfuClient) return;
@@ -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
+ }
@@ -0,0 +1,90 @@
1
+ import { BehaviorSubject, Observable, distinctUntilChanged } from 'rxjs';
2
+ import { RxUtils } from '../store';
3
+ import { checkIfAudioOutputChangeSupported } from './devices';
4
+
5
+ export class SpeakerState {
6
+ protected selectedDeviceSubject = new BehaviorSubject<string>('');
7
+ protected volumeSubject = new BehaviorSubject<number>(1);
8
+ /**
9
+ * [Tells if the browser supports audio output change on 'audio' elements](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId).
10
+ */
11
+ readonly isDeviceSelectionSupported = checkIfAudioOutputChangeSupported();
12
+
13
+ /**
14
+ * An Observable that emits the currently selected device
15
+ *
16
+ * Note: this feature is not supported in React Native
17
+ */
18
+ selectedDevice$: Observable<string>;
19
+
20
+ /**
21
+ * An Observable that emits the currently selected volume
22
+ *
23
+ * Note: this feature is not supported in React Native
24
+ */
25
+ volume$: Observable<number>;
26
+
27
+ constructor() {
28
+ this.selectedDevice$ = this.selectedDeviceSubject
29
+ .asObservable()
30
+ .pipe(distinctUntilChanged());
31
+ this.volume$ = this.volumeSubject
32
+ .asObservable()
33
+ .pipe(distinctUntilChanged());
34
+ }
35
+
36
+ /**
37
+ * The currently selected device
38
+ *
39
+ * Note: this feature is not supported in React Native
40
+ */
41
+ get selectedDevice() {
42
+ return this.getCurrentValue(this.selectedDevice$);
43
+ }
44
+
45
+ /**
46
+ * The currently selected volume
47
+ *
48
+ * Note: this feature is not supported in React Native
49
+ */
50
+ get volume() {
51
+ return this.getCurrentValue(this.volume$);
52
+ }
53
+
54
+ /**
55
+ * Gets the current value of an observable, or undefined if the observable has
56
+ * not emitted a value yet.
57
+ *
58
+ * @param observable$ the observable to get the value from.
59
+ */
60
+ getCurrentValue = RxUtils.getCurrentValue;
61
+
62
+ /**
63
+ * @internal
64
+ * @param deviceId
65
+ */
66
+ setDevice(deviceId: string) {
67
+ this.setCurrentValue(this.selectedDeviceSubject, deviceId);
68
+ }
69
+
70
+ /**
71
+ * @internal
72
+ * @param volume
73
+ */
74
+ setVolume(volume: number) {
75
+ this.setCurrentValue(this.volumeSubject, volume);
76
+ }
77
+
78
+ /**
79
+ * Updates the value of the provided Subject.
80
+ * An `update` can either be a new value or a function which takes
81
+ * the current value and returns a new value.
82
+ *
83
+ * @internal
84
+ *
85
+ * @param subject the subject to update.
86
+ * @param update the update to apply to the subject.
87
+ * @return the updated value.
88
+ */
89
+ protected setCurrentValue = RxUtils.setCurrentValue;
90
+ }
@@ -0,0 +1,66 @@
1
+ import { afterEach, beforeEach, describe, vi, it, expect } from 'vitest';
2
+ import { mockAudioDevices } from './mocks';
3
+ import { of } from 'rxjs';
4
+ import { SpeakerManager } from '../SpeakerManager';
5
+ import { checkIfAudioOutputChangeSupported } from '../devices';
6
+
7
+ vi.mock('../devices.ts', () => {
8
+ console.log('MOCKING devices');
9
+ return {
10
+ getAudioOutputDevices: vi.fn(() => of(mockAudioDevices)),
11
+ checkIfAudioOutputChangeSupported: vi.fn(() => true),
12
+ };
13
+ });
14
+
15
+ describe('SpeakerManager.test', () => {
16
+ let manager: SpeakerManager;
17
+
18
+ beforeEach(() => {
19
+ manager = new SpeakerManager();
20
+ });
21
+
22
+ it('list devices', () => {
23
+ const spy = vi.fn();
24
+ manager.listDevices().subscribe(spy);
25
+
26
+ expect(spy).toHaveBeenCalledWith(mockAudioDevices);
27
+ });
28
+
29
+ it('tell is browser supports audio output selection', async () => {
30
+ expect(checkIfAudioOutputChangeSupported).toHaveBeenCalled();
31
+ expect(manager.state.isDeviceSelectionSupported).toBe(true);
32
+ });
33
+
34
+ it('select', async () => {
35
+ expect(manager.state.selectedDevice).toBe('');
36
+
37
+ manager.select('new-device');
38
+
39
+ expect(manager.state.selectedDevice).toBe('new-device');
40
+ });
41
+
42
+ it('set volume', async () => {
43
+ expect(manager.state.volume).toBe(1);
44
+
45
+ expect(() => manager.setVolume(2)).toThrowError();
46
+
47
+ expect(manager.state.volume).toBe(1);
48
+
49
+ manager.setVolume(0);
50
+
51
+ expect(manager.state.volume).toBe(0);
52
+
53
+ manager.setVolume(1);
54
+
55
+ expect(manager.state.volume).toBe(1);
56
+
57
+ manager.setVolume(0.5);
58
+
59
+ expect(manager.state.volume).toBe(0.5);
60
+ });
61
+
62
+ afterEach(() => {
63
+ vi.clearAllMocks();
64
+ vi.resetModules();
65
+ });
66
+ });
@@ -6,8 +6,13 @@ import {
6
6
  VideoTrackType,
7
7
  VisibilityState,
8
8
  } from '../types';
9
- import { TrackType, VideoDimension } from '../gen/video/sfu/models/models';
10
9
  import {
10
+ SdkType,
11
+ TrackType,
12
+ VideoDimension,
13
+ } from '../gen/video/sfu/models/models';
14
+ import {
15
+ combineLatest,
11
16
  distinctUntilChanged,
12
17
  distinctUntilKeyChanged,
13
18
  map,
@@ -15,6 +20,7 @@ import {
15
20
  } from 'rxjs';
16
21
  import { ViewportTracker } from './ViewportTracker';
17
22
  import { getLogger } from '../logger';
23
+ import { getSdkInfo } from '../client-details';
18
24
 
19
25
  const DEFAULT_VIEWPORT_VISIBILITY_STATE: Record<
20
26
  VideoTrackType,
@@ -326,12 +332,23 @@ export class DynascaleManager {
326
332
  });
327
333
  });
328
334
 
329
- const sinkIdSubscription = this.call.state.localParticipant$.subscribe(
330
- (p) => {
331
- if (p && p.audioOutputDeviceId && 'setSinkId' in audioElement) {
332
- // @ts-expect-error setSinkId is not yet in the lib
333
- audioElement.setSinkId(p.audioOutputDeviceId);
334
- }
335
+ const sinkIdSubscription = combineLatest([
336
+ this.call.state.localParticipant$,
337
+ this.call.speaker.state.selectedDevice$,
338
+ ]).subscribe(([p, selectedDevice]) => {
339
+ const deviceId =
340
+ getSdkInfo()?.type === SdkType.REACT
341
+ ? p?.audioOutputDeviceId
342
+ : selectedDevice;
343
+ if ('setSinkId' in audioElement) {
344
+ // @ts-expect-error setSinkId is not yet in the lib
345
+ audioElement.setSinkId(deviceId);
346
+ }
347
+ });
348
+
349
+ const volumeSubscription = this.call.speaker.state.volume$.subscribe(
350
+ (volume) => {
351
+ audioElement.volume = volume;
335
352
  },
336
353
  );
337
354
 
@@ -339,6 +356,7 @@ export class DynascaleManager {
339
356
 
340
357
  return () => {
341
358
  sinkIdSubscription.unsubscribe();
359
+ volumeSubscription.unsubscribe();
342
360
  updateMediaStreamSubscription.unsubscribe();
343
361
  };
344
362
  };
@@ -4,14 +4,21 @@
4
4
 
5
5
  import '../../rtc/__tests__/mocks/webrtc.mocks';
6
6
 
7
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
+ import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
8
8
  import { DynascaleManager } from '../DynascaleManager';
9
9
  import { Call } from '../../Call';
10
10
  import { StreamClient } from '../../coordinator/connection/client';
11
11
  import { StreamVideoWriteableStateStore } from '../../store';
12
12
  import { DebounceType, VisibilityState } from '../../types';
13
13
  import { noopComparator } from '../../sorting';
14
- import { TrackType } from '../../gen/video/sfu/models/models';
14
+ import { SdkType, TrackType } from '../../gen/video/sfu/models/models';
15
+ import { getSdkInfo } from '../../client-details';
16
+
17
+ vi.mock('../../client-details.ts', () => {
18
+ return {
19
+ getSdkInfo: vi.fn(),
20
+ };
21
+ });
15
22
 
16
23
  describe('DynascaleManager', () => {
17
24
  let dynascaleManager: DynascaleManager;
@@ -131,6 +138,23 @@ describe('DynascaleManager', () => {
131
138
  expect(play).toHaveBeenCalled();
132
139
  expect(audioElement.srcObject).toBe(mediaStream);
133
140
 
141
+ expect(audioElement.volume).toBe(1);
142
+
143
+ // @ts-expect-error setSinkId is not defined in types
144
+ expect(audioElement.setSinkId).toHaveBeenCalledWith('');
145
+
146
+ call.speaker.select('different-device-id');
147
+
148
+ // @ts-expect-error setSinkId is not defined in types
149
+ expect(audioElement.setSinkId).toHaveBeenCalledWith(
150
+ 'different-device-id',
151
+ );
152
+
153
+ const mock = getSdkInfo as Mock;
154
+ mock.mockImplementation(() => ({
155
+ type: SdkType.REACT,
156
+ }));
157
+
134
158
  call.state.updateParticipant('session-id-local', {
135
159
  audioOutputDeviceId: 'new-device-id',
136
160
  });
@@ -138,6 +162,10 @@ describe('DynascaleManager', () => {
138
162
  // @ts-expect-error setSinkId is not defined in types
139
163
  expect(audioElement.setSinkId).toHaveBeenCalledWith('new-device-id');
140
164
 
165
+ call.speaker.setVolume(0.5);
166
+
167
+ expect(audioElement.volume).toBe(0.5);
168
+
141
169
  cleanup?.();
142
170
  });
143
171
 
package/src/types.ts CHANGED
@@ -103,6 +103,8 @@ export interface StreamVideoLocalParticipant extends StreamVideoParticipant {
103
103
  * 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))
104
104
  *
105
105
  * If the value is not defined, the user hasn't selected any device (in these cases the default system audio output could be used)
106
+ *
107
+ * @deprecated use call.speaker.state.selectedDevice
106
108
  */
107
109
  audioOutputDeviceId?: string;
108
110
  }