♻️(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,
|
EffectsConfiguration,
|
||||||
EffectsConfigurationProps,
|
EffectsConfigurationProps,
|
||||||
} from '../livekit/components/effects/EffectsConfiguration'
|
} from '../livekit/components/effects/EffectsConfiguration'
|
||||||
|
import { SelectDevice } from '../livekit/components/controls/Device/SelectDevice'
|
||||||
import { usePersistentUserChoices } from '../livekit/hooks/usePersistentUserChoices'
|
import { usePersistentUserChoices } from '../livekit/hooks/usePersistentUserChoices'
|
||||||
import { BackgroundProcessorFactory } from '../livekit/components/blur'
|
import { BackgroundProcessorFactory } from '../livekit/components/blur'
|
||||||
import { isMobileBrowser } from '@livekit/components-core'
|
import { isMobileBrowser } from '@livekit/components-core'
|
||||||
@@ -35,7 +36,6 @@ import { useLoginHint } from '@/hooks/useLoginHint'
|
|||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { openPermissionsDialog, permissionsStore } from '@/stores/permissions'
|
import { openPermissionsDialog, permissionsStore } from '@/stores/permissions'
|
||||||
import { ToggleDevice } from './join/ToggleDevice'
|
import { ToggleDevice } from './join/ToggleDevice'
|
||||||
import { SelectDevice } from './join/SelectDevice'
|
|
||||||
import { useResolveInitiallyDefaultDeviceId } from '../livekit/hooks/useResolveInitiallyDefaultDeviceId'
|
import { useResolveInitiallyDefaultDeviceId } from '../livekit/hooks/useResolveInitiallyDefaultDeviceId'
|
||||||
import { isSafari } from '@/utils/livekit'
|
import { isSafari } from '@/utils/livekit'
|
||||||
import type { LocalUserChoices } from '@/stores/userChoices'
|
import type { LocalUserChoices } from '@/stores/userChoices'
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
|
||||||
useMediaDeviceSelect,
|
import { Button, Popover } from '@/primitives'
|
||||||
useTrackToggle,
|
|
||||||
UseTrackToggleProps,
|
|
||||||
} from '@livekit/components-react'
|
|
||||||
import { Button, Menu, MenuList } from '@/primitives'
|
|
||||||
import { RiArrowUpSLine, RiMicLine, RiMicOffLine } from '@remixicon/react'
|
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 { ToggleDevice } from '@/features/rooms/livekit/components/controls/ToggleDevice.tsx'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
@@ -16,17 +12,16 @@ import { permissionsStore } from '@/stores/permissions'
|
|||||||
import { ToggleDeviceConfig } from '../../../config/ToggleDeviceConfig'
|
import { ToggleDeviceConfig } from '../../../config/ToggleDeviceConfig'
|
||||||
import Source = Track.Source
|
import Source = Track.Source
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import { SelectDevice } from './SelectDevice'
|
||||||
|
|
||||||
type AudioDevicesControlProps = Omit<
|
type AudioDevicesControlProps = Omit<
|
||||||
UseTrackToggleProps<Source.Microphone>,
|
UseTrackToggleProps<Source.Microphone>,
|
||||||
'source' | 'onChange'
|
'source' | 'onChange'
|
||||||
> & {
|
> & {
|
||||||
track?: LocalAudioTrack | LocalVideoTrack
|
|
||||||
hideMenu?: boolean
|
hideMenu?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AudioDevicesControl = ({
|
export const AudioDevicesControl = ({
|
||||||
track,
|
|
||||||
hideMenu,
|
hideMenu,
|
||||||
...props
|
...props
|
||||||
}: AudioDevicesControlProps) => {
|
}: AudioDevicesControlProps) => {
|
||||||
@@ -44,8 +39,12 @@ export const AudioDevicesControl = ({
|
|||||||
}
|
}
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||||
|
|
||||||
const { saveAudioInputDeviceId, saveAudioInputEnabled } =
|
const {
|
||||||
usePersistentUserChoices()
|
userChoices: { audioDeviceId, audioOutputDeviceId },
|
||||||
|
saveAudioInputDeviceId,
|
||||||
|
saveAudioInputEnabled,
|
||||||
|
saveAudioOutputDeviceId,
|
||||||
|
} = usePersistentUserChoices()
|
||||||
|
|
||||||
const onChange = React.useCallback(
|
const onChange = React.useCallback(
|
||||||
(enabled: boolean, isUserInitiated: boolean) =>
|
(enabled: boolean, isUserInitiated: boolean) =>
|
||||||
@@ -63,9 +62,6 @@ export const AudioDevicesControl = ({
|
|||||||
const isPermissionDeniedOrPrompted =
|
const isPermissionDeniedOrPrompted =
|
||||||
permissions.isMicrophoneDenied || permissions.isMicrophonePrompted
|
permissions.isMicrophoneDenied || permissions.isMicrophonePrompted
|
||||||
|
|
||||||
const { devices, activeDeviceId, setActiveMediaDevice } =
|
|
||||||
useMediaDeviceSelect({ kind: 'audioinput', track })
|
|
||||||
|
|
||||||
const selectLabel = t('audioinput.choose')
|
const selectLabel = t('audioinput.choose')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -90,7 +86,7 @@ export const AudioDevicesControl = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!hideMenu && (
|
{!hideMenu && (
|
||||||
<Menu variant="dark">
|
<Popover variant="dark" withArrow={false}>
|
||||||
<Button
|
<Button
|
||||||
isDisabled={isPermissionDeniedOrPrompted}
|
isDisabled={isPermissionDeniedOrPrompted}
|
||||||
tooltip={selectLabel}
|
tooltip={selectLabel}
|
||||||
@@ -105,19 +101,42 @@ export const AudioDevicesControl = ({
|
|||||||
>
|
>
|
||||||
<RiArrowUpSLine />
|
<RiArrowUpSLine />
|
||||||
</Button>
|
</Button>
|
||||||
<MenuList
|
<div
|
||||||
items={devices.map((d) => ({
|
className={css({
|
||||||
value: d.deviceId,
|
maxWidth: '36rem',
|
||||||
label: d.label,
|
padding: '0.15rem',
|
||||||
}))}
|
display: 'flex',
|
||||||
selectedItem={activeDeviceId}
|
gap: '0.5rem',
|
||||||
onAction={(value) => {
|
})}
|
||||||
setActiveMediaDevice(value as string)
|
>
|
||||||
saveAudioInputDeviceId(value as string)
|
<div
|
||||||
}}
|
style={{
|
||||||
variant="dark"
|
flex: '1 1 0',
|
||||||
/>
|
minWidth: 0,
|
||||||
</Menu>
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useEffect, useMemo } from 'react'
|
|||||||
import { Select } from '@/primitives/Select'
|
import { Select } from '@/primitives/Select'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { permissionsStore } from '@/stores/permissions'
|
import { permissionsStore } from '@/stores/permissions'
|
||||||
|
import { Placement } from '@react-types/overlays'
|
||||||
|
|
||||||
type DeviceItems = Array<{ value: string; label: string }>
|
type DeviceItems = Array<{ value: string; label: string }>
|
||||||
|
|
||||||
@@ -17,14 +18,21 @@ type DeviceConfig = {
|
|||||||
icon: RemixiconComponentType
|
icon: RemixiconComponentType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SelectDeviceContext = {
|
||||||
|
variant?: 'light' | 'dark'
|
||||||
|
placement?: Placement
|
||||||
|
}
|
||||||
|
|
||||||
type SelectDeviceProps = {
|
type SelectDeviceProps = {
|
||||||
id?: string
|
id?: string
|
||||||
onSubmit?: (id: string) => void
|
onSubmit?: (id: string) => void
|
||||||
kind: MediaDeviceKind
|
kind: MediaDeviceKind
|
||||||
|
context?: 'join' | 'room'
|
||||||
}
|
}
|
||||||
|
|
||||||
type SelectDevicePermissionsProps = SelectDeviceProps & {
|
type SelectDevicePermissionsProps = SelectDeviceProps & {
|
||||||
config: DeviceConfig
|
config: DeviceConfig
|
||||||
|
contextProps: SelectDeviceContext
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectDevicePermissions = ({
|
const SelectDevicePermissions = ({
|
||||||
@@ -32,6 +40,7 @@ const SelectDevicePermissions = ({
|
|||||||
kind,
|
kind,
|
||||||
config,
|
config,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
contextProps,
|
||||||
}: SelectDevicePermissionsProps) => {
|
}: SelectDevicePermissionsProps) => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||||
|
|
||||||
@@ -78,15 +87,28 @@ const SelectDevicePermissions = ({
|
|||||||
onSubmit?.(key as string)
|
onSubmit?.(key as string)
|
||||||
setActiveMediaDevice(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 { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||||
|
|
||||||
const permissions = useSnapshot(permissionsStore)
|
const permissions = useSnapshot(permissionsStore)
|
||||||
|
|
||||||
|
const contextProps = useMemo<SelectDeviceContext>(() => {
|
||||||
|
if (context == 'room') {
|
||||||
|
return { variant: 'dark', placement: 'top' }
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}, [context])
|
||||||
|
|
||||||
const config = useMemo<DeviceConfig | undefined>(() => {
|
const config = useMemo<DeviceConfig | undefined>(() => {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case 'audioinput':
|
case 'audioinput':
|
||||||
@@ -128,6 +150,7 @@ export const SelectDevice = ({ id, onSubmit, kind }: SelectDeviceProps) => {
|
|||||||
items={[]}
|
items={[]}
|
||||||
iconComponent={config?.icon}
|
iconComponent={config?.icon}
|
||||||
placeholder={t('selectDevice.permissionsNeeded')}
|
placeholder={t('selectDevice.permissionsNeeded')}
|
||||||
|
{...contextProps}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -138,6 +161,7 @@ export const SelectDevice = ({ id, onSubmit, kind }: SelectDeviceProps) => {
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
kind={kind}
|
kind={kind}
|
||||||
config={config}
|
config={config}
|
||||||
|
contextProps={contextProps}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
|
||||||
useMediaDeviceSelect,
|
import { Button, Popover } from '@/primitives'
|
||||||
useTrackToggle,
|
|
||||||
UseTrackToggleProps,
|
|
||||||
} from '@livekit/components-react'
|
|
||||||
import { Button, Menu, MenuList } from '@/primitives'
|
|
||||||
import { RiArrowUpSLine, RiVideoOffLine, RiVideoOnLine } from '@remixicon/react'
|
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 { ToggleDevice } from '@/features/rooms/livekit/components/controls/ToggleDevice'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
@@ -17,17 +13,16 @@ import { permissionsStore } from '@/stores/permissions'
|
|||||||
import { ToggleDeviceConfig } from '../../../config/ToggleDeviceConfig'
|
import { ToggleDeviceConfig } from '../../../config/ToggleDeviceConfig'
|
||||||
import Source = Track.Source
|
import Source = Track.Source
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import { SelectDevice } from './SelectDevice'
|
||||||
|
|
||||||
type VideoDeviceControlProps = Omit<
|
type VideoDeviceControlProps = Omit<
|
||||||
UseTrackToggleProps<Source.Camera>,
|
UseTrackToggleProps<Source.Camera>,
|
||||||
'source' | 'onChange'
|
'source' | 'onChange'
|
||||||
> & {
|
> & {
|
||||||
track?: LocalVideoTrack
|
|
||||||
hideMenu?: boolean
|
hideMenu?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VideoDeviceControl = ({
|
export const VideoDeviceControl = ({
|
||||||
track,
|
|
||||||
hideMenu,
|
hideMenu,
|
||||||
...props
|
...props
|
||||||
}: VideoDeviceControlProps) => {
|
}: VideoDeviceControlProps) => {
|
||||||
@@ -90,9 +85,6 @@ export const VideoDeviceControl = ({
|
|||||||
} as VideoCaptureOptions)
|
} as VideoCaptureOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { devices, activeDeviceId, setActiveMediaDevice } =
|
|
||||||
useMediaDeviceSelect({ kind: 'videoinput', track })
|
|
||||||
|
|
||||||
const selectLabel = t('videoinput.choose')
|
const selectLabel = t('videoinput.choose')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -117,7 +109,7 @@ export const VideoDeviceControl = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!hideMenu && (
|
{!hideMenu && (
|
||||||
<Menu variant="dark">
|
<Popover variant="dark" withArrow={false}>
|
||||||
<Button
|
<Button
|
||||||
isDisabled={isPermissionDeniedOrPrompted}
|
isDisabled={isPermissionDeniedOrPrompted}
|
||||||
tooltip={selectLabel}
|
tooltip={selectLabel}
|
||||||
@@ -132,19 +124,29 @@ export const VideoDeviceControl = ({
|
|||||||
>
|
>
|
||||||
<RiArrowUpSLine />
|
<RiArrowUpSLine />
|
||||||
</Button>
|
</Button>
|
||||||
<MenuList
|
<div
|
||||||
items={devices.map((d) => ({
|
className={css({
|
||||||
value: d.deviceId,
|
maxWidth: '36rem',
|
||||||
label: d.label,
|
padding: '0.15rem',
|
||||||
}))}
|
display: 'flex',
|
||||||
selectedItem={activeDeviceId}
|
gap: '0.5rem',
|
||||||
onAction={(value) => {
|
})}
|
||||||
setActiveMediaDevice(value as string)
|
>
|
||||||
saveVideoInputDeviceId(value as string)
|
<div
|
||||||
}}
|
style={{
|
||||||
variant="dark"
|
flex: '1 1 0',
|
||||||
/>
|
minWidth: 0,
|
||||||
</Menu>
|
}}
|
||||||
|
>
|
||||||
|
<SelectDevice
|
||||||
|
context="room"
|
||||||
|
kind="videoinput"
|
||||||
|
id={userChoices.videoDeviceId}
|
||||||
|
onSubmit={saveVideoInputDeviceId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user