@stream-io/video-client 1.46.1 → 1.47.0
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 +10 -0
- package/dist/index.browser.es.js +24 -7
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +24 -7
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +24 -7
- package/dist/index.es.js.map +1 -1
- package/dist/src/gen/coordinator/index.d.ts +6 -0
- package/package.json +1 -1
- package/src/devices/CameraManager.ts +9 -2
- package/src/devices/DeviceManager.ts +13 -3
- package/src/devices/MicrophoneManager.ts +8 -1
- package/src/devices/SpeakerManager.ts +16 -4
- package/src/devices/__tests__/CameraManager.test.ts +32 -0
- package/src/devices/__tests__/DeviceManager.test.ts +71 -0
- package/src/devices/__tests__/MicrophoneManager.test.ts +23 -0
- package/src/devices/__tests__/SpeakerManager.test.ts +28 -0
- package/src/gen/coordinator/index.ts +6 -0
|
@@ -5340,6 +5340,12 @@ export interface JoinCallRequest {
|
|
|
5340
5340
|
* @memberof JoinCallRequest
|
|
5341
5341
|
*/
|
|
5342
5342
|
data?: CallRequest;
|
|
5343
|
+
/**
|
|
5344
|
+
* if true, the participant will be marked as publsihing to large audience
|
|
5345
|
+
* @type {boolean}
|
|
5346
|
+
* @memberof JoinCallRequest
|
|
5347
|
+
*/
|
|
5348
|
+
hint_high_scale_livestream_publisher?: boolean;
|
|
5343
5349
|
/**
|
|
5344
5350
|
*
|
|
5345
5351
|
* @type {string}
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Observable } from 'rxjs';
|
|
1
|
+
import { firstValueFrom, Observable } from 'rxjs';
|
|
2
2
|
import { Call } from '../Call';
|
|
3
3
|
import { CameraDirection, CameraManagerState } from './CameraManagerState';
|
|
4
4
|
import { DeviceManager } from './DeviceManager';
|
|
@@ -140,7 +140,14 @@ export class CameraManager extends DeviceManager<CameraManagerState> {
|
|
|
140
140
|
this.state.status === undefined &&
|
|
141
141
|
this.state.optimisticStatus === undefined;
|
|
142
142
|
let persistedPreferencesApplied = false;
|
|
143
|
-
|
|
143
|
+
const permissionState = await firstValueFrom(
|
|
144
|
+
this.state.browserPermissionState$,
|
|
145
|
+
);
|
|
146
|
+
if (
|
|
147
|
+
shouldApplyDefaults &&
|
|
148
|
+
this.devicePersistence.enabled &&
|
|
149
|
+
permissionState === 'granted'
|
|
150
|
+
) {
|
|
144
151
|
persistedPreferencesApplied =
|
|
145
152
|
await this.applyPersistedPreferences(enabledInCallType);
|
|
146
153
|
}
|
|
@@ -89,9 +89,19 @@ export abstract class DeviceManager<
|
|
|
89
89
|
if (this.devicePersistence.enabled) {
|
|
90
90
|
this.subscriptions.push(
|
|
91
91
|
createSubscription(
|
|
92
|
-
combineLatest([
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
combineLatest([
|
|
93
|
+
this.state.selectedDevice$,
|
|
94
|
+
this.state.status$,
|
|
95
|
+
this.state.browserPermissionState$,
|
|
96
|
+
]),
|
|
97
|
+
([selectedDevice, status, browserPermissionState]) => {
|
|
98
|
+
if (
|
|
99
|
+
!status ||
|
|
100
|
+
(this.isTrackStoppedDueToTrackEnd && status === 'disabled') ||
|
|
101
|
+
browserPermissionState !== 'granted'
|
|
102
|
+
)
|
|
103
|
+
return;
|
|
104
|
+
|
|
95
105
|
this.persistPreference(selectedDevice, status);
|
|
96
106
|
},
|
|
97
107
|
),
|
|
@@ -356,7 +356,14 @@ export class MicrophoneManager extends AudioDeviceManager<MicrophoneManagerState
|
|
|
356
356
|
this.state.status === undefined &&
|
|
357
357
|
this.state.optimisticStatus === undefined;
|
|
358
358
|
let persistedPreferencesApplied = false;
|
|
359
|
-
|
|
359
|
+
const permissionState = await firstValueFrom(
|
|
360
|
+
this.state.browserPermissionState$,
|
|
361
|
+
);
|
|
362
|
+
if (
|
|
363
|
+
shouldApplyDefaults &&
|
|
364
|
+
this.devicePersistence.enabled &&
|
|
365
|
+
permissionState === 'granted'
|
|
366
|
+
) {
|
|
360
367
|
persistedPreferencesApplied = await this.applyPersistedPreferences(true);
|
|
361
368
|
}
|
|
362
369
|
|
|
@@ -2,7 +2,11 @@ import { combineLatest } from 'rxjs';
|
|
|
2
2
|
import { Call } from '../Call';
|
|
3
3
|
import { isReactNative } from '../helpers/platforms';
|
|
4
4
|
import { SpeakerState } from './SpeakerState';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
deviceIds$,
|
|
7
|
+
getAudioBrowserPermission,
|
|
8
|
+
getAudioOutputDevices,
|
|
9
|
+
} from './devices';
|
|
6
10
|
import {
|
|
7
11
|
AudioSettingsRequestDefaultDeviceEnum,
|
|
8
12
|
CallSettingsResponse,
|
|
@@ -111,9 +115,17 @@ export class SpeakerManager {
|
|
|
111
115
|
|
|
112
116
|
if (!isReactNative() && this.devicePersistence.enabled) {
|
|
113
117
|
this.subscriptions.push(
|
|
114
|
-
createSubscription(
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
createSubscription(
|
|
119
|
+
combineLatest([
|
|
120
|
+
this.state.selectedDevice$,
|
|
121
|
+
getAudioBrowserPermission(this.call.tracer).asStateObservable(),
|
|
122
|
+
]),
|
|
123
|
+
([selectedDevice, browserPermissionState]) => {
|
|
124
|
+
if (!selectedDevice || browserPermissionState !== 'granted') return;
|
|
125
|
+
|
|
126
|
+
this.persistSpeakerDevicePreference(selectedDevice);
|
|
127
|
+
},
|
|
128
|
+
),
|
|
117
129
|
);
|
|
118
130
|
}
|
|
119
131
|
}
|
|
@@ -306,6 +306,9 @@ describe('CameraManager', () => {
|
|
|
306
306
|
});
|
|
307
307
|
|
|
308
308
|
it('should skip defaults when preferences are applied', async () => {
|
|
309
|
+
vi.spyOn(mockBrowserPermission, 'asStateObservable').mockReturnValue(
|
|
310
|
+
of('granted'),
|
|
311
|
+
);
|
|
309
312
|
const devicePersistence = { enabled: true, storageKey: '' };
|
|
310
313
|
const persistedManager = new CameraManager(call, devicePersistence);
|
|
311
314
|
const applySpy = vi
|
|
@@ -329,6 +332,32 @@ describe('CameraManager', () => {
|
|
|
329
332
|
expect(enableSpy).not.toHaveBeenCalled();
|
|
330
333
|
});
|
|
331
334
|
|
|
335
|
+
it('should skip persisted preferences when permission is not granted', async () => {
|
|
336
|
+
vi.spyOn(mockBrowserPermission, 'asStateObservable').mockReturnValue(
|
|
337
|
+
of('prompt'),
|
|
338
|
+
);
|
|
339
|
+
const devicePersistence = { enabled: true, storageKey: '' };
|
|
340
|
+
const persistedManager = new CameraManager(call, devicePersistence);
|
|
341
|
+
const applySpy = vi.spyOn(
|
|
342
|
+
persistedManager as never,
|
|
343
|
+
'applyPersistedPreferences',
|
|
344
|
+
);
|
|
345
|
+
const enableSpy = vi.spyOn(persistedManager, 'enable');
|
|
346
|
+
|
|
347
|
+
await persistedManager.apply(
|
|
348
|
+
fromPartial({
|
|
349
|
+
enabled: true,
|
|
350
|
+
target_resolution: { width: 640, height: 480 },
|
|
351
|
+
camera_facing: 'front',
|
|
352
|
+
camera_default_on: true,
|
|
353
|
+
}),
|
|
354
|
+
true,
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
expect(applySpy).not.toHaveBeenCalled();
|
|
358
|
+
expect(enableSpy).toHaveBeenCalled();
|
|
359
|
+
});
|
|
360
|
+
|
|
332
361
|
it('should not apply defaults when device is not pristine', async () => {
|
|
333
362
|
manager.state.setStatus('enabled');
|
|
334
363
|
const selectDirectionSpy = vi.spyOn(manager, 'selectDirection');
|
|
@@ -445,6 +474,9 @@ describe('CameraManager', () => {
|
|
|
445
474
|
createVideoStreamForDevice(selectedDevice.deviceId),
|
|
446
475
|
);
|
|
447
476
|
});
|
|
477
|
+
vi.spyOn(mockBrowserPermission, 'asStateObservable').mockReturnValue(
|
|
478
|
+
of('granted'),
|
|
479
|
+
);
|
|
448
480
|
|
|
449
481
|
const stressManager = new CameraManager(call, {
|
|
450
482
|
enabled: true,
|
|
@@ -76,6 +76,9 @@ describe('Device Manager', () => {
|
|
|
76
76
|
beforeEach(() => {
|
|
77
77
|
storageKey = '@test/device-preferences';
|
|
78
78
|
localStorageMock = createLocalStorageMock();
|
|
79
|
+
vi.spyOn(mockBrowserPermission, 'asStateObservable').mockReturnValue(
|
|
80
|
+
of('granted'),
|
|
81
|
+
);
|
|
79
82
|
Object.defineProperty(window, 'localStorage', {
|
|
80
83
|
configurable: true,
|
|
81
84
|
value: localStorageMock,
|
|
@@ -455,6 +458,74 @@ describe('Device Manager', () => {
|
|
|
455
458
|
},
|
|
456
459
|
]);
|
|
457
460
|
});
|
|
461
|
+
|
|
462
|
+
it('stores preferences when permission is granted', async () => {
|
|
463
|
+
const persistenceEnabledManager = new TestInputMediaDeviceManager(
|
|
464
|
+
manager['call'],
|
|
465
|
+
{ enabled: true, storageKey },
|
|
466
|
+
);
|
|
467
|
+
const listDevicesSpy = vi.spyOn(persistenceEnabledManager, 'listDevices');
|
|
468
|
+
|
|
469
|
+
emitDeviceIds(mockVideoDevices);
|
|
470
|
+
persistenceEnabledManager.state.setDevice(mockVideoDevices[0].deviceId);
|
|
471
|
+
persistenceEnabledManager.state.setStatus('enabled');
|
|
472
|
+
|
|
473
|
+
expect(readPreferences(storageKey).camera).toBeDefined();
|
|
474
|
+
expect(listDevicesSpy).toHaveBeenCalled();
|
|
475
|
+
expect(readPreferences(storageKey).camera).toEqual([
|
|
476
|
+
{
|
|
477
|
+
selectedDeviceId: mockVideoDevices[0].deviceId,
|
|
478
|
+
selectedDeviceLabel: mockVideoDevices[0].label,
|
|
479
|
+
muted: false,
|
|
480
|
+
},
|
|
481
|
+
]);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('does not store preferences when permission is not granted', async () => {
|
|
485
|
+
vi.spyOn(mockBrowserPermission, 'asStateObservable').mockReturnValue(
|
|
486
|
+
of('prompt'),
|
|
487
|
+
);
|
|
488
|
+
const persistenceEnabledManager = new TestInputMediaDeviceManager(
|
|
489
|
+
manager['call'],
|
|
490
|
+
{ enabled: true, storageKey },
|
|
491
|
+
);
|
|
492
|
+
const listDevicesSpy = vi.spyOn(persistenceEnabledManager, 'listDevices');
|
|
493
|
+
|
|
494
|
+
emitDeviceIds(mockVideoDevices);
|
|
495
|
+
persistenceEnabledManager.state.setDevice(mockVideoDevices[0].deviceId);
|
|
496
|
+
persistenceEnabledManager.state.setStatus('enabled');
|
|
497
|
+
|
|
498
|
+
expect(readPreferences(storageKey).camera).toBeUndefined();
|
|
499
|
+
expect(listDevicesSpy).not.toHaveBeenCalled();
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('does not overwrite preferences when track ends unexpectedly', async () => {
|
|
503
|
+
const persistenceEnabledManager = new TestInputMediaDeviceManager(
|
|
504
|
+
manager['call'],
|
|
505
|
+
{ enabled: true, storageKey },
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
await persistenceEnabledManager.enable();
|
|
509
|
+
|
|
510
|
+
expect(readPreferences(storageKey).camera).toEqual([
|
|
511
|
+
{
|
|
512
|
+
selectedDeviceId: mockVideoDevices[0].deviceId,
|
|
513
|
+
selectedDeviceLabel: mockVideoDevices[0].label,
|
|
514
|
+
muted: false,
|
|
515
|
+
},
|
|
516
|
+
]);
|
|
517
|
+
|
|
518
|
+
const [track] = persistenceEnabledManager.state.mediaStream!.getTracks();
|
|
519
|
+
await ((track as MockTrack).eventHandlers['ended'] as Function)();
|
|
520
|
+
|
|
521
|
+
expect(readPreferences(storageKey).camera).toEqual([
|
|
522
|
+
{
|
|
523
|
+
selectedDeviceId: mockVideoDevices[0].deviceId,
|
|
524
|
+
selectedDeviceLabel: mockVideoDevices[0].label,
|
|
525
|
+
muted: false,
|
|
526
|
+
},
|
|
527
|
+
]);
|
|
528
|
+
});
|
|
458
529
|
});
|
|
459
530
|
|
|
460
531
|
describe('applyPersistedPreferences', () => {
|
|
@@ -479,6 +479,29 @@ describe('MicrophoneManager', () => {
|
|
|
479
479
|
expect(enableSpy).not.toHaveBeenCalled();
|
|
480
480
|
});
|
|
481
481
|
|
|
482
|
+
it('should skip persisted preferences when permission is not granted', async () => {
|
|
483
|
+
vi.spyOn(mockBrowserPermission, 'asStateObservable').mockReturnValue(
|
|
484
|
+
of('prompt'),
|
|
485
|
+
);
|
|
486
|
+
const devicePersistence = { enabled: true, storageKey: '' };
|
|
487
|
+
const persistedManager = new MicrophoneManager(
|
|
488
|
+
call,
|
|
489
|
+
devicePersistence,
|
|
490
|
+
'disable-tracks',
|
|
491
|
+
);
|
|
492
|
+
const applySpy = vi.spyOn(
|
|
493
|
+
persistedManager as never,
|
|
494
|
+
'applyPersistedPreferences',
|
|
495
|
+
);
|
|
496
|
+
const enableSpy = vi.spyOn(persistedManager, 'enable');
|
|
497
|
+
|
|
498
|
+
// @ts-expect-error - partial data
|
|
499
|
+
await persistedManager.apply({ mic_default_on: true }, true);
|
|
500
|
+
|
|
501
|
+
expect(applySpy).not.toHaveBeenCalled();
|
|
502
|
+
expect(enableSpy).toHaveBeenCalled();
|
|
503
|
+
});
|
|
504
|
+
|
|
482
505
|
it('should not apply defaults when mic is not pristine', async () => {
|
|
483
506
|
manager.state.setStatus('enabled');
|
|
484
507
|
const applySpy = vi.spyOn(manager as never, 'applyPersistedPreferences');
|
|
@@ -37,6 +37,9 @@ describe('SpeakerManager.test', () => {
|
|
|
37
37
|
beforeEach(() => {
|
|
38
38
|
storageKey = '@test/speaker-preferences';
|
|
39
39
|
localStorageMock = createLocalStorageMock();
|
|
40
|
+
vi.spyOn(mockBrowserPermission, 'asStateObservable').mockReturnValue(
|
|
41
|
+
of('granted'),
|
|
42
|
+
);
|
|
40
43
|
Object.defineProperty(window, 'localStorage', {
|
|
41
44
|
configurable: true,
|
|
42
45
|
value: localStorageMock,
|
|
@@ -125,6 +128,31 @@ describe('SpeakerManager.test', () => {
|
|
|
125
128
|
expect(manager.state.selectedDevice).toBe('');
|
|
126
129
|
});
|
|
127
130
|
|
|
131
|
+
it('persists speaker selection when permission is granted', async () => {
|
|
132
|
+
const persistedManager = new SpeakerManager(
|
|
133
|
+
new Call({
|
|
134
|
+
id: '',
|
|
135
|
+
type: '',
|
|
136
|
+
streamClient: new StreamClient('abc123'),
|
|
137
|
+
clientStore: new StreamVideoWriteableStateStore(),
|
|
138
|
+
}),
|
|
139
|
+
{ enabled: true, storageKey },
|
|
140
|
+
);
|
|
141
|
+
const listDevicesSpy = vi.spyOn(persistedManager, 'listDevices');
|
|
142
|
+
const audioOutputDevice = {
|
|
143
|
+
deviceId: 'speaker-1',
|
|
144
|
+
kind: 'audiooutput',
|
|
145
|
+
label: 'Speaker 1',
|
|
146
|
+
groupId: 'speaker-group',
|
|
147
|
+
} as MediaDeviceInfo;
|
|
148
|
+
|
|
149
|
+
emitDeviceIds([audioOutputDevice]);
|
|
150
|
+
persistedManager.select(audioOutputDevice.deviceId);
|
|
151
|
+
|
|
152
|
+
expect(listDevicesSpy).toHaveBeenCalled();
|
|
153
|
+
expect(persistedManager.state.selectedDevice).toBe('speaker-1');
|
|
154
|
+
});
|
|
155
|
+
|
|
128
156
|
describe('apply (web)', () => {
|
|
129
157
|
it('does nothing when persistence is disabled', () => {
|
|
130
158
|
const selectSpy = vi.spyOn(manager, 'select');
|
|
@@ -5344,6 +5344,12 @@ export interface JoinCallRequest {
|
|
|
5344
5344
|
* @memberof JoinCallRequest
|
|
5345
5345
|
*/
|
|
5346
5346
|
data?: CallRequest;
|
|
5347
|
+
/**
|
|
5348
|
+
* if true, the participant will be marked as publsihing to large audience
|
|
5349
|
+
* @type {boolean}
|
|
5350
|
+
* @memberof JoinCallRequest
|
|
5351
|
+
*/
|
|
5352
|
+
hint_high_scale_livestream_publisher?: boolean;
|
|
5347
5353
|
/**
|
|
5348
5354
|
*
|
|
5349
5355
|
* @type {string}
|