@stream-io/video-client 1.46.1 → 1.48.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 +16 -0
- package/dist/index.browser.es.js +31 -197
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +30 -197
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.es.js +31 -197
- package/dist/index.es.js.map +1 -1
- package/dist/src/devices/MicrophoneManager.d.ts +0 -1
- package/dist/src/gen/coordinator/index.d.ts +6 -0
- package/dist/src/types.d.ts +11 -0
- package/index.ts +0 -1
- package/package.json +1 -1
- package/src/devices/CameraManager.ts +9 -2
- package/src/devices/DeviceManager.ts +13 -3
- package/src/devices/MicrophoneManager.ts +17 -6
- 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__/MicrophoneManagerRN.test.ts +28 -29
- package/src/devices/__tests__/SpeakerManager.test.ts +28 -0
- package/src/gen/coordinator/index.ts +6 -0
- package/src/types.ts +9 -0
- package/dist/src/helpers/RNSpeechDetector.d.ts +0 -23
- package/src/helpers/RNSpeechDetector.ts +0 -224
- package/src/helpers/__tests__/RNSpeechDetector.test.ts +0 -52
|
@@ -13,7 +13,6 @@ export declare class MicrophoneManager extends AudioDeviceManager<MicrophoneMana
|
|
|
13
13
|
private soundDetectorCleanup?;
|
|
14
14
|
private soundDetectorDeviceId?;
|
|
15
15
|
private noAudioDetectorCleanup?;
|
|
16
|
-
private rnSpeechDetector;
|
|
17
16
|
private noiseCancellation;
|
|
18
17
|
private noiseCancellationChangeUnsubscribe;
|
|
19
18
|
private noiseCancellationRegistration?;
|
|
@@ -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/dist/src/types.d.ts
CHANGED
|
@@ -371,6 +371,17 @@ export type StreamRNVideoSDKGlobals = {
|
|
|
371
371
|
*/
|
|
372
372
|
check(permission: 'microphone' | 'camera'): Promise<boolean>;
|
|
373
373
|
};
|
|
374
|
+
nativeEvents: {
|
|
375
|
+
speechActivity: {
|
|
376
|
+
/**
|
|
377
|
+
* Subscribes to native speech activity events.
|
|
378
|
+
* Returns an unsubscribe function.
|
|
379
|
+
*/
|
|
380
|
+
subscribe(cb: (state: {
|
|
381
|
+
isSoundDetected: boolean;
|
|
382
|
+
}) => void): () => void;
|
|
383
|
+
};
|
|
384
|
+
};
|
|
374
385
|
};
|
|
375
386
|
declare global {
|
|
376
387
|
var streamRNVideoSDK: StreamRNVideoSDKGlobals | undefined;
|
package/index.ts
CHANGED
|
@@ -23,7 +23,6 @@ export * from './src/helpers/DynascaleManager';
|
|
|
23
23
|
export * from './src/helpers/ViewportTracker';
|
|
24
24
|
export * from './src/helpers/sound-detector';
|
|
25
25
|
export * from './src/helpers/participantUtils';
|
|
26
|
-
export * from './src/helpers/RNSpeechDetector';
|
|
27
26
|
export * as Browsers from './src/helpers/browsers';
|
|
28
27
|
|
|
29
28
|
export * from './src/logger';
|
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
|
),
|
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
createSafeAsyncSubscription,
|
|
25
25
|
createSubscription,
|
|
26
26
|
} from '../store/rxUtils';
|
|
27
|
-
import { RNSpeechDetector } from '../helpers/RNSpeechDetector';
|
|
28
27
|
import { withoutConcurrency } from '../helpers/concurrency';
|
|
29
28
|
import { disposeOfMediaStream } from './utils';
|
|
30
29
|
import { promiseWithResolvers } from '../helpers/promise';
|
|
@@ -36,7 +35,6 @@ export class MicrophoneManager extends AudioDeviceManager<MicrophoneManagerState
|
|
|
36
35
|
private soundDetectorCleanup?: () => Promise<void>;
|
|
37
36
|
private soundDetectorDeviceId?: string;
|
|
38
37
|
private noAudioDetectorCleanup?: () => Promise<void>;
|
|
39
|
-
private rnSpeechDetector: RNSpeechDetector | undefined;
|
|
40
38
|
private noiseCancellation: INoiseCancellation | undefined;
|
|
41
39
|
private noiseCancellationChangeUnsubscribe: (() => void) | undefined;
|
|
42
40
|
private noiseCancellationRegistration?: Promise<void>;
|
|
@@ -356,7 +354,14 @@ export class MicrophoneManager extends AudioDeviceManager<MicrophoneManagerState
|
|
|
356
354
|
this.state.status === undefined &&
|
|
357
355
|
this.state.optimisticStatus === undefined;
|
|
358
356
|
let persistedPreferencesApplied = false;
|
|
359
|
-
|
|
357
|
+
const permissionState = await firstValueFrom(
|
|
358
|
+
this.state.browserPermissionState$,
|
|
359
|
+
);
|
|
360
|
+
if (
|
|
361
|
+
shouldApplyDefaults &&
|
|
362
|
+
this.devicePersistence.enabled &&
|
|
363
|
+
permissionState === 'granted'
|
|
364
|
+
) {
|
|
360
365
|
persistedPreferencesApplied = await this.applyPersistedPreferences(true);
|
|
361
366
|
}
|
|
362
367
|
|
|
@@ -415,13 +420,19 @@ export class MicrophoneManager extends AudioDeviceManager<MicrophoneManagerState
|
|
|
415
420
|
await this.teardownSpeakingWhileMutedDetection();
|
|
416
421
|
|
|
417
422
|
if (isReactNative()) {
|
|
418
|
-
|
|
419
|
-
|
|
423
|
+
const speechActivity =
|
|
424
|
+
globalThis.streamRNVideoSDK?.nativeEvents?.speechActivity;
|
|
425
|
+
if (!speechActivity) {
|
|
426
|
+
this.logger.warn(
|
|
427
|
+
'Native speech activity not available, make sure the "@stream-io/react-native-webrtc" peer dependency version is satisfied',
|
|
428
|
+
);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const unsubscribe = speechActivity.subscribe((event) => {
|
|
420
432
|
this.state.setSpeakingWhileMuted(event.isSoundDetected);
|
|
421
433
|
});
|
|
422
434
|
this.soundDetectorCleanup = async () => {
|
|
423
435
|
unsubscribe();
|
|
424
|
-
this.rnSpeechDetector = undefined;
|
|
425
436
|
};
|
|
426
437
|
} else {
|
|
427
438
|
// Need to start a new stream that's not connected to publisher
|
|
@@ -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');
|
|
@@ -12,11 +12,12 @@ import {
|
|
|
12
12
|
import { of } from 'rxjs';
|
|
13
13
|
import '../../rtc/__tests__/mocks/webrtc.mocks';
|
|
14
14
|
import { OwnCapability } from '../../gen/coordinator';
|
|
15
|
-
import { SoundStateChangeHandler } from '../../helpers/sound-detector';
|
|
16
15
|
import { settled, withoutConcurrency } from '../../helpers/concurrency';
|
|
17
16
|
|
|
18
|
-
let
|
|
19
|
-
|
|
17
|
+
let speechActivityCallback:
|
|
18
|
+
| ((state: { isSoundDetected: boolean }) => void)
|
|
19
|
+
| null = null;
|
|
20
|
+
let unsubscribeMocks: ReturnType<typeof vi.fn>[] = [];
|
|
20
21
|
|
|
21
22
|
vi.mock('../../helpers/platforms.ts', () => {
|
|
22
23
|
return {
|
|
@@ -46,28 +47,21 @@ vi.mock('../../Call.ts', () => {
|
|
|
46
47
|
};
|
|
47
48
|
});
|
|
48
49
|
|
|
49
|
-
vi.mock('../../helpers/RNSpeechDetector.ts', () => {
|
|
50
|
-
console.log('MOCKING RNSpeechDetector');
|
|
51
|
-
return {
|
|
52
|
-
RNSpeechDetector: vi.fn().mockImplementation(() => ({
|
|
53
|
-
start: vi.fn((callback) => {
|
|
54
|
-
handler = callback;
|
|
55
|
-
const unsubscribe = vi.fn();
|
|
56
|
-
unsubscribeHandlers.push(unsubscribe);
|
|
57
|
-
return unsubscribe;
|
|
58
|
-
}),
|
|
59
|
-
stop: vi.fn(),
|
|
60
|
-
onSpeakingDetectedStateChange: vi.fn(),
|
|
61
|
-
})),
|
|
62
|
-
};
|
|
63
|
-
});
|
|
64
|
-
|
|
65
50
|
describe('MicrophoneManager React Native', () => {
|
|
66
51
|
let manager: MicrophoneManager;
|
|
67
52
|
let checkPermissionMock: ReturnType<typeof vi.fn>;
|
|
53
|
+
let subscribeMock: ReturnType<typeof vi.fn>;
|
|
54
|
+
|
|
68
55
|
beforeEach(() => {
|
|
69
|
-
|
|
56
|
+
speechActivityCallback = null;
|
|
57
|
+
unsubscribeMocks = [];
|
|
70
58
|
checkPermissionMock = vi.fn(async () => true);
|
|
59
|
+
subscribeMock = vi.fn((cb) => {
|
|
60
|
+
speechActivityCallback = cb;
|
|
61
|
+
const unsub = vi.fn();
|
|
62
|
+
unsubscribeMocks.push(unsub);
|
|
63
|
+
return unsub;
|
|
64
|
+
});
|
|
71
65
|
|
|
72
66
|
globalThis.streamRNVideoSDK = {
|
|
73
67
|
callManager: {
|
|
@@ -78,6 +72,11 @@ describe('MicrophoneManager React Native', () => {
|
|
|
78
72
|
permissions: {
|
|
79
73
|
check: checkPermissionMock,
|
|
80
74
|
},
|
|
75
|
+
nativeEvents: {
|
|
76
|
+
speechActivity: {
|
|
77
|
+
subscribe: subscribeMock,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
81
80
|
};
|
|
82
81
|
|
|
83
82
|
const devicePersistence = { enabled: false, storageKey: '' };
|
|
@@ -100,7 +99,7 @@ describe('MicrophoneManager React Native', () => {
|
|
|
100
99
|
|
|
101
100
|
await vi.waitUntil(() => fn.mock.calls.length > 0, { timeout: 100 });
|
|
102
101
|
expect(fn).toHaveBeenCalled();
|
|
103
|
-
expect(
|
|
102
|
+
expect(subscribeMock).toHaveBeenCalled();
|
|
104
103
|
});
|
|
105
104
|
|
|
106
105
|
it('should check native microphone permission before starting detection', async () => {
|
|
@@ -146,15 +145,15 @@ describe('MicrophoneManager React Native', () => {
|
|
|
146
145
|
|
|
147
146
|
it('should update speaking while muted state', async () => {
|
|
148
147
|
await manager['startSpeakingWhileMutedDetection']();
|
|
149
|
-
expect(
|
|
148
|
+
expect(subscribeMock).toHaveBeenCalled();
|
|
150
149
|
|
|
151
150
|
expect(manager.state.speakingWhileMuted).toBe(false);
|
|
152
151
|
|
|
153
|
-
|
|
152
|
+
speechActivityCallback!({ isSoundDetected: true });
|
|
154
153
|
|
|
155
154
|
expect(manager.state.speakingWhileMuted).toBe(true);
|
|
156
155
|
|
|
157
|
-
|
|
156
|
+
speechActivityCallback!({ isSoundDetected: false });
|
|
158
157
|
|
|
159
158
|
expect(manager.state.speakingWhileMuted).toBe(false);
|
|
160
159
|
});
|
|
@@ -163,21 +162,21 @@ describe('MicrophoneManager React Native', () => {
|
|
|
163
162
|
await manager['startSpeakingWhileMutedDetection']('device-1');
|
|
164
163
|
await manager['startSpeakingWhileMutedDetection']('device-1');
|
|
165
164
|
|
|
166
|
-
expect(
|
|
165
|
+
expect(unsubscribeMocks).toHaveLength(1);
|
|
167
166
|
|
|
168
167
|
await manager['stopSpeakingWhileMutedDetection']();
|
|
169
|
-
expect(
|
|
168
|
+
expect(unsubscribeMocks[0]).toHaveBeenCalledTimes(1);
|
|
170
169
|
});
|
|
171
170
|
|
|
172
171
|
it('should cleanup previous speech detector before starting a new one', async () => {
|
|
173
172
|
await manager['startSpeakingWhileMutedDetection']('device-1');
|
|
174
173
|
await manager['startSpeakingWhileMutedDetection']('device-2');
|
|
175
174
|
|
|
176
|
-
expect(
|
|
177
|
-
expect(
|
|
175
|
+
expect(unsubscribeMocks).toHaveLength(2);
|
|
176
|
+
expect(unsubscribeMocks[0]).toHaveBeenCalledTimes(1);
|
|
178
177
|
|
|
179
178
|
await manager['stopSpeakingWhileMutedDetection']();
|
|
180
|
-
expect(
|
|
179
|
+
expect(unsubscribeMocks[1]).toHaveBeenCalledTimes(1);
|
|
181
180
|
});
|
|
182
181
|
|
|
183
182
|
it('should stop speaking while muted notifications if user loses permission to send audio', async () => {
|
|
@@ -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}
|
package/src/types.ts
CHANGED
|
@@ -462,6 +462,15 @@ export type StreamRNVideoSDKGlobals = {
|
|
|
462
462
|
*/
|
|
463
463
|
check(permission: 'microphone' | 'camera'): Promise<boolean>;
|
|
464
464
|
};
|
|
465
|
+
nativeEvents: {
|
|
466
|
+
speechActivity: {
|
|
467
|
+
/**
|
|
468
|
+
* Subscribes to native speech activity events.
|
|
469
|
+
* Returns an unsubscribe function.
|
|
470
|
+
*/
|
|
471
|
+
subscribe(cb: (state: { isSoundDetected: boolean }) => void): () => void;
|
|
472
|
+
};
|
|
473
|
+
};
|
|
465
474
|
};
|
|
466
475
|
|
|
467
476
|
declare global {
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { SoundStateChangeHandler } from './sound-detector';
|
|
2
|
-
export declare class RNSpeechDetector {
|
|
3
|
-
private readonly pc1;
|
|
4
|
-
private readonly pc2;
|
|
5
|
-
private audioStream;
|
|
6
|
-
private externalAudioStream;
|
|
7
|
-
private isStopped;
|
|
8
|
-
constructor(externalAudioStream?: MediaStream);
|
|
9
|
-
/**
|
|
10
|
-
* Starts the speech detection.
|
|
11
|
-
*/
|
|
12
|
-
start(onSoundDetectedStateChanged: SoundStateChangeHandler): Promise<() => void>;
|
|
13
|
-
/**
|
|
14
|
-
* Stops the speech detection and releases all allocated resources.
|
|
15
|
-
*/
|
|
16
|
-
private stop;
|
|
17
|
-
/**
|
|
18
|
-
* Public method that detects the audio levels and returns the status.
|
|
19
|
-
*/
|
|
20
|
-
private onSpeakingDetectedStateChange;
|
|
21
|
-
private cleanupAudioStream;
|
|
22
|
-
private forwardIceCandidate;
|
|
23
|
-
}
|