@stream-io/video-client 1.43.0 → 1.44.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 +15 -0
- package/dist/index.browser.es.js +206 -59
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +205 -58
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +206 -59
- package/dist/index.es.js.map +1 -1
- package/dist/src/StreamVideoClient.d.ts +2 -8
- package/dist/src/coordinator/connection/types.d.ts +5 -0
- package/dist/src/devices/CameraManager.d.ts +7 -2
- package/dist/src/devices/DeviceManager.d.ts +7 -15
- package/dist/src/devices/MicrophoneManager.d.ts +2 -1
- package/dist/src/devices/SpeakerManager.d.ts +6 -1
- package/dist/src/devices/devicePersistence.d.ts +27 -0
- package/dist/src/helpers/clientUtils.d.ts +1 -1
- package/dist/src/permissions/PermissionsContext.d.ts +1 -1
- package/dist/src/types.d.ts +38 -2
- package/package.json +1 -1
- package/src/Call.ts +5 -3
- package/src/StreamVideoClient.ts +1 -9
- package/src/coordinator/connection/types.ts +6 -0
- package/src/devices/CameraManager.ts +31 -11
- package/src/devices/DeviceManager.ts +113 -31
- package/src/devices/MicrophoneManager.ts +26 -8
- package/src/devices/ScreenShareManager.ts +7 -1
- package/src/devices/SpeakerManager.ts +62 -18
- package/src/devices/__tests__/CameraManager.test.ts +184 -21
- package/src/devices/__tests__/DeviceManager.test.ts +184 -2
- package/src/devices/__tests__/DeviceManagerFilters.test.ts +2 -0
- package/src/devices/__tests__/MicrophoneManager.test.ts +146 -2
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +2 -0
- package/src/devices/__tests__/ScreenShareManager.test.ts +2 -0
- package/src/devices/__tests__/SpeakerManager.test.ts +90 -0
- package/src/devices/__tests__/devicePersistence.test.ts +142 -0
- package/src/devices/__tests__/devices.test.ts +390 -0
- package/src/devices/__tests__/mediaStreamTestHelpers.ts +58 -0
- package/src/devices/__tests__/mocks.ts +35 -0
- package/src/devices/devicePersistence.ts +106 -0
- package/src/devices/devices.ts +3 -3
- package/src/helpers/__tests__/DynascaleManager.test.ts +3 -1
- package/src/helpers/clientUtils.ts +1 -1
- package/src/permissions/PermissionsContext.ts +1 -0
- package/src/sorting/presets.ts +1 -1
- package/src/store/CallState.ts +1 -1
- package/src/types.ts +49 -2
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
/* @vitest-environment happy-dom */
|
|
1
2
|
import { Call } from '../../Call';
|
|
2
3
|
import { StreamClient } from '../../coordinator/connection/client';
|
|
3
4
|
import { CallingState, StreamVideoWriteableStateStore } from '../../store';
|
|
4
5
|
|
|
5
6
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
7
|
import {
|
|
8
|
+
createLocalStorageMock,
|
|
7
9
|
emitDeviceIds,
|
|
10
|
+
LocalStorageMock,
|
|
8
11
|
mockBrowserPermission,
|
|
9
12
|
mockCall,
|
|
10
13
|
mockDeviceIds$,
|
|
@@ -16,6 +19,8 @@ import { DeviceManager } from '../DeviceManager';
|
|
|
16
19
|
import { DeviceManagerState } from '../DeviceManagerState';
|
|
17
20
|
import { of } from 'rxjs';
|
|
18
21
|
import { TrackType } from '../../gen/video/sfu/models/models';
|
|
22
|
+
import { PermissionsContext } from '../../permissions';
|
|
23
|
+
import { readPreferences } from '../devicePersistence';
|
|
19
24
|
|
|
20
25
|
vi.mock('../../Call.ts', () => {
|
|
21
26
|
console.log('MOCKING Call');
|
|
@@ -47,7 +52,10 @@ class TestInputMediaDeviceManager extends DeviceManager<TestInputMediaDeviceMana
|
|
|
47
52
|
public stopPublishStream = vi.fn();
|
|
48
53
|
public getTracks = () => this.state.mediaStream?.getTracks() ?? [];
|
|
49
54
|
|
|
50
|
-
constructor(
|
|
55
|
+
constructor(
|
|
56
|
+
call: Call,
|
|
57
|
+
devicePersistence = { enabled: false, storageKey: '' },
|
|
58
|
+
) {
|
|
51
59
|
super(
|
|
52
60
|
call,
|
|
53
61
|
new TestInputMediaDeviceManagerState(
|
|
@@ -55,14 +63,23 @@ class TestInputMediaDeviceManager extends DeviceManager<TestInputMediaDeviceMana
|
|
|
55
63
|
mockBrowserPermission,
|
|
56
64
|
),
|
|
57
65
|
TrackType.VIDEO,
|
|
66
|
+
devicePersistence,
|
|
58
67
|
);
|
|
59
68
|
}
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
describe('
|
|
71
|
+
describe('Device Manager', () => {
|
|
63
72
|
let manager: TestInputMediaDeviceManager;
|
|
73
|
+
let localStorageMock: LocalStorageMock;
|
|
74
|
+
let storageKey: string;
|
|
64
75
|
|
|
65
76
|
beforeEach(() => {
|
|
77
|
+
storageKey = '@test/device-preferences';
|
|
78
|
+
localStorageMock = createLocalStorageMock();
|
|
79
|
+
Object.defineProperty(window, 'localStorage', {
|
|
80
|
+
configurable: true,
|
|
81
|
+
value: localStorageMock,
|
|
82
|
+
});
|
|
66
83
|
manager = new TestInputMediaDeviceManager(
|
|
67
84
|
new Call({
|
|
68
85
|
id: '',
|
|
@@ -70,6 +87,7 @@ describe('InputMediaDeviceManager.test', () => {
|
|
|
70
87
|
streamClient: new StreamClient('abc123'),
|
|
71
88
|
clientStore: new StreamVideoWriteableStateStore(),
|
|
72
89
|
}),
|
|
90
|
+
{ enabled: false, storageKey },
|
|
73
91
|
);
|
|
74
92
|
});
|
|
75
93
|
|
|
@@ -366,8 +384,172 @@ describe('InputMediaDeviceManager.test', () => {
|
|
|
366
384
|
vi.useRealTimers();
|
|
367
385
|
});
|
|
368
386
|
|
|
387
|
+
describe('persistPreference', () => {
|
|
388
|
+
it('stores selected device and muted state', () => {
|
|
389
|
+
const persistenceEnabledManager = new TestInputMediaDeviceManager(
|
|
390
|
+
manager['call'],
|
|
391
|
+
{ enabled: true, storageKey },
|
|
392
|
+
);
|
|
393
|
+
persistenceEnabledManager.state.setDevice(mockVideoDevices[1].deviceId);
|
|
394
|
+
persistenceEnabledManager.state.setStatus('enabled');
|
|
395
|
+
|
|
396
|
+
const preferences = readPreferences(storageKey);
|
|
397
|
+
expect(preferences.camera).toEqual([
|
|
398
|
+
{
|
|
399
|
+
selectedDeviceId: mockVideoDevices[1].deviceId,
|
|
400
|
+
selectedDeviceLabel: mockVideoDevices[1].label,
|
|
401
|
+
muted: false,
|
|
402
|
+
},
|
|
403
|
+
]);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('stores default device when selection is cleared', () => {
|
|
407
|
+
const persistenceEnabledManager = new TestInputMediaDeviceManager(
|
|
408
|
+
manager['call'],
|
|
409
|
+
{ enabled: true, storageKey },
|
|
410
|
+
);
|
|
411
|
+
persistenceEnabledManager.state.setDevice(undefined);
|
|
412
|
+
persistenceEnabledManager.state.setStatus('disabled');
|
|
413
|
+
|
|
414
|
+
const preferences = readPreferences(storageKey);
|
|
415
|
+
expect(preferences.camera).toEqual([
|
|
416
|
+
{
|
|
417
|
+
selectedDeviceId: 'default',
|
|
418
|
+
selectedDeviceLabel: '',
|
|
419
|
+
muted: true,
|
|
420
|
+
},
|
|
421
|
+
]);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('persists device history when selection changes', () => {
|
|
425
|
+
const persistenceEnabledManager = new TestInputMediaDeviceManager(
|
|
426
|
+
manager['call'],
|
|
427
|
+
{ enabled: true, storageKey },
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
persistenceEnabledManager.state.setDevice(mockVideoDevices[0].deviceId);
|
|
431
|
+
persistenceEnabledManager.state.setStatus('enabled');
|
|
432
|
+
|
|
433
|
+
persistenceEnabledManager.state.setDevice(mockVideoDevices[1].deviceId);
|
|
434
|
+
persistenceEnabledManager.state.setStatus('enabled');
|
|
435
|
+
|
|
436
|
+
const preferences = readPreferences(storageKey);
|
|
437
|
+
expect(preferences.camera).toEqual([
|
|
438
|
+
{
|
|
439
|
+
selectedDeviceId: mockVideoDevices[1].deviceId,
|
|
440
|
+
selectedDeviceLabel: mockVideoDevices[1].label,
|
|
441
|
+
muted: false,
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
selectedDeviceId: mockVideoDevices[0].deviceId,
|
|
445
|
+
selectedDeviceLabel: mockVideoDevices[0].label,
|
|
446
|
+
muted: false,
|
|
447
|
+
},
|
|
448
|
+
]);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
describe('applyPersistedPreferences', () => {
|
|
453
|
+
beforeEach(() => {
|
|
454
|
+
manager.dispose();
|
|
455
|
+
manager = new TestInputMediaDeviceManager(manager['call'], {
|
|
456
|
+
enabled: true,
|
|
457
|
+
storageKey,
|
|
458
|
+
});
|
|
459
|
+
// @ts-expect-error - read only property
|
|
460
|
+
manager['call'].permissionsContext = new PermissionsContext();
|
|
461
|
+
manager['call'].permissionsContext.canPublish = vi
|
|
462
|
+
.fn()
|
|
463
|
+
.mockReturnValue(true);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('returns false when no preferences exist', async () => {
|
|
467
|
+
// @ts-expect-error - private api
|
|
468
|
+
const result = await manager.applyPersistedPreferences(true);
|
|
469
|
+
expect(result).toBe(false);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('selects device by id and applies muted state', async () => {
|
|
473
|
+
localStorageMock.setItem(
|
|
474
|
+
storageKey,
|
|
475
|
+
JSON.stringify({
|
|
476
|
+
camera: [
|
|
477
|
+
{
|
|
478
|
+
selectedDeviceId: mockVideoDevices[0].deviceId,
|
|
479
|
+
selectedDeviceLabel: mockVideoDevices[0].label,
|
|
480
|
+
muted: true,
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
}),
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
const selectSpy = vi.spyOn(manager, 'select');
|
|
487
|
+
const disableSpy = vi.spyOn(manager, 'disable');
|
|
488
|
+
|
|
489
|
+
// @ts-expect-error - private API
|
|
490
|
+
const result = await manager.applyPersistedPreferences(true);
|
|
491
|
+
|
|
492
|
+
expect(result).toBe(true);
|
|
493
|
+
expect(selectSpy).toHaveBeenCalledWith(mockVideoDevices[0].deviceId);
|
|
494
|
+
expect(disableSpy).toHaveBeenCalled();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('selects device by label when device id is not found', async () => {
|
|
498
|
+
localStorageMock.setItem(
|
|
499
|
+
storageKey,
|
|
500
|
+
JSON.stringify({
|
|
501
|
+
camera: [
|
|
502
|
+
{
|
|
503
|
+
selectedDeviceId: 'missing-device',
|
|
504
|
+
selectedDeviceLabel: mockVideoDevices[1].label,
|
|
505
|
+
muted: false,
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
}),
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
const selectSpy = vi.spyOn(manager, 'select');
|
|
512
|
+
|
|
513
|
+
// @ts-expect-error private api
|
|
514
|
+
const result = await manager.applyPersistedPreferences(true);
|
|
515
|
+
|
|
516
|
+
expect(result).toBe(true);
|
|
517
|
+
expect(selectSpy).toHaveBeenCalledWith(mockVideoDevices[1].deviceId);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('applies muted state without selecting when default device is stored', async () => {
|
|
521
|
+
localStorageMock.setItem(
|
|
522
|
+
storageKey,
|
|
523
|
+
JSON.stringify({
|
|
524
|
+
camera: [
|
|
525
|
+
{
|
|
526
|
+
selectedDeviceId: 'default',
|
|
527
|
+
selectedDeviceLabel: '',
|
|
528
|
+
muted: true,
|
|
529
|
+
},
|
|
530
|
+
],
|
|
531
|
+
}),
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
const selectSpy = vi.spyOn(manager, 'select');
|
|
535
|
+
const disableSpy = vi.spyOn(manager, 'disable');
|
|
536
|
+
|
|
537
|
+
// @ts-expect-error private api
|
|
538
|
+
const result = await manager.applyPersistedPreferences(true);
|
|
539
|
+
|
|
540
|
+
expect(result).toBe(true);
|
|
541
|
+
expect(selectSpy).not.toHaveBeenCalled();
|
|
542
|
+
expect(disableSpy).toHaveBeenCalled();
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
369
546
|
afterEach(() => {
|
|
547
|
+
manager.dispose();
|
|
370
548
|
vi.clearAllMocks();
|
|
371
549
|
vi.resetModules();
|
|
550
|
+
Object.defineProperty(window, 'localStorage', {
|
|
551
|
+
configurable: true,
|
|
552
|
+
value: undefined,
|
|
553
|
+
});
|
|
372
554
|
});
|
|
373
555
|
});
|
|
@@ -28,6 +28,7 @@ class TestInputMediaDeviceManager extends DeviceManager<TestInputMediaDeviceMana
|
|
|
28
28
|
public getTracks = () => this.state.mediaStream?.getTracks() ?? [];
|
|
29
29
|
|
|
30
30
|
constructor(call: Call) {
|
|
31
|
+
const devicePersistence = { enabled: false, storageKey: '' };
|
|
31
32
|
super(
|
|
32
33
|
call,
|
|
33
34
|
new TestInputMediaDeviceManagerState(
|
|
@@ -35,6 +36,7 @@ class TestInputMediaDeviceManager extends DeviceManager<TestInputMediaDeviceMana
|
|
|
35
36
|
mockBrowserPermission,
|
|
36
37
|
),
|
|
37
38
|
TrackType.VIDEO,
|
|
39
|
+
devicePersistence,
|
|
38
40
|
);
|
|
39
41
|
}
|
|
40
42
|
}
|
|
@@ -14,12 +14,15 @@ import {
|
|
|
14
14
|
} from '../../gen/video/sfu/models/models';
|
|
15
15
|
import { CallingState, StreamVideoWriteableStateStore } from '../../store';
|
|
16
16
|
import {
|
|
17
|
+
createLocalStorageMock,
|
|
18
|
+
emitDeviceIds,
|
|
17
19
|
mockAudioDevices,
|
|
18
20
|
mockAudioStream,
|
|
19
21
|
mockBrowserPermission,
|
|
20
22
|
mockCall,
|
|
21
23
|
mockDeviceIds$,
|
|
22
24
|
} from './mocks';
|
|
25
|
+
import { createAudioStreamForDevice } from './mediaStreamTestHelpers';
|
|
23
26
|
import { setupAudioContextMock } from './web-audio.mocks';
|
|
24
27
|
import { getAudioStream } from '../devices';
|
|
25
28
|
import { MicrophoneManager } from '../MicrophoneManager';
|
|
@@ -35,6 +38,11 @@ import {
|
|
|
35
38
|
import { PermissionsContext } from '../../permissions';
|
|
36
39
|
import { Tracer } from '../../stats';
|
|
37
40
|
import { settled, withoutConcurrency } from '../../helpers/concurrency';
|
|
41
|
+
import {
|
|
42
|
+
defaultDeviceId,
|
|
43
|
+
readPreferences,
|
|
44
|
+
toPreferenceList,
|
|
45
|
+
} from '../devicePersistence';
|
|
38
46
|
|
|
39
47
|
vi.mock('../devices.ts', () => {
|
|
40
48
|
console.log('MOCKING devices API');
|
|
@@ -88,7 +96,8 @@ describe('MicrophoneManager', () => {
|
|
|
88
96
|
streamClient: new StreamClient('abc123'),
|
|
89
97
|
clientStore: new StreamVideoWriteableStateStore(),
|
|
90
98
|
});
|
|
91
|
-
|
|
99
|
+
const devicePersistence = { enabled: false, storageKey: '' };
|
|
100
|
+
manager = new MicrophoneManager(call, devicePersistence, 'disable-tracks');
|
|
92
101
|
});
|
|
93
102
|
it('list devices', () => {
|
|
94
103
|
const spy = vi.fn();
|
|
@@ -174,7 +183,12 @@ describe('MicrophoneManager', () => {
|
|
|
174
183
|
vi.spyOn(mockBrowserPermission, 'asStateObservable').mockReturnValue(
|
|
175
184
|
of('denied'),
|
|
176
185
|
);
|
|
177
|
-
const
|
|
186
|
+
const devicePersistence = { enabled: false, storageKey: '' };
|
|
187
|
+
const innerManager = new MicrophoneManager(
|
|
188
|
+
call,
|
|
189
|
+
devicePersistence,
|
|
190
|
+
'disable-tracks',
|
|
191
|
+
);
|
|
178
192
|
// @ts-expect-error private api
|
|
179
193
|
const fn = vi.spyOn(innerManager, 'startSpeakingWhileMutedDetection');
|
|
180
194
|
|
|
@@ -408,6 +422,13 @@ describe('MicrophoneManager', () => {
|
|
|
408
422
|
call.permissionsContext.canPublish = vi.fn().mockReturnValue(true);
|
|
409
423
|
});
|
|
410
424
|
|
|
425
|
+
it('should apply defaults when mic_default_on is true and enabled is pristine', async () => {
|
|
426
|
+
const enable = vi.spyOn(manager, 'enable');
|
|
427
|
+
// @ts-expect-error - partial data
|
|
428
|
+
await manager.apply({ mic_default_on: true }, true);
|
|
429
|
+
expect(enable).toHaveBeenCalled();
|
|
430
|
+
});
|
|
431
|
+
|
|
411
432
|
it('should turn the mic on when set on dashboard', async () => {
|
|
412
433
|
const enable = vi.spyOn(manager, 'enable');
|
|
413
434
|
// @ts-expect-error - partial data
|
|
@@ -438,6 +459,37 @@ describe('MicrophoneManager', () => {
|
|
|
438
459
|
await manager.apply({ mic_default_on: true }, true);
|
|
439
460
|
expect(manager['publishStream']).toHaveBeenCalled();
|
|
440
461
|
});
|
|
462
|
+
|
|
463
|
+
it('should skip defaults when preferences are applied', async () => {
|
|
464
|
+
const devicePersistence = { enabled: true, storageKey: '' };
|
|
465
|
+
const persistedManager = new MicrophoneManager(
|
|
466
|
+
call,
|
|
467
|
+
devicePersistence,
|
|
468
|
+
'disable-tracks',
|
|
469
|
+
);
|
|
470
|
+
const applySpy = vi
|
|
471
|
+
.spyOn(persistedManager as never, 'applyPersistedPreferences')
|
|
472
|
+
.mockResolvedValue(true);
|
|
473
|
+
const enableSpy = vi.spyOn(persistedManager, 'enable');
|
|
474
|
+
|
|
475
|
+
// @ts-expect-error - partial data
|
|
476
|
+
await persistedManager.apply({ mic_default_on: true }, true);
|
|
477
|
+
|
|
478
|
+
expect(applySpy).toHaveBeenCalledWith(true);
|
|
479
|
+
expect(enableSpy).not.toHaveBeenCalled();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('should not apply defaults when mic is not pristine', async () => {
|
|
483
|
+
manager.state.setStatus('enabled');
|
|
484
|
+
const applySpy = vi.spyOn(manager as never, 'applyPersistedPreferences');
|
|
485
|
+
const enableSpy = vi.spyOn(manager, 'enable');
|
|
486
|
+
|
|
487
|
+
// @ts-expect-error - partial data
|
|
488
|
+
await manager.apply({ mic_default_on: true }, true);
|
|
489
|
+
|
|
490
|
+
expect(applySpy).not.toHaveBeenCalled();
|
|
491
|
+
expect(enableSpy).not.toHaveBeenCalled();
|
|
492
|
+
});
|
|
441
493
|
});
|
|
442
494
|
|
|
443
495
|
describe('Hi-Fi Audio', () => {
|
|
@@ -582,6 +634,98 @@ describe('MicrophoneManager', () => {
|
|
|
582
634
|
});
|
|
583
635
|
});
|
|
584
636
|
|
|
637
|
+
describe('Device Persistence Stress', () => {
|
|
638
|
+
it('persists the final microphone and muted state after rapid toggles, switches, and unplug', async () => {
|
|
639
|
+
const storageKey = '@test/device-preferences-microphone-stress';
|
|
640
|
+
const localStorageMock = createLocalStorageMock();
|
|
641
|
+
const originalWindow = globalThis.window;
|
|
642
|
+
Object.defineProperty(globalThis, 'window', {
|
|
643
|
+
configurable: true,
|
|
644
|
+
value: { localStorage: localStorageMock },
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
const getAudioStreamMock = vi.mocked(getAudioStream);
|
|
648
|
+
getAudioStreamMock.mockImplementation((constraints) => {
|
|
649
|
+
const requestedDeviceId = (constraints?.deviceId as { exact?: string })
|
|
650
|
+
?.exact;
|
|
651
|
+
const selectedDevice =
|
|
652
|
+
mockAudioDevices.find((d) => d.deviceId === requestedDeviceId) ??
|
|
653
|
+
mockAudioDevices[0];
|
|
654
|
+
return Promise.resolve(
|
|
655
|
+
createAudioStreamForDevice(
|
|
656
|
+
selectedDevice.deviceId,
|
|
657
|
+
selectedDevice.label,
|
|
658
|
+
),
|
|
659
|
+
);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
const stressManager = new MicrophoneManager(
|
|
663
|
+
call,
|
|
664
|
+
{ enabled: true, storageKey },
|
|
665
|
+
'disable-tracks',
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
try {
|
|
669
|
+
const finalDevice = mockAudioDevices[2];
|
|
670
|
+
emitDeviceIds(mockAudioDevices);
|
|
671
|
+
|
|
672
|
+
await Promise.allSettled([
|
|
673
|
+
stressManager.enable(),
|
|
674
|
+
stressManager.select(mockAudioDevices[1].deviceId),
|
|
675
|
+
stressManager.toggle(),
|
|
676
|
+
stressManager.select(finalDevice.deviceId),
|
|
677
|
+
stressManager.toggle(),
|
|
678
|
+
stressManager.enable(),
|
|
679
|
+
]);
|
|
680
|
+
await stressManager.statusChangeSettled();
|
|
681
|
+
await stressManager.select(finalDevice.deviceId);
|
|
682
|
+
await stressManager.enable();
|
|
683
|
+
await stressManager.statusChangeSettled();
|
|
684
|
+
|
|
685
|
+
expect(stressManager.state.selectedDevice).toBe(finalDevice.deviceId);
|
|
686
|
+
expect(stressManager.state.status).toBe('enabled');
|
|
687
|
+
|
|
688
|
+
const persistedBeforeUnplug = toPreferenceList(
|
|
689
|
+
readPreferences(storageKey).microphone,
|
|
690
|
+
);
|
|
691
|
+
expect(persistedBeforeUnplug[0]).toEqual({
|
|
692
|
+
selectedDeviceId: finalDevice.deviceId,
|
|
693
|
+
selectedDeviceLabel: finalDevice.label,
|
|
694
|
+
muted: false,
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
emitDeviceIds(
|
|
698
|
+
mockAudioDevices.filter((d) => d.deviceId !== finalDevice.deviceId),
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
await vi.waitFor(() => {
|
|
702
|
+
expect(stressManager.state.selectedDevice).toBe(undefined);
|
|
703
|
+
expect(stressManager.state.status).toBe('disabled');
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
const persistedAfterUnplug = toPreferenceList(
|
|
707
|
+
readPreferences(storageKey).microphone,
|
|
708
|
+
);
|
|
709
|
+
expect(persistedAfterUnplug[0]).toEqual({
|
|
710
|
+
selectedDeviceId: defaultDeviceId,
|
|
711
|
+
selectedDeviceLabel: '',
|
|
712
|
+
muted: true,
|
|
713
|
+
});
|
|
714
|
+
expect(persistedAfterUnplug).toContainEqual({
|
|
715
|
+
selectedDeviceId: finalDevice.deviceId,
|
|
716
|
+
selectedDeviceLabel: finalDevice.label,
|
|
717
|
+
muted: true,
|
|
718
|
+
});
|
|
719
|
+
} finally {
|
|
720
|
+
stressManager.dispose();
|
|
721
|
+
Object.defineProperty(globalThis, 'window', {
|
|
722
|
+
configurable: true,
|
|
723
|
+
value: originalWindow,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
|
|
585
729
|
afterEach(() => {
|
|
586
730
|
vi.restoreAllMocks();
|
|
587
731
|
vi.clearAllMocks();
|
|
@@ -80,6 +80,7 @@ describe('MicrophoneManager React Native', () => {
|
|
|
80
80
|
},
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
const devicePersistence = { enabled: false, storageKey: '' };
|
|
83
84
|
manager = new MicrophoneManager(
|
|
84
85
|
new Call({
|
|
85
86
|
id: '',
|
|
@@ -87,6 +88,7 @@ describe('MicrophoneManager React Native', () => {
|
|
|
87
88
|
streamClient: new StreamClient('abc123'),
|
|
88
89
|
clientStore: new StreamVideoWriteableStateStore(),
|
|
89
90
|
}),
|
|
91
|
+
devicePersistence,
|
|
90
92
|
);
|
|
91
93
|
});
|
|
92
94
|
|
|
@@ -34,6 +34,7 @@ describe('ScreenShareManager', () => {
|
|
|
34
34
|
let manager: ScreenShareManager;
|
|
35
35
|
|
|
36
36
|
beforeEach(() => {
|
|
37
|
+
const devicePersistence = { enabled: false, storageKey: '' };
|
|
37
38
|
manager = new ScreenShareManager(
|
|
38
39
|
new Call({
|
|
39
40
|
id: '',
|
|
@@ -41,6 +42,7 @@ describe('ScreenShareManager', () => {
|
|
|
41
42
|
streamClient: new StreamClient('abc123'),
|
|
42
43
|
clientStore: new StreamVideoWriteableStateStore(),
|
|
43
44
|
}),
|
|
45
|
+
devicePersistence,
|
|
44
46
|
);
|
|
45
47
|
});
|
|
46
48
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
/* @vitest-environment happy-dom */
|
|
1
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
3
|
import { fromPartial } from '@total-typescript/shoehorn';
|
|
3
4
|
import {
|
|
5
|
+
createLocalStorageMock,
|
|
4
6
|
emitDeviceIds,
|
|
7
|
+
LocalStorageMock,
|
|
5
8
|
mockAudioDevices,
|
|
6
9
|
mockBrowserPermission,
|
|
7
10
|
mockDeviceIds$,
|
|
@@ -12,6 +15,7 @@ import { checkIfAudioOutputChangeSupported } from '../devices';
|
|
|
12
15
|
import { Call } from '../../Call';
|
|
13
16
|
import { StreamClient } from '../../coordinator/connection/client';
|
|
14
17
|
import { StreamVideoWriteableStateStore } from '../../store';
|
|
18
|
+
import { defaultDeviceId } from '../devicePersistence';
|
|
15
19
|
|
|
16
20
|
vi.mock('../devices.ts', () => {
|
|
17
21
|
console.log('MOCKING devices');
|
|
@@ -27,8 +31,17 @@ vi.mock('../devices.ts', () => {
|
|
|
27
31
|
|
|
28
32
|
describe('SpeakerManager.test', () => {
|
|
29
33
|
let manager: SpeakerManager;
|
|
34
|
+
let storageKey: string;
|
|
35
|
+
let localStorageMock: LocalStorageMock;
|
|
30
36
|
|
|
31
37
|
beforeEach(() => {
|
|
38
|
+
storageKey = '@test/speaker-preferences';
|
|
39
|
+
localStorageMock = createLocalStorageMock();
|
|
40
|
+
Object.defineProperty(window, 'localStorage', {
|
|
41
|
+
configurable: true,
|
|
42
|
+
value: localStorageMock,
|
|
43
|
+
});
|
|
44
|
+
const devicePersistence = { enabled: false, storageKey };
|
|
32
45
|
manager = new SpeakerManager(
|
|
33
46
|
new Call({
|
|
34
47
|
id: '',
|
|
@@ -36,6 +49,7 @@ describe('SpeakerManager.test', () => {
|
|
|
36
49
|
streamClient: new StreamClient('abc123'),
|
|
37
50
|
clientStore: new StreamVideoWriteableStateStore(),
|
|
38
51
|
}),
|
|
52
|
+
devicePersistence,
|
|
39
53
|
);
|
|
40
54
|
});
|
|
41
55
|
|
|
@@ -111,8 +125,84 @@ describe('SpeakerManager.test', () => {
|
|
|
111
125
|
expect(manager.state.selectedDevice).toBe('');
|
|
112
126
|
});
|
|
113
127
|
|
|
128
|
+
describe('apply (web)', () => {
|
|
129
|
+
it('does nothing when persistence is disabled', () => {
|
|
130
|
+
const selectSpy = vi.spyOn(manager, 'select');
|
|
131
|
+
// @ts-expect-error - partial data
|
|
132
|
+
manager.apply({});
|
|
133
|
+
expect(selectSpy).not.toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('selects the persisted speaker device', () => {
|
|
137
|
+
const persistedManager = new SpeakerManager(
|
|
138
|
+
new Call({
|
|
139
|
+
id: '',
|
|
140
|
+
type: '',
|
|
141
|
+
streamClient: new StreamClient('abc123'),
|
|
142
|
+
clientStore: new StreamVideoWriteableStateStore(),
|
|
143
|
+
}),
|
|
144
|
+
{ enabled: true, storageKey },
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
localStorageMock.setItem(
|
|
148
|
+
storageKey,
|
|
149
|
+
JSON.stringify({
|
|
150
|
+
speaker: [
|
|
151
|
+
{
|
|
152
|
+
selectedDeviceId: 'speaker-1',
|
|
153
|
+
selectedDeviceLabel: 'Speaker 1',
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const selectSpy = vi.spyOn(persistedManager, 'select');
|
|
160
|
+
// @ts-expect-error - partial data
|
|
161
|
+
persistedManager.apply({});
|
|
162
|
+
|
|
163
|
+
expect(selectSpy).toHaveBeenCalledWith('speaker-1');
|
|
164
|
+
expect(persistedManager.state.selectedDevice).toBe('speaker-1');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('selects system default when persisted device is default', () => {
|
|
168
|
+
const persistedManager = new SpeakerManager(
|
|
169
|
+
new Call({
|
|
170
|
+
id: '',
|
|
171
|
+
type: '',
|
|
172
|
+
streamClient: new StreamClient('abc123'),
|
|
173
|
+
clientStore: new StreamVideoWriteableStateStore(),
|
|
174
|
+
}),
|
|
175
|
+
{ enabled: true, storageKey },
|
|
176
|
+
);
|
|
177
|
+
persistedManager.select('previous-device');
|
|
178
|
+
|
|
179
|
+
localStorageMock.setItem(
|
|
180
|
+
storageKey,
|
|
181
|
+
JSON.stringify({
|
|
182
|
+
speaker: [
|
|
183
|
+
{
|
|
184
|
+
selectedDeviceId: defaultDeviceId,
|
|
185
|
+
selectedDeviceLabel: '',
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const selectSpy = vi.spyOn(persistedManager, 'select');
|
|
192
|
+
// @ts-expect-error - partial data
|
|
193
|
+
persistedManager.apply({});
|
|
194
|
+
|
|
195
|
+
expect(selectSpy).toHaveBeenCalledWith('');
|
|
196
|
+
expect(persistedManager.state.selectedDevice).toBe('');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
114
200
|
afterEach(() => {
|
|
115
201
|
vi.clearAllMocks();
|
|
116
202
|
vi.resetModules();
|
|
203
|
+
Object.defineProperty(window, 'localStorage', {
|
|
204
|
+
configurable: true,
|
|
205
|
+
value: undefined,
|
|
206
|
+
});
|
|
117
207
|
});
|
|
118
208
|
});
|