import { useEffect, useState } from "react"; import { MdVolumeOff, MdVolumeUp, MdGraphicEq, MdMic, MdMicOff, MdRefresh } from "react-icons/md"; import { LuActivity, LuSettings, LuSignal } from "react-icons/lu"; import { Button } from "@components/Button"; import { AudioLevelMeter } from "@components/AudioLevelMeter"; import { cx } from "@/cva.config"; import { useUiStore } from "@/hooks/stores"; import { useAudioDevices } from "@/hooks/useAudioDevices"; import { useAudioLevel } from "@/hooks/useAudioLevel"; import { useAudioEvents } from "@/hooks/useAudioEvents"; import api from "@/api"; import notifications from "@/notifications"; import { AUDIO_CONFIG } from "@/config/constants"; import audioQualityService from "@/services/audioQualityService"; // Type for microphone error interface MicrophoneError { type: 'permission' | 'device' | 'network' | 'unknown'; message: string; } // Type for microphone hook return value interface MicrophoneHookReturn { isMicrophoneActive: boolean; isMicrophoneMuted: boolean; microphoneStream: MediaStream | null; startMicrophone: (deviceId?: string) => Promise<{ success: boolean; error?: MicrophoneError }>; stopMicrophone: () => Promise<{ success: boolean; error?: MicrophoneError }>; toggleMicrophoneMute: () => Promise<{ success: boolean; error?: MicrophoneError }>; syncMicrophoneState: () => Promise; // Loading states isStarting: boolean; isStopping: boolean; isToggling: boolean; } interface AudioConfig { Quality: number; Bitrate: number; SampleRate: number; Channels: number; FrameSize: string; } // Quality labels will be managed by the audio quality service const getQualityLabels = () => audioQualityService.getQualityLabels(); interface AudioControlPopoverProps { microphone: MicrophoneHookReturn; open?: boolean; // whether the popover is open (controls analysis) } export default function AudioControlPopover({ microphone, open }: AudioControlPopoverProps) { const [currentConfig, setCurrentConfig] = useState(null); const [currentMicrophoneConfig, setCurrentMicrophoneConfig] = useState(null); const [showAdvanced, setShowAdvanced] = useState(false); const [isLoading, setIsLoading] = useState(false); // Add cache flags to prevent unnecessary API calls const [configsLoaded, setConfigsLoaded] = useState(false); // Add cooldown to prevent rapid clicking const [lastClickTime, setLastClickTime] = useState(0); const CLICK_COOLDOWN = 500; // 500ms cooldown between clicks // Use WebSocket-based audio events for real-time updates const { audioMuted, audioMetrics, microphoneMetrics, isConnected: wsConnected } = useAudioEvents(); // WebSocket-only implementation - no fallback polling // Microphone state from props const { isMicrophoneActive, isMicrophoneMuted, microphoneStream, startMicrophone, stopMicrophone, toggleMicrophoneMute, syncMicrophoneState, // Loading states isStarting, isStopping, isToggling, } = microphone; // Use WebSocket data exclusively - no polling fallback const isMuted = audioMuted ?? false; const metrics = audioMetrics; const micMetrics = microphoneMetrics; const isConnected = wsConnected; // Audio level monitoring - enable only when popover is open and microphone is active to save resources const analysisEnabled = (open ?? true) && isMicrophoneActive; const { audioLevel, isAnalyzing } = useAudioLevel(analysisEnabled ? microphoneStream : null, { enabled: analysisEnabled, updateInterval: 120, // 8-10 fps to reduce CPU without losing UX quality }); // Audio devices const { audioInputDevices, audioOutputDevices, selectedInputDevice, selectedOutputDevice, setSelectedInputDevice, setSelectedOutputDevice, isLoading: devicesLoading, error: devicesError, refreshDevices } = useAudioDevices(); const { toggleSidebarView } = useUiStore(); // Load initial configurations once - cache to prevent repeated calls useEffect(() => { if (!configsLoaded) { loadAudioConfigurations(); } }, [configsLoaded]); // WebSocket-only implementation - sync microphone state when needed useEffect(() => { // Always sync microphone state, but debounce it const syncTimeout = setTimeout(() => { syncMicrophoneState(); }, 500); return () => clearTimeout(syncTimeout); }, [syncMicrophoneState]); const loadAudioConfigurations = async () => { try { // Use centralized audio quality service const { audio, microphone } = await audioQualityService.loadAllConfigurations(); if (audio) { setCurrentConfig(audio.current); } if (microphone) { setCurrentMicrophoneConfig(microphone.current); } setConfigsLoaded(true); } catch { // Failed to load audio configurations } }; const handleToggleMute = async () => { setIsLoading(true); try { const resp = await api.POST("/audio/mute", { muted: !isMuted }); if (!resp.ok) { // Failed to toggle mute } // WebSocket will handle the state update automatically } catch { // Failed to toggle mute } finally { setIsLoading(false); } }; const handleQualityChange = async (quality: number) => { setIsLoading(true); try { const resp = await api.POST("/audio/quality", { quality }); if (resp.ok) { const data = await resp.json(); setCurrentConfig(data.config); } } catch { // Failed to change audio quality } finally { setIsLoading(false); } }; const handleMicrophoneQualityChange = async (quality: number) => { try { const resp = await api.POST("/microphone/quality", { quality }); if (resp.ok) { const data = await resp.json(); setCurrentMicrophoneConfig(data.config); } } catch { // Failed to change microphone quality } }; const handleToggleMicrophone = async () => { const now = Date.now(); // Prevent rapid clicking - if any operation is in progress or within cooldown, ignore the click if (isStarting || isStopping || isToggling || (now - lastClickTime < CLICK_COOLDOWN)) { return; } setLastClickTime(now); try { const result = isMicrophoneActive ? await stopMicrophone() : await startMicrophone(selectedInputDevice); if (!result.success && result.error) { notifications.error(result.error.message); } } catch { // Failed to toggle microphone notifications.error("An unexpected error occurred"); } }; const handleToggleMicrophoneMute = async () => { const now = Date.now(); // Prevent rapid clicking - if any operation is in progress or within cooldown, ignore the click if (isStarting || isStopping || isToggling || (now - lastClickTime < CLICK_COOLDOWN)) { return; } setLastClickTime(now); try { const result = await toggleMicrophoneMute(); if (!result.success && result.error) { notifications.error(result.error.message); } } catch { // Failed to toggle microphone mute notifications.error("Failed to toggle microphone mute"); } }; // Handle microphone device change const handleMicrophoneDeviceChange = async (deviceId: string) => { setSelectedInputDevice(deviceId); // If microphone is currently active, restart it with the new device if (isMicrophoneActive) { try { // Stop current microphone await stopMicrophone(); // Start with new device const result = await startMicrophone(deviceId); if (!result.success && result.error) { notifications.error(result.error.message); } } catch { // Failed to change microphone device notifications.error("Failed to change microphone device"); } } }; const handleAudioOutputDeviceChange = async (deviceId: string) => { setSelectedOutputDevice(deviceId); // Find the video element and set the audio output device const videoElement = document.querySelector('video'); if (videoElement && 'setSinkId' in videoElement) { try { await (videoElement as HTMLVideoElement & { setSinkId: (deviceId: string) => Promise }).setSinkId(deviceId); } catch { // Failed to change audio output device } } else { // setSinkId not supported or video element not found } }; const formatBytes = (bytes: number) => { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; }; const formatNumber = (num: number) => { return new Intl.NumberFormat().format(num); }; return (
{/* Header */}

