♻️(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:
committed by
aleb_the_flash
parent
40cedba8ae
commit
ebf676529f
@@ -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,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.Microphone>,
|
||||
'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 && (
|
||||
<Menu variant="dark">
|
||||
<Popover variant="dark" withArrow={false}>
|
||||
<Button
|
||||
isDisabled={isPermissionDeniedOrPrompted}
|
||||
tooltip={selectLabel}
|
||||
@@ -105,19 +101,42 @@ export const AudioDevicesControl = ({
|
||||
>
|
||||
<RiArrowUpSLine />
|
||||
</Button>
|
||||
<MenuList
|
||||
items={devices.map((d) => ({
|
||||
value: d.deviceId,
|
||||
label: d.label,
|
||||
}))}
|
||||
selectedItem={activeDeviceId}
|
||||
onAction={(value) => {
|
||||
setActiveMediaDevice(value as string)
|
||||
saveAudioInputDeviceId(value as string)
|
||||
}}
|
||||
variant="dark"
|
||||
/>
|
||||
</Menu>
|
||||
<div
|
||||
className={css({
|
||||
maxWidth: '36rem',
|
||||
padding: '0.15rem',
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: '1 1 0',
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<SelectDevice
|
||||
context="room"
|
||||
kind="audioinput"
|
||||
id={audioDeviceId}
|
||||
onSubmit={saveAudioInputDeviceId}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flex: '1 1 0',
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<SelectDevice
|
||||
context="room"
|
||||
kind="audiooutput"
|
||||
id={audioOutputDeviceId}
|
||||
onSubmit={saveAudioOutputDeviceId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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<SelectDeviceContext>(() => {
|
||||
if (context == 'room') {
|
||||
return { variant: 'dark', placement: 'top' }
|
||||
}
|
||||
return {}
|
||||
}, [context])
|
||||
|
||||
const config = useMemo<DeviceConfig | undefined>(() => {
|
||||
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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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.Camera>,
|
||||
'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 && (
|
||||
<Menu variant="dark">
|
||||
<Popover variant="dark" withArrow={false}>
|
||||
<Button
|
||||
isDisabled={isPermissionDeniedOrPrompted}
|
||||
tooltip={selectLabel}
|
||||
@@ -132,19 +124,29 @@ export const VideoDeviceControl = ({
|
||||
>
|
||||
<RiArrowUpSLine />
|
||||
</Button>
|
||||
<MenuList
|
||||
items={devices.map((d) => ({
|
||||
value: d.deviceId,
|
||||
label: d.label,
|
||||
}))}
|
||||
selectedItem={activeDeviceId}
|
||||
onAction={(value) => {
|
||||
setActiveMediaDevice(value as string)
|
||||
saveVideoInputDeviceId(value as string)
|
||||
}}
|
||||
variant="dark"
|
||||
/>
|
||||
</Menu>
|
||||
<div
|
||||
className={css({
|
||||
maxWidth: '36rem',
|
||||
padding: '0.15rem',
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: '1 1 0',
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<SelectDevice
|
||||
context="room"
|
||||
kind="videoinput"
|
||||
id={userChoices.videoDeviceId}
|
||||
onSubmit={saveVideoInputDeviceId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user