From ebf676529f0c5a9c6a64f2eff7422ca1eee13a2d Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Thu, 21 Aug 2025 18:20:25 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20refactor=20in-ro?= =?UTF-8?q?om=20device=20selection=20with=20audio=20output=20control?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../src/features/rooms/components/Join.tsx | 2 +- .../controls/Device/AudioDevicesControl.tsx | 75 ++++++++++++------- .../controls/Device}/SelectDevice.tsx | 26 ++++++- .../controls/Device/VideoDeviceControl.tsx | 54 ++++++------- 4 files changed, 101 insertions(+), 56 deletions(-) rename src/frontend/src/features/rooms/{components/join => livekit/components/controls/Device}/SelectDevice.tsx (85%) diff --git a/src/frontend/src/features/rooms/components/Join.tsx b/src/frontend/src/features/rooms/components/Join.tsx index 04544af2..f5de1f9c 100644 --- a/src/frontend/src/features/rooms/components/Join.tsx +++ b/src/frontend/src/features/rooms/components/Join.tsx @@ -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' diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioDevicesControl.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioDevicesControl.tsx index 44e1fee9..9eb301f7 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioDevicesControl.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioDevicesControl.tsx @@ -1,12 +1,8 @@ import { useTranslation } from 'react-i18next' -import { - useMediaDeviceSelect, - useTrackToggle, - UseTrackToggleProps, -} from '@livekit/components-react' -import { Button, Menu, MenuList } from '@/primitives' +import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react' +import { Button, Popover } from '@/primitives' import { RiArrowUpSLine, RiMicLine, RiMicOffLine } from '@remixicon/react' -import { LocalAudioTrack, LocalVideoTrack, Track } from 'livekit-client' +import { Track } from 'livekit-client' import { ToggleDevice } from '@/features/rooms/livekit/components/controls/ToggleDevice.tsx' import { css } from '@/styled-system/css' @@ -16,17 +12,16 @@ import { permissionsStore } from '@/stores/permissions' import { ToggleDeviceConfig } from '../../../config/ToggleDeviceConfig' import Source = Track.Source import * as React from 'react' +import { SelectDevice } from './SelectDevice' type AudioDevicesControlProps = Omit< UseTrackToggleProps, 'source' | 'onChange' > & { - track?: LocalAudioTrack | LocalVideoTrack hideMenu?: boolean } export const AudioDevicesControl = ({ - track, hideMenu, ...props }: AudioDevicesControlProps) => { @@ -44,8 +39,12 @@ export const AudioDevicesControl = ({ } const { t } = useTranslation('rooms', { keyPrefix: 'join' }) - const { saveAudioInputDeviceId, saveAudioInputEnabled } = - usePersistentUserChoices() + const { + userChoices: { audioDeviceId, audioOutputDeviceId }, + saveAudioInputDeviceId, + saveAudioInputEnabled, + saveAudioOutputDeviceId, + } = usePersistentUserChoices() const onChange = React.useCallback( (enabled: boolean, isUserInitiated: boolean) => @@ -63,9 +62,6 @@ export const AudioDevicesControl = ({ const isPermissionDeniedOrPrompted = permissions.isMicrophoneDenied || permissions.isMicrophonePrompted - const { devices, activeDeviceId, setActiveMediaDevice } = - useMediaDeviceSelect({ kind: 'audioinput', track }) - const selectLabel = t('audioinput.choose') return ( @@ -90,7 +86,7 @@ export const AudioDevicesControl = ({ }} /> {!hideMenu && ( - + - ({ - value: d.deviceId, - label: d.label, - }))} - selectedItem={activeDeviceId} - onAction={(value) => { - setActiveMediaDevice(value as string) - saveAudioInputDeviceId(value as string) - }} - variant="dark" - /> - +
+
+ +
+
+ +
+
+ )} ) diff --git a/src/frontend/src/features/rooms/components/join/SelectDevice.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Device/SelectDevice.tsx similarity index 85% rename from src/frontend/src/features/rooms/components/join/SelectDevice.tsx rename to src/frontend/src/features/rooms/livekit/components/controls/Device/SelectDevice.tsx index 3d151ff2..0c0cad39 100644 --- a/src/frontend/src/features/rooms/components/join/SelectDevice.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Device/SelectDevice.tsx @@ -10,6 +10,7 @@ import { useEffect, useMemo } from 'react' import { Select } from '@/primitives/Select' import { useSnapshot } from 'valtio' import { permissionsStore } from '@/stores/permissions' +import { Placement } from '@react-types/overlays' type DeviceItems = Array<{ value: string; label: string }> @@ -17,14 +18,21 @@ type DeviceConfig = { icon: RemixiconComponentType } +type SelectDeviceContext = { + variant?: 'light' | 'dark' + placement?: Placement +} + type SelectDeviceProps = { id?: string onSubmit?: (id: string) => void kind: MediaDeviceKind + context?: 'join' | 'room' } type SelectDevicePermissionsProps = SelectDeviceProps & { config: DeviceConfig + contextProps: SelectDeviceContext } const SelectDevicePermissions = ({ @@ -32,6 +40,7 @@ const SelectDevicePermissions = ({ kind, config, onSubmit, + contextProps, }: SelectDevicePermissionsProps) => { const { t } = useTranslation('rooms', { keyPrefix: 'join' }) @@ -78,15 +87,28 @@ const SelectDevicePermissions = ({ onSubmit?.(key as string) setActiveMediaDevice(key as string) }} + {...contextProps} /> ) } -export const SelectDevice = ({ id, onSubmit, kind }: SelectDeviceProps) => { +export const SelectDevice = ({ + id, + onSubmit, + kind, + context = 'join', +}: SelectDeviceProps) => { const { t } = useTranslation('rooms', { keyPrefix: 'join' }) const permissions = useSnapshot(permissionsStore) + const contextProps = useMemo(() => { + if (context == 'room') { + return { variant: 'dark', placement: 'top' } + } + return {} + }, [context]) + const config = useMemo(() => { switch (kind) { case 'audioinput': @@ -128,6 +150,7 @@ export const SelectDevice = ({ id, onSubmit, kind }: SelectDeviceProps) => { items={[]} iconComponent={config?.icon} placeholder={t('selectDevice.permissionsNeeded')} + {...contextProps} /> ) } @@ -138,6 +161,7 @@ export const SelectDevice = ({ id, onSubmit, kind }: SelectDeviceProps) => { onSubmit={onSubmit} kind={kind} config={config} + contextProps={contextProps} /> ) } diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Device/VideoDeviceControl.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Device/VideoDeviceControl.tsx index 2d95265e..10fa9675 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Device/VideoDeviceControl.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Device/VideoDeviceControl.tsx @@ -1,12 +1,8 @@ import { useTranslation } from 'react-i18next' -import { - useMediaDeviceSelect, - useTrackToggle, - UseTrackToggleProps, -} from '@livekit/components-react' -import { Button, Menu, MenuList } from '@/primitives' +import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react' +import { Button, Popover } from '@/primitives' import { RiArrowUpSLine, RiVideoOffLine, RiVideoOnLine } from '@remixicon/react' -import { LocalVideoTrack, Track, VideoCaptureOptions } from 'livekit-client' +import { Track, VideoCaptureOptions } from 'livekit-client' import { ToggleDevice } from '@/features/rooms/livekit/components/controls/ToggleDevice' import { css } from '@/styled-system/css' @@ -17,17 +13,16 @@ import { permissionsStore } from '@/stores/permissions' import { ToggleDeviceConfig } from '../../../config/ToggleDeviceConfig' import Source = Track.Source import * as React from 'react' +import { SelectDevice } from './SelectDevice' type VideoDeviceControlProps = Omit< UseTrackToggleProps, 'source' | 'onChange' > & { - track?: LocalVideoTrack hideMenu?: boolean } export const VideoDeviceControl = ({ - track, hideMenu, ...props }: VideoDeviceControlProps) => { @@ -90,9 +85,6 @@ export const VideoDeviceControl = ({ } as VideoCaptureOptions) } - const { devices, activeDeviceId, setActiveMediaDevice } = - useMediaDeviceSelect({ kind: 'videoinput', track }) - const selectLabel = t('videoinput.choose') return ( @@ -117,7 +109,7 @@ export const VideoDeviceControl = ({ }} /> {!hideMenu && ( - + - ({ - value: d.deviceId, - label: d.label, - }))} - selectedItem={activeDeviceId} - onAction={(value) => { - setActiveMediaDevice(value as string) - saveVideoInputDeviceId(value as string) - }} - variant="dark" - /> - +
+
+ +
+
+ )} )