♻️(frontend) refactor in-room device selection with audio output control

Refactor device selection within rooms and add audio output selection
to audio controls as requested by users.

Ensures code reuse between join and room components by sharing device
selection logic across both contexts.
This commit is contained in:
lebaudantoine
2025-08-21 18:20:25 +02:00
committed by aleb_the_flash
parent 40cedba8ae
commit ebf676529f
4 changed files with 101 additions and 56 deletions
@@ -20,6 +20,7 @@ import {
EffectsConfiguration,
EffectsConfigurationProps,
} from '../livekit/components/effects/EffectsConfiguration'
import { SelectDevice } from '../livekit/components/controls/Device/SelectDevice'
import { usePersistentUserChoices } from '../livekit/hooks/usePersistentUserChoices'
import { BackgroundProcessorFactory } from '../livekit/components/blur'
import { isMobileBrowser } from '@livekit/components-core'
@@ -35,7 +36,6 @@ import { useLoginHint } from '@/hooks/useLoginHint'
import { useSnapshot } from 'valtio'
import { openPermissionsDialog, permissionsStore } from '@/stores/permissions'
import { ToggleDevice } from './join/ToggleDevice'
import { SelectDevice } from './join/SelectDevice'
import { useResolveInitiallyDefaultDeviceId } from '../livekit/hooks/useResolveInitiallyDefaultDeviceId'
import { isSafari } from '@/utils/livekit'
import type { LocalUserChoices } from '@/stores/userChoices'
@@ -1,143 +0,0 @@
import {
RemixiconComponentType,
RiMicLine,
RiVideoOnLine,
RiVolumeDownLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useMediaDeviceSelect } from '@livekit/components-react'
import { useEffect, useMemo } from 'react'
import { Select } from '@/primitives/Select'
import { useSnapshot } from 'valtio'
import { permissionsStore } from '@/stores/permissions'
type DeviceItems = Array<{ value: string; label: string }>
type DeviceConfig = {
icon: RemixiconComponentType
}
type SelectDeviceProps = {
id?: string
onSubmit?: (id: string) => void
kind: MediaDeviceKind
}
type SelectDevicePermissionsProps = SelectDeviceProps & {
config: DeviceConfig
}
const SelectDevicePermissions = ({
id,
kind,
config,
onSubmit,
}: SelectDevicePermissionsProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
const { devices, activeDeviceId, setActiveMediaDevice } =
useMediaDeviceSelect({ kind: kind, requestPermissions: true })
const items: DeviceItems = devices
.filter((d) => !!d.deviceId)
.map((d) => ({
value: d.deviceId,
label: d.label,
}))
/**
* FALLBACK AUDIO OUTPUT DEVICE SELECTION
* Auto-selects the only available audio output device when currently on 'default'
*/
useEffect(() => {
if (
kind !== 'audiooutput' ||
items.length !== 1 ||
items[0].value === 'default' ||
activeDeviceId !== 'default'
)
return
onSubmit?.(items[0].value)
setActiveMediaDevice(items[0].value)
}, [items, onSubmit, kind, setActiveMediaDevice, activeDeviceId])
return (
<Select
aria-label={t(`${kind}.choose`)}
label=""
isDisabled={items.length === 0}
items={items}
iconComponent={config?.icon}
placeholder={
items.length === 0
? t('selectDevice.loading')
: t('selectDevice.select')
}
selectedKey={id || activeDeviceId}
onSelectionChange={(key) => {
onSubmit?.(key as string)
setActiveMediaDevice(key as string)
}}
/>
)
}
export const SelectDevice = ({ id, onSubmit, kind }: SelectDeviceProps) => {
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
const permissions = useSnapshot(permissionsStore)
const config = useMemo<DeviceConfig | undefined>(() => {
switch (kind) {
case 'audioinput':
return {
icon: RiMicLine,
}
case 'audiooutput':
return {
icon: RiVolumeDownLine,
}
case 'videoinput':
return {
icon: RiVideoOnLine,
}
}
}, [kind])
const isPermissionDeniedOrPrompted = useMemo(() => {
if (kind == 'audioinput') {
return permissions.isMicrophoneDenied || permissions.isMicrophonePrompted
}
if (kind == 'videoinput') {
return permissions.isCameraDenied || permissions.isCameraPrompted
}
if (kind == 'audiooutput') {
return permissions.isMicrophoneDenied || permissions.isMicrophonePrompted
}
return false
}, [kind, permissions])
if (!config) return null
if (isPermissionDeniedOrPrompted || permissions.isLoading) {
return (
<Select
aria-label={t(`${kind}.permissionsNeeded`)}
label=""
isDisabled={true}
items={[]}
iconComponent={config?.icon}
placeholder={t('selectDevice.permissionsNeeded')}
/>
)
}
return (
<SelectDevicePermissions
id={id}
onSubmit={onSubmit}
kind={kind}
config={config}
/>
)
}