@stream-io/video-client 1.40.0 → 1.40.1
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/CHANGELOG.md +6 -0
- package/dist/index.browser.es.js +48 -46
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +48 -46
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +48 -46
- package/dist/index.es.js.map +1 -1
- package/dist/src/devices/DeviceManager.d.ts +13 -0
- package/package.json +1 -1
- package/src/devices/CameraManager.ts +26 -38
- package/src/devices/DeviceManager.ts +14 -0
- package/src/devices/MicrophoneManager.ts +10 -14
- package/src/devices/__tests__/CameraManager.test.ts +6 -6
- package/src/devices/__tests__/MicrophoneManager.test.ts +2 -9
|
@@ -12,6 +12,19 @@ export declare abstract class DeviceManager<S extends DeviceManagerState<C>, C =
|
|
|
12
12
|
stopOnLeave: boolean;
|
|
13
13
|
logger: ScopedLogger;
|
|
14
14
|
state: S;
|
|
15
|
+
/**
|
|
16
|
+
* When `true`, the `apply()` method will skip automatically enabling/disabling
|
|
17
|
+
* the device based on server defaults (`mic_default_on`, `camera_default_on`).
|
|
18
|
+
*
|
|
19
|
+
* This is useful when application code wants to handle device preferences
|
|
20
|
+
* (e.g., persisted user preferences) and prevent server defaults from
|
|
21
|
+
* overriding them.
|
|
22
|
+
*
|
|
23
|
+
* @default false
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
deferServerDefaults: boolean;
|
|
15
28
|
protected readonly call: Call;
|
|
16
29
|
protected readonly trackType: TrackType;
|
|
17
30
|
protected subscriptions: Function[];
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ import { Call } from '../Call';
|
|
|
3
3
|
import { CameraDirection, CameraManagerState } from './CameraManagerState';
|
|
4
4
|
import { DeviceManager } from './DeviceManager';
|
|
5
5
|
import { getVideoDevices, getVideoStream } from './devices';
|
|
6
|
-
import {
|
|
6
|
+
import { VideoSettingsResponse } from '../gen/coordinator';
|
|
7
7
|
import { TrackType } from '../gen/video/sfu/models/models';
|
|
8
8
|
import { isMobile } from '../helpers/compatibility';
|
|
9
9
|
import { isReactNative } from '../helpers/platforms';
|
|
@@ -79,8 +79,15 @@ export class CameraManager extends DeviceManager<CameraManagerState> {
|
|
|
79
79
|
* @internal
|
|
80
80
|
*/
|
|
81
81
|
async selectTargetResolution(resolution: { width: number; height: number }) {
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
// normalize target resolution to landscape format.
|
|
83
|
+
// on mobile devices, the device itself adjusts the resolution to portrait or landscape
|
|
84
|
+
// depending on the orientation of the device. using portrait resolution
|
|
85
|
+
// will result in falling back to the default resolution (640x480).
|
|
86
|
+
let { width, height } = resolution;
|
|
87
|
+
if (width < height) [width, height] = [height, width];
|
|
88
|
+
this.targetResolution.height = height;
|
|
89
|
+
this.targetResolution.width = width;
|
|
90
|
+
|
|
84
91
|
if (this.state.optimisticStatus === 'enabled') {
|
|
85
92
|
try {
|
|
86
93
|
await this.statusChangeSettled();
|
|
@@ -92,11 +99,8 @@ export class CameraManager extends DeviceManager<CameraManagerState> {
|
|
|
92
99
|
if (this.enabled && this.state.mediaStream) {
|
|
93
100
|
const [videoTrack] = this.state.mediaStream.getVideoTracks();
|
|
94
101
|
if (!videoTrack) return;
|
|
95
|
-
const { width, height } = videoTrack.getSettings();
|
|
96
|
-
if (
|
|
97
|
-
width !== this.targetResolution.width ||
|
|
98
|
-
height !== this.targetResolution.height
|
|
99
|
-
) {
|
|
102
|
+
const { width: w, height: h } = videoTrack.getSettings();
|
|
103
|
+
if (w !== width || h !== height) {
|
|
100
104
|
await this.applySettingsToStream();
|
|
101
105
|
this.logger.debug(
|
|
102
106
|
`${width}x${height} target resolution applied to media stream`,
|
|
@@ -112,43 +116,27 @@ export class CameraManager extends DeviceManager<CameraManagerState> {
|
|
|
112
116
|
* @param publish whether to publish the stream after applying the settings.
|
|
113
117
|
*/
|
|
114
118
|
async apply(settings: VideoSettingsResponse, publish: boolean) {
|
|
115
|
-
const hasPublishedVideo = !!this.call.state.localParticipant?.videoStream;
|
|
116
|
-
const hasPermission = this.call.permissionsContext.hasPermission(
|
|
117
|
-
OwnCapability.SEND_AUDIO,
|
|
118
|
-
);
|
|
119
|
-
if (hasPublishedVideo || !hasPermission) return;
|
|
120
|
-
|
|
121
119
|
// Wait for any in progress camera operation
|
|
122
120
|
await this.statusChangeSettled();
|
|
121
|
+
await this.selectTargetResolution(settings.target_resolution);
|
|
122
|
+
|
|
123
|
+
// apply a direction and enable the camera only if in "pristine" state
|
|
124
|
+
// and server defaults are not deferred to application code
|
|
125
|
+
const canPublish = this.call.permissionsContext.canPublish(this.trackType);
|
|
126
|
+
if (this.state.status === undefined && !this.deferServerDefaults) {
|
|
127
|
+
if (!this.state.direction && !this.state.selectedDevice) {
|
|
128
|
+
const direction = settings.camera_facing === 'front' ? 'front' : 'back';
|
|
129
|
+
await this.selectDirection(direction);
|
|
130
|
+
}
|
|
123
131
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// on mobile devices, the device itself adjusts the resolution to portrait or landscape
|
|
128
|
-
// depending on the orientation of the device. using portrait resolution
|
|
129
|
-
// will result in falling back to the default resolution (640x480).
|
|
130
|
-
let { width, height } = target_resolution;
|
|
131
|
-
if (width < height) [width, height] = [height, width];
|
|
132
|
-
await this.selectTargetResolution({ width, height });
|
|
133
|
-
|
|
134
|
-
// Set camera direction if it's not yet set
|
|
135
|
-
if (!this.state.direction && !this.state.selectedDevice) {
|
|
136
|
-
this.state.setDirection(camera_facing === 'front' ? 'front' : 'back');
|
|
132
|
+
if (canPublish && settings.camera_default_on && settings.enabled) {
|
|
133
|
+
await this.enable();
|
|
134
|
+
}
|
|
137
135
|
}
|
|
138
136
|
|
|
139
|
-
if (!publish) return;
|
|
140
|
-
|
|
141
137
|
const { mediaStream } = this.state;
|
|
142
|
-
if (this.enabled && mediaStream) {
|
|
143
|
-
// The camera is already enabled (e.g. lobby screen). Publish the stream
|
|
138
|
+
if (canPublish && publish && this.enabled && mediaStream) {
|
|
144
139
|
await this.publishStream(mediaStream);
|
|
145
|
-
} else if (
|
|
146
|
-
this.state.status === undefined &&
|
|
147
|
-
camera_default_on &&
|
|
148
|
-
enabled
|
|
149
|
-
) {
|
|
150
|
-
// Start camera if backend config specifies, and there is no local setting
|
|
151
|
-
await this.enable();
|
|
152
140
|
}
|
|
153
141
|
}
|
|
154
142
|
|
|
@@ -32,6 +32,20 @@ export abstract class DeviceManager<
|
|
|
32
32
|
|
|
33
33
|
state: S;
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* When `true`, the `apply()` method will skip automatically enabling/disabling
|
|
37
|
+
* the device based on server defaults (`mic_default_on`, `camera_default_on`).
|
|
38
|
+
*
|
|
39
|
+
* This is useful when application code wants to handle device preferences
|
|
40
|
+
* (e.g., persisted user preferences) and prevent server defaults from
|
|
41
|
+
* overriding them.
|
|
42
|
+
*
|
|
43
|
+
* @default false
|
|
44
|
+
*
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
deferServerDefaults = false;
|
|
48
|
+
|
|
35
49
|
protected readonly call: Call;
|
|
36
50
|
protected readonly trackType: TrackType;
|
|
37
51
|
protected subscriptions: Function[] = [];
|
|
@@ -230,25 +230,21 @@ export class MicrophoneManager extends AudioDeviceManager<MicrophoneManagerState
|
|
|
230
230
|
* @param publish whether to publish the stream after applying the settings.
|
|
231
231
|
*/
|
|
232
232
|
async apply(settings: AudioSettingsResponse, publish: boolean) {
|
|
233
|
-
if (!publish) return;
|
|
234
|
-
|
|
235
|
-
const hasPublishedAudio = !!this.call.state.localParticipant?.audioStream;
|
|
236
|
-
const hasPermission = this.call.permissionsContext.hasPermission(
|
|
237
|
-
OwnCapability.SEND_AUDIO,
|
|
238
|
-
);
|
|
239
|
-
if (hasPublishedAudio || !hasPermission) return;
|
|
240
|
-
|
|
241
233
|
// Wait for any in progress mic operation
|
|
242
234
|
await this.statusChangeSettled();
|
|
243
235
|
|
|
244
|
-
|
|
236
|
+
const canPublish = this.call.permissionsContext.canPublish(this.trackType);
|
|
237
|
+
// apply server-side settings only when the device state is pristine
|
|
238
|
+
// and server defaults are not deferred to application code
|
|
239
|
+
if (this.state.status === undefined && !this.deferServerDefaults) {
|
|
240
|
+
if (canPublish && settings.mic_default_on) {
|
|
241
|
+
await this.enable();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
245
|
const { mediaStream } = this.state;
|
|
246
|
-
if (this.enabled && mediaStream) {
|
|
247
|
-
// The mic is already enabled (e.g. lobby screen). Publish the stream
|
|
246
|
+
if (canPublish && publish && this.enabled && mediaStream) {
|
|
248
247
|
await this.publishStream(mediaStream);
|
|
249
|
-
} else if (this.state.status === undefined && settings.mic_default_on) {
|
|
250
|
-
// Start mic if backend config specifies, and there is no local setting
|
|
251
|
-
await this.enable();
|
|
252
248
|
}
|
|
253
249
|
}
|
|
254
250
|
|
|
@@ -3,6 +3,7 @@ import { StreamClient } from '../../coordinator/connection/client';
|
|
|
3
3
|
import { CallingState, StreamVideoWriteableStateStore } from '../../store';
|
|
4
4
|
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
|
|
6
|
+
import { fromPartial } from '@total-typescript/shoehorn';
|
|
6
7
|
import {
|
|
7
8
|
mockBrowserPermission,
|
|
8
9
|
mockCall,
|
|
@@ -296,21 +297,20 @@ describe('CameraManager', () => {
|
|
|
296
297
|
});
|
|
297
298
|
|
|
298
299
|
it('should not enable the camera when the user does not have permission', async () => {
|
|
299
|
-
call.permissionsContext.
|
|
300
|
+
call.permissionsContext.canPublish = vi.fn().mockReturnValue(false);
|
|
300
301
|
vi.spyOn(manager, 'enable');
|
|
301
302
|
await manager.apply(
|
|
302
|
-
|
|
303
|
-
{
|
|
303
|
+
fromPartial({
|
|
304
304
|
target_resolution: { width: 640, height: 480 },
|
|
305
305
|
camera_facing: 'front',
|
|
306
306
|
camera_default_on: true,
|
|
307
|
-
},
|
|
307
|
+
}),
|
|
308
308
|
true,
|
|
309
309
|
);
|
|
310
310
|
|
|
311
|
-
expect(manager.state.direction).toBe(
|
|
311
|
+
expect(manager.state.direction).toBe('front');
|
|
312
312
|
expect(manager.state.status).toBe(undefined);
|
|
313
|
-
expect(manager['targetResolution']).toEqual({ width:
|
|
313
|
+
expect(manager['targetResolution']).toEqual({ width: 640, height: 480 });
|
|
314
314
|
expect(manager.enable).not.toHaveBeenCalled();
|
|
315
315
|
});
|
|
316
316
|
|
|
@@ -329,7 +329,7 @@ describe('MicrophoneManager', () => {
|
|
|
329
329
|
beforeEach(() => {
|
|
330
330
|
// @ts-expect-error - read only property
|
|
331
331
|
call.permissionsContext = new PermissionsContext();
|
|
332
|
-
call.permissionsContext.
|
|
332
|
+
call.permissionsContext.canPublish = vi.fn().mockReturnValue(true);
|
|
333
333
|
});
|
|
334
334
|
|
|
335
335
|
it('should turn the mic on when set on dashboard', async () => {
|
|
@@ -346,15 +346,8 @@ describe('MicrophoneManager', () => {
|
|
|
346
346
|
expect(enable).not.toHaveBeenCalled();
|
|
347
347
|
});
|
|
348
348
|
|
|
349
|
-
it('should not turn on the mic when publish is false', async () => {
|
|
350
|
-
const enable = vi.spyOn(manager, 'enable');
|
|
351
|
-
// @ts-expect-error - partial data
|
|
352
|
-
await manager.apply({ mic_default_on: true }, false);
|
|
353
|
-
expect(enable).not.toHaveBeenCalled();
|
|
354
|
-
});
|
|
355
|
-
|
|
356
349
|
it('should not turn on the mic when permission is missing', async () => {
|
|
357
|
-
call.permissionsContext.
|
|
350
|
+
call.permissionsContext.canPublish = vi.fn().mockReturnValue(false);
|
|
358
351
|
const enable = vi.spyOn(manager, 'enable');
|
|
359
352
|
// @ts-expect-error - partial data
|
|
360
353
|
await manager.apply({ mic_default_on: true }, true);
|