From 13f2c252995ba027bff2ef38342278a7daf0850b Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Sat, 22 Aug 2026 22:02:37 +0200 Subject: [PATCH] wip handle firefox error --- .../features/rooms/components/Conference.tsx | 3 ++- .../rooms/livekit/hooks/useJoinTracks.ts | 3 ++- .../hooks/useWatchMediaDeviceErrors.ts | 3 ++- .../rooms/livekit/prefabs/VideoConference.tsx | 3 ++- .../rooms/livekit/utils/mediaPermissions.ts | 23 ++++++++++++++++++- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/frontend/src/features/rooms/components/Conference.tsx b/src/frontend/src/features/rooms/components/Conference.tsx index bb9e2a11..866ce41f 100644 --- a/src/frontend/src/features/rooms/components/Conference.tsx +++ b/src/frontend/src/features/rooms/components/Conference.tsx @@ -14,6 +14,7 @@ import { type RoomOptions, VideoPresets, } from 'livekit-client' +import { getMediaDeviceFailure } from '@/features/rooms/livekit/utils/mediaPermissions' import { keys } from '@/api/queryKeys' import { queryClient } from '@/api/queryClient' import { Screen } from '@/layout/Screen' @@ -231,7 +232,7 @@ export const Conference = ({ backgroundColor: 'primaryDark.50 !important', })} onError={(e) => { - const failure = MediaDeviceFailure.getFailure(e) + const failure = getMediaDeviceFailure(e) if (failure && failure !== MediaDeviceFailure.Other) return // connect() was aborted by a disconnect() before the join completed diff --git a/src/frontend/src/features/rooms/livekit/hooks/useJoinTracks.ts b/src/frontend/src/features/rooms/livekit/hooks/useJoinTracks.ts index 8522dbfd..91ff8e01 100644 --- a/src/frontend/src/features/rooms/livekit/hooks/useJoinTracks.ts +++ b/src/frontend/src/features/rooms/livekit/hooks/useJoinTracks.ts @@ -14,6 +14,7 @@ import { type PermissionKind, } from '@/stores/permissions' import { + getMediaDeviceFailure, noteDeviceReady, onMediaPermissionError, } from '../utils/mediaPermissions' @@ -83,7 +84,7 @@ function useWarmupPermissions(): WarmupState { bothReady() } catch (error) { if ( - MediaDeviceFailure.getFailure(error as Error) === + getMediaDeviceFailure(error as Error) === MediaDeviceFailure.PermissionDenied && !isSystemPermissionError(error) ) { diff --git a/src/frontend/src/features/rooms/livekit/hooks/useWatchMediaDeviceErrors.ts b/src/frontend/src/features/rooms/livekit/hooks/useWatchMediaDeviceErrors.ts index ac8edd8d..985ad5fe 100644 --- a/src/frontend/src/features/rooms/livekit/hooks/useWatchMediaDeviceErrors.ts +++ b/src/frontend/src/features/rooms/livekit/hooks/useWatchMediaDeviceErrors.ts @@ -21,6 +21,7 @@ import { syncDeviceAvailability, } from '@/stores/deviceAvailability' import { captureMediaEvent } from '@/features/analytics/telemetry' +import { getMediaDeviceFailure } from '../utils/mediaPermissions' import { getOS } from '@/utils/os' type MediaDeviceAlert = { @@ -63,7 +64,7 @@ export const useWatchMediaDeviceErrors = (): MediaDeviceAlert & { useEffect(() => { const onDeviceError = (error: Error, kind?: MediaDeviceKind) => { - const failure = MediaDeviceFailure.getFailure(error) + const failure = getMediaDeviceFailure(error) if (!failure) return if (failure != MediaDeviceFailure.Other) { void captureMediaEvent('media-device-error', { diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx index 0c9f96ca..9fd112f7 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx @@ -1,5 +1,6 @@ import { isWeb } from '@livekit/components-core' import { MediaDeviceFailure, Track } from 'livekit-client' +import { getMediaDeviceFailure } from '../utils/mediaPermissions' import React, { useState } from 'react' import { ConnectionStateToast, @@ -101,7 +102,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) { } } - if (MediaDeviceFailure.getFailure(error) != MediaDeviceFailure.Other) { + if (getMediaDeviceFailure(error) !== MediaDeviceFailure.Other) { return } diff --git a/src/frontend/src/features/rooms/livekit/utils/mediaPermissions.ts b/src/frontend/src/features/rooms/livekit/utils/mediaPermissions.ts index a7cbc333..3d307b2c 100644 --- a/src/frontend/src/features/rooms/livekit/utils/mediaPermissions.ts +++ b/src/frontend/src/features/rooms/livekit/utils/mediaPermissions.ts @@ -22,6 +22,27 @@ import { getOS } from '@/utils/os' */ export type MediaPath = 'join_preview' | 'room' +/** + * LiveKit only maps NotReadableError/TrackStartError to DeviceInUse. + * Firefox reports a device held by another app as + * `AbortError: Starting videoinput failed` (or audioinput), which LiveKit + * classifies as Other. Normalise it here; every classification in the app + * should go through this instead of MediaDeviceFailure.getFailure. + */ +export const getMediaDeviceFailure = ( + error: Error +): MediaDeviceFailure | undefined => { + const failure = MediaDeviceFailure.getFailure(error) + if ( + failure === MediaDeviceFailure.Other && + error.name === 'AbortError' && + /^Starting (video|audio)input failed/i.test(error.message) + ) { + return MediaDeviceFailure.DeviceInUse + } + return failure +} + export const PERMISSION_KIND: Record< 'audioinput' | 'videoinput', PermissionKind @@ -40,7 +61,7 @@ export const onMediaPermissionError = ( kind?: PermissionKind, path: MediaPath = 'join_preview' ) => { - const failure = MediaDeviceFailure.getFailure(e) + const failure = getMediaDeviceFailure(e) if (failure === MediaDeviceFailure.PermissionDenied) { void classifyPermissionError(e, kind).then((scope) => {