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