🐛(frontend) handle Firefox/Windows AbortError on device start

Some versions of Firefox on Windows do not raise the
`NotReadableError` that LiveKit expects when the camera or
microphone fails to start. Instead, they surface an `AbortError`
with a message explaining the browser could not start the video or
microphone input.

This case was observed in PostHog error tracking and was not
handled, so users hit an unhelpful failure state.

Detect that specific `AbortError` and route it through the same
device-in-use / not-readable handling as the standard error, so
users get a clear explanation.
This commit is contained in:
lebaudantoine
2026-08-22 22:02:37 +02:00
committed by aleb_the_flash
parent d723c14d84
commit 2ec3f54532
6 changed files with 31 additions and 5 deletions
+1
View File
@@ -33,6 +33,7 @@ and this project adheres to
- 🐛(frontend) hoist mute confirmation dialog to VideoConference level
- 🐛(frontend) fix joined notification tile no longer rendering properly
- 🐛(frontend) handle device-in-use errors on Chrome / Windows 10
- 🐛(frontend) handle Firefox/Windows AbortError on device start
## [1.27.0] - 2026-08-14
@@ -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
@@ -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)
) {
@@ -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', {
@@ -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
}
@@ -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) => {