From 2c2f2d416b7e40458b609302a62da9ca3a9861ef Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 5 Sep 2025 17:22:14 +0000 Subject: [PATCH] Refactoring: Move most audio business logic into the audio package --- audio_handlers.go | 162 ++++++++++++++++++++++--------------- audio_session_provider.go | 24 ++++++ internal/audio/handlers.go | 159 ++++++++++++++++++++++++++++++++++++ web.go | 4 + 4 files changed, 283 insertions(+), 66 deletions(-) create mode 100644 audio_session_provider.go create mode 100644 internal/audio/handlers.go diff --git a/audio_handlers.go b/audio_handlers.go index 049026f4..fbc1cbe2 100644 --- a/audio_handlers.go +++ b/audio_handlers.go @@ -2,7 +2,7 @@ package kvm import ( "context" - "time" + "net/http" "github.com/coder/websocket" "github.com/gin-gonic/gin" @@ -10,6 +10,15 @@ import ( "github.com/rs/zerolog" ) +var audioControlService *audio.AudioControlService + +func initAudioControlService() { + if audioControlService == nil { + sessionProvider := &SessionProviderImpl{} + audioControlService = audio.NewAudioControlService(sessionProvider, logger) + } +} + // handleAudioMute handles POST /audio/mute requests func handleAudioMute(c *gin.Context) { type muteReq struct { @@ -20,13 +29,13 @@ func handleAudioMute(c *gin.Context) { c.JSON(400, gin.H{"error": "invalid request"}) return } - audio.SetAudioMuted(req.Muted) - // Also set relay mute state if in main process - audio.SetAudioRelayMuted(req.Muted) + initAudioControlService() - // Broadcast audio mute state change via WebSocket - broadcaster := audio.GetAudioEventBroadcaster() - broadcaster.BroadcastAudioDeviceChanged(!req.Muted, "audio_mute_changed") + err := audioControlService.MuteAudio(req.Muted) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } c.JSON(200, gin.H{ "status": "audio mute state updated", @@ -36,35 +45,15 @@ func handleAudioMute(c *gin.Context) { // handleMicrophoneStart handles POST /microphone/start requests func handleMicrophoneStart(c *gin.Context) { - if currentSession == nil { - c.JSON(400, gin.H{"error": "no active session"}) - return - } + initAudioControlService() - if currentSession.AudioInputManager == nil { - c.JSON(500, gin.H{"error": "audio input manager not available"}) - return - } - - // Check cooldown using atomic operations - // Note: Cooldown check would be implemented in audio package if needed - - logger.Info().Msg("starting microphone via HTTP request") - - err := currentSession.AudioInputManager.Start() + err := audioControlService.StartMicrophone() if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - // Broadcast microphone state change via WebSocket - broadcaster := audio.GetAudioEventBroadcaster() - broadcaster.BroadcastAudioDeviceChanged(true, "microphone_started") - - c.JSON(200, gin.H{ - "status": "microphone started", - "is_running": currentSession.AudioInputManager.IsRunning(), - }) + c.JSON(http.StatusOK, gin.H{"success": true}) } // handleMicrophoneMute handles POST /microphone/mute requests @@ -74,51 +63,32 @@ func handleMicrophoneMute(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(400, gin.H{"error": "invalid request body"}) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - // Note: Microphone muting is typically handled at the frontend level - // This endpoint is provided for consistency but doesn't affect backend processing - c.JSON(200, gin.H{ - "status": "mute state updated", - "muted": req.Muted, - }) + initAudioControlService() + + err := audioControlService.MuteMicrophone(req.Muted) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"success": true}) } - - // handleMicrophoneReset handles POST /microphone/reset requests func handleMicrophoneReset(c *gin.Context) { - if currentSession == nil { - c.JSON(400, gin.H{"error": "no active session"}) + initAudioControlService() + + err := audioControlService.ResetMicrophone() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - if currentSession.AudioInputManager == nil { - c.JSON(500, gin.H{"error": "audio input manager not available"}) - return - } - - // Check cooldown using atomic operations - // Note: Cooldown check would be implemented in audio package if needed - - logger.Info().Msg("forcing microphone state reset") - - // Force stop the AudioInputManager - currentSession.AudioInputManager.Stop() - - // Wait a bit to ensure everything is stopped - time.Sleep(100 * time.Millisecond) - - // Broadcast microphone state change via WebSocket - broadcaster := audio.GetAudioEventBroadcaster() - broadcaster.BroadcastAudioDeviceChanged(false, "microphone_reset") - - c.JSON(200, gin.H{ - "status": "microphone reset completed", - "is_running": currentSession.AudioInputManager.IsRunning(), - }) + c.JSON(http.StatusOK, gin.H{"success": true}) } // handleSubscribeAudioEvents handles WebSocket audio event subscription @@ -134,3 +104,63 @@ func handleUnsubscribeAudioEvents(connectionID string, l *zerolog.Logger) { broadcaster := audio.GetAudioEventBroadcaster() broadcaster.Unsubscribe(connectionID) } + +// handleAudioQuality handles GET requests for audio quality presets +func handleAudioQuality(c *gin.Context) { + presets := audio.GetAudioQualityPresets() + c.JSON(200, gin.H{ + "presets": presets, + }) +} + +// handleSetAudioQuality handles POST requests to set audio quality +func handleSetAudioQuality(c *gin.Context) { + var req struct { + Quality int `json:"quality"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + initAudioControlService() + + // Convert int to AudioQuality type + quality := audio.AudioQuality(req.Quality) + + // Set the audio quality + audioControlService.SetAudioQuality(quality) + + c.JSON(200, gin.H{"success": true}) +} + +// handleMicrophoneQuality handles GET requests for microphone quality presets +func handleMicrophoneQuality(c *gin.Context) { + presets := audio.GetMicrophoneQualityPresets() + c.JSON(http.StatusOK, gin.H{ + "presets": presets, + }) +} + +// handleSetMicrophoneQuality handles POST requests to set microphone quality +func handleSetMicrophoneQuality(c *gin.Context) { + var req struct { + Quality int `json:"quality"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + initAudioControlService() + + // Convert int to AudioQuality type + quality := audio.AudioQuality(req.Quality) + + // Set the microphone quality + audioControlService.SetMicrophoneQuality(quality) + + c.JSON(http.StatusOK, gin.H{"success": true}) +} diff --git a/audio_session_provider.go b/audio_session_provider.go new file mode 100644 index 00000000..bc93303d --- /dev/null +++ b/audio_session_provider.go @@ -0,0 +1,24 @@ +package kvm + +import "github.com/jetkvm/kvm/internal/audio" + +// SessionProviderImpl implements the audio.SessionProvider interface +type SessionProviderImpl struct{} + +// NewSessionProvider creates a new session provider +func NewSessionProvider() *SessionProviderImpl { + return &SessionProviderImpl{} +} + +// IsSessionActive returns whether there's an active session +func (sp *SessionProviderImpl) IsSessionActive() bool { + return currentSession != nil +} + +// GetAudioInputManager returns the current session's audio input manager +func (sp *SessionProviderImpl) GetAudioInputManager() *audio.AudioInputManager { + if currentSession == nil { + return nil + } + return currentSession.AudioInputManager +} diff --git a/internal/audio/handlers.go b/internal/audio/handlers.go new file mode 100644 index 00000000..1afd052a --- /dev/null +++ b/internal/audio/handlers.go @@ -0,0 +1,159 @@ +package audio + +import ( + "context" + "errors" + + "github.com/coder/websocket" + "github.com/rs/zerolog" +) + +// AudioControlService provides core audio control operations +type AudioControlService struct { + sessionProvider SessionProvider + logger *zerolog.Logger +} + +// NewAudioControlService creates a new audio control service +func NewAudioControlService(sessionProvider SessionProvider, logger *zerolog.Logger) *AudioControlService { + return &AudioControlService{ + sessionProvider: sessionProvider, + logger: logger, + } +} + +// MuteAudio sets the audio mute state +func (s *AudioControlService) MuteAudio(muted bool) error { + SetAudioMuted(muted) + SetAudioRelayMuted(muted) + + // Broadcast audio mute state change via WebSocket + broadcaster := GetAudioEventBroadcaster() + broadcaster.BroadcastAudioDeviceChanged(!muted, "audio_mute_changed") + + return nil +} + +// StartMicrophone starts the microphone input +func (s *AudioControlService) StartMicrophone() error { + if !s.sessionProvider.IsSessionActive() { + return errors.New("no active session") + } + + audioInputManager := s.sessionProvider.GetAudioInputManager() + if audioInputManager == nil { + return errors.New("audio input manager not available") + } + + if audioInputManager.IsRunning() { + s.logger.Info().Msg("microphone already running") + return nil + } + + if err := audioInputManager.Start(); err != nil { + s.logger.Error().Err(err).Msg("failed to start microphone") + return err + } + + s.logger.Info().Msg("microphone started successfully") + return nil +} + +// MuteMicrophone sets the microphone mute state +func (s *AudioControlService) MuteMicrophone(muted bool) error { + // Set microphone mute state using the audio relay + SetAudioRelayMuted(muted) + + // Broadcast microphone mute state change via WebSocket + broadcaster := GetAudioEventBroadcaster() + broadcaster.BroadcastAudioDeviceChanged(!muted, "microphone_mute_changed") + + s.logger.Info().Bool("muted", muted).Msg("microphone mute state updated") + return nil +} + +// ResetMicrophone resets the microphone +func (s *AudioControlService) ResetMicrophone() error { + if !s.sessionProvider.IsSessionActive() { + return errors.New("no active session") + } + + audioInputManager := s.sessionProvider.GetAudioInputManager() + if audioInputManager == nil { + return errors.New("audio input manager not available") + } + + if audioInputManager.IsRunning() { + audioInputManager.Stop() + s.logger.Info().Msg("stopped microphone for reset") + } + + if err := audioInputManager.Start(); err != nil { + s.logger.Error().Err(err).Msg("failed to restart microphone during reset") + return err + } + + s.logger.Info().Msg("microphone reset successfully") + return nil +} + +// GetMicrophoneStatus returns the current microphone status +func (s *AudioControlService) GetMicrophoneStatus() map[string]interface{} { + if s.sessionProvider == nil { + return map[string]interface{}{ + "error": "no session provider", + } + } + + if !s.sessionProvider.IsSessionActive() { + return map[string]interface{}{ + "error": "no active session", + } + } + + audioInputManager := s.sessionProvider.GetAudioInputManager() + if audioInputManager == nil { + return map[string]interface{}{ + "error": "no audio input manager", + } + } + + return map[string]interface{}{ + "running": audioInputManager.IsRunning(), + "ready": audioInputManager.IsReady(), + } +} + +// SetAudioQuality sets the audio output quality +func (s *AudioControlService) SetAudioQuality(quality AudioQuality) { + SetAudioQuality(quality) +} + +// SetMicrophoneQuality sets the microphone input quality +func (s *AudioControlService) SetMicrophoneQuality(quality AudioQuality) { + SetMicrophoneQuality(quality) +} + +// GetAudioQualityPresets returns available audio quality presets +func (s *AudioControlService) GetAudioQualityPresets() map[AudioQuality]AudioConfig { + return GetAudioQualityPresets() +} + +// GetMicrophoneQualityPresets returns available microphone quality presets +func (s *AudioControlService) GetMicrophoneQualityPresets() map[AudioQuality]AudioConfig { + return GetMicrophoneQualityPresets() +} + +// SubscribeToAudioEvents subscribes to audio events via WebSocket +func (s *AudioControlService) SubscribeToAudioEvents(connectionID string, wsCon *websocket.Conn, runCtx context.Context, logger *zerolog.Logger) { + logger.Info().Msg("client subscribing to audio events") + broadcaster := GetAudioEventBroadcaster() + broadcaster.Subscribe(connectionID, wsCon, runCtx, logger) +} + +// UnsubscribeFromAudioEvents unsubscribes from audio events +func (s *AudioControlService) UnsubscribeFromAudioEvents(connectionID string, logger *zerolog.Logger) { + logger.Info().Str("connection_id", connectionID).Msg("client unsubscribing from audio events") + broadcaster := GetAudioEventBroadcaster() + broadcaster.Unsubscribe(connectionID) +} diff --git a/web.go b/web.go index 5f0e488c..1a3d7c21 100644 --- a/web.go +++ b/web.go @@ -157,6 +157,10 @@ func setupRouter() *gin.Engine { // Audio handlers protected.POST("/audio/mute", handleAudioMute) + protected.GET("/audio/quality", handleAudioQuality) + protected.POST("/audio/quality", handleSetAudioQuality) + protected.GET("/microphone/quality", handleMicrophoneQuality) + protected.POST("/microphone/quality", handleSetMicrophoneQuality) protected.POST("/microphone/start", handleMicrophoneStart) protected.POST("/microphone/mute", handleMicrophoneMute) protected.POST("/microphone/reset", handleMicrophoneReset)