✨(frontend) add video tab to settings modal for camera configuration
Introduce new video tab in settings modal requested by users who found it misleading to lack camera configuration options in settings. Currently implements basic camera device selection. Future commits will expand functionality to include resolution management, subscription settings, and other video-related configurations.
This commit is contained in:
committed by
aleb_the_flash
parent
4cdf257b6a
commit
f380d0342d
@@ -9,11 +9,13 @@ import {
|
||||
RiNotification3Line,
|
||||
RiSettings3Line,
|
||||
RiSpeakerLine,
|
||||
RiVideoOnLine,
|
||||
} from '@remixicon/react'
|
||||
import { AccountTab } from './tabs/AccountTab'
|
||||
import { NotificationsTab } from './tabs/NotificationsTab'
|
||||
import { GeneralTab } from './tabs/GeneralTab'
|
||||
import { AudioTab } from './tabs/AudioTab'
|
||||
import { VideoTab } from './tabs/VideoTab'
|
||||
import { useRef } from 'react'
|
||||
import { useMediaQuery } from '@/features/rooms/livekit/hooks/useMediaQuery'
|
||||
|
||||
@@ -79,10 +81,14 @@ export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
|
||||
{isWideScreen && t('tabs.audio')}
|
||||
</Tab>
|
||||
<Tab icon highlight id="3">
|
||||
<RiVideoOnLine />
|
||||
{isWideScreen && t('tabs.video')}
|
||||
</Tab>
|
||||
<Tab icon highlight id="4">
|
||||
<RiSettings3Line />
|
||||
{isWideScreen && t('tabs.general')}
|
||||
</Tab>
|
||||
<Tab icon highlight id="4">
|
||||
<Tab icon highlight id="5">
|
||||
<RiNotification3Line />
|
||||
{isWideScreen && t('tabs.notifications')}
|
||||
</Tab>
|
||||
@@ -91,8 +97,9 @@ export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
|
||||
<div className={tabPanelContainerStyle}>
|
||||
<AccountTab id="1" onOpenChange={props.onOpenChange} />
|
||||
<AudioTab id="2" />
|
||||
<GeneralTab id="3" />
|
||||
<NotificationsTab id="4" />
|
||||
<VideoTab id="3" />
|
||||
<GeneralTab id="4" />
|
||||
<NotificationsTab id="5" />
|
||||
</div>
|
||||
</Tabs>
|
||||
</Dialog>
|
||||
|
||||
170
src/frontend/src/features/settings/components/tabs/VideoTab.tsx
Normal file
170
src/frontend/src/features/settings/components/tabs/VideoTab.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import { DialogProps, Field, H } from '@/primitives'
|
||||
|
||||
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
|
||||
import { useMediaDeviceSelect, useRoomContext } from '@livekit/components-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices'
|
||||
import { ReactNode, useCallback, useEffect, useState } from 'react'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { createLocalVideoTrack, LocalVideoTrack } from 'livekit-client'
|
||||
|
||||
type RowWrapperProps = {
|
||||
heading: string
|
||||
children: ReactNode[]
|
||||
}
|
||||
|
||||
const RowWrapper = ({ heading, children }: RowWrapperProps) => {
|
||||
return (
|
||||
<>
|
||||
<H lvl={2}>{heading}</H>
|
||||
<HStack
|
||||
gap={0}
|
||||
style={{
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: '1 1 215px',
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{children[0]}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: '10rem',
|
||||
justifyContent: 'center',
|
||||
display: 'flex',
|
||||
paddingLeft: '1.5rem',
|
||||
}}
|
||||
>
|
||||
{children[1]}
|
||||
</div>
|
||||
</HStack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export type VideoTabProps = Pick<DialogProps, 'onOpenChange'> &
|
||||
Pick<TabPanelProps, 'id'>
|
||||
|
||||
type DeviceItems = Array<{ value: string; label: string }>
|
||||
|
||||
export const VideoTab = ({ id }: VideoTabProps) => {
|
||||
const { t } = useTranslation('settings')
|
||||
const { localParticipant } = useRoomContext()
|
||||
|
||||
const {
|
||||
userChoices: { videoDeviceId },
|
||||
saveVideoInputDeviceId,
|
||||
} = usePersistentUserChoices()
|
||||
const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(
|
||||
null
|
||||
)
|
||||
|
||||
const videoCallbackRef = useCallback((element: HTMLVideoElement | null) => {
|
||||
setVideoElement(element)
|
||||
}, [])
|
||||
|
||||
const { devices: devicesIn, setActiveMediaDevice: setActiveMediaDeviceIn } =
|
||||
useMediaDeviceSelect({ kind: 'videoinput' })
|
||||
|
||||
const itemsIn: DeviceItems = devicesIn.map((d) => ({
|
||||
value: d.deviceId,
|
||||
label: d.label,
|
||||
}))
|
||||
|
||||
// The Permissions API is not fully supported in Firefox and Safari, and attempting to use it for camera permissions
|
||||
// may raise an error. As a workaround, we infer camera permission status by checking if the list of camera input
|
||||
// devices (devicesIn) is non-empty. If the list has one or more devices, we assume the user has granted camera access.
|
||||
const isCamEnabled = devicesIn?.length > 0
|
||||
|
||||
const disabledProps = isCamEnabled
|
||||
? {}
|
||||
: {
|
||||
placeholder: t('video.permissionsRequired'),
|
||||
isDisabled: true,
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let videoTrack: LocalVideoTrack | null = null
|
||||
|
||||
const setUpVideoTrack = async () => {
|
||||
if (videoElement) {
|
||||
videoTrack = await createLocalVideoTrack({ deviceId: videoDeviceId })
|
||||
videoTrack.attach(videoElement)
|
||||
}
|
||||
}
|
||||
|
||||
setUpVideoTrack()
|
||||
|
||||
return () => {
|
||||
if (videoElement && videoTrack) {
|
||||
videoTrack.detach()
|
||||
videoTrack.stop()
|
||||
}
|
||||
}
|
||||
}, [videoDeviceId, videoElement])
|
||||
|
||||
return (
|
||||
<TabPanel padding={'md'} flex id={id}>
|
||||
<RowWrapper heading={t('video.camera.heading')}>
|
||||
<Field
|
||||
type="select"
|
||||
label={t('video.camera.label')}
|
||||
items={itemsIn}
|
||||
selectedKey={videoDeviceId}
|
||||
onSelectionChange={async (key) => {
|
||||
await setActiveMediaDeviceIn(key as string)
|
||||
saveVideoInputDeviceId(key as string)
|
||||
}}
|
||||
{...disabledProps}
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
role="status"
|
||||
aria-label={t(
|
||||
`video.camera.previewAriaLabel.${localParticipant.isCameraEnabled ? 'enabled' : 'disabled'}`
|
||||
)}
|
||||
>
|
||||
{localParticipant.isCameraEnabled ? (
|
||||
<>
|
||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||
<video
|
||||
ref={videoCallbackRef}
|
||||
width="160px"
|
||||
height="56px"
|
||||
style={{
|
||||
display: !localParticipant.isCameraEnabled
|
||||
? 'none'
|
||||
: undefined,
|
||||
}}
|
||||
className={css({
|
||||
transform: 'rotateY(180deg)',
|
||||
height: '69px',
|
||||
width: '160px',
|
||||
})}
|
||||
disablePictureInPicture
|
||||
disableRemotePlayback
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<span
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
{t('video.camera.disabled')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</RowWrapper>
|
||||
</TabPanel>
|
||||
)
|
||||
}
|
||||
@@ -30,6 +30,18 @@
|
||||
},
|
||||
"permissionsRequired": "Berechtigungen erforderlich"
|
||||
},
|
||||
"video": {
|
||||
"camera": {
|
||||
"heading": "Kamera",
|
||||
"label": "Wählen Sie Ihre Videoeingabe aus",
|
||||
"disabled": "Kamera deaktiviert",
|
||||
"previewAriaLabel": {
|
||||
"enabled": "Videovorschau aktiviert",
|
||||
"disabled": "Videovorschau deaktiviert"
|
||||
}
|
||||
},
|
||||
"permissionsRequired": "Berechtigungen erforderlich"
|
||||
},
|
||||
"notifications": {
|
||||
"heading": "Tonbenachrichtigungen",
|
||||
"label": "Tonbenachrichtigungen für",
|
||||
@@ -54,6 +66,7 @@
|
||||
"tabs": {
|
||||
"account": "Profil",
|
||||
"audio": "Audio",
|
||||
"video": "Video",
|
||||
"general": "Allgemein",
|
||||
"notifications": "Benachrichtigungen"
|
||||
}
|
||||
|
||||
@@ -30,6 +30,18 @@
|
||||
},
|
||||
"permissionsRequired": "Permissions required"
|
||||
},
|
||||
"video": {
|
||||
"camera": {
|
||||
"heading": "Camera",
|
||||
"label": "Select your video input",
|
||||
"disabled": "Camera disabled",
|
||||
"previewAriaLabel": {
|
||||
"enabled": "Video preview enabled",
|
||||
"disabled": "Video preview disabled"
|
||||
}
|
||||
},
|
||||
"permissionsRequired": "Permissions required"
|
||||
},
|
||||
"notifications": {
|
||||
"heading": "Sound notifications",
|
||||
"label": "sound notifications for",
|
||||
@@ -54,6 +66,7 @@
|
||||
"tabs": {
|
||||
"account": "Profile",
|
||||
"audio": "Audio",
|
||||
"video": "Video",
|
||||
"general": "General",
|
||||
"notifications": "Notifications"
|
||||
}
|
||||
|
||||
@@ -30,6 +30,18 @@
|
||||
},
|
||||
"permissionsRequired": "Autorisations nécessaires"
|
||||
},
|
||||
"video": {
|
||||
"camera": {
|
||||
"heading": "Caméra",
|
||||
"label": "Sélectionner votre entrée vidéo",
|
||||
"disabled": "Caméra désactivée",
|
||||
"previewAriaLabel": {
|
||||
"enabled": "Aperçu vidéo activé",
|
||||
"disabled": "Aperçu vidéo désactivé"
|
||||
}
|
||||
},
|
||||
"permissionsRequired": "Autorisations nécessaires"
|
||||
},
|
||||
"notifications": {
|
||||
"heading": "Notifications sonores",
|
||||
"label": "la notification sonore pour",
|
||||
@@ -54,6 +66,7 @@
|
||||
"tabs": {
|
||||
"account": "Profil",
|
||||
"audio": "Audio",
|
||||
"video": "Vidéo",
|
||||
"general": "Général",
|
||||
"notifications": "Notifications"
|
||||
}
|
||||
|
||||
@@ -30,6 +30,18 @@
|
||||
},
|
||||
"permissionsRequired": "Machtigingen vereist"
|
||||
},
|
||||
"video": {
|
||||
"camera": {
|
||||
"heading": "Camera",
|
||||
"label": "Selecteer uw video-ingang",
|
||||
"disabled": "Camera uitgeschakeld",
|
||||
"previewAriaLabel": {
|
||||
"enabled": "Videovoorbeeld ingeschakeld",
|
||||
"disabled": "Videovoorbeeld uitgeschakeld"
|
||||
}
|
||||
},
|
||||
"permissionsRequired": "Machtigingen vereist"
|
||||
},
|
||||
"notifications": {
|
||||
"heading": "Geluidsmeldingen",
|
||||
"label": "Geluidsmeldingen voor",
|
||||
@@ -54,6 +66,7 @@
|
||||
"tabs": {
|
||||
"account": "Profiel",
|
||||
"audio": "Audio",
|
||||
"video": "Video",
|
||||
"general": "Algemeen",
|
||||
"notifications": "Meldingen"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user