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 { useJsonRpc, JsonRpcResponse } from "@/hooks/useJsonRpc"; import { useRTCStore } from "@/hooks/stores"; import notifications from "@/notifications"; // 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; // HTTP/HTTPS detection isHttpsRequired: boolean; } interface AudioConfig { Quality: number; Bitrate: number; SampleRate: number; Channels: number; FrameSize: string; } 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(); // RPC for device communication (works both locally and via cloud) const { rpcDataChannel } = useRTCStore(); const { send } = useJsonRpc(); // Initialize audio quality service with RPC for cloud compatibility // Audio quality service removed - using fixed optimal configuration // 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, // HTTP/HTTPS detection isHttpsRequired, } = microphone; // Use WebSocket data exclusively - no polling fallback const isMuted = audioMuted ?? false; const isConnected = wsConnected; // 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 { // Load audio configuration directly via RPC if (!send) return; await new Promise((resolve, reject) => { send("audioStatus", {}, (resp: JsonRpcResponse) => { if ("error" in resp) { reject(new Error(resp.error.message)); } else if ("result" in resp && resp.result) { const result = resp.result as any; if (result.config) { setCurrentConfig(result.config); } resolve(); } else { resolve(); } }); }); 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 { // Use RPC for device communication - works for both local and cloud if (rpcDataChannel?.readyState !== "open") { throw new Error("Device connection not available"); } await new Promise((resolve, reject) => { send("audioMute", { muted: !isMuted }, (resp: JsonRpcResponse) => { if ("error" in resp) { reject(new Error(resp.error.message)); } else { resolve(); } }); }); // 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); } }; // Quality change handler removed - quality is now fixed at optimal settings 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: Use the hook's stopMicrophone which handles both RPC and local cleanup const result = await stopMicrophone(); if (!result.success) { throw new Error(result.error?.message || "Failed to stop microphone"); } } else { // Enable: Use the hook's startMicrophone which handles both RPC and local setup 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) => { // Don't process device changes for HTTPS-required placeholder if (deviceId === 'https-required') { return; } 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"}
{/* HTTPS requirement notice */} {isHttpsRequired && (

HTTPS Required for Microphone Input

Microphone access requires a secure connection due to browser security policies. Audio output works fine on HTTP, but microphone input needs HTTPS.

Current: {window.location.protocol + '//' + window.location.host}
Secure: {'https://' + window.location.host}

)}
{/* Device Selection */}
Audio Devices {devicesLoading && (
)}
{devicesError && (
{devicesError}
)} {/* Microphone Selection */}
{isHttpsRequired ? (

HTTPS connection required for microphone device selection

) : isMicrophoneActiveFromHook ? (

Changing device will restart the microphone

) : null}
{/* Speaker Selection */}
{/* Audio Quality Info (fixed optimal configuration) */} {currentConfig && (
Audio Configuration
Optimized for S16_LE @ 48kHz stereo HDMI audio
Bitrate: {currentConfig.Bitrate} kbps | Sample Rate: {currentConfig.SampleRate} Hz | Channels: {currentConfig.Channels}
)}
); }