Audio Controls

{isConnected ? "Connected" : "Disconnected"}
{/* Mute Control */}
{isMuted ? ( ) : ( )} {isMuted ? "Muted" : "Unmuted"}
{/* Microphone Control */}
Microphone Input
{isMicrophoneActive ? ( isMicrophoneMuted ? ( ) : ( ) ) : ( )} {!isMicrophoneActive ? "Inactive" : isMicrophoneMuted ? "Muted" : "Active" }
{/* Audio Level Meter */} {isMicrophoneActive && (
{/* Debug information */}
Stream: {microphoneStream ? '✓' : '✗'} Analyzing: {isAnalyzing ? '✓' : '✗'} Active: {isMicrophoneActive ? '✓' : '✗'} Muted: {isMicrophoneMuted ? '✓' : '✗'}
{microphoneStream && (
Tracks: {microphoneStream.getAudioTracks().length} {microphoneStream.getAudioTracks().length > 0 && ( (Enabled: {microphoneStream.getAudioTracks().filter((t: MediaStreamTrack) => t.enabled).length}) )}
)}
)}
{/* Device Selection */}
Audio Devices {devicesLoading && (
)}
{devicesError && (
{devicesError}
)} {/* Microphone Selection */}
{isMicrophoneActive && (

Changing device will restart the microphone

)}
{/* Speaker Selection */}
{/* Microphone Quality Settings */} {isMicrophoneActive && (
Microphone Quality
{Object.entries(getQualityLabels()).map(([quality, label]) => ( ))}
{currentMicrophoneConfig && (
Sample Rate: {currentMicrophoneConfig.SampleRate}Hz Channels: {currentMicrophoneConfig.Channels} Bitrate: {currentMicrophoneConfig.Bitrate}kbps Frame: {currentMicrophoneConfig.FrameSize}
)}
)} {/* Quality Settings */}
Audio Output Quality
{Object.entries(getQualityLabels()).map(([quality, label]) => ( ))}
{currentConfig && (
Sample Rate: {currentConfig.SampleRate}Hz Channels: {currentConfig.Channels} Bitrate: {currentConfig.Bitrate}kbps Frame: {currentConfig.FrameSize}
)}
{/* Advanced Controls Toggle */} {/* Advanced Metrics */} {showAdvanced && (
Performance Metrics
{metrics ? ( <>

Audio Output

Frames Received
{formatNumber(metrics.frames_received)}
Frames Dropped
0 ? "text-red-600 dark:text-red-400" : "text-green-600 dark:text-green-400" )}> {formatNumber(metrics.frames_dropped)}
Data Processed
{formatBytes(metrics.bytes_processed)}
Connection Drops
0 ? "text-red-600 dark:text-red-400" : "text-green-600 dark:text-green-400" )}> {formatNumber(metrics.connection_drops)}
{micMetrics && (

Microphone Input

Frames Sent
{formatNumber(micMetrics.frames_sent)}
Frames Dropped
0 ? "text-red-600 dark:text-red-400" : "text-green-600 dark:text-green-400" )}> {formatNumber(micMetrics.frames_dropped)}
Data Processed
{formatBytes(micMetrics.bytes_processed)}
Connection Drops
0 ? "text-red-600 dark:text-red-400" : "text-green-600 dark:text-green-400" )}> {formatNumber(micMetrics.connection_drops)}
)} {metrics.frames_received > 0 && (
Drop Rate
AUDIO_CONFIG.DROP_RATE_CRITICAL_THRESHOLD ? "text-red-600 dark:text-red-400" : ((metrics.frames_dropped / metrics.frames_received) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER) > AUDIO_CONFIG.DROP_RATE_WARNING_THRESHOLD ? "text-yellow-600 dark:text-yellow-400" : "text-green-600 dark:text-green-400" )}> {((metrics.frames_dropped / metrics.frames_received) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER).toFixed(AUDIO_CONFIG.PERCENTAGE_DECIMAL_PLACES)}%
)}
Last updated: {new Date().toLocaleTimeString()}
) : (
Loading metrics...
)}
)} {/* Audio Metrics Dashboard Button */}
); }