♻️(frontend) refactor device select for controlled behavior
Major refactor of device select component with several key improvements: * Set permission=true for Firefox compatibility - without this flag, device list returns empty on Firefox * Implement controlled component pattern for active device selection, ensuring sync with preview track state * Remove default device handling as controlled behavior eliminates need * Render selectors only after permissions granted to prevent double permission prompts (separate for mic/camera) Ensures usePreviewTrack handles initial permission request, then selectors allow specific device choice once access is granted.
This commit is contained in:
committed by
aleb_the_flash
parent
7c6182cc4e
commit
cb8b415ef9
@@ -7,6 +7,8 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useMediaDeviceSelect } from '@livekit/components-react'
|
import { useMediaDeviceSelect } from '@livekit/components-react'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { Select } from '@/primitives/Select'
|
import { Select } from '@/primitives/Select'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
|
import { permissionsStore } from '@/stores/permissions'
|
||||||
|
|
||||||
type DeviceItems = Array<{ value: string; label: string }>
|
type DeviceItems = Array<{ value: string; label: string }>
|
||||||
|
|
||||||
@@ -20,34 +22,23 @@ type SelectDeviceProps = {
|
|||||||
kind: MediaDeviceKind
|
kind: MediaDeviceKind
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SelectDevice = ({ id, onSubmit, kind }: SelectDeviceProps) => {
|
type SelectDevicePermissionsProps = SelectDeviceProps & {
|
||||||
|
config: DeviceConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectDevicePermissions = ({
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
config,
|
||||||
|
onSubmit,
|
||||||
|
}: SelectDevicePermissionsProps) => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||||
|
|
||||||
const config = useMemo<DeviceConfig | undefined>(() => {
|
|
||||||
switch (kind) {
|
|
||||||
case 'audioinput':
|
|
||||||
return {
|
|
||||||
icon: RiMicLine,
|
|
||||||
}
|
|
||||||
case 'videoinput':
|
|
||||||
return {
|
|
||||||
icon: RiVideoOnLine,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [kind])
|
|
||||||
|
|
||||||
const getDefaultSelectedKey = (items: DeviceItems) => {
|
|
||||||
if (!items || items.length === 0) return
|
|
||||||
const defaultItem =
|
|
||||||
items.find((item) => item.value === 'default') || items[0]
|
|
||||||
return defaultItem.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
devices: devices,
|
devices: devices,
|
||||||
activeDeviceId: activeDeviceId,
|
activeDeviceId: activeDeviceId,
|
||||||
setActiveMediaDevice: setActiveMediaDevice,
|
setActiveMediaDevice: setActiveMediaDevice,
|
||||||
} = useMediaDeviceSelect({ kind, requestPermissions: false })
|
} = useMediaDeviceSelect({ kind: kind, requestPermissions: true })
|
||||||
|
|
||||||
const items: DeviceItems = devices
|
const items: DeviceItems = devices
|
||||||
.filter((d) => !!d.deviceId)
|
.filter((d) => !!d.deviceId)
|
||||||
@@ -64,7 +55,7 @@ export const SelectDevice = ({ id, onSubmit, kind }: SelectDeviceProps) => {
|
|||||||
items={items}
|
items={items}
|
||||||
iconComponent={config?.icon}
|
iconComponent={config?.icon}
|
||||||
placeholder={t('selectDevice.loading')}
|
placeholder={t('selectDevice.loading')}
|
||||||
defaultSelectedKey={id || activeDeviceId || getDefaultSelectedKey(items)}
|
selectedKey={id || activeDeviceId}
|
||||||
onSelectionChange={(key) => {
|
onSelectionChange={(key) => {
|
||||||
onSubmit?.(key as string)
|
onSubmit?.(key as string)
|
||||||
setActiveMediaDevice(key as string)
|
setActiveMediaDevice(key as string)
|
||||||
@@ -72,3 +63,56 @@ export const SelectDevice = ({ id, onSubmit, kind }: SelectDeviceProps) => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 'videoinput':
|
||||||
|
return {
|
||||||
|
icon: RiVideoOnLine,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [kind])
|
||||||
|
|
||||||
|
const isPermissionDeniedOrPrompted = useMemo(() => {
|
||||||
|
if (kind == 'audioinput') {
|
||||||
|
return permissions.isMicrophoneDenied || permissions.isMicrophonePrompted
|
||||||
|
}
|
||||||
|
if (kind == 'videoinput') {
|
||||||
|
return permissions.isCameraDenied || permissions.isCameraPrompted
|
||||||
|
}
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,10 +9,12 @@
|
|||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
"selectDevice": {
|
"selectDevice": {
|
||||||
"loading": "Laden…"
|
"loading": "Laden…",
|
||||||
|
"permissionsNeeded": "Genehmigung erforderlich"
|
||||||
},
|
},
|
||||||
"videoinput": {
|
"videoinput": {
|
||||||
"choose": "Kamera auswählen",
|
"choose": "Kamera auswählen",
|
||||||
|
"permissionsNeeded": "Kamera auswählen - genehmigung erforderlich",
|
||||||
"disable": "Kamera deaktivieren",
|
"disable": "Kamera deaktivieren",
|
||||||
"enable": "Kamera aktivieren",
|
"enable": "Kamera aktivieren",
|
||||||
"label": "Kamera",
|
"label": "Kamera",
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
},
|
},
|
||||||
"audioinput": {
|
"audioinput": {
|
||||||
"choose": "Mikrofon auswählen",
|
"choose": "Mikrofon auswählen",
|
||||||
|
"permissionsNeeded": "Mikrofon auswählen - genehmigung erforderlich",
|
||||||
"disable": "Mikrofon deaktivieren",
|
"disable": "Mikrofon deaktivieren",
|
||||||
"enable": "Mikrofon aktivieren",
|
"enable": "Mikrofon aktivieren",
|
||||||
"label": "Mikrofon"
|
"label": "Mikrofon"
|
||||||
|
|||||||
@@ -9,10 +9,12 @@
|
|||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
"selectDevice": {
|
"selectDevice": {
|
||||||
"loading": "Loading…"
|
"loading": "Loading…",
|
||||||
|
"permissionsNeeded": "Permission needed"
|
||||||
},
|
},
|
||||||
"videoinput": {
|
"videoinput": {
|
||||||
"choose": "Select camera",
|
"choose": "Select camera",
|
||||||
|
"permissionsNeeded": "Select camera - permission needed",
|
||||||
"disable": "Disable camera",
|
"disable": "Disable camera",
|
||||||
"enable": "Enable camera",
|
"enable": "Enable camera",
|
||||||
"label": "Camera",
|
"label": "Camera",
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
},
|
},
|
||||||
"audioinput": {
|
"audioinput": {
|
||||||
"choose": "Select microphone",
|
"choose": "Select microphone",
|
||||||
|
"permissionsNeeded": "Select microphone - permission needed",
|
||||||
"disable": "Disable microphone",
|
"disable": "Disable microphone",
|
||||||
"enable": "Enable microphone",
|
"enable": "Enable microphone",
|
||||||
"label": "Microphone"
|
"label": "Microphone"
|
||||||
|
|||||||
@@ -9,10 +9,12 @@
|
|||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
"selectDevice": {
|
"selectDevice": {
|
||||||
"loading": "Chargement…"
|
"loading": "Chargement…",
|
||||||
|
"permissionsNeeded": "Autorisations nécessaires"
|
||||||
},
|
},
|
||||||
"videoinput": {
|
"videoinput": {
|
||||||
"choose": "Choisir la webcam",
|
"choose": "Choisir la webcam",
|
||||||
|
"permissionsNeeded": "Choisir la webcam - autorisations nécessaires",
|
||||||
"disable": "Désactiver la webcam",
|
"disable": "Désactiver la webcam",
|
||||||
"enable": "Activer la webcam",
|
"enable": "Activer la webcam",
|
||||||
"label": "Webcam",
|
"label": "Webcam",
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
},
|
},
|
||||||
"audioinput": {
|
"audioinput": {
|
||||||
"choose": "Choisir le micro",
|
"choose": "Choisir le micro",
|
||||||
|
"permissionsNeeded": "Choisir le micro - autorisations nécessaires",
|
||||||
"disable": "Désactiver le micro",
|
"disable": "Désactiver le micro",
|
||||||
"enable": "Activer le micro",
|
"enable": "Activer le micro",
|
||||||
"label": "Microphone"
|
"label": "Microphone"
|
||||||
|
|||||||
@@ -9,10 +9,12 @@
|
|||||||
},
|
},
|
||||||
"join": {
|
"join": {
|
||||||
"selectDevice": {
|
"selectDevice": {
|
||||||
"loading": "Bezig met laden…"
|
"loading": "Bezig met laden…",
|
||||||
|
"permissionNeeded": "Toestemming vereist"
|
||||||
},
|
},
|
||||||
"videoinput": {
|
"videoinput": {
|
||||||
"choose": "Selecteer camera",
|
"choose": "Selecteer camera",
|
||||||
|
"permissionNeeded": "Selecteer camera - Toestemming vereist",
|
||||||
"disable": "Camera uitschakelen",
|
"disable": "Camera uitschakelen",
|
||||||
"enable": "Camera inschakelen",
|
"enable": "Camera inschakelen",
|
||||||
"label": "Camera",
|
"label": "Camera",
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
},
|
},
|
||||||
"audioinput": {
|
"audioinput": {
|
||||||
"choose": "Selecteer microfoon",
|
"choose": "Selecteer microfoon",
|
||||||
|
"permissionNeeded": "Selecteer microfoon - Toestemming vereist",
|
||||||
"disable": "Microfoon dempen",
|
"disable": "Microfoon dempen",
|
||||||
"enable": "Microfoon dempen opheffen",
|
"enable": "Microfoon dempen opheffen",
|
||||||
"label": "Microfoon"
|
"label": "Microfoon"
|
||||||
|
|||||||
Reference in New Issue
Block a user