diff --git a/CHANGELOG.md b/CHANGELOG.md index c26753f0..84327113 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to - 🐛(frontend) drop exact deviceId constraint on dynamic track creation - 🐛(frontend) fix permission store regression +- 🐛(frontend) handle missing device errors gracefully ## [1.25.2] - 2026-08-06 diff --git a/src/frontend/src/features/analytics/telemetry.ts b/src/frontend/src/features/analytics/telemetry.ts index 60bce040..3a565fd9 100644 --- a/src/frontend/src/features/analytics/telemetry.ts +++ b/src/frontend/src/features/analytics/telemetry.ts @@ -133,7 +133,8 @@ export const captureMediaEvent = async ( | 'media-device-error' | 'media-acquisition' | 'media-device-topology' - | 'media-device-success', + | 'media-device-success' + | 'device-not-found', props: Record ) => { captureEvent(event, { ...props, ...(await deviceSnapshot()) }) diff --git a/src/frontend/src/features/rooms/components/Conference.tsx b/src/frontend/src/features/rooms/components/Conference.tsx index 766a6836..2471a1b0 100644 --- a/src/frontend/src/features/rooms/components/Conference.tsx +++ b/src/frontend/src/features/rooms/components/Conference.tsx @@ -45,6 +45,7 @@ import { PERMISSION_BY_DEVICE_KIND, notePermissionDeniedFromGum, } from '@/stores/permissions' +import { syncDeviceAvailability } from '@/stores/deviceAvailability' export const Conference = ({ roomId, @@ -317,6 +318,10 @@ export const Conference = ({ case MediaDeviceFailure.DeviceInUse: setMediaDeviceError({ error: e, kind }) break + case MediaDeviceFailure.NotFound: + setMediaDeviceError({ error: e, kind }) + void syncDeviceAvailability() + break case MediaDeviceFailure.PermissionDenied: notePermissionDeniedFromGum(PERMISSION_BY_DEVICE_KIND[kind]) break diff --git a/src/frontend/src/features/rooms/components/Join.tsx b/src/frontend/src/features/rooms/components/Join.tsx index 6150bf71..9b3ee5dc 100644 --- a/src/frontend/src/features/rooms/components/Join.tsx +++ b/src/frontend/src/features/rooms/components/Join.tsx @@ -29,6 +29,7 @@ import { } from '@/stores/userChoices' import { useCannotUseDevice } from '../livekit/hooks/useCannotUseDevice' import { useJoinTracks } from '../livekit/hooks/useJoinTracks' +import { deviceAvailabilityStore } from '@/stores/deviceAvailability' const styles = { page: css({ @@ -214,16 +215,21 @@ const switchTrackDevice = } function getPreviewMessages({ + cameraFound, cameraDenied, micDenied, videoEnabled, videoStarted, }: { + cameraFound: boolean cameraDenied: boolean micDenied: boolean videoEnabled: boolean videoStarted: boolean }): { hint: string | null; permissionsButtonLabel: string | null } { + if (!cameraFound) { + return { hint: 'cameraNotFound', permissionsButtonLabel: null } + } if (cameraDenied) { const key = micDenied ? 'cameraAndMicNotGranted' : 'cameraNotGranted' return { hint: key, permissionsButtonLabel: key } @@ -320,10 +326,12 @@ const VideoPreview = ({ const cameraDenied = useCannotUseDevice('videoinput') const micDenied = useCannotUseDevice('audioinput') + const { hasCamera } = useSnapshot(deviceAvailabilityStore) const { videoEl, videoStarted } = useAttachedVideo(videoTrack, videoEnabled) const { hint, permissionsButtonLabel } = getPreviewMessages({ + cameraFound: hasCamera, cameraDenied, micDenied, videoEnabled, diff --git a/src/frontend/src/features/rooms/hooks/useWatchDeviceAvailability.ts b/src/frontend/src/features/rooms/hooks/useWatchDeviceAvailability.ts new file mode 100644 index 00000000..83f6cda5 --- /dev/null +++ b/src/frontend/src/features/rooms/hooks/useWatchDeviceAvailability.ts @@ -0,0 +1,19 @@ +import { useEffect } from 'react' +import { syncDeviceAvailability } from '@/stores/deviceAvailability' + +export function useWatchDeviceAvailability() { + useEffect(() => { + if (!navigator.mediaDevices) return + syncDeviceAvailability() + navigator.mediaDevices.addEventListener( + 'devicechange', + syncDeviceAvailability + ) + return () => { + navigator.mediaDevices.removeEventListener( + 'devicechange', + syncDeviceAvailability + ) + } + }, []) +} diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Device/PermissionNeededButton.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Device/PermissionNeededButton.tsx index 5a4afa5d..0641ae80 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Device/PermissionNeededButton.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Device/PermissionNeededButton.tsx @@ -4,8 +4,17 @@ import { openPermissionsDialog } from '@/stores/permissions' import { css } from '@/styled-system/css' import { useTranslation } from 'react-i18next' -export const PermissionNeededButton = () => { +type PermissionNeededButtonProps = { + tooltip?: string + onPress?: () => void +} + +export const PermissionNeededButton = ({ + tooltip, + onPress, +}: PermissionNeededButtonProps) => { const { t } = useTranslation('rooms', { keyPrefix: 'permissionsButton' }) + const label = tooltip ?? t('tooltip') return (
{ })} >