diff --git a/internal/audio/metrics.go b/internal/audio/metrics.go index ffddd3b..d2c2ed0 100644 --- a/internal/audio/metrics.go +++ b/internal/audio/metrics.go @@ -301,8 +301,45 @@ var ( micConnectionDropsValue int64 ) +// UnifiedAudioMetrics provides a common structure for both input and output audio streams +type UnifiedAudioMetrics struct { + FramesReceived int64 `json:"frames_received"` + FramesDropped int64 `json:"frames_dropped"` + FramesSent int64 `json:"frames_sent,omitempty"` + BytesProcessed int64 `json:"bytes_processed"` + ConnectionDrops int64 `json:"connection_drops"` + LastFrameTime time.Time `json:"last_frame_time"` + AverageLatency time.Duration `json:"average_latency"` +} + +// convertAudioMetricsToUnified converts AudioMetrics to UnifiedAudioMetrics +func convertAudioMetricsToUnified(metrics AudioMetrics) UnifiedAudioMetrics { + return UnifiedAudioMetrics{ + FramesReceived: metrics.FramesReceived, + FramesDropped: metrics.FramesDropped, + FramesSent: 0, // AudioMetrics doesn't have FramesSent + BytesProcessed: metrics.BytesProcessed, + ConnectionDrops: metrics.ConnectionDrops, + LastFrameTime: metrics.LastFrameTime, + AverageLatency: metrics.AverageLatency, + } +} + +// convertAudioInputMetricsToUnified converts AudioInputMetrics to UnifiedAudioMetrics +func convertAudioInputMetricsToUnified(metrics AudioInputMetrics) UnifiedAudioMetrics { + return UnifiedAudioMetrics{ + FramesReceived: 0, // AudioInputMetrics doesn't have FramesReceived + FramesDropped: metrics.FramesDropped, + FramesSent: metrics.FramesSent, + BytesProcessed: metrics.BytesProcessed, + ConnectionDrops: metrics.ConnectionDrops, + LastFrameTime: metrics.LastFrameTime, + AverageLatency: metrics.AverageLatency, + } +} + // UpdateAudioMetrics updates Prometheus metrics with current audio data -func UpdateAudioMetrics(metrics AudioMetrics) { +func UpdateAudioMetrics(metrics UnifiedAudioMetrics) { oldReceived := atomic.SwapInt64(&audioFramesReceivedValue, metrics.FramesReceived) if metrics.FramesReceived > oldReceived { audioFramesReceivedTotal.Add(float64(metrics.FramesReceived - oldReceived)) @@ -333,7 +370,7 @@ func UpdateAudioMetrics(metrics AudioMetrics) { } // UpdateMicrophoneMetrics updates Prometheus metrics with current microphone data -func UpdateMicrophoneMetrics(metrics AudioInputMetrics) { +func UpdateMicrophoneMetrics(metrics UnifiedAudioMetrics) { oldSent := atomic.SwapInt64(&micFramesSentValue, metrics.FramesSent) if metrics.FramesSent > oldSent { microphoneFramesSentTotal.Add(float64(metrics.FramesSent - oldSent)) @@ -457,11 +494,11 @@ func StartMetricsUpdater() { for range ticker.C { // Update audio output metrics audioMetrics := GetAudioMetrics() - UpdateAudioMetrics(audioMetrics) + UpdateAudioMetrics(convertAudioMetricsToUnified(audioMetrics)) // Update microphone input metrics micMetrics := GetAudioInputMetrics() - UpdateMicrophoneMetrics(micMetrics) + UpdateMicrophoneMetrics(convertAudioInputMetricsToUnified(micMetrics)) // Update microphone subprocess process metrics if inputSupervisor := GetAudioInputIPCSupervisor(); inputSupervisor != nil { diff --git a/ui/src/components/AudioMetricsDashboard.tsx b/ui/src/components/AudioMetricsDashboard.tsx index 910f7cd..a346366 100644 --- a/ui/src/components/AudioMetricsDashboard.tsx +++ b/ui/src/components/AudioMetricsDashboard.tsx @@ -9,6 +9,8 @@ import { useMicrophone } from "@/hooks/useMicrophone"; import { useAudioLevel } from "@/hooks/useAudioLevel"; import { useAudioEvents } from "@/hooks/useAudioEvents"; import api from "@/api"; +import { AUDIO_CONFIG } from "@/config/constants"; +import audioQualityService from "@/services/audioQualityService"; interface AudioMetrics { frames_received: number; @@ -44,12 +46,8 @@ interface AudioConfig { FrameSize: string; } -const qualityLabels = { - 0: "Low", - 1: "Medium", - 2: "High", - 3: "Ultra" -}; +// Quality labels will be managed by the audio quality service +const getQualityLabels = () => audioQualityService.getQualityLabels(); // Format percentage values to 2 decimal places function formatPercentage(value: number | null | undefined): string { @@ -246,22 +244,15 @@ export default function AudioMetricsDashboard() { const loadAudioConfig = async () => { try { - // Load config - const configResp = await api.GET("/audio/quality"); - if (configResp.ok) { - const configData = await configResp.json(); - setConfig(configData.current); + // Use centralized audio quality service + const { audio, microphone } = await audioQualityService.loadAllConfigurations(); + + if (audio) { + setConfig(audio.current); } - // Load microphone config - try { - const micConfigResp = await api.GET("/microphone/quality"); - if (micConfigResp.ok) { - const micConfigData = await micConfigResp.json(); - setMicrophoneConfig(micConfigData.current); - } - } catch { - // Microphone config not available + if (microphone) { + setMicrophoneConfig(microphone.current); } } catch (error) { console.error("Failed to load audio config:", error); @@ -397,7 +388,7 @@ export default function AudioMetricsDashboard() { const getDropRate = () => { if (!metrics || metrics.frames_received === 0) return 0; - return ((metrics.frames_dropped / metrics.frames_received) * 100); + return ((metrics.frames_dropped / metrics.frames_received) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER); }; @@ -449,7 +440,7 @@ export default function AudioMetricsDashboard() {
Quality: - {qualityLabels[config.Quality as keyof typeof qualityLabels]} + {getQualityLabels()[config.Quality]}
@@ -486,7 +477,7 @@ export default function AudioMetricsDashboard() {
Quality: - {qualityLabels[microphoneConfig.Quality as keyof typeof qualityLabels]} + {getQualityLabels()[microphoneConfig.Quality]}
@@ -668,26 +659,26 @@ export default function AudioMetricsDashboard() { 5 + getDropRate() > AUDIO_CONFIG.DROP_RATE_CRITICAL_THRESHOLD ? "text-red-600 dark:text-red-400" - : getDropRate() > 1 + : getDropRate() > AUDIO_CONFIG.DROP_RATE_WARNING_THRESHOLD ? "text-yellow-600 dark:text-yellow-400" : "text-green-600 dark:text-green-400" )}> - {getDropRate().toFixed(2)}% + {getDropRate().toFixed(AUDIO_CONFIG.PERCENTAGE_DECIMAL_PLACES)}%
5 + getDropRate() > AUDIO_CONFIG.DROP_RATE_CRITICAL_THRESHOLD ? "bg-red-500" - : getDropRate() > 1 + : getDropRate() > AUDIO_CONFIG.DROP_RATE_WARNING_THRESHOLD ? "bg-yellow-500" : "bg-green-500" )} - style={{ width: `${Math.min(getDropRate(), 100)}%` }} + style={{ width: `${Math.min(getDropRate(), AUDIO_CONFIG.MAX_LEVEL_PERCENTAGE)}%` }} />
@@ -734,27 +725,27 @@ export default function AudioMetricsDashboard() { 0 ? (microphoneMetrics.frames_dropped / microphoneMetrics.frames_sent) * 100 : 0) > 5 + (microphoneMetrics.frames_sent > 0 ? (microphoneMetrics.frames_dropped / microphoneMetrics.frames_sent) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER : 0) > AUDIO_CONFIG.DROP_RATE_CRITICAL_THRESHOLD ? "text-red-600 dark:text-red-400" - : (microphoneMetrics.frames_sent > 0 ? (microphoneMetrics.frames_dropped / microphoneMetrics.frames_sent) * 100 : 0) > 1 + : (microphoneMetrics.frames_sent > 0 ? (microphoneMetrics.frames_dropped / microphoneMetrics.frames_sent) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER : 0) > AUDIO_CONFIG.DROP_RATE_WARNING_THRESHOLD ? "text-yellow-600 dark:text-yellow-400" : "text-green-600 dark:text-green-400" )}> - {microphoneMetrics.frames_sent > 0 ? ((microphoneMetrics.frames_dropped / microphoneMetrics.frames_sent) * 100).toFixed(2) : "0.00"}% + {microphoneMetrics.frames_sent > 0 ? ((microphoneMetrics.frames_dropped / microphoneMetrics.frames_sent) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER).toFixed(AUDIO_CONFIG.PERCENTAGE_DECIMAL_PLACES) : "0.00"}%
0 ? (microphoneMetrics.frames_dropped / microphoneMetrics.frames_sent) * 100 : 0) > 5 + (microphoneMetrics.frames_sent > 0 ? (microphoneMetrics.frames_dropped / microphoneMetrics.frames_sent) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER : 0) > AUDIO_CONFIG.DROP_RATE_CRITICAL_THRESHOLD ? "bg-red-500" - : (microphoneMetrics.frames_sent > 0 ? (microphoneMetrics.frames_dropped / microphoneMetrics.frames_sent) * 100 : 0) > 1 + : (microphoneMetrics.frames_sent > 0 ? (microphoneMetrics.frames_dropped / microphoneMetrics.frames_sent) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER : 0) > AUDIO_CONFIG.DROP_RATE_WARNING_THRESHOLD ? "bg-yellow-500" : "bg-green-500" )} style={{ - width: `${Math.min(microphoneMetrics.frames_sent > 0 ? (microphoneMetrics.frames_dropped / microphoneMetrics.frames_sent) * 100 : 0, 100)}%` + width: `${Math.min(microphoneMetrics.frames_sent > 0 ? (microphoneMetrics.frames_dropped / microphoneMetrics.frames_sent) * AUDIO_CONFIG.PERCENTAGE_MULTIPLIER : 0, AUDIO_CONFIG.MAX_LEVEL_PERCENTAGE)}%` }} />
diff --git a/ui/src/components/popovers/AudioControlPopover.tsx b/ui/src/components/popovers/AudioControlPopover.tsx index 7cce34d..fb3364b 100644 --- a/ui/src/components/popovers/AudioControlPopover.tsx +++ b/ui/src/components/popovers/AudioControlPopover.tsx @@ -11,6 +11,8 @@ 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 { @@ -41,12 +43,8 @@ interface AudioConfig { FrameSize: string; } -const qualityLabels = { - 0: "Low (32kbps)", - 1: "Medium (64kbps)", - 2: "High (128kbps)", - 3: "Ultra (256kbps)" -}; +// Quality labels will be managed by the audio quality service +const getQualityLabels = () => audioQualityService.getQualityLabels(); interface AudioControlPopoverProps { microphone: MicrophoneHookReturn; @@ -138,20 +136,15 @@ export default function AudioControlPopover({ microphone, open }: AudioControlPo const loadAudioConfigurations = async () => { try { - // Parallel loading for better performance - const [qualityResp, micQualityResp] = await Promise.all([ - api.GET("/audio/quality"), - api.GET("/microphone/quality") - ]); + // Use centralized audio quality service + const { audio, microphone } = await audioQualityService.loadAllConfigurations(); - if (qualityResp.ok) { - const qualityData = await qualityResp.json(); - setCurrentConfig(qualityData.current); + if (audio) { + setCurrentConfig(audio.current); } - if (micQualityResp.ok) { - const micQualityData = await micQualityResp.json(); - setCurrentMicrophoneConfig(micQualityData.current); + if (microphone) { + setCurrentMicrophoneConfig(microphone.current); } setConfigsLoaded(true); @@ -511,7 +504,7 @@ export default function AudioControlPopover({ microphone, open }: AudioControlPo
- {Object.entries(qualityLabels).map(([quality, label]) => ( + {Object.entries(getQualityLabels()).map(([quality, label]) => (
- {Object.entries(qualityLabels).map(([quality, label]) => ( + {Object.entries(getQualityLabels()).map(([quality, label]) => (
)} diff --git a/ui/src/config/constants.ts b/ui/src/config/constants.ts new file mode 100644 index 0000000..db2a986 --- /dev/null +++ b/ui/src/config/constants.ts @@ -0,0 +1,167 @@ +// Centralized configuration constants + +// Network and API Configuration +export const NETWORK_CONFIG = { + WEBSOCKET_RECONNECT_INTERVAL: 3000, + LONG_PRESS_DURATION: 3000, + ERROR_MESSAGE_TIMEOUT: 3000, + AUDIO_TEST_DURATION: 5000, + BACKEND_RETRY_DELAY: 500, + RESET_DELAY: 200, + STATE_CHECK_DELAY: 100, + VERIFICATION_DELAY: 1000, +} as const; + +// Default URLs and Endpoints +export const DEFAULT_URLS = { + JETKVM_PROD_API: "https://api.jetkvm.com", + JETKVM_PROD_APP: "https://app.jetkvm.com", + JETKVM_DOCS_TROUBLESHOOTING: "https://jetkvm.com/docs/getting-started/troubleshooting", + JETKVM_DOCS_REMOTE_ACCESS: "https://jetkvm.com/docs/networking/remote-access", + JETKVM_DOCS_LOCAL_ACCESS_RESET: "https://jetkvm.com/docs/networking/local-access#reset-password", + JETKVM_GITHUB: "https://github.com/jetkvm", + CRONTAB_GURU: "https://crontab.guru/examples.html", +} as const; + +// Sample ISO URLs for mounting +export const SAMPLE_ISOS = { + UBUNTU_24_04: { + name: "Ubuntu 24.04.2 Desktop", + url: "https://releases.ubuntu.com/24.04.2/ubuntu-24.04.2-desktop-amd64.iso", + }, + DEBIAN_13: { + name: "Debian 13.0.0 (Testing)", + url: "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-13.0.0-amd64-netinst.iso", + }, + DEBIAN_12: { + name: "Debian 12.11.0 (Stable)", + url: "https://cdimage.debian.org/mirror/cdimage/archive/12.11.0/amd64/iso-cd/debian-12.11.0-amd64-netinst.iso", + }, + FEDORA_41: { + name: "Fedora 41 Workstation", + url: "https://download.fedoraproject.org/pub/fedora/linux/releases/41/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-41-1.4.iso", + }, + OPENSUSE_LEAP: { + name: "openSUSE Leap 15.6", + url: "https://download.opensuse.org/distribution/leap/15.6/iso/openSUSE-Leap-15.6-NET-x86_64-Media.iso", + }, + OPENSUSE_TUMBLEWEED: { + name: "openSUSE Tumbleweed", + url: "https://download.opensuse.org/tumbleweed/iso/openSUSE-Tumbleweed-NET-x86_64-Current.iso", + }, + ARCH_LINUX: { + name: "Arch Linux", + url: "https://archlinux.doridian.net/iso/2025.02.01/archlinux-2025.02.01-x86_64.iso", + }, + NETBOOT_XYZ: { + name: "netboot.xyz", + url: "https://boot.netboot.xyz/ipxe/netboot.xyz.iso", + }, +} as const; + +// Security and Access Configuration +export const SECURITY_CONFIG = { + LOCALHOST_ONLY_IP: "127.0.0.1", + LOCALHOST_HOSTNAME: "localhost", + HTTPS_PROTOCOL: "https:", +} as const; + +// Default Hardware Configuration +export const HARDWARE_CONFIG = { + DEFAULT_OFF_AFTER: 50000, + SAMPLE_EDID: "00FFFFFFFFFFFF00047265058A3F6101101E0104A53420783FC125A8554EA0260D5054BFEF80714F8140818081C081008B009500B300283C80A070B023403020360006442100001A000000FD00304C575716010A202020202020000000FC0042323436574C0A202020202020000000FF0054384E4545303033383532320A01F802031CF14F90020304050607011112131415161F2309070783010000011D8018711C1620582C250006442100009E011D007251D01E206E28550006442100001E8C0AD08A20E02D10103E9600064421000018C344806E70B028401720A80406442100001E00000000000000000000000000000000000000000000000000000096", +} as const; + +// Audio Configuration +export const AUDIO_CONFIG = { + // Audio Level Analysis + LEVEL_UPDATE_INTERVAL: 100, // ms - throttle audio level updates for performance + FFT_SIZE: 128, // reduced from 256 for better performance + SMOOTHING_TIME_CONSTANT: 0.8, + RELEVANT_FREQUENCY_BINS: 32, // focus on lower frequencies for voice + RMS_SCALING_FACTOR: 180, // for converting RMS to percentage + MAX_LEVEL_PERCENTAGE: 100, + + // Microphone Configuration + SAMPLE_RATE: 48000, // Hz - high quality audio sampling + CHANNEL_COUNT: 1, // mono for microphone input + OPERATION_DEBOUNCE_MS: 1000, // debounce microphone operations + SYNC_DEBOUNCE_MS: 1000, // debounce state synchronization + AUDIO_TEST_TIMEOUT: 100, // ms - timeout for audio testing + + // Audio Output Quality Bitrates (matching backend config_constants.go) + OUTPUT_QUALITY_BITRATES: { + LOW: 32, // AudioQualityLowOutputBitrate + MEDIUM: 64, // AudioQualityMediumOutputBitrate + HIGH: 128, // AudioQualityHighOutputBitrate + ULTRA: 192, // AudioQualityUltraOutputBitrate + } as const, + // Audio Input Quality Bitrates (matching backend config_constants.go) + INPUT_QUALITY_BITRATES: { + LOW: 16, // AudioQualityLowInputBitrate + MEDIUM: 32, // AudioQualityMediumInputBitrate + HIGH: 64, // AudioQualityHighInputBitrate + ULTRA: 96, // AudioQualityUltraInputBitrate + } as const, + // Sample Rates (matching backend config_constants.go) + QUALITY_SAMPLE_RATES: { + LOW: 22050, // AudioQualityLowSampleRate + MEDIUM: 44100, // AudioQualityMediumSampleRate + HIGH: 48000, // Default SampleRate + ULTRA: 48000, // Default SampleRate + } as const, + // Microphone Sample Rates + MIC_QUALITY_SAMPLE_RATES: { + LOW: 16000, // AudioQualityMicLowSampleRate + MEDIUM: 44100, // AudioQualityMediumSampleRate + HIGH: 48000, // Default SampleRate + ULTRA: 48000, // Default SampleRate + } as const, + // Channels (matching backend config_constants.go) + QUALITY_CHANNELS: { + LOW: 1, // AudioQualityLowChannels (mono) + MEDIUM: 2, // AudioQualityMediumChannels (stereo) + HIGH: 2, // AudioQualityHighChannels (stereo) + ULTRA: 2, // AudioQualityUltraChannels (stereo) + } as const, + // Frame Sizes in milliseconds (matching backend config_constants.go) + QUALITY_FRAME_SIZES: { + LOW: 40, // AudioQualityLowFrameSize (40ms) + MEDIUM: 20, // AudioQualityMediumFrameSize (20ms) + HIGH: 20, // AudioQualityHighFrameSize (20ms) + ULTRA: 10, // AudioQualityUltraFrameSize (10ms) + } as const, + // Updated Quality Labels with correct output bitrates + QUALITY_LABELS: { + 0: "Low (32 kbps)", + 1: "Medium (64 kbps)", + 2: "High (128 kbps)", + 3: "Ultra (192 kbps)", + } as const, + // Legacy support - keeping for backward compatibility + QUALITY_BITRATES: { + LOW: 32, + MEDIUM: 64, + HIGH: 128, + ULTRA: 192, // Updated to match backend + }, + + // Audio Analysis + ANALYSIS_FFT_SIZE: 256, // for detailed audio analysis + ANALYSIS_UPDATE_INTERVAL: 100, // ms - 10fps for audio level updates + LEVEL_SCALING_FACTOR: 255, // for RMS to percentage conversion + + // Audio Metrics Thresholds + DROP_RATE_WARNING_THRESHOLD: 1, // percentage - yellow warning + DROP_RATE_CRITICAL_THRESHOLD: 5, // percentage - red critical + PERCENTAGE_MULTIPLIER: 100, // for converting ratios to percentages + PERCENTAGE_DECIMAL_PLACES: 2, // decimal places for percentage display +} as const; + +// Placeholder URLs +export const PLACEHOLDERS = { + ISO_URL: "https://example.com/image.iso", + PROXY_URL: "http://proxy.example.com:8080/", + API_URL: "https://api.example.com", + APP_URL: "https://app.example.com", +} as const; \ No newline at end of file diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index e0817f3..c0b5d62 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -7,6 +7,8 @@ import { MAX_KEYS_PER_STEP, } from "@/constants/macros"; +import { devWarn } from '../utils/debug'; + // Define the JsonRpc types for better type checking interface JsonRpcResponse { jsonrpc: string; @@ -782,7 +784,7 @@ export const useNetworkStateStore = create((set, get) => ({ setDhcpLeaseExpiry: (expiry: Date) => { const lease = get().dhcp_lease; if (!lease) { - console.warn("No lease found"); + devWarn("No lease found"); return; } diff --git a/ui/src/hooks/useAppNavigation.ts b/ui/src/hooks/useAppNavigation.ts index 6c9270a..5c67407 100644 --- a/ui/src/hooks/useAppNavigation.ts +++ b/ui/src/hooks/useAppNavigation.ts @@ -2,6 +2,7 @@ import { useNavigate, useParams, NavigateOptions } from "react-router-dom"; import { useCallback, useMemo } from "react"; import { isOnDevice } from "../main"; +import { devError } from '../utils/debug'; /** * Generates the correct path based on whether the app is running on device or in cloud mode @@ -21,7 +22,7 @@ export function getDeviceUiPath(path: string, deviceId?: string): string { return normalizedPath; } else { if (!deviceId) { - console.error("No device ID provided when generating path in cloud mode"); + devError("No device ID provided when generating path in cloud mode"); throw new Error("Device ID is required for cloud mode path generation"); } return `/devices/${deviceId}${normalizedPath}`; diff --git a/ui/src/hooks/useAudioDevices.ts b/ui/src/hooks/useAudioDevices.ts index 12dd1c5..38862ca 100644 --- a/ui/src/hooks/useAudioDevices.ts +++ b/ui/src/hooks/useAudioDevices.ts @@ -1,5 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; +import { devError } from '../utils/debug'; + export interface AudioDevice { deviceId: string; label: string; @@ -66,7 +68,7 @@ export function useAudioDevices(): UseAudioDevicesReturn { // Audio devices enumerated } catch (err) { - console.error('Failed to enumerate audio devices:', err); + devError('Failed to enumerate audio devices:', err); setError(err instanceof Error ? err.message : 'Failed to access audio devices'); } finally { setIsLoading(false); diff --git a/ui/src/hooks/useAudioEvents.ts b/ui/src/hooks/useAudioEvents.ts index bb4bc14..fb78857 100644 --- a/ui/src/hooks/useAudioEvents.ts +++ b/ui/src/hooks/useAudioEvents.ts @@ -1,6 +1,9 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import useWebSocket, { ReadyState } from 'react-use-websocket'; +import { devError, devWarn } from '../utils/debug'; +import { NETWORK_CONFIG } from '../config/constants'; + // Audio event types matching the backend export type AudioEventType = | 'audio-mute-changed' @@ -121,7 +124,7 @@ export function useAudioEvents(onAudioDeviceChanged?: (data: AudioDeviceChangedD } = useWebSocket(getWebSocketUrl(), { shouldReconnect: () => true, reconnectAttempts: 10, - reconnectInterval: 3000, + reconnectInterval: NETWORK_CONFIG.WEBSOCKET_RECONNECT_INTERVAL, share: true, // Share the WebSocket connection across multiple hooks onOpen: () => { // WebSocket connected @@ -137,7 +140,7 @@ export function useAudioEvents(onAudioDeviceChanged?: (data: AudioDeviceChangedD globalSubscriptionState.connectionId = null; }, onError: (event) => { - console.error('[AudioEvents] WebSocket error:', event); + devError('[AudioEvents] WebSocket error:', event); }, }); @@ -270,7 +273,7 @@ export function useAudioEvents(onAudioDeviceChanged?: (data: AudioDeviceChangedD } catch (error) { // Ignore parsing errors for non-JSON messages (like "pong") if (lastMessage.data !== 'pong') { - console.warn('[AudioEvents] Failed to parse WebSocket message:', error); + devWarn('[AudioEvents] Failed to parse WebSocket message:', error); } } } diff --git a/ui/src/hooks/useAudioLevel.ts b/ui/src/hooks/useAudioLevel.ts index 769c167..93fa1ab 100644 --- a/ui/src/hooks/useAudioLevel.ts +++ b/ui/src/hooks/useAudioLevel.ts @@ -1,5 +1,7 @@ import { useEffect, useRef, useState } from 'react'; +import { AUDIO_CONFIG } from '@/config/constants'; + interface AudioLevelHookResult { audioLevel: number; // 0-100 percentage isAnalyzing: boolean; @@ -7,14 +9,14 @@ interface AudioLevelHookResult { interface AudioLevelOptions { enabled?: boolean; // Allow external control of analysis - updateInterval?: number; // Throttle updates (default: 100ms for 10fps instead of 60fps) + updateInterval?: number; // Throttle updates (default from AUDIO_CONFIG) } export const useAudioLevel = ( stream: MediaStream | null, options: AudioLevelOptions = {} ): AudioLevelHookResult => { - const { enabled = true, updateInterval = 100 } = options; + const { enabled = true, updateInterval = AUDIO_CONFIG.LEVEL_UPDATE_INTERVAL } = options; const [audioLevel, setAudioLevel] = useState(0); const [isAnalyzing, setIsAnalyzing] = useState(false); @@ -59,8 +61,8 @@ export const useAudioLevel = ( const source = audioContext.createMediaStreamSource(stream); // Configure analyser - use smaller FFT for better performance - analyser.fftSize = 128; // Reduced from 256 for better performance - analyser.smoothingTimeConstant = 0.8; + analyser.fftSize = AUDIO_CONFIG.FFT_SIZE; + analyser.smoothingTimeConstant = AUDIO_CONFIG.SMOOTHING_TIME_CONSTANT; // Connect nodes source.connect(analyser); @@ -87,7 +89,7 @@ export const useAudioLevel = ( // Optimized RMS calculation - process only relevant frequency bands let sum = 0; - const relevantBins = Math.min(dataArray.length, 32); // Focus on lower frequencies for voice + const relevantBins = Math.min(dataArray.length, AUDIO_CONFIG.RELEVANT_FREQUENCY_BINS); for (let i = 0; i < relevantBins; i++) { const value = dataArray[i]; sum += value * value; @@ -95,7 +97,7 @@ export const useAudioLevel = ( const rms = Math.sqrt(sum / relevantBins); // Convert to percentage (0-100) with better scaling - const level = Math.min(100, Math.max(0, (rms / 180) * 100)); // Adjusted scaling for better sensitivity + const level = Math.min(AUDIO_CONFIG.MAX_LEVEL_PERCENTAGE, Math.max(0, (rms / AUDIO_CONFIG.RMS_SCALING_FACTOR) * AUDIO_CONFIG.MAX_LEVEL_PERCENTAGE)); setAudioLevel(Math.round(level)); }; diff --git a/ui/src/hooks/useJsonRpc.ts b/ui/src/hooks/useJsonRpc.ts index fdb144d..112930b 100644 --- a/ui/src/hooks/useJsonRpc.ts +++ b/ui/src/hooks/useJsonRpc.ts @@ -2,6 +2,8 @@ import { useCallback, useEffect } from "react"; import { useRTCStore } from "@/hooks/stores"; +import { devError } from '../utils/debug'; + export interface JsonRpcRequest { jsonrpc: string; method: string; @@ -61,7 +63,7 @@ export function useJsonRpc(onRequest?: (payload: JsonRpcRequest) => void) { return; } - if ("error" in payload) console.error(payload.error); + if ("error" in payload) devError(payload.error); if (!payload.id) return; const callback = callbackStore.get(payload.id); diff --git a/ui/src/hooks/useMicrophone.ts b/ui/src/hooks/useMicrophone.ts index 87bc078..dc37a83 100644 --- a/ui/src/hooks/useMicrophone.ts +++ b/ui/src/hooks/useMicrophone.ts @@ -2,6 +2,8 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useRTCStore } from "@/hooks/stores"; import api from "@/api"; +import { devLog, devInfo, devWarn, devError, devOnly } from "@/utils/debug"; +import { NETWORK_CONFIG, AUDIO_CONFIG } from "@/config/constants"; export interface MicrophoneError { type: 'permission' | 'device' | 'network' | 'unknown'; @@ -31,15 +33,14 @@ export function useMicrophone() { // Add debouncing refs to prevent rapid operations const lastOperationRef = useRef(0); const operationTimeoutRef = useRef(null); - const OPERATION_DEBOUNCE_MS = 1000; // 1 second debounce // Debounced operation wrapper const debouncedOperation = useCallback((operation: () => Promise, operationType: string) => { const now = Date.now(); const timeSinceLastOp = now - lastOperationRef.current; - if (timeSinceLastOp < OPERATION_DEBOUNCE_MS) { - console.log(`Debouncing ${operationType} operation - too soon (${timeSinceLastOp}ms since last)`); + if (timeSinceLastOp < AUDIO_CONFIG.OPERATION_DEBOUNCE_MS) { + devLog(`Debouncing ${operationType} operation - too soon (${timeSinceLastOp}ms since last)`); return; } @@ -51,7 +52,7 @@ export function useMicrophone() { lastOperationRef.current = now; operation().catch(error => { - console.error(`Debounced ${operationType} operation failed:`, error); + devError(`Debounced ${operationType} operation failed:`, error); }); }, []); @@ -72,7 +73,7 @@ export function useMicrophone() { try { await microphoneSender.replaceTrack(null); } catch (error) { - console.warn("Failed to replace track with null:", error); + devWarn("Failed to replace track with null:", error); // Fallback to removing the track peerConnection.removeTrack(microphoneSender); } @@ -110,14 +111,14 @@ export function useMicrophone() { } : "No peer connection", streamMatch: refStream === microphoneStream }; - console.log("Microphone Debug State:", state); + devLog("Microphone Debug State:", state); // Also check if streams are active if (refStream) { - console.log("Ref stream active tracks:", refStream.getAudioTracks().filter(t => t.readyState === 'live').length); + devLog("Ref stream active tracks:", refStream.getAudioTracks().filter(t => t.readyState === 'live').length); } if (microphoneStream && microphoneStream !== refStream) { - console.log("Store stream active tracks:", microphoneStream.getAudioTracks().filter(t => t.readyState === 'live').length); + devLog("Store stream active tracks:", microphoneStream.getAudioTracks().filter(t => t.readyState === 'live').length); } return state; @@ -137,15 +138,15 @@ export function useMicrophone() { const syncMicrophoneState = useCallback(async () => { // Debounce sync calls to prevent race conditions const now = Date.now(); - if (now - lastSyncRef.current < 1000) { // Increased debounce time - console.log("Skipping sync - too frequent"); + if (now - lastSyncRef.current < AUDIO_CONFIG.SYNC_DEBOUNCE_MS) { + devLog("Skipping sync - too frequent"); return; } lastSyncRef.current = now; // Don't sync if we're in the middle of starting the microphone if (isStartingRef.current) { - console.log("Skipping sync - microphone is starting"); + devLog("Skipping sync - microphone is starting"); return; } @@ -157,27 +158,27 @@ export function useMicrophone() { // Only sync if there's a significant state difference and we're not in a transition if (backendRunning !== isMicrophoneActive) { - console.info(`Syncing microphone state: backend=${backendRunning}, frontend=${isMicrophoneActive}`); + devInfo(`Syncing microphone state: backend=${backendRunning}, frontend=${isMicrophoneActive}`); // If backend is running but frontend thinks it's not, just update frontend state if (backendRunning && !isMicrophoneActive) { - console.log("Backend running, updating frontend state to active"); + devLog("Backend running, updating frontend state to active"); setMicrophoneActive(true); } // If backend is not running but frontend thinks it is, clean up and update state else if (!backendRunning && isMicrophoneActive) { - console.log("Backend not running, cleaning up frontend state"); + devLog("Backend not running, cleaning up frontend state"); setMicrophoneActive(false); // Only clean up stream if we actually have one if (microphoneStreamRef.current) { - console.log("Cleaning up orphaned stream"); + devLog("Cleaning up orphaned stream"); await stopMicrophoneStream(); } } } } } catch (error) { - console.warn("Failed to sync microphone state:", error); + devWarn("Failed to sync microphone state:", error); } }, [isMicrophoneActive, setMicrophoneActive, stopMicrophoneStream]); @@ -185,7 +186,7 @@ export function useMicrophone() { const startMicrophone = useCallback(async (deviceId?: string): Promise<{ success: boolean; error?: MicrophoneError }> => { // Prevent multiple simultaneous start operations if (isStarting || isStopping || isToggling) { - console.log("Microphone operation already in progress, skipping start"); + devLog("Microphone operation already in progress, skipping start"); return { success: false, error: { type: 'unknown', message: 'Operation already in progress' } }; } @@ -198,8 +199,8 @@ export function useMicrophone() { echoCancellation: true, noiseSuppression: true, autoGainControl: true, - sampleRate: 48000, - channelCount: 1, + sampleRate: AUDIO_CONFIG.SAMPLE_RATE, + channelCount: AUDIO_CONFIG.CHANNEL_COUNT, }; // Add device ID if specified @@ -207,7 +208,7 @@ export function useMicrophone() { audioConstraints.deviceId = { exact: deviceId }; } - console.log("Requesting microphone with constraints:", audioConstraints); + devLog("Requesting microphone with constraints:", audioConstraints); const stream = await navigator.mediaDevices.getUserMedia({ audio: audioConstraints }); @@ -219,14 +220,14 @@ export function useMicrophone() { setMicrophoneStream(stream); // Verify the stream was stored correctly - console.log("Stream storage verification:", { + devLog("Stream storage verification:", { refSet: !!microphoneStreamRef.current, refId: microphoneStreamRef.current?.id, storeWillBeSet: true // Store update is async }); // Add audio track to peer connection if available - console.log("Peer connection state:", peerConnection ? { + devLog("Peer connection state:", peerConnection ? { connectionState: peerConnection.connectionState, iceConnectionState: peerConnection.iceConnectionState, signalingState: peerConnection.signalingState @@ -234,11 +235,11 @@ export function useMicrophone() { if (peerConnection && stream.getAudioTracks().length > 0) { const audioTrack = stream.getAudioTracks()[0]; - console.log("Starting microphone with audio track:", audioTrack.id, "kind:", audioTrack.kind); + devLog("Starting microphone with audio track:", audioTrack.id, "kind:", audioTrack.kind); // Find the audio transceiver (should already exist with sendrecv direction) const transceivers = peerConnection.getTransceivers(); - console.log("Available transceivers:", transceivers.map(t => ({ + devLog("Available transceivers:", transceivers.map(t => ({ direction: t.direction, mid: t.mid, senderTrack: t.sender.track?.kind, @@ -264,7 +265,7 @@ export function useMicrophone() { return false; }); - console.log("Found audio transceiver:", audioTransceiver ? { + devLog("Found audio transceiver:", audioTransceiver ? { direction: audioTransceiver.direction, mid: audioTransceiver.mid, senderTrack: audioTransceiver.sender.track?.kind, @@ -276,10 +277,10 @@ export function useMicrophone() { // Use the existing audio transceiver's sender await audioTransceiver.sender.replaceTrack(audioTrack); sender = audioTransceiver.sender; - console.log("Replaced audio track on existing transceiver"); + devLog("Replaced audio track on existing transceiver"); // Verify the track was set correctly - console.log("Transceiver after track replacement:", { + devLog("Transceiver after track replacement:", { direction: audioTransceiver.direction, senderTrack: audioTransceiver.sender.track?.id, senderTrackKind: audioTransceiver.sender.track?.kind, @@ -289,11 +290,11 @@ export function useMicrophone() { } else { // Fallback: add new track if no transceiver found sender = peerConnection.addTrack(audioTrack, stream); - console.log("Added new audio track to peer connection"); + devLog("Added new audio track to peer connection"); // Find the transceiver that was created for this track const newTransceiver = peerConnection.getTransceivers().find(t => t.sender === sender); - console.log("New transceiver created:", newTransceiver ? { + devLog("New transceiver created:", newTransceiver ? { direction: newTransceiver.direction, senderTrack: newTransceiver.sender.track?.id, senderTrackKind: newTransceiver.sender.track?.kind @@ -301,7 +302,7 @@ export function useMicrophone() { } setMicrophoneSender(sender); - console.log("Microphone sender set:", { + devLog("Microphone sender set:", { senderId: sender, track: sender.track?.id, trackKind: sender.track?.kind, @@ -310,28 +311,30 @@ export function useMicrophone() { }); // Check sender stats to verify audio is being transmitted - setTimeout(async () => { - try { - const stats = await sender.getStats(); - console.log("Sender stats after 2 seconds:"); - stats.forEach((report, id) => { - if (report.type === 'outbound-rtp' && report.kind === 'audio') { - console.log("Outbound audio RTP stats:", { - id, - packetsSent: report.packetsSent, - bytesSent: report.bytesSent, - timestamp: report.timestamp - }); - } - }); - } catch (error) { - console.error("Failed to get sender stats:", error); - } - }, 2000); + devOnly(() => { + setTimeout(async () => { + try { + const stats = await sender.getStats(); + devLog("Sender stats after 2 seconds:"); + stats.forEach((report, id) => { + if (report.type === 'outbound-rtp' && report.kind === 'audio') { + devLog("Outbound audio RTP stats:", { + id, + packetsSent: report.packetsSent, + bytesSent: report.bytesSent, + timestamp: report.timestamp + }); + } + }); + } catch (error) { + devError("Failed to get sender stats:", error); + } + }, 2000); + }); } // Notify backend that microphone is started - console.log("Notifying backend about microphone start..."); + devLog("Notifying backend about microphone start..."); // Retry logic for backend failures let backendSuccess = false; @@ -341,12 +344,12 @@ export function useMicrophone() { try { // If this is a retry, first try to reset the backend microphone state if (attempt > 1) { - console.log(`Backend start attempt ${attempt}, first trying to reset backend state...`); + devLog(`Backend start attempt ${attempt}, first trying to reset backend state...`); try { // Try the new reset endpoint first const resetResp = await api.POST("/microphone/reset", {}); if (resetResp.ok) { - console.log("Backend reset successful"); + devLog("Backend reset successful"); } else { // Fallback to stop await api.POST("/microphone/stop", {}); @@ -354,59 +357,59 @@ export function useMicrophone() { // Wait a bit for the backend to reset await new Promise(resolve => setTimeout(resolve, 200)); } catch (resetError) { - console.warn("Failed to reset backend state:", resetError); + devWarn("Failed to reset backend state:", resetError); } } const backendResp = await api.POST("/microphone/start", {}); - console.log(`Backend response status (attempt ${attempt}):`, backendResp.status, "ok:", backendResp.ok); + devLog(`Backend response status (attempt ${attempt}):`, backendResp.status, "ok:", backendResp.ok); if (!backendResp.ok) { lastError = `Backend returned status ${backendResp.status}`; - console.error(`Backend microphone start failed with status: ${backendResp.status} (attempt ${attempt})`); + devError(`Backend microphone start failed with status: ${backendResp.status} (attempt ${attempt})`); // For 500 errors, try again after a short delay if (backendResp.status === 500 && attempt < 3) { - console.log(`Retrying backend start in 500ms (attempt ${attempt + 1}/3)...`); + devLog(`Retrying backend start in 500ms (attempt ${attempt + 1}/3)...`); await new Promise(resolve => setTimeout(resolve, 500)); continue; } } else { // Success! const responseData = await backendResp.json(); - console.log("Backend response data:", responseData); + devLog("Backend response data:", responseData); if (responseData.status === "already running") { - console.info("Backend microphone was already running"); + devInfo("Backend microphone was already running"); // If we're on the first attempt and backend says "already running", // but frontend thinks it's not active, this might be a stuck state if (attempt === 1 && !isMicrophoneActive) { - console.warn("Backend reports 'already running' but frontend is not active - possible stuck state"); - console.log("Attempting to reset backend state and retry..."); + devWarn("Backend reports 'already running' but frontend is not active - possible stuck state"); + devLog("Attempting to reset backend state and retry..."); try { const resetResp = await api.POST("/microphone/reset", {}); if (resetResp.ok) { - console.log("Backend reset successful, retrying start..."); + devLog("Backend reset successful, retrying start..."); await new Promise(resolve => setTimeout(resolve, 200)); continue; // Retry the start } } catch (resetError) { - console.warn("Failed to reset stuck backend state:", resetError); + devWarn("Failed to reset stuck backend state:", resetError); } } } - console.log("Backend microphone start successful"); + devLog("Backend microphone start successful"); backendSuccess = true; break; } } catch (error) { lastError = error instanceof Error ? error : String(error); - console.error(`Backend microphone start threw error (attempt ${attempt}):`, error); + devError(`Backend microphone start threw error (attempt ${attempt}):`, error); // For network errors, try again after a short delay if (attempt < 3) { - console.log(`Retrying backend start in 500ms (attempt ${attempt + 1}/3)...`); + devLog(`Retrying backend start in 500ms (attempt ${attempt + 1}/3)...`); await new Promise(resolve => setTimeout(resolve, 500)); continue; } @@ -415,7 +418,7 @@ export function useMicrophone() { // If all backend attempts failed, cleanup and return error if (!backendSuccess) { - console.error("All backend start attempts failed, cleaning up stream"); + devError("All backend start attempts failed, cleaning up stream"); await stopMicrophoneStream(); isStartingRef.current = false; setIsStarting(false); @@ -432,7 +435,7 @@ export function useMicrophone() { setMicrophoneActive(true); setMicrophoneMuted(false); - console.log("Microphone state set to active. Verifying state:", { + devLog("Microphone state set to active. Verifying state:", { streamInRef: !!microphoneStreamRef.current, streamInStore: !!microphoneStream, isActive: true, @@ -441,15 +444,17 @@ export function useMicrophone() { // Don't sync immediately after starting - it causes race conditions // The sync will happen naturally through other triggers - setTimeout(() => { - // Just verify state after a delay for debugging - console.log("State check after delay:", { - streamInRef: !!microphoneStreamRef.current, - streamInStore: !!microphoneStream, - isActive: isMicrophoneActive, - isMuted: isMicrophoneMuted - }); - }, 100); + devOnly(() => { + setTimeout(() => { + // Just verify state after a delay for debugging + devLog("State check after delay:", { + streamInRef: !!microphoneStreamRef.current, + streamInStore: !!microphoneStream, + isActive: isMicrophoneActive, + isMuted: isMicrophoneMuted + }); + }, AUDIO_CONFIG.AUDIO_TEST_TIMEOUT); + }); // Clear the starting flag isStartingRef.current = false; @@ -493,12 +498,12 @@ export function useMicrophone() { // Reset backend microphone state const resetBackendMicrophoneState = useCallback(async (): Promise => { try { - console.log("Resetting backend microphone state..."); + devLog("Resetting backend microphone state..."); const response = await api.POST("/microphone/reset", {}); if (response.ok) { const data = await response.json(); - console.log("Backend microphone reset successful:", data); + devLog("Backend microphone reset successful:", data); // Update frontend state to match backend setMicrophoneActive(false); @@ -506,7 +511,7 @@ export function useMicrophone() { // Clean up any orphaned streams if (microphoneStreamRef.current) { - console.log("Cleaning up orphaned stream after reset"); + devLog("Cleaning up orphaned stream after reset"); await stopMicrophoneStream(); } @@ -518,19 +523,19 @@ export function useMicrophone() { return true; } else { - console.error("Backend microphone reset failed:", response.status); + devError("Backend microphone reset failed:", response.status); return false; } } catch (error) { - console.warn("Failed to reset backend microphone state:", error); + devWarn("Failed to reset backend microphone state:", error); // Fallback to old method try { - console.log("Trying fallback reset method..."); + devLog("Trying fallback reset method..."); await api.POST("/microphone/stop", {}); await new Promise(resolve => setTimeout(resolve, 300)); return true; } catch (fallbackError) { - console.error("Fallback reset also failed:", fallbackError); + devError("Fallback reset also failed:", fallbackError); return false; } } @@ -540,7 +545,7 @@ export function useMicrophone() { const stopMicrophone = useCallback(async (): Promise<{ success: boolean; error?: MicrophoneError }> => { // Prevent multiple simultaneous stop operations if (isStarting || isStopping || isToggling) { - console.log("Microphone operation already in progress, skipping stop"); + devLog("Microphone operation already in progress, skipping stop"); return { success: false, error: { type: 'unknown', message: 'Operation already in progress' } }; } @@ -552,9 +557,9 @@ export function useMicrophone() { // Then notify backend that microphone is stopped try { await api.POST("/microphone/stop", {}); - console.log("Backend notified about microphone stop"); + devLog("Backend notified about microphone stop"); } catch (error) { - console.warn("Failed to notify backend about microphone stop:", error); + devWarn("Failed to notify backend about microphone stop:", error); } // Update frontend state immediately @@ -567,7 +572,7 @@ export function useMicrophone() { setIsStopping(false); return { success: true }; } catch (error) { - console.error("Failed to stop microphone:", error); + devError("Failed to stop microphone:", error); setIsStopping(false); return { success: false, @@ -583,7 +588,7 @@ export function useMicrophone() { const toggleMicrophoneMute = useCallback(async (): Promise<{ success: boolean; error?: MicrophoneError }> => { // Prevent multiple simultaneous toggle operations if (isStarting || isStopping || isToggling) { - console.log("Microphone operation already in progress, skipping toggle"); + devLog("Microphone operation already in progress, skipping toggle"); return { success: false, error: { type: 'unknown', message: 'Operation already in progress' } }; } @@ -592,7 +597,7 @@ export function useMicrophone() { // Use the ref instead of store value to avoid race conditions const currentStream = microphoneStreamRef.current || microphoneStream; - console.log("Toggle microphone mute - current state:", { + devLog("Toggle microphone mute - current state:", { hasRefStream: !!microphoneStreamRef.current, hasStoreStream: !!microphoneStream, isActive: isMicrophoneActive, @@ -610,7 +615,7 @@ export function useMicrophone() { streamId: currentStream?.id, audioTracks: currentStream?.getAudioTracks().length || 0 }; - console.warn("Microphone mute failed: stream or active state missing", errorDetails); + devWarn("Microphone mute failed: stream or active state missing", errorDetails); // Provide more specific error message let errorMessage = 'Microphone is not active'; @@ -647,7 +652,7 @@ export function useMicrophone() { // Mute/unmute the audio track audioTracks.forEach(track => { track.enabled = !newMutedState; - console.log(`Audio track ${track.id} enabled: ${track.enabled}`); + devLog(`Audio track ${track.id} enabled: ${track.enabled}`); }); setMicrophoneMuted(newMutedState); @@ -656,13 +661,13 @@ export function useMicrophone() { try { await api.POST("/microphone/mute", { muted: newMutedState }); } catch (error) { - console.warn("Failed to notify backend about microphone mute:", error); + devWarn("Failed to notify backend about microphone mute:", error); } setIsToggling(false); return { success: true }; } catch (error) { - console.error("Failed to toggle microphone mute:", error); + devError("Failed to toggle microphone mute:", error); setIsToggling(false); return { success: false, @@ -677,7 +682,7 @@ export function useMicrophone() { // Function to check WebRTC audio transmission stats const checkAudioTransmissionStats = useCallback(async () => { if (!microphoneSender) { - console.log("No microphone sender available"); + devLog("No microphone sender available"); return null; } @@ -707,38 +712,38 @@ export function useMicrophone() { } }); - console.log("Audio transmission stats:", audioStats); + devLog("Audio transmission stats:", audioStats); return audioStats; } catch (error) { - console.error("Failed to get audio transmission stats:", error); + devError("Failed to get audio transmission stats:", error); return null; } }, [microphoneSender]); // Comprehensive test function to diagnose microphone issues const testMicrophoneAudio = useCallback(async () => { - console.log("=== MICROPHONE AUDIO TEST ==="); + devLog("=== MICROPHONE AUDIO TEST ==="); // 1. Check if we have a stream const stream = microphoneStreamRef.current; if (!stream) { - console.log("❌ No microphone stream available"); + devLog("❌ No microphone stream available"); return; } - console.log("✅ Microphone stream exists:", stream.id); + devLog("✅ Microphone stream exists:", stream.id); // 2. Check audio tracks const audioTracks = stream.getAudioTracks(); - console.log("Audio tracks:", audioTracks.length); + devLog("Audio tracks:", audioTracks.length); if (audioTracks.length === 0) { - console.log("❌ No audio tracks in stream"); + devLog("❌ No audio tracks in stream"); return; } const track = audioTracks[0]; - console.log("✅ Audio track details:", { + devLog("✅ Audio track details:", { id: track.id, label: track.label, enabled: track.enabled, @@ -752,13 +757,13 @@ export function useMicrophone() { const analyser = audioContext.createAnalyser(); const source = audioContext.createMediaStreamSource(stream); - analyser.fftSize = 256; + analyser.fftSize = AUDIO_CONFIG.ANALYSIS_FFT_SIZE; source.connect(analyser); const dataArray = new Uint8Array(analyser.frequencyBinCount); - console.log("🎤 Testing audio level detection for 5 seconds..."); - console.log("Please speak into your microphone now!"); + devLog("🎤 Testing audio level detection for 5 seconds..."); + devLog("Please speak into your microphone now!"); let maxLevel = 0; let sampleCount = 0; @@ -771,39 +776,39 @@ export function useMicrophone() { sum += value * value; } const rms = Math.sqrt(sum / dataArray.length); - const level = Math.min(100, (rms / 255) * 100); + const level = Math.min(AUDIO_CONFIG.MAX_LEVEL_PERCENTAGE, (rms / AUDIO_CONFIG.LEVEL_SCALING_FACTOR) * AUDIO_CONFIG.MAX_LEVEL_PERCENTAGE); maxLevel = Math.max(maxLevel, level); sampleCount++; if (sampleCount % 10 === 0) { // Log every 10th sample - console.log(`Audio level: ${level.toFixed(1)}% (max so far: ${maxLevel.toFixed(1)}%)`); + devLog(`Audio level: ${level.toFixed(1)}% (max so far: ${maxLevel.toFixed(1)}%)`); } - }, 100); + }, AUDIO_CONFIG.ANALYSIS_UPDATE_INTERVAL); setTimeout(() => { clearInterval(testInterval); source.disconnect(); audioContext.close(); - console.log("🎤 Audio test completed!"); - console.log(`Maximum audio level detected: ${maxLevel.toFixed(1)}%`); + devLog("🎤 Audio test completed!"); + devLog(`Maximum audio level detected: ${maxLevel.toFixed(1)}%`); if (maxLevel > 5) { - console.log("✅ Microphone is detecting audio!"); + devLog("✅ Microphone is detecting audio!"); } else { - console.log("❌ No significant audio detected. Check microphone permissions and hardware."); + devLog("❌ No significant audio detected. Check microphone permissions and hardware."); } - }, 5000); + }, NETWORK_CONFIG.AUDIO_TEST_DURATION); } catch (error) { - console.error("❌ Failed to test audio level:", error); + devError("❌ Failed to test audio level:", error); } // 4. Check WebRTC sender if (microphoneSender) { - console.log("✅ WebRTC sender exists"); - console.log("Sender track:", { + devLog("✅ WebRTC sender exists"); + devLog("Sender track:", { id: microphoneSender.track?.id, kind: microphoneSender.track?.kind, enabled: microphoneSender.track?.enabled, @@ -812,45 +817,45 @@ export function useMicrophone() { // Check if sender track matches stream track if (microphoneSender.track === track) { - console.log("✅ Sender track matches stream track"); + devLog("✅ Sender track matches stream track"); } else { - console.log("❌ Sender track does NOT match stream track"); + devLog("❌ Sender track does NOT match stream track"); } } else { - console.log("❌ No WebRTC sender available"); + devLog("❌ No WebRTC sender available"); } // 5. Check peer connection if (peerConnection) { - console.log("✅ Peer connection exists"); - console.log("Connection state:", peerConnection.connectionState); - console.log("ICE connection state:", peerConnection.iceConnectionState); + devLog("✅ Peer connection exists"); + devLog("Connection state:", peerConnection.connectionState); + devLog("ICE connection state:", peerConnection.iceConnectionState); const transceivers = peerConnection.getTransceivers(); const audioTransceivers = transceivers.filter(t => t.sender.track?.kind === 'audio' || t.receiver.track?.kind === 'audio' ); - console.log("Audio transceivers:", audioTransceivers.map(t => ({ + devLog("Audio transceivers:", audioTransceivers.map(t => ({ direction: t.direction, senderTrack: t.sender.track?.id, receiverTrack: t.receiver.track?.id }))); } else { - console.log("❌ No peer connection available"); + devLog("❌ No peer connection available"); } }, [microphoneSender, peerConnection]); const startMicrophoneDebounced = useCallback((deviceId?: string) => { debouncedOperation(async () => { - await startMicrophone(deviceId).catch(console.error); + await startMicrophone(deviceId).catch(devError); }, "start"); }, [startMicrophone, debouncedOperation]); const stopMicrophoneDebounced = useCallback(() => { debouncedOperation(async () => { - await stopMicrophone().catch(console.error); + await stopMicrophone().catch(devError); }, "stop"); }, [stopMicrophone, debouncedOperation]); @@ -919,10 +924,10 @@ export function useMicrophone() { // Clean up stream directly without depending on the callback const stream = microphoneStreamRef.current; if (stream) { - console.log("Cleanup: stopping microphone stream on unmount"); + devLog("Cleanup: stopping microphone stream on unmount"); stream.getAudioTracks().forEach(track => { track.stop(); - console.log(`Cleanup: stopped audio track ${track.id}`); + devLog(`Cleanup: stopped audio track ${track.id}`); }); microphoneStreamRef.current = null; } diff --git a/ui/src/hooks/useUsbDeviceConfig.ts b/ui/src/hooks/useUsbDeviceConfig.ts index 9ee427a..41e09ae 100644 --- a/ui/src/hooks/useUsbDeviceConfig.ts +++ b/ui/src/hooks/useUsbDeviceConfig.ts @@ -1,5 +1,7 @@ import { useCallback, useEffect, useState } from "react"; +import { devError } from '../utils/debug'; + import { JsonRpcResponse, useJsonRpc } from "./useJsonRpc"; import { useAudioEvents } from "./useAudioEvents"; @@ -25,7 +27,7 @@ export function useUsbDeviceConfig() { setLoading(false); if ("error" in resp) { - console.error("Failed to load USB devices:", resp.error); + devError("Failed to load USB devices:", resp.error); setError(resp.error.data || "Unknown error"); setUsbDeviceConfig(null); } else { diff --git a/ui/src/services/audioQualityService.ts b/ui/src/services/audioQualityService.ts new file mode 100644 index 0000000..94cd490 --- /dev/null +++ b/ui/src/services/audioQualityService.ts @@ -0,0 +1,142 @@ +import api from '@/api'; + +interface AudioConfig { + Quality: number; + Bitrate: number; + SampleRate: number; + Channels: number; + FrameSize: string; +} + +type QualityPresets = Record; + +interface AudioQualityResponse { + current: AudioConfig; + presets: QualityPresets; +} + +class AudioQualityService { + private audioPresets: QualityPresets | null = null; + private microphonePresets: QualityPresets | null = null; + private qualityLabels: Record = { + 0: 'Low', + 1: 'Medium', + 2: 'High', + 3: 'Ultra' + }; + + /** + * Fetch audio quality presets from the backend + */ + async fetchAudioQualityPresets(): Promise { + try { + const response = await api.GET('/audio/quality'); + if (response.ok) { + const data = await response.json(); + this.audioPresets = data.presets; + this.updateQualityLabels(data.presets); + return data; + } + } catch (error) { + console.error('Failed to fetch audio quality presets:', error); + } + return null; + } + + /** + * Fetch microphone quality presets from the backend + */ + async fetchMicrophoneQualityPresets(): Promise { + try { + const response = await api.GET('/microphone/quality'); + if (response.ok) { + const data = await response.json(); + this.microphonePresets = data.presets; + return data; + } + } catch (error) { + console.error('Failed to fetch microphone quality presets:', error); + } + return null; + } + + /** + * Update quality labels with actual bitrates from presets + */ + private updateQualityLabels(presets: QualityPresets): void { + const newQualityLabels: Record = {}; + Object.entries(presets).forEach(([qualityNum, preset]) => { + const quality = parseInt(qualityNum); + const qualityNames = ['Low', 'Medium', 'High', 'Ultra']; + const name = qualityNames[quality] || `Quality ${quality}`; + newQualityLabels[quality] = `${name} (${preset.Bitrate}kbps)`; + }); + this.qualityLabels = newQualityLabels; + } + + /** + * Get quality labels with bitrates + */ + getQualityLabels(): Record { + return this.qualityLabels; + } + + /** + * Get cached audio presets + */ + getAudioPresets(): QualityPresets | null { + return this.audioPresets; + } + + /** + * Get cached microphone presets + */ + getMicrophonePresets(): QualityPresets | null { + return this.microphonePresets; + } + + /** + * Set audio quality + */ + async setAudioQuality(quality: number): Promise { + try { + const response = await api.POST('/audio/quality', { quality }); + return response.ok; + } catch (error) { + console.error('Failed to set audio quality:', error); + return false; + } + } + + /** + * Set microphone quality + */ + async setMicrophoneQuality(quality: number): Promise { + try { + const response = await api.POST('/microphone/quality', { quality }); + return response.ok; + } catch (error) { + console.error('Failed to set microphone quality:', error); + return false; + } + } + + /** + * Load both audio and microphone configurations + */ + async loadAllConfigurations(): Promise<{ + audio: AudioQualityResponse | null; + microphone: AudioQualityResponse | null; + }> { + const [audio, microphone] = await Promise.all([ + this.fetchAudioQualityPresets(), + this.fetchMicrophoneQualityPresets() + ]); + + return { audio, microphone }; + } +} + +// Export a singleton instance +export const audioQualityService = new AudioQualityService(); +export default audioQualityService; \ No newline at end of file diff --git a/ui/src/utils/debug.ts b/ui/src/utils/debug.ts new file mode 100644 index 0000000..916ae01 --- /dev/null +++ b/ui/src/utils/debug.ts @@ -0,0 +1,64 @@ +/** + * Debug utilities for development mode logging + */ + +// Check if we're in development mode +const isDevelopment = import.meta.env.DEV || import.meta.env.MODE === 'development'; + +/** + * Development-only console.log wrapper + * Only logs in development mode, silent in production + */ +export const devLog = (...args: unknown[]): void => { + if (isDevelopment) { + console.log(...args); + } +}; + +/** + * Development-only console.info wrapper + * Only logs in development mode, silent in production + */ +export const devInfo = (...args: unknown[]): void => { + if (isDevelopment) { + console.info(...args); + } +}; + +/** + * Development-only console.warn wrapper + * Only logs in development mode, silent in production + */ +export const devWarn = (...args: unknown[]): void => { + if (isDevelopment) { + console.warn(...args); + } +}; + +/** + * Development-only console.error wrapper + * Always logs errors, but with dev prefix in development + */ +export const devError = (...args: unknown[]): void => { + if (isDevelopment) { + console.error('[DEV]', ...args); + } else { + console.error(...args); + } +}; + +/** + * Development-only debug function wrapper + * Only executes the function in development mode + */ +export const devOnly = (fn: () => T): T | undefined => { + if (isDevelopment) { + return fn(); + } + return undefined; +}; + +/** + * Check if we're in development mode + */ +export const isDevMode = (): boolean => isDevelopment; \ No newline at end of file