diff --git a/CHANGELOG.md b/CHANGELOG.md index 8144b79b..c6fd47c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to ### Added - 📈(frontend) capture media diagnostics on media errors +- ✨(frontend) add an audio gauge to the microphone select menu ### Changed diff --git a/src/frontend/src/features/rooms/components/Join.tsx b/src/frontend/src/features/rooms/components/Join.tsx index 3d83c767..f7ee31d6 100644 --- a/src/frontend/src/features/rooms/components/Join.tsx +++ b/src/frontend/src/features/rooms/components/Join.tsx @@ -729,6 +729,7 @@ export const Join = ({ { try { saveAudioInputDeviceId(id) diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioDevicesControl.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioDevicesControl.tsx index b6b52815..914760c8 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioDevicesControl.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioDevicesControl.tsx @@ -1,8 +1,12 @@ import { useTranslation } from 'react-i18next' -import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react' +import { + useLocalParticipant, + useTrackToggle, + UseTrackToggleProps, +} from '@livekit/components-react' import { Button, Popover } from '@/primitives' import { RiArrowUpSLine } from '@remixicon/react' -import { Track } from 'livekit-client' +import { LocalAudioTrack, Track } from 'livekit-client' import { ToggleDevice } from './ToggleDevice' import { css } from '@/styled-system/css' @@ -51,6 +55,12 @@ export const AudioDevicesControl = ({ ...props, }) + const { microphoneTrack } = useLocalParticipant() + const localAudioTrack = + microphoneTrack?.track instanceof LocalAudioTrack + ? microphoneTrack.track + : undefined + const kind = 'audioinput' const cannotUseDevice = useCannotUseDevice(kind) const selectLabel = t(`settings.${SettingsDialogExtendedKey.AUDIO}`) @@ -111,6 +121,7 @@ export const AudioDevicesControl = ({ context="room" kind={kind} id={audioDeviceId} + track={localAudioTrack} onSubmit={saveAudioInputDeviceId} /> diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioLevelGauge.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioLevelGauge.tsx new file mode 100644 index 00000000..b3da87a7 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioLevelGauge.tsx @@ -0,0 +1,154 @@ +import { useEffect, useState } from 'react' +import { LocalAudioTrack, TrackEvent } from 'livekit-client' +import { useTrackVolume } from '@livekit/components-react' +import { useTranslation } from 'react-i18next' +import { RiMicLine, RiMicOffLine } from '@remixicon/react' +import { styled } from '@/styled-system/jsx' +import { Text } from '@/primitives' + +const StyledContainer = styled('div', { + base: { + display: 'flex', + alignItems: 'center', + gap: '0.75rem', + padding: '0.75rem 0.25rem', + marginTop: '0.5rem', + borderTop: '1px solid', + minHeight: '2.5rem', + }, + variants: { + theme: { + light: { + borderColor: 'gray.200', + color: 'greyscale.600', + }, + dark: { + borderColor: 'primaryDark.300', + color: 'rgba(255 255 255 / 0.7)', + }, + }, + }, +}) + +const StyledGaugeContainer = styled('div', { + base: { + flexGrow: 1, + height: '0.375rem', + borderRadius: '0.1875rem', + overflow: 'hidden', + }, + variants: { + theme: { + light: { + backgroundColor: 'greyscale.250', + }, + dark: { + backgroundColor: 'rgba(255 255 255 / 0.25)', + }, + }, + }, +}) + +const StyledGauge = styled('div', { + base: { + width: '100%', + height: '100%', + borderRadius: 'inherit', + transformOrigin: 'left center', + transform: 'scaleX(0)', + transition: 'transform 0.06s linear', + }, + variants: { + theme: { + light: { + backgroundColor: 'primary.500', + }, + dark: { + backgroundColor: 'primaryDark.800', + }, + }, + }, +}) + +type Theme = 'light' | 'dark' + +type AudioLevelGaugeProps = { + track?: LocalAudioTrack + variant?: Theme +} + +const useIsTrackMuted = (track?: LocalAudioTrack) => { + const [isMuted, setIsMuted] = useState(() => track?.isMuted ?? true) + + useEffect(() => { + if (!track) { + setIsMuted(true) + return + } + setIsMuted(track.isMuted) + const onMuted = () => setIsMuted(true) + const onUnmuted = () => setIsMuted(false) + track.on(TrackEvent.Muted, onMuted) + track.on(TrackEvent.Unmuted, onUnmuted) + return () => { + track.off(TrackEvent.Muted, onMuted) + track.off(TrackEvent.Unmuted, onUnmuted) + } + }, [track]) + + return isMuted +} + +const LevelBar = ({ + track, + theme, +}: { + track: LocalAudioTrack + theme: Theme +}) => { + const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' }) + const volume = useTrackVolume(track, { + fftSize: 256, + smoothingTimeConstant: 0.7, + }) + const level = Math.min(1, volume) + + return ( + <> +