From f380d0342d7c751bf8a5614199e15236641c6528 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 12 Aug 2025 17:36:39 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20video=20tab=20to=20?= =?UTF-8?q?settings=20modal=20for=20camera=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../components/SettingsDialogExtended.tsx | 13 +- .../settings/components/tabs/VideoTab.tsx | 170 ++++++++++++++++++ src/frontend/src/locales/de/settings.json | 13 ++ src/frontend/src/locales/en/settings.json | 13 ++ src/frontend/src/locales/fr/settings.json | 13 ++ src/frontend/src/locales/nl/settings.json | 13 ++ 6 files changed, 232 insertions(+), 3 deletions(-) create mode 100644 src/frontend/src/features/settings/components/tabs/VideoTab.tsx diff --git a/src/frontend/src/features/settings/components/SettingsDialogExtended.tsx b/src/frontend/src/features/settings/components/SettingsDialogExtended.tsx index e3ca7eb9..381a957f 100644 --- a/src/frontend/src/features/settings/components/SettingsDialogExtended.tsx +++ b/src/frontend/src/features/settings/components/SettingsDialogExtended.tsx @@ -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')} + + {isWideScreen && t('tabs.video')} + + {isWideScreen && t('tabs.general')} - + {isWideScreen && t('tabs.notifications')} @@ -91,8 +97,9 @@ export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
- - + + +
diff --git a/src/frontend/src/features/settings/components/tabs/VideoTab.tsx b/src/frontend/src/features/settings/components/tabs/VideoTab.tsx new file mode 100644 index 00000000..7f18b7f0 --- /dev/null +++ b/src/frontend/src/features/settings/components/tabs/VideoTab.tsx @@ -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 ( + <> + {heading} + +
+ {children[0]} +
+
+ {children[1]} +
+
+ + ) +} + +export type VideoTabProps = Pick & + Pick + +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( + 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 ( + + + { + await setActiveMediaDeviceIn(key as string) + saveVideoInputDeviceId(key as string) + }} + {...disabledProps} + style={{ + width: '100%', + }} + /> +
+ {localParticipant.isCameraEnabled ? ( + <> + {/* eslint-disable-next-line jsx-a11y/media-has-caption */} +
+
+
+ ) +} diff --git a/src/frontend/src/locales/de/settings.json b/src/frontend/src/locales/de/settings.json index 1fdb4c42..d4a367af 100644 --- a/src/frontend/src/locales/de/settings.json +++ b/src/frontend/src/locales/de/settings.json @@ -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" } diff --git a/src/frontend/src/locales/en/settings.json b/src/frontend/src/locales/en/settings.json index bc5ee6ac..4283a37b 100644 --- a/src/frontend/src/locales/en/settings.json +++ b/src/frontend/src/locales/en/settings.json @@ -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" } diff --git a/src/frontend/src/locales/fr/settings.json b/src/frontend/src/locales/fr/settings.json index 6a8a7fbc..b7d66269 100644 --- a/src/frontend/src/locales/fr/settings.json +++ b/src/frontend/src/locales/fr/settings.json @@ -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" } diff --git a/src/frontend/src/locales/nl/settings.json b/src/frontend/src/locales/nl/settings.json index 95ad95d4..32354ab7 100644 --- a/src/frontend/src/locales/nl/settings.json +++ b/src/frontend/src/locales/nl/settings.json @@ -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" }