@stream-io/video-client 0.3.6 → 0.3.7

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.
@@ -3,6 +3,7 @@ import { Call } from '../Call';
3
3
  import { CameraDirection, CameraManagerState } from './CameraManagerState';
4
4
  import { InputMediaDeviceManager } from './InputMediaDeviceManager';
5
5
  export declare class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
6
+ private targetResolution;
6
7
  constructor(call: Call);
7
8
  /**
8
9
  * Select the camera direaction
@@ -16,6 +17,13 @@ export declare class CameraManager extends InputMediaDeviceManager<CameraManager
16
17
  * @returns
17
18
  */
18
19
  flip(): Promise<void>;
20
+ /**
21
+ * @internal
22
+ */
23
+ selectTargetResolution(resolution: {
24
+ width: number;
25
+ height: number;
26
+ }): Promise<void>;
19
27
  protected getDevices(): Observable<MediaDeviceInfo[]>;
20
28
  protected getStream(constraints: MediaTrackConstraints): Promise<MediaStream>;
21
29
  protected publishStream(stream: MediaStream): Promise<void>;
@@ -4,6 +4,14 @@ import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
4
4
  export declare abstract class InputMediaDeviceManager<T extends InputMediaDeviceManagerState> {
5
5
  protected readonly call: Call;
6
6
  readonly state: T;
7
+ /**
8
+ * @internal
9
+ */
10
+ enablePromise?: Promise<void>;
11
+ /**
12
+ * @internal
13
+ */
14
+ disablePromise?: Promise<void>;
7
15
  constructor(call: Call, state: T);
8
16
  /**
9
17
  * Lists the available audio/video devices
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "0.3.6";
1
+ export declare const version = "0.3.7";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
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
@@ -1718,6 +1718,14 @@ export class Call {
1718
1718
  };
1719
1719
 
1720
1720
  private async initCamera() {
1721
+ // Wait for any in progress camera operation
1722
+ if (this.camera.enablePromise) {
1723
+ await this.camera.enablePromise;
1724
+ }
1725
+ if (this.camera.disablePromise) {
1726
+ await this.camera.disablePromise;
1727
+ }
1728
+
1721
1729
  if (
1722
1730
  this.state.localParticipant?.videoStream ||
1723
1731
  !this.permissionsContext.hasPermission('send-video')
@@ -1726,27 +1734,31 @@ export class Call {
1726
1734
  }
1727
1735
 
1728
1736
  // Set camera direction if it's not yet set
1729
- // This will also start publishing if camera is enabled
1730
1737
  if (!this.camera.state.direction && !this.camera.state.selectedDevice) {
1731
1738
  let defaultDirection: CameraDirection = 'front';
1732
1739
  const backendSetting = this.state.settings?.video.camera_facing;
1733
1740
  if (backendSetting) {
1734
1741
  defaultDirection = backendSetting === 'front' ? 'front' : 'back';
1735
1742
  }
1736
- this.camera.selectDirection(defaultDirection);
1737
- } else if (this.camera.state.status === 'enabled') {
1738
- // Publish already started media streams (this is the case if there is a lobby screen before join)
1739
- // Wait for media stream
1740
- this.camera.state.mediaStream$
1741
- .pipe(takeWhile((s) => s === undefined, true))
1742
- .subscribe((stream) => {
1743
- if (!this.state.localParticipant?.videoStream) {
1744
- this.publishVideoStream(stream!);
1745
- }
1746
- });
1743
+ this.camera.state.setDirection(defaultDirection);
1744
+ }
1745
+
1746
+ // Set target resolution
1747
+ const targetResolution = this.state.settings?.video.target_resolution;
1748
+ if (targetResolution) {
1749
+ await this.camera.selectTargetResolution(targetResolution);
1750
+ }
1751
+
1752
+ // Publish already that was set before we joined
1753
+ if (
1754
+ this.camera.state.status === 'enabled' &&
1755
+ this.camera.state.mediaStream &&
1756
+ !this.publisher?.isPublishing(TrackType.VIDEO)
1757
+ ) {
1758
+ await this.publishVideoStream(this.camera.state.mediaStream);
1747
1759
  }
1748
1760
 
1749
- // Apply backend config (this is the case if there is no lobby screen before join)
1761
+ // Start camera if backend config speicifies, and there is no local setting
1750
1762
  if (
1751
1763
  this.camera.state.status === undefined &&
1752
1764
  this.state.settings?.video.camera_default_on
@@ -1756,6 +1768,14 @@ export class Call {
1756
1768
  }
1757
1769
 
1758
1770
  private async initMic() {
1771
+ // Wait for any in progress mic operation
1772
+ if (this.microphone.enablePromise) {
1773
+ await this.microphone.enablePromise;
1774
+ }
1775
+ if (this.microphone.disablePromise) {
1776
+ await this.microphone.disablePromise;
1777
+ }
1778
+
1759
1779
  if (
1760
1780
  this.state.localParticipant?.audioStream ||
1761
1781
  !this.permissionsContext.hasPermission('send-audio')
@@ -1763,19 +1783,16 @@ export class Call {
1763
1783
  return;
1764
1784
  }
1765
1785
 
1766
- // Publish already started media streams (this is the case if there is a lobby screen before join)
1767
- if (this.microphone.state.status === 'enabled') {
1768
- // Wait for media stream
1769
- this.microphone.state.mediaStream$
1770
- .pipe(takeWhile((s) => s === undefined, true))
1771
- .subscribe((stream) => {
1772
- if (!this.state.localParticipant?.audioStream) {
1773
- this.publishAudioStream(stream!);
1774
- }
1775
- });
1786
+ // Publish media stream that was set before we joined
1787
+ if (
1788
+ this.microphone.state.status === 'enabled' &&
1789
+ this.microphone.state.mediaStream &&
1790
+ !this.publisher?.isPublishing(TrackType.AUDIO)
1791
+ ) {
1792
+ this.publishAudioStream(this.microphone.state.mediaStream);
1776
1793
  }
1777
1794
 
1778
- // Apply backend config (this is the case if there is no lobby screen before join)
1795
+ // Start mic if backend config speicifies, and there is no local setting
1779
1796
  if (
1780
1797
  this.microphone.state.status === undefined &&
1781
1798
  this.state.settings?.audio.mic_default_on
@@ -6,6 +6,11 @@ import { getVideoDevices, getVideoStream } from './devices';
6
6
  import { TrackType } from '../gen/video/sfu/models/models';
7
7
 
8
8
  export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
9
+ private targetResolution = {
10
+ width: 1280,
11
+ height: 720,
12
+ };
13
+
9
14
  constructor(call: Call) {
10
15
  super(call, new CameraManagerState());
11
16
  }
@@ -32,12 +37,39 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
32
37
  this.selectDirection(newDirection);
33
38
  }
34
39
 
40
+ /**
41
+ * @internal
42
+ */
43
+ async selectTargetResolution(resolution: { width: number; height: number }) {
44
+ this.targetResolution.height = resolution.height;
45
+ this.targetResolution.width = resolution.width;
46
+ if (this.enablePromise) {
47
+ try {
48
+ await this.enablePromise;
49
+ } catch (error) {
50
+ // couldn't enable device, target resolution will be applied the next time user attempts to start the device
51
+ }
52
+ }
53
+ if (this.state.status === 'enabled') {
54
+ const { width, height } = this.state
55
+ .mediaStream!.getVideoTracks()[0]
56
+ ?.getSettings();
57
+ if (
58
+ width !== this.targetResolution.width ||
59
+ height !== this.targetResolution.height
60
+ )
61
+ await this.applySettingsToStream();
62
+ }
63
+ }
64
+
35
65
  protected getDevices(): Observable<MediaDeviceInfo[]> {
36
66
  return getVideoDevices();
37
67
  }
38
68
  protected getStream(
39
69
  constraints: MediaTrackConstraints,
40
70
  ): Promise<MediaStream> {
71
+ constraints.width = this.targetResolution.width;
72
+ constraints.height = this.targetResolution.height;
41
73
  // We can't set both device id and facing mode
42
74
  // Device id has higher priority
43
75
  if (!constraints.deviceId && this.state.direction) {
@@ -8,6 +8,14 @@ import { isReactNative } from '../helpers/platforms';
8
8
  export abstract class InputMediaDeviceManager<
9
9
  T extends InputMediaDeviceManagerState,
10
10
  > {
11
+ /**
12
+ * @internal
13
+ */
14
+ enablePromise?: Promise<void>;
15
+ /**
16
+ * @internal
17
+ */
18
+ disablePromise?: Promise<void>;
11
19
  constructor(protected readonly call: Call, public readonly state: T) {}
12
20
 
13
21
  /**
@@ -28,8 +36,14 @@ export abstract class InputMediaDeviceManager<
28
36
  if (this.state.status === 'enabled') {
29
37
  return;
30
38
  }
31
- await this.unmuteStream();
32
- this.state.setStatus('enabled');
39
+ this.enablePromise = this.unmuteStream();
40
+ try {
41
+ await this.enablePromise;
42
+ this.state.setStatus('enabled');
43
+ } catch (error) {
44
+ this.enablePromise = undefined;
45
+ throw error;
46
+ }
33
47
  }
34
48
 
35
49
  /**
@@ -42,8 +56,17 @@ export abstract class InputMediaDeviceManager<
42
56
  if (this.state.status === 'disabled') {
43
57
  return;
44
58
  }
45
- await this.muteStream(this.state.disableMode === 'stop-tracks');
46
- this.state.setStatus('disabled');
59
+ this.disablePromise = this.muteStream(
60
+ this.state.disableMode === 'stop-tracks',
61
+ );
62
+ try {
63
+ await this.disablePromise;
64
+ this.state.setStatus('disabled');
65
+ this.disablePromise = undefined;
66
+ } catch (error) {
67
+ this.disablePromise = undefined;
68
+ throw error;
69
+ }
47
70
  }
48
71
 
49
72
  /**
@@ -53,6 +53,8 @@ describe('CameraManager', () => {
53
53
 
54
54
  expect(getVideoStream).toHaveBeenCalledWith({
55
55
  deviceId: undefined,
56
+ width: 1280,
57
+ height: 720,
56
58
  });
57
59
  });
58
60
 
@@ -101,12 +103,18 @@ describe('CameraManager', () => {
101
103
 
102
104
  await manager.enable();
103
105
 
104
- expect(getVideoStream).toHaveBeenCalledWith({ deviceId: undefined });
106
+ expect(getVideoStream).toHaveBeenCalledWith({
107
+ deviceId: undefined,
108
+ width: 1280,
109
+ height: 720,
110
+ });
105
111
 
106
112
  await manager.selectDirection('front');
107
113
 
108
114
  expect(getVideoStream).toHaveBeenCalledWith({
109
115
  deviceId: undefined,
116
+ width: 1280,
117
+ height: 720,
110
118
  facingMode: 'user',
111
119
  });
112
120
 
@@ -115,6 +123,8 @@ describe('CameraManager', () => {
115
123
  expect(getVideoStream).toHaveBeenCalledWith({
116
124
  deviceId: undefined,
117
125
  facingMode: 'environment',
126
+ width: 1280,
127
+ height: 720,
118
128
  });
119
129
  });
120
130
 
@@ -123,16 +133,57 @@ describe('CameraManager', () => {
123
133
 
124
134
  await manager.flip();
125
135
 
126
- expect(getVideoStream).toHaveBeenCalledWith({ facingMode: 'environment' });
136
+ expect(getVideoStream).toHaveBeenCalledWith({
137
+ facingMode: 'environment',
138
+ width: 1280,
139
+ height: 720,
140
+ });
127
141
 
128
142
  const deviceId = mockVideoDevices[1].deviceId;
129
143
  await manager.select(deviceId);
130
144
 
131
145
  expect((getVideoStream as Mock).mock.lastCall[0]).toEqual({
132
146
  deviceId,
147
+ width: 1280,
148
+ height: 720,
149
+ });
150
+ });
151
+
152
+ it(`should set target resolution, but shouldn't change device status`, async () => {
153
+ manager['targetResolution'] = { width: 640, height: 480 };
154
+
155
+ expect(manager.state.status).toBe(undefined);
156
+
157
+ await manager.selectTargetResolution({ width: 1280, height: 720 });
158
+
159
+ const targetResolution = manager['targetResolution'];
160
+
161
+ expect(targetResolution.width).toBe(1280);
162
+ expect(targetResolution.height).toBe(720);
163
+ expect(manager.state.status).toBe(undefined);
164
+ });
165
+
166
+ it('should apply target resolution to existing media stream track', async () => {
167
+ await manager.enable();
168
+ await manager.selectTargetResolution({ width: 640, height: 480 });
169
+
170
+ expect((getVideoStream as Mock).mock.lastCall[0]).toEqual({
171
+ deviceId: mockVideoDevices[0].deviceId,
172
+ width: 640,
173
+ height: 480,
133
174
  });
134
175
  });
135
176
 
177
+ it(`should do nothing if existing track has the correct resolution`, async () => {
178
+ await manager.enable();
179
+
180
+ expect(getVideoStream).toHaveBeenCalledOnce();
181
+
182
+ await manager.selectTargetResolution({ width: 1280, height: 720 });
183
+
184
+ expect(getVideoStream).toHaveBeenCalledOnce();
185
+ });
186
+
136
187
  afterEach(() => {
137
188
  vi.clearAllMocks();
138
189
  vi.resetModules();
@@ -89,6 +89,8 @@ export const mockVideoStream = () => {
89
89
  const track = {
90
90
  getSettings: () => ({
91
91
  deviceId: mockVideoDevices[0].deviceId,
92
+ width: 1280,
93
+ height: 720,
92
94
  }),
93
95
  enabled: true,
94
96
  };