@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
|
@@ -2,15 +2,9 @@ import { Call } from './Call';
|
|
|
2
2
|
import { StreamClient } from './coordinator/connection/client';
|
|
3
3
|
import { StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore } from './store';
|
|
4
4
|
import type { ConnectedEvent, CreateDeviceRequest, CreateGuestRequest, CreateGuestResponse, GetEdgesResponse, ListDevicesResponse, QueryAggregateCallStatsRequest, QueryAggregateCallStatsResponse, QueryCallsRequest, QueryCallStatsRequest, QueryCallStatsResponse } from './gen/coordinator';
|
|
5
|
-
import { AllClientEvents, ClientEventListener, StreamClientOptions, TokenOrProvider,
|
|
5
|
+
import { AllClientEvents, ClientEventListener, StreamClientOptions, TokenOrProvider, User } from './coordinator/connection/types';
|
|
6
|
+
import type { StreamVideoClientOptions } from './types';
|
|
6
7
|
import { ScopedLogger } from './logger';
|
|
7
|
-
export type StreamVideoClientOptions = {
|
|
8
|
-
apiKey: string;
|
|
9
|
-
options?: StreamClientOptions;
|
|
10
|
-
user?: User;
|
|
11
|
-
token?: string;
|
|
12
|
-
tokenProvider?: TokenProvider;
|
|
13
|
-
};
|
|
14
8
|
/**
|
|
15
9
|
* A `StreamVideoClient` instance lets you communicate with our API, and authenticate users.
|
|
16
10
|
*/
|
|
@@ -2,6 +2,7 @@ import { AxiosRequestConfig, AxiosResponse } from 'axios';
|
|
|
2
2
|
import { ConnectedEvent, UserRequest, VideoEvent } from '../../gen/coordinator';
|
|
3
3
|
import { AllSfuEvents } from '../../rtc';
|
|
4
4
|
import type { ConfigureLoggersOptions, LogLevel } from '@stream-io/logger';
|
|
5
|
+
import type { DevicePersistenceOptions } from '../../devices/devicePersistence';
|
|
5
6
|
export type UR = Record<string, unknown>;
|
|
6
7
|
export type User = (Omit<UserRequest, 'role'> & {
|
|
7
8
|
type?: 'authenticated';
|
|
@@ -185,6 +186,10 @@ export type StreamClientOptions = Partial<AxiosRequestConfig> & {
|
|
|
185
186
|
* When set to true, the incoming calls are rejected when the user is busy in an another call.
|
|
186
187
|
*/
|
|
187
188
|
rejectCallWhenBusy?: boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Device persistence preference options (web only).
|
|
191
|
+
*/
|
|
192
|
+
devicePersistence?: DevicePersistenceOptions;
|
|
188
193
|
};
|
|
189
194
|
export type ClientAppIdentifier = {
|
|
190
195
|
sdkName?: 'react' | 'react-native' | 'plain-javascript' | (string & {});
|
|
@@ -3,21 +3,26 @@ import { Call } from '../Call';
|
|
|
3
3
|
import { CameraDirection, CameraManagerState } from './CameraManagerState';
|
|
4
4
|
import { DeviceManager } from './DeviceManager';
|
|
5
5
|
import { VideoSettingsResponse } from '../gen/coordinator';
|
|
6
|
+
import { DevicePersistenceOptions } from './devicePersistence';
|
|
6
7
|
export declare class CameraManager extends DeviceManager<CameraManagerState> {
|
|
7
8
|
private targetResolution;
|
|
8
9
|
/**
|
|
9
10
|
* Constructs a new CameraManager.
|
|
10
11
|
*
|
|
11
12
|
* @param call the call instance.
|
|
13
|
+
* @param devicePersistence the device persistence preferences to use.
|
|
12
14
|
*/
|
|
13
|
-
constructor(call: Call);
|
|
15
|
+
constructor(call: Call, devicePersistence: Required<DevicePersistenceOptions>);
|
|
14
16
|
private isDirectionSupportedByDevice;
|
|
15
17
|
/**
|
|
16
18
|
* Select the camera direction.
|
|
17
19
|
*
|
|
18
20
|
* @param direction the direction of the camera to select.
|
|
21
|
+
* @param options additional direction selection options.
|
|
19
22
|
*/
|
|
20
|
-
selectDirection(direction: Exclude<CameraDirection, undefined
|
|
23
|
+
selectDirection(direction: Exclude<CameraDirection, undefined>, options?: {
|
|
24
|
+
enableCamera?: boolean;
|
|
25
|
+
}): Promise<void>;
|
|
21
26
|
/**
|
|
22
27
|
* Flips the camera direction: if it's front it will change to back, if it's back, it will change to front.
|
|
23
28
|
*
|
|
@@ -5,6 +5,7 @@ import { DeviceManagerState } from './DeviceManagerState';
|
|
|
5
5
|
import { ScopedLogger } from '../logger';
|
|
6
6
|
import { TrackType } from '../gen/video/sfu/models/models';
|
|
7
7
|
import { MediaStreamFilter, MediaStreamFilterRegistrationResult } from './filters';
|
|
8
|
+
import { DevicePersistenceOptions } from './devicePersistence';
|
|
8
9
|
export declare abstract class DeviceManager<S extends DeviceManagerState<C>, C = MediaTrackConstraints> {
|
|
9
10
|
/**
|
|
10
11
|
* if true, stops the media stream when call is left
|
|
@@ -12,28 +13,16 @@ export declare abstract class DeviceManager<S extends DeviceManagerState<C>, C =
|
|
|
12
13
|
stopOnLeave: boolean;
|
|
13
14
|
logger: ScopedLogger;
|
|
14
15
|
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;
|
|
28
16
|
protected readonly call: Call;
|
|
29
17
|
protected readonly trackType: TrackType;
|
|
30
|
-
protected subscriptions:
|
|
18
|
+
protected subscriptions: (() => void)[];
|
|
19
|
+
protected devicePersistence: Required<DevicePersistenceOptions>;
|
|
31
20
|
protected areSubscriptionsSetUp: boolean;
|
|
32
21
|
private isTrackStoppedDueToTrackEnd;
|
|
33
22
|
private filters;
|
|
34
23
|
private statusChangeConcurrencyTag;
|
|
35
24
|
private filterRegistrationConcurrencyTag;
|
|
36
|
-
protected constructor(call: Call, state: S, trackType: TrackType);
|
|
25
|
+
protected constructor(call: Call, state: S, trackType: TrackType, devicePersistence: Required<DevicePersistenceOptions>);
|
|
37
26
|
setup(): void;
|
|
38
27
|
/**
|
|
39
28
|
* Lists the available audio/video devices
|
|
@@ -115,4 +104,7 @@ export declare abstract class DeviceManager<S extends DeviceManagerState<C>, C =
|
|
|
115
104
|
private get mediaDeviceKind();
|
|
116
105
|
private handleDisconnectedOrReplacedDevices;
|
|
117
106
|
protected findDevice(devices: MediaDeviceInfo[], deviceId: string): MediaDeviceInfo | undefined;
|
|
107
|
+
private persistPreference;
|
|
108
|
+
protected applyPersistedPreferences(enabledInCallType: boolean): Promise<boolean>;
|
|
109
|
+
private applyMutedState;
|
|
118
110
|
}
|
|
@@ -6,6 +6,7 @@ import { MicrophoneManagerState } from './MicrophoneManagerState';
|
|
|
6
6
|
import { TrackDisableMode } from './DeviceManagerState';
|
|
7
7
|
import { AudioBitrateProfile } from '../gen/video/sfu/models/models';
|
|
8
8
|
import { AudioSettingsResponse } from '../gen/coordinator';
|
|
9
|
+
import { DevicePersistenceOptions } from './devicePersistence';
|
|
9
10
|
export declare class MicrophoneManager extends AudioDeviceManager<MicrophoneManagerState> {
|
|
10
11
|
private speakingWhileMutedNotificationEnabled;
|
|
11
12
|
private soundDetectorConcurrencyTag;
|
|
@@ -18,7 +19,7 @@ export declare class MicrophoneManager extends AudioDeviceManager<MicrophoneMana
|
|
|
18
19
|
private noiseCancellationRegistration?;
|
|
19
20
|
private unregisterNoiseCancellation?;
|
|
20
21
|
private silenceThresholdMs;
|
|
21
|
-
constructor(call: Call, disableMode?: TrackDisableMode);
|
|
22
|
+
constructor(call: Call, devicePersistence: Required<DevicePersistenceOptions>, disableMode?: TrackDisableMode);
|
|
22
23
|
setup(): void;
|
|
23
24
|
/**
|
|
24
25
|
* Enables noise cancellation for the microphone.
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { Call } from '../Call';
|
|
2
2
|
import { SpeakerState } from './SpeakerState';
|
|
3
3
|
import { CallSettingsResponse } from '../gen/coordinator';
|
|
4
|
+
import { DevicePersistenceOptions } from './devicePersistence';
|
|
4
5
|
export declare class SpeakerManager {
|
|
5
6
|
readonly state: SpeakerState;
|
|
6
7
|
private subscriptions;
|
|
7
8
|
private areSubscriptionsSetUp;
|
|
8
9
|
private readonly call;
|
|
9
10
|
private defaultDevice?;
|
|
10
|
-
|
|
11
|
+
private readonly devicePersistence;
|
|
12
|
+
constructor(call: Call, devicePreferences: Required<DevicePersistenceOptions>);
|
|
11
13
|
apply(settings: CallSettingsResponse): void;
|
|
14
|
+
private applyWeb;
|
|
15
|
+
private applyRN;
|
|
12
16
|
setup(): void;
|
|
13
17
|
/**
|
|
14
18
|
* Lists the available audio output devices
|
|
@@ -47,4 +51,5 @@ export declare class SpeakerManager {
|
|
|
47
51
|
* @param volume a number between 0 and 1. Set it to `undefined` to use the default volume.
|
|
48
52
|
*/
|
|
49
53
|
setParticipantVolume(sessionId: string, volume: number | undefined): void;
|
|
54
|
+
private persistSpeakerDevicePreference;
|
|
50
55
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type DevicePersistenceOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* Enables device preference persistence on web.
|
|
4
|
+
* @default true
|
|
5
|
+
*/
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Storage key for persisted preferences.
|
|
9
|
+
* @default '@stream-io/device-preferences'
|
|
10
|
+
*/
|
|
11
|
+
storageKey?: string;
|
|
12
|
+
};
|
|
13
|
+
export type DevicePreferenceKey = 'microphone' | 'camera' | 'speaker';
|
|
14
|
+
export type LocalDevicePreference = {
|
|
15
|
+
selectedDeviceId: string;
|
|
16
|
+
selectedDeviceLabel: string;
|
|
17
|
+
muted?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export type LocalDevicePreferences = {
|
|
20
|
+
[type in DevicePreferenceKey]?: LocalDevicePreference | LocalDevicePreference[];
|
|
21
|
+
};
|
|
22
|
+
export declare const defaultDeviceId = "default";
|
|
23
|
+
export declare const normalize: (options: DevicePersistenceOptions | undefined) => Required<DevicePersistenceOptions>;
|
|
24
|
+
export declare const createSyntheticDevice: (deviceId: string, kind: MediaDeviceKind) => MediaDeviceInfo;
|
|
25
|
+
export declare const readPreferences: (storageKey: string) => LocalDevicePreferences;
|
|
26
|
+
export declare const writePreferences: (currentDevice: MediaDeviceInfo | undefined, deviceKey: DevicePreferenceKey, muted: boolean | undefined, storageKey: string) => void;
|
|
27
|
+
export declare const toPreferenceList: (preference?: LocalDevicePreference | LocalDevicePreference[]) => LocalDevicePreference[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { StreamClientOptions, TokenOrProvider, User } from '../coordinator/connection/types';
|
|
2
2
|
import { StreamClient } from '../coordinator/connection/client';
|
|
3
|
-
import type { StreamVideoClientOptions } from '../
|
|
3
|
+
import type { StreamVideoClientOptions } from '../types';
|
|
4
4
|
/**
|
|
5
5
|
* Utility function to get the instance key.
|
|
6
6
|
*/
|
|
@@ -33,7 +33,7 @@ export declare class PermissionsContext {
|
|
|
33
33
|
* Helper method that checks whether the current user has the permission
|
|
34
34
|
* to publish the given track type.
|
|
35
35
|
*/
|
|
36
|
-
canPublish: (trackType: TrackType) => boolean
|
|
36
|
+
canPublish: (trackType: TrackType) => boolean;
|
|
37
37
|
/**
|
|
38
38
|
* Checks if the current user can request a specific permission
|
|
39
39
|
* within the call.
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Participant, TrackType, VideoDimension } from './gen/video/sfu/models/models';
|
|
2
|
-
import type { CallRecordingStartedEventRecordingTypeEnum, JoinCallRequest, MemberResponse, OwnCapability, ReactionResponse,
|
|
2
|
+
import type { AudioSettingsRequestDefaultDeviceEnum, CallRecordingStartedEventRecordingTypeEnum, JoinCallRequest, MemberResponse, OwnCapability, ReactionResponse, StartRecordingRequest, StartRecordingResponse } from './gen/coordinator';
|
|
3
3
|
import type { StreamClient } from './coordinator/connection/client';
|
|
4
|
+
import type { RejectReason, StreamClientOptions, TokenProvider, User } from './coordinator/connection/types';
|
|
4
5
|
import type { Comparator } from './sorting';
|
|
5
6
|
import type { StreamVideoWriteableStateStore } from './store';
|
|
6
7
|
import { AxiosError } from 'axios';
|
|
7
|
-
import { RejectReason } from './coordinator/connection/types';
|
|
8
8
|
export type StreamReaction = Pick<ReactionResponse, 'type' | 'emoji_code' | 'custom'>;
|
|
9
9
|
export declare enum VisibilityState {
|
|
10
10
|
UNKNOWN = "UNKNOWN",
|
|
@@ -273,6 +273,42 @@ export type CallConstructor = {
|
|
|
273
273
|
*/
|
|
274
274
|
clientStore: StreamVideoWriteableStateStore;
|
|
275
275
|
};
|
|
276
|
+
type StreamVideoClientBaseOptions = {
|
|
277
|
+
apiKey: string;
|
|
278
|
+
options?: StreamClientOptions;
|
|
279
|
+
};
|
|
280
|
+
type StreamVideoClientOptionsWithoutUser = StreamVideoClientBaseOptions & {
|
|
281
|
+
user?: undefined;
|
|
282
|
+
token?: never;
|
|
283
|
+
tokenProvider?: never;
|
|
284
|
+
};
|
|
285
|
+
type GuestUser = Extract<User, {
|
|
286
|
+
type: 'guest';
|
|
287
|
+
}>;
|
|
288
|
+
type AnonymousUser = Extract<User, {
|
|
289
|
+
type: 'anonymous';
|
|
290
|
+
}>;
|
|
291
|
+
type AuthenticatedUser = Exclude<User, GuestUser | AnonymousUser>;
|
|
292
|
+
type StreamVideoClientOptionsWithGuestUser = StreamVideoClientBaseOptions & {
|
|
293
|
+
user: GuestUser;
|
|
294
|
+
token?: never;
|
|
295
|
+
tokenProvider?: never;
|
|
296
|
+
};
|
|
297
|
+
type StreamVideoClientOptionsWithAnonymousUser = StreamVideoClientBaseOptions & {
|
|
298
|
+
user: AnonymousUser;
|
|
299
|
+
token?: string;
|
|
300
|
+
tokenProvider?: TokenProvider;
|
|
301
|
+
};
|
|
302
|
+
type StreamVideoClientOptionsWithAuthenticatedUser = StreamVideoClientBaseOptions & {
|
|
303
|
+
user: AuthenticatedUser;
|
|
304
|
+
} & ({
|
|
305
|
+
token: string;
|
|
306
|
+
tokenProvider?: TokenProvider;
|
|
307
|
+
} | {
|
|
308
|
+
token?: string;
|
|
309
|
+
tokenProvider: TokenProvider;
|
|
310
|
+
});
|
|
311
|
+
export type StreamVideoClientOptions = StreamVideoClientOptionsWithoutUser | StreamVideoClientOptionsWithGuestUser | StreamVideoClientOptionsWithAnonymousUser | StreamVideoClientOptionsWithAuthenticatedUser;
|
|
276
312
|
export type CallRecordingType = CallRecordingStartedEventRecordingTypeEnum;
|
|
277
313
|
export type StartCallRecordingFnType = {
|
|
278
314
|
(): Promise<StartRecordingResponse>;
|
package/package.json
CHANGED
package/src/Call.ts
CHANGED
|
@@ -162,6 +162,7 @@ import {
|
|
|
162
162
|
ScreenShareManager,
|
|
163
163
|
SpeakerManager,
|
|
164
164
|
} from './devices';
|
|
165
|
+
import { normalize } from './devices/devicePersistence';
|
|
165
166
|
import { hasPending, withoutConcurrency } from './helpers/concurrency';
|
|
166
167
|
import { ensureExhausted } from './helpers/ensureExhausted';
|
|
167
168
|
import { pushToIfMissing } from './helpers/array';
|
|
@@ -341,9 +342,10 @@ export class Call {
|
|
|
341
342
|
ringing ? CallingState.RINGING : CallingState.IDLE,
|
|
342
343
|
);
|
|
343
344
|
|
|
344
|
-
|
|
345
|
-
this.
|
|
346
|
-
this.
|
|
345
|
+
const preferences = normalize(streamClient.options.devicePersistence);
|
|
346
|
+
this.camera = new CameraManager(this, preferences);
|
|
347
|
+
this.microphone = new MicrophoneManager(this, preferences);
|
|
348
|
+
this.speaker = new SpeakerManager(this, preferences);
|
|
347
349
|
this.screenShare = new ScreenShareManager(this);
|
|
348
350
|
this.dynascaleManager = new DynascaleManager(
|
|
349
351
|
this.state,
|
package/src/StreamVideoClient.ts
CHANGED
|
@@ -27,10 +27,10 @@ import {
|
|
|
27
27
|
ErrorFromResponse,
|
|
28
28
|
StreamClientOptions,
|
|
29
29
|
TokenOrProvider,
|
|
30
|
-
TokenProvider,
|
|
31
30
|
User,
|
|
32
31
|
UserWithId,
|
|
33
32
|
} from './coordinator/connection/types';
|
|
33
|
+
import type { StreamVideoClientOptions } from './types';
|
|
34
34
|
import { retryInterval, sleep } from './coordinator/connection/utils';
|
|
35
35
|
import {
|
|
36
36
|
createCoordinatorClient,
|
|
@@ -42,14 +42,6 @@ import { logToConsole, ScopedLogger, videoLoggerSystem } from './logger';
|
|
|
42
42
|
import { withoutConcurrency } from './helpers/concurrency';
|
|
43
43
|
import { enableTimerWorker } from './timers';
|
|
44
44
|
|
|
45
|
-
export type StreamVideoClientOptions = {
|
|
46
|
-
apiKey: string;
|
|
47
|
-
options?: StreamClientOptions;
|
|
48
|
-
user?: User;
|
|
49
|
-
token?: string;
|
|
50
|
-
tokenProvider?: TokenProvider;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
45
|
/**
|
|
54
46
|
* A `StreamVideoClient` instance lets you communicate with our API, and authenticate users.
|
|
55
47
|
*/
|
|
@@ -2,6 +2,7 @@ import { AxiosRequestConfig, AxiosResponse } from 'axios';
|
|
|
2
2
|
import { ConnectedEvent, UserRequest, VideoEvent } from '../../gen/coordinator';
|
|
3
3
|
import { AllSfuEvents } from '../../rtc';
|
|
4
4
|
import type { ConfigureLoggersOptions, LogLevel } from '@stream-io/logger';
|
|
5
|
+
import type { DevicePersistenceOptions } from '../../devices/devicePersistence';
|
|
5
6
|
|
|
6
7
|
export type UR = Record<string, unknown>;
|
|
7
8
|
|
|
@@ -258,6 +259,11 @@ export type StreamClientOptions = Partial<AxiosRequestConfig> & {
|
|
|
258
259
|
* When set to true, the incoming calls are rejected when the user is busy in an another call.
|
|
259
260
|
*/
|
|
260
261
|
rejectCallWhenBusy?: boolean;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Device persistence preference options (web only).
|
|
265
|
+
*/
|
|
266
|
+
devicePersistence?: DevicePersistenceOptions;
|
|
261
267
|
};
|
|
262
268
|
|
|
263
269
|
export type ClientAppIdentifier = {
|
|
@@ -7,6 +7,7 @@ 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';
|
|
10
|
+
import { DevicePersistenceOptions } from './devicePersistence';
|
|
10
11
|
|
|
11
12
|
export class CameraManager extends DeviceManager<CameraManagerState> {
|
|
12
13
|
private targetResolution = {
|
|
@@ -18,9 +19,13 @@ export class CameraManager extends DeviceManager<CameraManagerState> {
|
|
|
18
19
|
* Constructs a new CameraManager.
|
|
19
20
|
*
|
|
20
21
|
* @param call the call instance.
|
|
22
|
+
* @param devicePersistence the device persistence preferences to use.
|
|
21
23
|
*/
|
|
22
|
-
constructor(
|
|
23
|
-
|
|
24
|
+
constructor(
|
|
25
|
+
call: Call,
|
|
26
|
+
devicePersistence: Required<DevicePersistenceOptions>,
|
|
27
|
+
) {
|
|
28
|
+
super(call, new CameraManagerState(), TrackType.VIDEO, devicePersistence);
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
private isDirectionSupportedByDevice() {
|
|
@@ -31,8 +36,12 @@ export class CameraManager extends DeviceManager<CameraManagerState> {
|
|
|
31
36
|
* Select the camera direction.
|
|
32
37
|
*
|
|
33
38
|
* @param direction the direction of the camera to select.
|
|
39
|
+
* @param options additional direction selection options.
|
|
34
40
|
*/
|
|
35
|
-
async selectDirection(
|
|
41
|
+
async selectDirection(
|
|
42
|
+
direction: Exclude<CameraDirection, undefined>,
|
|
43
|
+
options: { enableCamera?: boolean } = {},
|
|
44
|
+
) {
|
|
36
45
|
if (!this.isDirectionSupportedByDevice()) {
|
|
37
46
|
this.logger.warn('Setting direction is not supported on this device');
|
|
38
47
|
return;
|
|
@@ -47,9 +56,10 @@ export class CameraManager extends DeviceManager<CameraManagerState> {
|
|
|
47
56
|
// providing both device id and direction doesn't work, so we deselect the device
|
|
48
57
|
this.state.setDirection(direction);
|
|
49
58
|
this.state.setDevice(undefined);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
|
|
60
|
+
const { enableCamera = true } = options;
|
|
61
|
+
if (isReactNative() || !enableCamera) return;
|
|
62
|
+
|
|
53
63
|
this.getTracks().forEach((track) => track.stop());
|
|
54
64
|
try {
|
|
55
65
|
await this.unmuteStream();
|
|
@@ -120,16 +130,26 @@ export class CameraManager extends DeviceManager<CameraManagerState> {
|
|
|
120
130
|
await this.statusChangeSettled();
|
|
121
131
|
await this.selectTargetResolution(settings.target_resolution);
|
|
122
132
|
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
const enabledInCallType = settings.enabled ?? true;
|
|
134
|
+
const shouldApplyDefaults =
|
|
135
|
+
this.state.status === undefined &&
|
|
136
|
+
this.state.optimisticStatus === undefined;
|
|
137
|
+
let persistedPreferencesApplied = false;
|
|
138
|
+
if (shouldApplyDefaults && this.devicePersistence.enabled) {
|
|
139
|
+
persistedPreferencesApplied =
|
|
140
|
+
await this.applyPersistedPreferences(enabledInCallType);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// apply a direction and enable the camera only if in "pristine" state,
|
|
144
|
+
// and there are no persisted preferences
|
|
125
145
|
const canPublish = this.call.permissionsContext.canPublish(this.trackType);
|
|
126
|
-
if (
|
|
146
|
+
if (shouldApplyDefaults && !persistedPreferencesApplied) {
|
|
127
147
|
if (!this.state.direction && !this.state.selectedDevice) {
|
|
128
148
|
const direction = settings.camera_facing === 'front' ? 'front' : 'back';
|
|
129
|
-
await this.selectDirection(direction);
|
|
149
|
+
await this.selectDirection(direction, { enableCamera: false });
|
|
130
150
|
}
|
|
131
151
|
|
|
132
|
-
if (canPublish && settings.camera_default_on &&
|
|
152
|
+
if (canPublish && settings.camera_default_on && enabledInCallType) {
|
|
133
153
|
await this.enable();
|
|
134
154
|
}
|
|
135
155
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { combineLatest, Observable, pairwise } from 'rxjs';
|
|
1
|
+
import { combineLatest, firstValueFrom, Observable, pairwise } from 'rxjs';
|
|
2
2
|
import { Call } from '../Call';
|
|
3
3
|
import { TrackPublishOptions } from '../rtc';
|
|
4
4
|
import { CallingState } from '../store';
|
|
5
|
-
import { createSubscription } from '../store/rxUtils';
|
|
6
|
-
import {
|
|
5
|
+
import { createSubscription, getCurrentValue } from '../store/rxUtils';
|
|
6
|
+
import {
|
|
7
|
+
DeviceManagerState,
|
|
8
|
+
type InputDeviceStatus,
|
|
9
|
+
} from './DeviceManagerState';
|
|
7
10
|
import { isMobile } from '../helpers/compatibility';
|
|
8
11
|
import { isReactNative } from '../helpers/platforms';
|
|
9
12
|
import { ScopedLogger, videoLoggerSystem } from '../logger';
|
|
@@ -19,6 +22,15 @@ import {
|
|
|
19
22
|
MediaStreamFilterEntry,
|
|
20
23
|
MediaStreamFilterRegistrationResult,
|
|
21
24
|
} from './filters';
|
|
25
|
+
import {
|
|
26
|
+
createSyntheticDevice,
|
|
27
|
+
defaultDeviceId,
|
|
28
|
+
DevicePersistenceOptions,
|
|
29
|
+
DevicePreferenceKey,
|
|
30
|
+
readPreferences,
|
|
31
|
+
toPreferenceList,
|
|
32
|
+
writePreferences,
|
|
33
|
+
} from './devicePersistence';
|
|
22
34
|
|
|
23
35
|
export abstract class DeviceManager<
|
|
24
36
|
S extends DeviceManagerState<C>,
|
|
@@ -32,23 +44,10 @@ export abstract class DeviceManager<
|
|
|
32
44
|
|
|
33
45
|
state: S;
|
|
34
46
|
|
|
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
|
-
|
|
49
47
|
protected readonly call: Call;
|
|
50
48
|
protected readonly trackType: TrackType;
|
|
51
|
-
protected subscriptions:
|
|
49
|
+
protected subscriptions: (() => void)[] = [];
|
|
50
|
+
protected devicePersistence: Required<DevicePersistenceOptions>;
|
|
52
51
|
protected areSubscriptionsSetUp = false;
|
|
53
52
|
private isTrackStoppedDueToTrackEnd = false;
|
|
54
53
|
private filters: MediaStreamFilterEntry[] = [];
|
|
@@ -57,10 +56,16 @@ export abstract class DeviceManager<
|
|
|
57
56
|
'filterRegistrationConcurrencyTag',
|
|
58
57
|
);
|
|
59
58
|
|
|
60
|
-
protected constructor(
|
|
59
|
+
protected constructor(
|
|
60
|
+
call: Call,
|
|
61
|
+
state: S,
|
|
62
|
+
trackType: TrackType,
|
|
63
|
+
devicePersistence: Required<DevicePersistenceOptions>,
|
|
64
|
+
) {
|
|
61
65
|
this.call = call;
|
|
62
66
|
this.state = state;
|
|
63
67
|
this.trackType = trackType;
|
|
68
|
+
this.devicePersistence = devicePersistence;
|
|
64
69
|
this.logger = videoLoggerSystem.getLogger(
|
|
65
70
|
`${TrackType[trackType].toLowerCase()} manager`,
|
|
66
71
|
);
|
|
@@ -68,10 +73,7 @@ export abstract class DeviceManager<
|
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
setup() {
|
|
71
|
-
if (this.areSubscriptionsSetUp)
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
76
|
+
if (this.areSubscriptionsSetUp) return;
|
|
75
77
|
this.areSubscriptionsSetUp = true;
|
|
76
78
|
|
|
77
79
|
if (
|
|
@@ -81,6 +83,18 @@ export abstract class DeviceManager<
|
|
|
81
83
|
) {
|
|
82
84
|
this.handleDisconnectedOrReplacedDevices();
|
|
83
85
|
}
|
|
86
|
+
|
|
87
|
+
if (this.devicePersistence.enabled) {
|
|
88
|
+
this.subscriptions.push(
|
|
89
|
+
createSubscription(
|
|
90
|
+
combineLatest([this.state.selectedDevice$, this.state.status$]),
|
|
91
|
+
([selectedDevice, status]) => {
|
|
92
|
+
if (!status) return;
|
|
93
|
+
this.persistPreference(selectedDevice, status);
|
|
94
|
+
},
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
84
98
|
}
|
|
85
99
|
|
|
86
100
|
/**
|
|
@@ -495,14 +509,10 @@ export abstract class DeviceManager<
|
|
|
495
509
|
}
|
|
496
510
|
}
|
|
497
511
|
|
|
498
|
-
private get mediaDeviceKind() {
|
|
499
|
-
if (this.trackType === TrackType.AUDIO)
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
if (this.trackType === TrackType.VIDEO) {
|
|
503
|
-
return 'videoinput';
|
|
504
|
-
}
|
|
505
|
-
return '';
|
|
512
|
+
private get mediaDeviceKind(): MediaDeviceKind {
|
|
513
|
+
if (this.trackType === TrackType.AUDIO) return 'audioinput';
|
|
514
|
+
if (this.trackType === TrackType.VIDEO) return 'videoinput';
|
|
515
|
+
throw new Error('Invalid track type');
|
|
506
516
|
}
|
|
507
517
|
|
|
508
518
|
private handleDisconnectedOrReplacedDevices() {
|
|
@@ -562,4 +572,76 @@ export abstract class DeviceManager<
|
|
|
562
572
|
const kind = this.mediaDeviceKind;
|
|
563
573
|
return devices.find((d) => d.deviceId === deviceId && d.kind === kind);
|
|
564
574
|
}
|
|
575
|
+
|
|
576
|
+
private persistPreference(
|
|
577
|
+
selectedDevice: string | undefined,
|
|
578
|
+
status: InputDeviceStatus,
|
|
579
|
+
) {
|
|
580
|
+
const deviceKind = this.mediaDeviceKind;
|
|
581
|
+
const deviceKey = deviceKind === 'audioinput' ? 'microphone' : 'camera';
|
|
582
|
+
const muted =
|
|
583
|
+
status === 'disabled' ? true : status === 'enabled' ? false : undefined;
|
|
584
|
+
|
|
585
|
+
const { storageKey } = this.devicePersistence;
|
|
586
|
+
if (!selectedDevice) {
|
|
587
|
+
writePreferences(undefined, deviceKey, muted, storageKey);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const devices = getCurrentValue(this.listDevices()) || [];
|
|
592
|
+
const currentDevice =
|
|
593
|
+
this.findDevice(devices, selectedDevice) ??
|
|
594
|
+
createSyntheticDevice(selectedDevice, deviceKind);
|
|
595
|
+
|
|
596
|
+
writePreferences(currentDevice, deviceKey, muted, storageKey);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
protected async applyPersistedPreferences(enabledInCallType: boolean) {
|
|
600
|
+
const deviceKey: DevicePreferenceKey =
|
|
601
|
+
this.trackType === TrackType.AUDIO ? 'microphone' : 'camera';
|
|
602
|
+
const preferences = readPreferences(this.devicePersistence.storageKey);
|
|
603
|
+
const preferenceList = toPreferenceList(preferences[deviceKey]);
|
|
604
|
+
|
|
605
|
+
if (preferenceList.length === 0) return false;
|
|
606
|
+
|
|
607
|
+
let muted: boolean | undefined;
|
|
608
|
+
let appliedDevice = false;
|
|
609
|
+
let appliedMute = false;
|
|
610
|
+
|
|
611
|
+
const devices = await firstValueFrom(this.listDevices());
|
|
612
|
+
for (const preference of preferenceList) {
|
|
613
|
+
muted ??= preference.muted;
|
|
614
|
+
if (preference.selectedDeviceId === defaultDeviceId) break;
|
|
615
|
+
|
|
616
|
+
const device =
|
|
617
|
+
devices.find((d) => d.deviceId === preference.selectedDeviceId) ??
|
|
618
|
+
devices.find((d) => d.label === preference.selectedDeviceLabel);
|
|
619
|
+
|
|
620
|
+
if (device) {
|
|
621
|
+
appliedDevice = true;
|
|
622
|
+
if (!this.state.selectedDevice) {
|
|
623
|
+
await this.select(device.deviceId);
|
|
624
|
+
}
|
|
625
|
+
muted = preference.muted;
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const canPublish = this.call.permissionsContext.canPublish(this.trackType);
|
|
631
|
+
if (typeof muted === 'boolean' && enabledInCallType && canPublish) {
|
|
632
|
+
await this.applyMutedState(muted);
|
|
633
|
+
appliedMute = true;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return appliedDevice || appliedMute;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private async applyMutedState(muted: boolean) {
|
|
640
|
+
if (this.state.status !== undefined) return;
|
|
641
|
+
if (muted) {
|
|
642
|
+
await this.disable();
|
|
643
|
+
} else {
|
|
644
|
+
await this.enable();
|
|
645
|
+
}
|
|
646
|
+
}
|
|
565
647
|
}
|