diff --git a/cloud.go b/cloud.go index fb138508..f7cab08f 100644 --- a/cloud.go +++ b/cloud.go @@ -20,6 +20,7 @@ import ( "github.com/coder/websocket" "github.com/gin-gonic/gin" + "github.com/jetkvm/kvm/internal/audio" "github.com/rs/zerolog" ) @@ -480,6 +481,16 @@ func handleSessionRequest( cancelKeyboardMacro() currentSession = session + + // Set up audio relay callback to get current session's audio track + // This is needed for audio output to work after enable/disable cycles + audio.SetCurrentSessionCallback(func() audio.AudioTrackWriter { + if currentSession != nil { + return currentSession.AudioTrack + } + return nil + }) + _ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd}) return nil } diff --git a/main.go b/main.go index 7f61dbb8..603e8bb9 100644 --- a/main.go +++ b/main.go @@ -21,16 +21,6 @@ var ( audioSupervisor *audio.AudioOutputSupervisor ) -// runAudioServer is now handled by audio.RunAudioOutputServer -// This function is kept for backward compatibility but delegates to the audio package -func runAudioServer() { - err := audio.RunAudioOutputServer() - if err != nil { - logger.Error().Err(err).Msg("audio output server failed") - os.Exit(1) - } -} - func startAudioSubprocess() error { // Initialize validation cache for optimal performance audio.InitValidationCache() @@ -47,14 +37,14 @@ func startAudioSubprocess() error { audio.SetAudioInputSupervisor(audioInputSupervisor) // Set default OPUS configuration for audio input supervisor (low quality for single-core RV1106) - config := audio.Config + audioConfig := audio.Config audioInputSupervisor.SetOpusConfig( - config.AudioQualityLowInputBitrate*1000, // Convert kbps to bps - config.AudioQualityLowOpusComplexity, - config.AudioQualityLowOpusVBR, - config.AudioQualityLowOpusSignalType, - config.AudioQualityLowOpusBandwidth, - config.AudioQualityLowOpusDTX, + audioConfig.AudioQualityLowInputBitrate*1000, // Convert kbps to bps + audioConfig.AudioQualityLowOpusComplexity, + audioConfig.AudioQualityLowOpusVBR, + audioConfig.AudioQualityLowOpusSignalType, + audioConfig.AudioQualityLowOpusBandwidth, + audioConfig.AudioQualityLowOpusDTX, ) // Note: Audio input supervisor is NOT started here - it will be started on-demand @@ -110,6 +100,12 @@ func startAudioSubprocess() error { }, ) + // Check if USB audio device is enabled before starting audio processes + if config.UsbDevices == nil || !config.UsbDevices.Audio { + logger.Info().Msg("USB audio device disabled - skipping audio supervisor startup") + return nil + } + // Start the supervisor if err := audioSupervisor.Start(); err != nil { return fmt.Errorf("failed to start audio supervisor: %w", err) @@ -137,7 +133,11 @@ func Main(audioServer bool, audioInputServer bool) { // If running as audio server, only initialize audio processing if isAudioServer { - runAudioServer() + err := audio.RunAudioOutputServer() + if err != nil { + logger.Error().Err(err).Msg("audio output server failed") + os.Exit(1) + } return } diff --git a/ui/src/hooks/useMicrophone.ts b/ui/src/hooks/useMicrophone.ts index 8bf97f3a..25124743 100644 --- a/ui/src/hooks/useMicrophone.ts +++ b/ui/src/hooks/useMicrophone.ts @@ -113,14 +113,12 @@ export function useMicrophone() { // Debounce sync calls to prevent race conditions const now = Date.now(); 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) { - devLog("Skipping sync - microphone is starting"); return; } @@ -197,7 +195,6 @@ export function useMicrophone() { audioConstraints.deviceId = { exact: deviceId }; } - devLog("Requesting microphone with constraints:", audioConstraints); const stream = await navigator.mediaDevices.getUserMedia({ audio: audioConstraints }); @@ -265,7 +262,6 @@ export function useMicrophone() { } // Notify backend that microphone is started - devLog("Notifying backend about microphone start..."); // Retry logic for backend failures let backendSuccess = false; @@ -274,7 +270,6 @@ export function useMicrophone() { for (let attempt = 1; attempt <= 3; attempt++) { // If this is a retry, first try to reset the backend microphone state if (attempt > 1) { - devLog(`Backend start attempt ${attempt}, first trying to reset backend state...`); try { // Use RPC for reset (cloud-compatible) if (rpcDataChannel?.readyState === "open") { @@ -290,7 +285,6 @@ export function useMicrophone() { resolve(); // Continue even if both fail }); } else { - devLog("RPC microphone reset successful"); resolve(); } }); @@ -315,7 +309,6 @@ export function useMicrophone() { // For RPC errors, try again after a short delay if (attempt < 3) { - devLog(`Retrying backend start in 500ms (attempt ${attempt + 1}/3)...`); await new Promise(resolve => setTimeout(resolve, 500)); continue; } @@ -556,7 +549,6 @@ export function useMicrophone() { const autoRestoreMicrophone = async () => { // Wait for RPC connection to be ready before attempting any operations if (rpcDataChannel?.readyState !== "open") { - devLog("RPC connection not ready for microphone auto-restore, skipping"); return; } @@ -565,7 +557,6 @@ export function useMicrophone() { // If microphone was enabled before page reload and is not currently active, restore it if (microphoneWasEnabled && !isMicrophoneActive && peerConnection) { - devLog("Auto-restoring microphone after page reload"); try { const result = await startMicrophone(); if (result.success) { diff --git a/web.go b/web.go index d761fb72..66e697fa 100644 --- a/web.go +++ b/web.go @@ -20,6 +20,7 @@ import ( gin_logger "github.com/gin-contrib/logger" "github.com/gin-gonic/gin" "github.com/google/uuid" + "github.com/jetkvm/kvm/internal/audio" "github.com/jetkvm/kvm/internal/logging" "github.com/pion/webrtc/v4" "github.com/prometheus/client_golang/prometheus" @@ -233,6 +234,16 @@ func handleWebRTCSession(c *gin.Context) { cancelKeyboardMacro() currentSession = session + + // Set up audio relay callback to get current session's audio track + // This is needed for audio output to work after enable/disable cycles + audio.SetCurrentSessionCallback(func() audio.AudioTrackWriter { + if currentSession != nil { + return currentSession.AudioTrack + } + return nil + }) + c.JSON(http.StatusOK, gin.H{"sd": sd}) }