@stream-io/video-client 1.41.0 → 1.41.2
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 +11 -0
- package/dist/index.browser.es.js +94 -23
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +94 -23
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +94 -23
- package/dist/index.es.js.map +1 -1
- package/dist/src/devices/SpeakerManager.d.ts +3 -0
- package/dist/src/helpers/DynascaleManager.d.ts +13 -1
- package/dist/src/helpers/types.d.ts +16 -0
- package/dist/src/types.d.ts +2 -2
- package/package.json +2 -2
- package/src/Call.ts +6 -4
- package/src/devices/SpeakerManager.ts +51 -15
- package/src/helpers/DynascaleManager.ts +57 -9
- package/src/helpers/__tests__/DynascaleManager.test.ts +8 -1
- package/src/helpers/types.ts +26 -0
- package/src/rtc/__tests__/mocks/webrtc.mocks.ts +1 -0
- package/src/types.ts +2 -2
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { Call } from '../Call';
|
|
2
2
|
import { SpeakerState } from './SpeakerState';
|
|
3
|
+
import { CallSettingsResponse } from '../gen/coordinator';
|
|
3
4
|
export declare class SpeakerManager {
|
|
4
5
|
readonly state: SpeakerState;
|
|
5
6
|
private subscriptions;
|
|
6
7
|
private areSubscriptionsSetUp;
|
|
7
8
|
private readonly call;
|
|
9
|
+
private defaultDevice?;
|
|
8
10
|
constructor(call: Call);
|
|
11
|
+
apply(settings: CallSettingsResponse): void;
|
|
9
12
|
setup(): void;
|
|
10
13
|
/**
|
|
11
14
|
* Lists the available audio output devices
|
|
@@ -5,6 +5,7 @@ import type { TrackSubscriptionDetails } from '../gen/video/sfu/signal_rpc/signa
|
|
|
5
5
|
import type { CallState } from '../store';
|
|
6
6
|
import type { StreamSfuClient } from '../StreamSfuClient';
|
|
7
7
|
import { SpeakerManager } from '../devices';
|
|
8
|
+
import { Tracer } from '../stats';
|
|
8
9
|
type VideoTrackSubscriptionOverride = {
|
|
9
10
|
enabled: true;
|
|
10
11
|
dimension: VideoDimension;
|
|
@@ -34,6 +35,8 @@ export declare class DynascaleManager {
|
|
|
34
35
|
private logger;
|
|
35
36
|
private callState;
|
|
36
37
|
private speaker;
|
|
38
|
+
private tracer;
|
|
39
|
+
private useWebAudio;
|
|
37
40
|
private audioContext;
|
|
38
41
|
private sfuClient;
|
|
39
42
|
private pendingSubscriptionsUpdate;
|
|
@@ -53,7 +56,7 @@ export declare class DynascaleManager {
|
|
|
53
56
|
/**
|
|
54
57
|
* Creates a new DynascaleManager instance.
|
|
55
58
|
*/
|
|
56
|
-
constructor(callState: CallState, speaker: SpeakerManager);
|
|
59
|
+
constructor(callState: CallState, speaker: SpeakerManager, tracer: Tracer);
|
|
57
60
|
/**
|
|
58
61
|
* Disposes the allocated resources and closes the audio context if it was created.
|
|
59
62
|
*/
|
|
@@ -79,6 +82,15 @@ export declare class DynascaleManager {
|
|
|
79
82
|
* @param element the viewport element.
|
|
80
83
|
*/
|
|
81
84
|
setViewport: <T extends HTMLElement>(element: T) => () => void;
|
|
85
|
+
/**
|
|
86
|
+
* Sets whether to use WebAudio API for audio playback.
|
|
87
|
+
* Must be set before joining the call.
|
|
88
|
+
*
|
|
89
|
+
* @internal
|
|
90
|
+
*
|
|
91
|
+
* @param useWebAudio whether to use WebAudio API.
|
|
92
|
+
*/
|
|
93
|
+
setUseWebAudio: (useWebAudio: boolean) => void;
|
|
82
94
|
/**
|
|
83
95
|
* Binds a DOM <video> element to the given session id.
|
|
84
96
|
* This method will make sure that the video element will play
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type AudioSessionState = 'inactive' | 'active' | 'interrupted';
|
|
2
|
+
export type AudioSessionType = 'auto' | 'playback' | 'transient' | 'transient-solo' | 'ambient' | 'play-and-record';
|
|
3
|
+
export interface AudioSession extends EventTarget {
|
|
4
|
+
type: AudioSessionType;
|
|
5
|
+
state: AudioSessionState;
|
|
6
|
+
onstatechange: EventListenerOrEventListenerObject;
|
|
7
|
+
}
|
|
8
|
+
declare global {
|
|
9
|
+
interface Navigator {
|
|
10
|
+
/**
|
|
11
|
+
* `audioSession` is available in Safari only. See:
|
|
12
|
+
* https://github.com/w3c/audio-session/blob/main/explainer.md
|
|
13
|
+
*/
|
|
14
|
+
audioSession?: AudioSession;
|
|
15
|
+
}
|
|
16
|
+
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -285,8 +285,8 @@ export type StreamRNVideoSDKGlobals = {
|
|
|
285
285
|
/**
|
|
286
286
|
* Sets up the in call manager.
|
|
287
287
|
*/
|
|
288
|
-
setup({
|
|
289
|
-
|
|
288
|
+
setup({ defaultDevice, }: {
|
|
289
|
+
defaultDevice: AudioSettingsRequestDefaultDeviceEnum;
|
|
290
290
|
}): void;
|
|
291
291
|
/**
|
|
292
292
|
* Starts the in call manager.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stream-io/video-client",
|
|
3
|
-
"version": "1.41.
|
|
3
|
+
"version": "1.41.2",
|
|
4
4
|
"main": "dist/index.cjs.js",
|
|
5
5
|
"module": "dist/index.es.js",
|
|
6
6
|
"browser": "dist/index.browser.es.js",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@openapitools/openapi-generator-cli": "^2.25.0",
|
|
47
47
|
"@rollup/plugin-replace": "^6.0.2",
|
|
48
48
|
"@rollup/plugin-typescript": "^12.1.4",
|
|
49
|
-
"@stream-io/audio-filters-web": "^0.7.
|
|
49
|
+
"@stream-io/audio-filters-web": "^0.7.2",
|
|
50
50
|
"@stream-io/node-sdk": "^0.7.28",
|
|
51
51
|
"@total-typescript/shoehorn": "^0.1.2",
|
|
52
52
|
"@types/sdp-transform": "^2.15.0",
|
package/src/Call.ts
CHANGED
|
@@ -339,7 +339,11 @@ export class Call {
|
|
|
339
339
|
this.microphone = new MicrophoneManager(this);
|
|
340
340
|
this.speaker = new SpeakerManager(this);
|
|
341
341
|
this.screenShare = new ScreenShareManager(this);
|
|
342
|
-
this.dynascaleManager = new DynascaleManager(
|
|
342
|
+
this.dynascaleManager = new DynascaleManager(
|
|
343
|
+
this.state,
|
|
344
|
+
this.speaker,
|
|
345
|
+
this.tracer,
|
|
346
|
+
);
|
|
343
347
|
}
|
|
344
348
|
|
|
345
349
|
/**
|
|
@@ -2694,9 +2698,7 @@ export class Call {
|
|
|
2694
2698
|
settings: CallSettingsResponse,
|
|
2695
2699
|
publish: boolean,
|
|
2696
2700
|
) => {
|
|
2697
|
-
|
|
2698
|
-
default_device: settings.audio.default_device,
|
|
2699
|
-
});
|
|
2701
|
+
this.speaker.apply(settings);
|
|
2700
2702
|
await this.camera.apply(settings.video, publish).catch((err) => {
|
|
2701
2703
|
this.logger.warn('Camera init failed', err);
|
|
2702
2704
|
});
|
|
@@ -3,12 +3,17 @@ import { Call } from '../Call';
|
|
|
3
3
|
import { isReactNative } from '../helpers/platforms';
|
|
4
4
|
import { SpeakerState } from './SpeakerState';
|
|
5
5
|
import { deviceIds$, getAudioOutputDevices } from './devices';
|
|
6
|
+
import {
|
|
7
|
+
CallSettingsResponse,
|
|
8
|
+
AudioSettingsRequestDefaultDeviceEnum,
|
|
9
|
+
} from '../gen/coordinator';
|
|
6
10
|
|
|
7
11
|
export class SpeakerManager {
|
|
8
12
|
readonly state: SpeakerState;
|
|
9
13
|
private subscriptions: Subscription[] = [];
|
|
10
14
|
private areSubscriptionsSetUp = false;
|
|
11
15
|
private readonly call: Call;
|
|
16
|
+
private defaultDevice?: AudioSettingsRequestDefaultDeviceEnum;
|
|
12
17
|
|
|
13
18
|
constructor(call: Call) {
|
|
14
19
|
this.call = call;
|
|
@@ -16,6 +21,41 @@ export class SpeakerManager {
|
|
|
16
21
|
this.setup();
|
|
17
22
|
}
|
|
18
23
|
|
|
24
|
+
apply(settings: CallSettingsResponse) {
|
|
25
|
+
if (!isReactNative()) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
/// Determines if the speaker should be enabled based on a priority hierarchy of
|
|
29
|
+
/// settings.
|
|
30
|
+
///
|
|
31
|
+
/// The priority order is as follows:
|
|
32
|
+
/// 1. If video camera is set to be on by default, speaker is enabled
|
|
33
|
+
/// 2. If audio speaker is set to be on by default, speaker is enabled
|
|
34
|
+
/// 3. If the default audio device is set to speaker, speaker is enabled
|
|
35
|
+
///
|
|
36
|
+
/// This ensures that the speaker state aligns with the most important user
|
|
37
|
+
/// preference or system requirement.
|
|
38
|
+
const speakerOnWithSettingsPriority =
|
|
39
|
+
settings.video.camera_default_on ||
|
|
40
|
+
settings.audio.speaker_default_on ||
|
|
41
|
+
settings.audio.default_device ===
|
|
42
|
+
AudioSettingsRequestDefaultDeviceEnum.SPEAKER;
|
|
43
|
+
|
|
44
|
+
const defaultDevice = speakerOnWithSettingsPriority
|
|
45
|
+
? AudioSettingsRequestDefaultDeviceEnum.SPEAKER
|
|
46
|
+
: AudioSettingsRequestDefaultDeviceEnum.EARPIECE;
|
|
47
|
+
|
|
48
|
+
if (this.defaultDevice !== defaultDevice) {
|
|
49
|
+
this.call.logger.debug('SpeakerManager: setting default device', {
|
|
50
|
+
defaultDevice,
|
|
51
|
+
});
|
|
52
|
+
this.defaultDevice = defaultDevice;
|
|
53
|
+
globalThis.streamRNVideoSDK?.callManager.setup({
|
|
54
|
+
defaultDevice,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
19
59
|
setup() {
|
|
20
60
|
if (this.areSubscriptionsSetUp) {
|
|
21
61
|
return;
|
|
@@ -51,11 +91,7 @@ export class SpeakerManager {
|
|
|
51
91
|
* @returns an Observable that will be updated if a device is connected or disconnected
|
|
52
92
|
*/
|
|
53
93
|
listDevices() {
|
|
54
|
-
|
|
55
|
-
throw new Error(
|
|
56
|
-
'This feature is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for more details',
|
|
57
|
-
);
|
|
58
|
-
}
|
|
94
|
+
assertUnsupportedInReactNative();
|
|
59
95
|
return getAudioOutputDevices(this.call.tracer);
|
|
60
96
|
}
|
|
61
97
|
|
|
@@ -67,11 +103,7 @@ export class SpeakerManager {
|
|
|
67
103
|
* @param deviceId empty string means the system default
|
|
68
104
|
*/
|
|
69
105
|
select(deviceId: string) {
|
|
70
|
-
|
|
71
|
-
throw new Error(
|
|
72
|
-
'This feature is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for more details',
|
|
73
|
-
);
|
|
74
|
-
}
|
|
106
|
+
assertUnsupportedInReactNative();
|
|
75
107
|
this.state.setDevice(deviceId);
|
|
76
108
|
}
|
|
77
109
|
|
|
@@ -93,11 +125,7 @@ export class SpeakerManager {
|
|
|
93
125
|
* Note: This method is not supported in React Native
|
|
94
126
|
*/
|
|
95
127
|
setVolume(volume: number) {
|
|
96
|
-
|
|
97
|
-
throw new Error(
|
|
98
|
-
'This feature is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for more details',
|
|
99
|
-
);
|
|
100
|
-
}
|
|
128
|
+
assertUnsupportedInReactNative();
|
|
101
129
|
if (volume && (volume < 0 || volume > 1)) {
|
|
102
130
|
throw new Error('Volume must be between 0 and 1');
|
|
103
131
|
}
|
|
@@ -125,3 +153,11 @@ export class SpeakerManager {
|
|
|
125
153
|
});
|
|
126
154
|
}
|
|
127
155
|
}
|
|
156
|
+
|
|
157
|
+
const assertUnsupportedInReactNative = () => {
|
|
158
|
+
if (isReactNative()) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
'Unsupported in React Native. See: https://getstream.io/video/docs/react-native/guides/camera-and-microphone/#speaker-management',
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
@@ -27,6 +27,7 @@ import type { StreamSfuClient } from '../StreamSfuClient';
|
|
|
27
27
|
import { SpeakerManager } from '../devices';
|
|
28
28
|
import { getCurrentValue, setCurrentValue } from '../store/rxUtils';
|
|
29
29
|
import { videoLoggerSystem } from '../logger';
|
|
30
|
+
import { Tracer } from '../stats';
|
|
30
31
|
|
|
31
32
|
const DEFAULT_VIEWPORT_VISIBILITY_STATE: Record<
|
|
32
33
|
VideoTrackType,
|
|
@@ -69,6 +70,8 @@ export class DynascaleManager {
|
|
|
69
70
|
private logger = videoLoggerSystem.getLogger('DynascaleManager');
|
|
70
71
|
private callState: CallState;
|
|
71
72
|
private speaker: SpeakerManager;
|
|
73
|
+
private tracer: Tracer;
|
|
74
|
+
private useWebAudio = isSafari();
|
|
72
75
|
private audioContext: AudioContext | undefined;
|
|
73
76
|
private sfuClient: StreamSfuClient | undefined;
|
|
74
77
|
private pendingSubscriptionsUpdate: NodeJS.Timeout | null = null;
|
|
@@ -113,9 +116,10 @@ export class DynascaleManager {
|
|
|
113
116
|
/**
|
|
114
117
|
* Creates a new DynascaleManager instance.
|
|
115
118
|
*/
|
|
116
|
-
constructor(callState: CallState, speaker: SpeakerManager) {
|
|
119
|
+
constructor(callState: CallState, speaker: SpeakerManager, tracer: Tracer) {
|
|
117
120
|
this.callState = callState;
|
|
118
121
|
this.speaker = speaker;
|
|
122
|
+
this.tracer = tracer;
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
/**
|
|
@@ -190,6 +194,10 @@ export class DynascaleManager {
|
|
|
190
194
|
override: VideoTrackSubscriptionOverride | undefined,
|
|
191
195
|
sessionIds?: string[],
|
|
192
196
|
) => {
|
|
197
|
+
this.tracer.trace('setVideoTrackSubscriptionOverrides', [
|
|
198
|
+
override,
|
|
199
|
+
sessionIds,
|
|
200
|
+
]);
|
|
193
201
|
if (!sessionIds) {
|
|
194
202
|
return setCurrentValue(
|
|
195
203
|
this.videoTrackSubscriptionOverridesSubject,
|
|
@@ -297,6 +305,19 @@ export class DynascaleManager {
|
|
|
297
305
|
return this.viewportTracker.setViewport(element);
|
|
298
306
|
};
|
|
299
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Sets whether to use WebAudio API for audio playback.
|
|
310
|
+
* Must be set before joining the call.
|
|
311
|
+
*
|
|
312
|
+
* @internal
|
|
313
|
+
*
|
|
314
|
+
* @param useWebAudio whether to use WebAudio API.
|
|
315
|
+
*/
|
|
316
|
+
setUseWebAudio = (useWebAudio: boolean) => {
|
|
317
|
+
this.tracer.trace('setUseWebAudio', useWebAudio);
|
|
318
|
+
this.useWebAudio = useWebAudio;
|
|
319
|
+
};
|
|
320
|
+
|
|
300
321
|
/**
|
|
301
322
|
* Binds a DOM <video> element to the given session id.
|
|
302
323
|
* This method will make sure that the video element will play
|
|
@@ -580,6 +601,7 @@ export class DynascaleManager {
|
|
|
580
601
|
// we will play audio directly through the audio element in other browsers
|
|
581
602
|
audioElement.muted = false;
|
|
582
603
|
audioElement.play().catch((e) => {
|
|
604
|
+
this.tracer.trace('audioPlaybackError', e.message);
|
|
583
605
|
this.logger.warn(`Failed to play audio stream`, e);
|
|
584
606
|
});
|
|
585
607
|
}
|
|
@@ -618,28 +640,54 @@ export class DynascaleManager {
|
|
|
618
640
|
};
|
|
619
641
|
|
|
620
642
|
private getOrCreateAudioContext = (): AudioContext | undefined => {
|
|
621
|
-
if (this.
|
|
643
|
+
if (!this.useWebAudio) return;
|
|
644
|
+
if (this.audioContext) return this.audioContext;
|
|
622
645
|
const context = new AudioContext();
|
|
646
|
+
this.tracer.trace('audioContext.create', context.state);
|
|
623
647
|
if (context.state === 'suspended') {
|
|
624
648
|
document.addEventListener('click', this.resumeAudioContext);
|
|
625
649
|
}
|
|
626
|
-
|
|
650
|
+
context.addEventListener('statechange', () => {
|
|
651
|
+
this.tracer.trace('audioContext.state', context.state);
|
|
652
|
+
if (context.state === 'interrupted') {
|
|
653
|
+
this.resumeAudioContext();
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
|
|
627
657
|
const audioSession = navigator.audioSession;
|
|
628
658
|
if (audioSession) {
|
|
629
659
|
// https://github.com/w3c/audio-session/blob/main/explainer.md
|
|
630
660
|
audioSession.type = 'play-and-record';
|
|
661
|
+
|
|
662
|
+
let isSessionInterrupted = false;
|
|
663
|
+
audioSession.addEventListener('statechange', () => {
|
|
664
|
+
this.tracer.trace('audioSession.state', audioSession.state);
|
|
665
|
+
if (audioSession.state === 'interrupted') {
|
|
666
|
+
isSessionInterrupted = true;
|
|
667
|
+
} else if (isSessionInterrupted) {
|
|
668
|
+
this.resumeAudioContext();
|
|
669
|
+
isSessionInterrupted = false;
|
|
670
|
+
}
|
|
671
|
+
});
|
|
631
672
|
}
|
|
632
673
|
return (this.audioContext = context);
|
|
633
674
|
};
|
|
634
675
|
|
|
635
676
|
private resumeAudioContext = () => {
|
|
636
|
-
if (this.audioContext
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
677
|
+
if (!this.audioContext) return;
|
|
678
|
+
const { state } = this.audioContext;
|
|
679
|
+
if (state === 'suspended' || state === 'interrupted') {
|
|
680
|
+
const tag = 'audioContext.resume';
|
|
681
|
+
this.audioContext.resume().then(
|
|
682
|
+
() => {
|
|
683
|
+
this.tracer.trace(tag, this.audioContext?.state);
|
|
641
684
|
document.removeEventListener('click', this.resumeAudioContext);
|
|
642
|
-
}
|
|
685
|
+
},
|
|
686
|
+
(err) => {
|
|
687
|
+
this.tracer.trace(`${tag}Error`, this.audioContext?.state);
|
|
688
|
+
this.logger.warn(`Can't resume audio context`, err);
|
|
689
|
+
},
|
|
690
|
+
);
|
|
643
691
|
}
|
|
644
692
|
};
|
|
645
693
|
}
|
|
@@ -34,7 +34,11 @@ describe('DynascaleManager', () => {
|
|
|
34
34
|
clientStore: new StreamVideoWriteableStateStore(),
|
|
35
35
|
});
|
|
36
36
|
call.setSortParticipantsBy(noopComparator());
|
|
37
|
-
dynascaleManager = new DynascaleManager(
|
|
37
|
+
dynascaleManager = new DynascaleManager(
|
|
38
|
+
call.state,
|
|
39
|
+
call.speaker,
|
|
40
|
+
call.tracer,
|
|
41
|
+
);
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
afterEach(() => {
|
|
@@ -108,6 +112,8 @@ describe('DynascaleManager', () => {
|
|
|
108
112
|
};
|
|
109
113
|
});
|
|
110
114
|
|
|
115
|
+
dynascaleManager.setUseWebAudio(false);
|
|
116
|
+
|
|
111
117
|
videoElement = document.createElement('video');
|
|
112
118
|
|
|
113
119
|
// circumvent happy-dom's extensive validation rules
|
|
@@ -189,6 +195,7 @@ describe('DynascaleManager', () => {
|
|
|
189
195
|
|
|
190
196
|
it('audio: Safari should use AudioContext for audio playback', () => {
|
|
191
197
|
globalThis._isSafari = true;
|
|
198
|
+
dynascaleManager.setUseWebAudio(true); // enabled by default on Safari
|
|
192
199
|
|
|
193
200
|
vi.useFakeTimers();
|
|
194
201
|
const audioElement = document.createElement('audio');
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type AudioSessionState = 'inactive' | 'active' | 'interrupted';
|
|
2
|
+
|
|
3
|
+
export type AudioSessionType =
|
|
4
|
+
| 'auto'
|
|
5
|
+
| 'playback'
|
|
6
|
+
| 'transient'
|
|
7
|
+
| 'transient-solo'
|
|
8
|
+
| 'ambient'
|
|
9
|
+
| 'play-and-record';
|
|
10
|
+
|
|
11
|
+
export interface AudioSession extends EventTarget {
|
|
12
|
+
type: AudioSessionType;
|
|
13
|
+
state: AudioSessionState;
|
|
14
|
+
|
|
15
|
+
onstatechange: EventListenerOrEventListenerObject;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
declare global {
|
|
19
|
+
interface Navigator {
|
|
20
|
+
/**
|
|
21
|
+
* `audioSession` is available in Safari only. See:
|
|
22
|
+
* https://github.com/w3c/audio-session/blob/main/explainer.md
|
|
23
|
+
*/
|
|
24
|
+
audioSession?: AudioSession;
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -351,9 +351,9 @@ export type StreamRNVideoSDKGlobals = {
|
|
|
351
351
|
* Sets up the in call manager.
|
|
352
352
|
*/
|
|
353
353
|
setup({
|
|
354
|
-
|
|
354
|
+
defaultDevice,
|
|
355
355
|
}: {
|
|
356
|
-
|
|
356
|
+
defaultDevice: AudioSettingsRequestDefaultDeviceEnum;
|
|
357
357
|
}): void;
|
|
358
358
|
|
|
359
359
|
/**
|