import { useEffect, useState } from "react"; import { MdVolumeOff, MdVolumeUp, MdGraphicEq, MdMic, MdMicOff, MdRefresh } from "react-icons/md"; import { Button } from "@components/Button"; import { cx } from "@/cva.config"; import { useAudioDevices } from "@/hooks/useAudioDevices"; import { useAudioEvents } from "@/hooks/useAudioEvents"; import api from "@/api"; import notifications from "@/notifications"; 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; } export default function AudioControlPopover({ microphone }: AudioControlPopoverProps) { const [currentConfig, setCurrentConfig] = useState(null); 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, // microphoneState - now using hook state instead isConnected: wsConnected } = useAudioEvents(); // WebSocket-only implementation - no fallback polling // Microphone state from props (keeping hook for legacy device operations) const { isMicrophoneActive: isMicrophoneActiveFromHook, startMicrophone, stopMicrophone, syncMicrophoneState, // Loading states isStarting, isStopping, isToggling, } = microphone; // Use WebSocket data exclusively - no polling fallback const isMuted = audioMuted ?? false; const isConnected = wsConnected; // Note: We now use hook state instead of WebSocket state for microphone Enable/Disable // const isMicrophoneActiveFromWS = microphoneState?.running ?? false; // Audio devices const { audioInputDevices, audioOutputDevices, selectedInputDevice, selectedOutputDevice, setSelectedInputDevice, setSelectedOutputDevice, isLoading: devicesLoading, error: devicesError, refreshDevices } = useAudioDevices(); // 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 } = await audioQualityService.loadAllConfigurations(); if (audio) { setCurrentConfig(audio.current); } setConfigsLoaded(true); } catch { // Failed to load audio configurations } }; const handleToggleMute = async () => { const now = Date.now(); // Prevent rapid clicking if (isLoading || (now - lastClickTime < CLICK_COOLDOWN)) { return; } setLastClickTime(now); setIsLoading(true); try { if (isMuted) { // Unmute: Start audio output process and notify backend const resp = await api.POST("/audio/mute", { muted: false }); if (!resp.ok) { throw new Error(`Failed to unmute audio: ${resp.status}`); } // WebSocket will handle the state update automatically } else { // Mute: Stop audio output process and notify backend const resp = await api.POST("/audio/mute", { muted: true }); if (!resp.ok) { throw new Error(`Failed to mute audio: ${resp.status}`); } // WebSocket will handle the state update automatically } } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to toggle audio mute"; notifications.error(errorMessage); } 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 handleToggleMicrophoneEnable = 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); setIsLoading(true); try { if (isMicrophoneActiveFromHook) { // Disable: Stop microphone subprocess AND remove WebRTC tracks const result = await stopMicrophone(); if (!result.success) { throw new Error(result.error?.message || "Failed to stop microphone"); } } else { // Enable: Start microphone subprocess AND add WebRTC tracks const result = await startMicrophone(); if (!result.success) { throw new Error(result.error?.message || "Failed to start microphone"); } } } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to toggle microphone"; notifications.error(errorMessage); } finally { setIsLoading(false); } }; // Handle microphone device change const handleMicrophoneDeviceChange = async (deviceId: string) => { setSelectedInputDevice(deviceId); // If microphone is currently active, restart it with the new device if (isMicrophoneActiveFromHook) { 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 } }; return (
{/* Header */}

Audio Controls

{isConnected ? "Connected" : "Disconnected"}
{/* Mute Control */}
{isMuted ? ( ) : ( )} {isMuted ? "Muted" : "Unmuted"}
{/* Microphone Control */}
Microphone Input
{isMicrophoneActiveFromHook ? ( ) : ( )} {isMicrophoneActiveFromHook ? "Enabled" : "Disabled"}
{/* Device Selection */}
Audio Devices {devicesLoading && (
)}
{devicesError && (
{devicesError}
)} {/* Microphone Selection */}
{isMicrophoneActiveFromHook && (

Changing device will restart the microphone

)}
{/* Speaker Selection */}
{/* Quality Settings */}
Audio Output Quality
{Object.entries(getQualityLabels()).map(([quality, label]) => ( ))}
{currentConfig && (
Bitrate: {currentConfig.Bitrate}kbps | Sample Rate: {currentConfig.SampleRate}Hz
)}
); }