(frontend) add the AudioTab component

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.
This commit is contained in:
lebaudantoine
2024-08-20 10:31:48 +02:00
committed by aleb_the_flash
parent 74b296aa37
commit 4d5aec9a49
6 changed files with 132 additions and 4 deletions

View File

@@ -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) => {
</div>
<div className={tabPanelContainerStyle}>
<AccountTab id="1" onOpenChange={props.onOpenChange} />
<TabPanel flex id="2">
There are your audio settings
</TabPanel>
<AudioTab id="2" />
<GeneralTab id="3" />
</div>
</Tabs>

View File

@@ -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<DialogProps, 'onOpenChange'> &
Pick<TabPanelProps, 'id'>
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 (
<TabPanel padding={'md'} flex id={id}>
<H lvl={2}>{t('audio.microphone.heading')}</H>
<Field
type="select"
label={t('audio.microphone.label')}
items={itemsIn}
defaultSelectedKey={activeDeviceIdIn || getDefaultSelectedKey(itemsIn)}
onSelectionChange={(key) => 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() && (
<>
<H lvl={2}>{t('audio.speakers.heading')}</H>
<Field
type="select"
label={t('audio.speakers.label')}
items={itemsOut}
defaultSelectedKey={
activeDeviceIdOut || getDefaultSelectedKey(itemsOut)
}
onSelectionChange={(key) => setActiveMediaDeviceOut(key as string)}
{...disabledProps}
/>
</>
)}
</TabPanel>
)
}

View File

@@ -6,6 +6,17 @@
"nameLabel": "",
"authentication": ""
},
"audio": {
"microphone": {
"heading": "",
"label": ""
},
"speakers": {
"heading": "",
"label": ""
},
"permissionsRequired": ""
},
"dialog": {
"heading": ""
},

View File

@@ -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"
},

View File

@@ -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"
},

View File

@@ -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)',
},
},
})