Skip to content

Commit

Permalink
fix: cover some device selection edge cases
Browse files Browse the repository at this point in the history
  • Loading branch information
myandrienko committed Nov 27, 2024
1 parent da6461b commit c238dd8
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 50 deletions.
38 changes: 16 additions & 22 deletions packages/client/src/devices/devices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export const getAudioStream = async (
const constraints: MediaStreamConstraints = {
audio: {
...audioDeviceConstraints.audio,
...trackConstraints,
...normalizeContraints(trackConstraints),
},
};

Expand All @@ -195,16 +195,6 @@ export const getAudioStream = async (
});
return await getStream(constraints);
} catch (error) {
if (error instanceof OverconstrainedError && trackConstraints?.deviceId) {
const { deviceId, ...relaxedContraints } = trackConstraints;
getLogger(['devices'])(
'warn',
'Failed to get audio stream, will try again with relaxed contraints',
{ error, constraints, relaxedContraints },
);
return getAudioStream(relaxedContraints);
}

getLogger(['devices'])('error', 'Failed to get audio stream', {
error,
constraints,
Expand All @@ -227,7 +217,7 @@ export const getVideoStream = async (
const constraints: MediaStreamConstraints = {
video: {
...videoDeviceConstraints.video,
...trackConstraints,
...normalizeContraints(trackConstraints),
},
};
try {
Expand All @@ -237,16 +227,6 @@ export const getVideoStream = async (
});
return await getStream(constraints);
} catch (error) {
if (error instanceof OverconstrainedError && trackConstraints?.deviceId) {
const { deviceId, ...relaxedContraints } = trackConstraints;
getLogger(['devices'])(
'warn',
'Failed to get video stream, will try again with relaxed contraints',
{ error, constraints, relaxedContraints },
);
return getVideoStream(relaxedContraints);
}

getLogger(['devices'])('error', 'Failed to get video stream', {
error,
constraints,
Expand All @@ -255,6 +235,20 @@ export const getVideoStream = async (
}
};

function normalizeContraints(constraints: MediaTrackConstraints | undefined) {
if (
constraints?.deviceId === 'default' ||
(typeof constraints?.deviceId === 'object' &&
'exact' in constraints.deviceId &&
constraints.deviceId.exact === 'default')
) {
const { deviceId, ...contraintsWithoutDeviceId } = constraints;
return contraintsWithoutDeviceId;
}

return constraints;
}

/**
* Prompts the user for a permission to share a screen.
* If the user grants the permission, a screen sharing stream is returned. Throws otherwise.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
import { useCallStateHooks } from '@stream-io/video-react-bindings';
import { DeviceSelector } from './DeviceSelector';
import {
createCallControlHandler,
PropsWithErrorHandler,
} from '../../utilities/callControlHandler';

export type DeviceSelectorAudioInputProps = {
export type DeviceSelectorAudioInputProps = PropsWithErrorHandler<{
title?: string;
visualType?: 'list' | 'dropdown';
};
}>;

export const DeviceSelectorAudioInput = ({
title,
visualType,
}: DeviceSelectorAudioInputProps) => {
export const DeviceSelectorAudioInput = (
props: DeviceSelectorAudioInputProps,
) => {
const { useMicrophoneState } = useCallStateHooks();
const { microphone, selectedDevice, devices } = useMicrophoneState();
const handleChange = createCallControlHandler(
props,
async (deviceId: string) => {
await microphone.select(deviceId);
},
);

return (
<DeviceSelector
devices={devices || []}
selectedDeviceId={selectedDevice}
type="audioinput"
onChange={async (deviceId) => {
await microphone.select(deviceId);
}}
title={title}
visualType={visualType}
onChange={handleChange}
title={props.title}
visualType={props.visualType}
icon="mic"
/>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
import {
createCallControlHandler,
PropsWithErrorHandler,
} from '../../utilities/callControlHandler';
import { DeviceSelector } from './DeviceSelector';
import { useCallStateHooks } from '@stream-io/video-react-bindings';

export type DeviceSelectorVideoProps = {
export type DeviceSelectorVideoProps = PropsWithErrorHandler<{
title?: string;
visualType?: 'list' | 'dropdown';
};
}>;

export const DeviceSelectorVideo = ({
title,
visualType,
}: DeviceSelectorVideoProps) => {
export const DeviceSelectorVideo = (props: DeviceSelectorVideoProps) => {
const { useCameraState } = useCallStateHooks();
const { camera, devices, selectedDevice } = useCameraState();
const handleChange = createCallControlHandler(
props,
async (deviceId: string) => {
await camera.select(deviceId);
},
);

return (
<DeviceSelector
devices={devices || []}
type="videoinput"
selectedDeviceId={selectedDevice}
onChange={async (deviceId) => {
await camera.select(deviceId);
}}
title={title}
visualType={visualType}
onChange={handleChange}
title={props.title}
visualType={props.visualType}
icon="camera"
/>
);
Expand Down
15 changes: 13 additions & 2 deletions packages/react-sdk/src/hooks/usePersistedDevicePreferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,14 @@ const usePersistDevicePreferences = (
*
* @param key the key to use for local storage.
*/
const useApplyDevicePreferences = (key: string, onApplied: () => void) => {
const useApplyDevicePreferences = (
key: string,
onWillApply: () => void,
onApplied: () => void,
) => {
const call = useCall();
const onWillApplyRef = useRef(onWillApply);
onWillApplyRef.current = onWillApply;
const onAppliedRef = useRef(onApplied);
onAppliedRef.current = onApplied;
useEffect(() => {
Expand Down Expand Up @@ -121,6 +127,7 @@ const useApplyDevicePreferences = (key: string, onApplied: () => void) => {
}
};

onWillApplyRef.current();
apply()
.then(() => onAppliedRef.current())
.catch((err) => {
Expand All @@ -142,7 +149,11 @@ export const usePersistedDevicePreferences = (
key: string = '@stream-io/device-preferences',
) => {
const shouldPersistRef = useRef(false);
useApplyDevicePreferences(key, () => (shouldPersistRef.current = true));
useApplyDevicePreferences(
key,
() => (shouldPersistRef.current = false),
() => (shouldPersistRef.current = true),
);
usePersistDevicePreferences(key, shouldPersistRef);
};

Expand Down
8 changes: 4 additions & 4 deletions packages/react-sdk/src/utilities/callControlHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ export type PropsWithErrorHandler<T = unknown> = T & {
* @param props component props, including the onError callback
* @param handler event handler to wrap
*/
export const createCallControlHandler = (
export const createCallControlHandler = <P extends unknown[]>(
props: PropsWithErrorHandler,
handler: () => Promise<void>,
handler: (...args: P) => Promise<void>,
): (() => Promise<void>) => {
const logger = getLogger(['react-sdk']);

return async () => {
return async (...args: P) => {
try {
await handler();
await handler(...args);
} catch (error) {
if (props.onError) {
props.onError(error);
Expand Down

0 comments on commit c238dd8

Please sign in to comment.