From 4d5aec9a49fd60537654fbcad7bd8cb30f6ca3a7 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 20 Aug 2024 10:31:48 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20the=20AudioTab=20co?= =?UTF-8?q?mponent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First draft, it lacks important features, as a visual indicator when the mic is active, and a trigger to test the audio output. I made some heuristics (e.g. default output/input, etc..) to ship a first version of this setting tabs that should work good enough to create value for our current users. Please refer to the inline comments. --- .../components/SettingsDialogExtended.tsx | 7 +- .../settings/components/tabs/AudioTab.tsx | 90 +++++++++++++++++++ src/frontend/src/locales/de/settings.json | 11 +++ src/frontend/src/locales/en/settings.json | 11 +++ src/frontend/src/locales/fr/settings.json | 11 +++ src/frontend/src/primitives/Select.tsx | 6 ++ 6 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 src/frontend/src/features/settings/components/tabs/AudioTab.tsx diff --git a/src/frontend/src/features/settings/components/SettingsDialogExtended.tsx b/src/frontend/src/features/settings/components/SettingsDialogExtended.tsx index 0db29ea2..262b8a43 100644 --- a/src/frontend/src/features/settings/components/SettingsDialogExtended.tsx +++ b/src/frontend/src/features/settings/components/SettingsDialogExtended.tsx @@ -1,5 +1,5 @@ import { Dialog, type DialogProps } from '@/primitives' -import { Tab, Tabs, TabPanel, TabList } from '@/primitives/Tabs.tsx' +import { Tab, Tabs, TabList } from '@/primitives/Tabs.tsx' import { css } from '@/styled-system/css' import { text } from '@/primitives/Text.tsx' import { Heading } from 'react-aria-components' @@ -11,6 +11,7 @@ import { } from '@remixicon/react' import { AccountTab } from './tabs/AccountTab' import { GeneralTab } from '@/features/settings/components/tabs/GeneralTab.tsx' +import { AudioTab } from '@/features/settings/components/tabs/AudioTab.tsx' const tabsStyle = css({ maxHeight: '40.625rem', // fixme size copied from meet settings modal @@ -68,9 +69,7 @@ export const SettingsDialogExtended = (props: SettingsDialogExtended) => {
- - There are your audio settings - +
diff --git a/src/frontend/src/features/settings/components/tabs/AudioTab.tsx b/src/frontend/src/features/settings/components/tabs/AudioTab.tsx new file mode 100644 index 00000000..e276f135 --- /dev/null +++ b/src/frontend/src/features/settings/components/tabs/AudioTab.tsx @@ -0,0 +1,90 @@ +import { DialogProps, Field, H } from '@/primitives' + +import { TabPanel, TabPanelProps } from '@/primitives/Tabs' +import { useMediaDeviceSelect } from '@livekit/components-react' +import { isSafari } from '@/utils/livekit' +import { useTranslation } from 'react-i18next' + +export type AudioTabProps = Pick & + Pick + +type DeviceItems = Array<{ value: string; label: string }> + +export const AudioTab = ({ id }: AudioTabProps) => { + const { t } = useTranslation('settings') + + const { + devices: devicesOut, + activeDeviceId: activeDeviceIdOut, + setActiveMediaDevice: setActiveMediaDeviceOut, + } = useMediaDeviceSelect({ kind: 'audiooutput' }) + + const { + devices: devicesIn, + activeDeviceId: activeDeviceIdIn, + setActiveMediaDevice: setActiveMediaDeviceIn, + } = useMediaDeviceSelect({ kind: 'audioinput' }) + + const itemsOut: DeviceItems = devicesOut.map((d) => ({ + value: d.deviceId, + label: d.label, + })) + + 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 microphone permissions + // may raise an error. As a workaround, we infer microphone permission status by checking if the list of audio input + // devices (devicesIn) is non-empty. If the list has one or more devices, we assume the user has granted microphone access. + const isMicEnabled = devicesIn?.length > 0 + + const disabledProps = isMicEnabled + ? {} + : { + placeholder: t('audio.permissionsRequired'), + isDisabled: true, + defaultSelectedKey: undefined, + } + + // No API to directly query the default audio device; this function heuristically finds it. + // Returns the item with value 'default' if present; otherwise, returns the first item in the list. + const getDefaultSelectedKey = (items: DeviceItems) => { + if (!items || items.length === 0) return + const defaultItem = + items.find((item) => item.value === 'default') || items[0] + return defaultItem.value + } + + return ( + + {t('audio.microphone.heading')} + setActiveMediaDeviceIn(key as string)} + {...disabledProps} + /> + {/* Safari has a known limitation where its implementation of 'enumerateDevices' does not include audio output devices. + To prevent errors or an empty selection list, we only render the speakers selection field on non-Safari browsers. */} + {!isSafari() && ( + <> + {t('audio.speakers.heading')} + setActiveMediaDeviceOut(key as string)} + {...disabledProps} + /> + + )} + + ) +} diff --git a/src/frontend/src/locales/de/settings.json b/src/frontend/src/locales/de/settings.json index e5f90fec..b3c09aab 100644 --- a/src/frontend/src/locales/de/settings.json +++ b/src/frontend/src/locales/de/settings.json @@ -6,6 +6,17 @@ "nameLabel": "", "authentication": "" }, + "audio": { + "microphone": { + "heading": "", + "label": "" + }, + "speakers": { + "heading": "", + "label": "" + }, + "permissionsRequired": "" + }, "dialog": { "heading": "" }, diff --git a/src/frontend/src/locales/en/settings.json b/src/frontend/src/locales/en/settings.json index c7519c18..a1b51c49 100644 --- a/src/frontend/src/locales/en/settings.json +++ b/src/frontend/src/locales/en/settings.json @@ -6,6 +6,17 @@ "nameLabel": "Votre Nom", "authentication": "Authentication" }, + "audio": { + "microphone": { + "heading": "Microphone", + "label": "Select your audio input" + }, + "speakers": { + "heading": "Speakers", + "label": "Select your audio output" + }, + "permissionsRequired": "Permissions required" + }, "dialog": { "heading": "Settings" }, diff --git a/src/frontend/src/locales/fr/settings.json b/src/frontend/src/locales/fr/settings.json index da9b6943..da64441f 100644 --- a/src/frontend/src/locales/fr/settings.json +++ b/src/frontend/src/locales/fr/settings.json @@ -6,6 +6,17 @@ "nameLabel": "Votre Nom", "authentication": "Authentification" }, + "audio": { + "microphone": { + "heading": "Micro", + "label": "Sélectionner votre entrée audio" + }, + "speakers": { + "heading": "Haut-parleurs", + "label": "Sélectionner votre sortie audio" + }, + "permissionsRequired": "Autorisations nécessaires" + }, "dialog": { "heading": "Paramètres" }, diff --git a/src/frontend/src/primitives/Select.tsx b/src/frontend/src/primitives/Select.tsx index 382435ec..4fe7191b 100644 --- a/src/frontend/src/primitives/Select.tsx +++ b/src/frontend/src/primitives/Select.tsx @@ -32,6 +32,12 @@ const StyledButton = styled(Button, { '&[data-pressed]': { backgroundColor: 'control.hover', }, + // fixme disabled style is being overridden by placeholder one and needs refinement. + '&[data-disabled]': { + color: 'default.subtle-text', + borderColor: 'gray.200', + boxShadow: '0 1px 2px rgba(0 0 0 / 0.02)', + }, }, })