diff --git a/.devcontainer/install_audio_deps.sh b/.devcontainer/install_audio_deps.sh index 8d369db4..a4a6d6d5 100755 --- a/.devcontainer/install_audio_deps.sh +++ b/.devcontainer/install_audio_deps.sh @@ -4,14 +4,11 @@ set -e # Sudo wrapper function -SUDO_PATH=$(which sudo 2>/dev/null || echo "") function use_sudo() { - if [ "$UID" -eq 0 ]; then + if [ "$UID" -eq 0 ] || [ -z "$(which sudo 2>/dev/null)" ]; then "$@" - elif [ -n "$SUDO_PATH" ]; then - ${SUDO_PATH} -E "$@" else - "$@" + sudo -E "$@" fi } diff --git a/audio.go b/audio.go index 58e56fdd..6f381ed4 100644 --- a/audio.go +++ b/audio.go @@ -49,49 +49,47 @@ func initAudio() { func getAudioConfig() audio.AudioConfig { cfg := audio.DefaultAudioConfig() - // Helper to validate numeric ranges and return sanitized values - // Returns (value, true) if valid, (0, false) if invalid - validateAndApply := func(value int, min int, max int, paramName string) (int, bool) { - if value >= min && value <= max { - return value, true - } - if value != 0 { - audioLogger.Warn().Int(paramName, value).Msgf("Invalid %s, using default", paramName) - } - return 0, false + // Apply bitrate (64-256 kbps) + if config.AudioBitrate >= 64 && config.AudioBitrate <= 256 { + cfg.Bitrate = uint16(config.AudioBitrate) + } else if config.AudioBitrate != 0 { + audioLogger.Warn().Int("bitrate", config.AudioBitrate).Msg("Invalid audio bitrate, using default") } - // Validate and apply bitrate - if bitrate, valid := validateAndApply(config.AudioBitrate, 64, 256, "audio bitrate"); valid { - cfg.Bitrate = uint16(bitrate) + // Apply complexity (0-10) + if config.AudioComplexity >= 0 && config.AudioComplexity <= 10 { + cfg.Complexity = uint8(config.AudioComplexity) + } else if config.AudioComplexity != 0 { + audioLogger.Warn().Int("complexity", config.AudioComplexity).Msg("Invalid audio complexity, using default") } - // Validate and apply complexity - if complexity, valid := validateAndApply(config.AudioComplexity, 0, 10, "audio complexity"); valid { - cfg.Complexity = uint8(complexity) + // Apply buffer periods (2-24) + if config.AudioBufferPeriods >= 2 && config.AudioBufferPeriods <= 24 { + cfg.BufferPeriods = uint8(config.AudioBufferPeriods) + } else if config.AudioBufferPeriods != 0 { + audioLogger.Warn().Int("buffer_periods", config.AudioBufferPeriods).Msg("Invalid buffer periods, using default") + } + + // Apply sample rate (Opus supports: 8k, 12k, 16k, 24k, 48k) + switch config.AudioSampleRate { + case 8000, 12000, 16000, 24000, 48000: + cfg.SampleRate = uint32(config.AudioSampleRate) + default: + if config.AudioSampleRate != 0 { + audioLogger.Warn().Int("sample_rate", config.AudioSampleRate).Msg("Invalid sample rate, using default") + } + } + + // Apply packet loss percentage (0-100) + if config.AudioPacketLossPerc >= 0 && config.AudioPacketLossPerc <= 100 { + cfg.PacketLossPerc = uint8(config.AudioPacketLossPerc) + } else if config.AudioPacketLossPerc != 0 { + audioLogger.Warn().Int("packet_loss_perc", config.AudioPacketLossPerc).Msg("Invalid packet loss percentage, using default") } cfg.DTXEnabled = config.AudioDTXEnabled cfg.FECEnabled = config.AudioFECEnabled - // Validate and apply buffer periods - if periods, valid := validateAndApply(config.AudioBufferPeriods, 2, 24, "buffer periods"); valid { - cfg.BufferPeriods = uint8(periods) - } - - // Opus-compatible rates only: 8k, 12k, 16k, 24k, 48k - validRates := map[int]bool{8000: true, 12000: true, 16000: true, 24000: true, 48000: true} - if validRates[config.AudioSampleRate] { - cfg.SampleRate = uint32(config.AudioSampleRate) - } else if config.AudioSampleRate != 0 { - audioLogger.Warn().Int("sample_rate", config.AudioSampleRate).Uint32("default", cfg.SampleRate).Msg("Invalid sample rate, using default") - } - - // Validate and apply packet loss percentage - if pktLoss, valid := validateAndApply(config.AudioPacketLossPerc, 0, 100, "packet loss percentage"); valid { - cfg.PacketLossPerc = uint8(pktLoss) - } - return cfg } @@ -120,13 +118,17 @@ func startAudio() error { inputErr = startInputAudioUnderMutex(getAlsaDevice("usb")) } - if outputErr != nil && inputErr != nil { - return fmt.Errorf("audio start failed - output: %w, input: %v", outputErr, inputErr) + // Simplified error handling - both errors are worth reporting + if outputErr != nil || inputErr != nil { + if outputErr != nil && inputErr != nil { + return fmt.Errorf("audio start failed - output: %w, input: %v", outputErr, inputErr) + } + if outputErr != nil { + return outputErr + } + return inputErr } - if outputErr != nil { - return outputErr - } - return inputErr + return nil } func startOutputAudioUnderMutex(alsaOutputDevice string) error { @@ -250,9 +252,8 @@ func setAudioTrack(audioTrack *webrtc.TrackLocalStaticSample) { } func setPendingInputTrack(track *webrtc.TrackRemote) { - trackID := new(string) - *trackID = track.ID() - currentInputTrack.Store(trackID) + trackID := track.ID() + currentInputTrack.Store(&trackID) go handleInputTrackForSession(track) } @@ -397,22 +398,11 @@ func handleInputTrackForSession(track *webrtc.TrackRemote) { // processInputPacket handles writing audio data to the input source func processInputPacket(opusData []byte) error { - // Early check to avoid mutex acquisition if source is nil - if inputSource.Load() == nil { - return nil - } - inputSourceMutex.Lock() defer inputSourceMutex.Unlock() - // Reload source inside mutex to ensure we have the currently active source source := inputSource.Load() - if source == nil { - return nil - } - - // Defensive null check - ensure dereferenced pointer is valid - if *source == nil { + if source == nil || *source == nil { return nil } diff --git a/config.go b/config.go index f9d49c8b..c7bc27a8 100644 --- a/config.go +++ b/config.go @@ -290,6 +290,7 @@ func LoadConfig() { loadedConfig.JigglerConfig = getDefaultConfig().JigglerConfig } + // Apply audio defaults for new configs if loadedConfig.AudioBitrate == 0 { defaults := getDefaultConfig() loadedConfig.AudioBitrate = defaults.AudioBitrate @@ -297,13 +298,8 @@ func LoadConfig() { loadedConfig.AudioDTXEnabled = defaults.AudioDTXEnabled loadedConfig.AudioFECEnabled = defaults.AudioFECEnabled loadedConfig.AudioBufferPeriods = defaults.AudioBufferPeriods - } - - if loadedConfig.AudioSampleRate == 0 { - loadedConfig.AudioSampleRate = getDefaultConfig().AudioSampleRate - } - if loadedConfig.AudioPacketLossPerc == 0 { - loadedConfig.AudioPacketLossPerc = getDefaultConfig().AudioPacketLossPerc + loadedConfig.AudioSampleRate = defaults.AudioSampleRate + loadedConfig.AudioPacketLossPerc = defaults.AudioPacketLossPerc } // fixup old keyboard layout value diff --git a/internal/audio/relay.go b/internal/audio/relay.go index 6a523b43..fa576b2e 100644 --- a/internal/audio/relay.go +++ b/internal/audio/relay.go @@ -70,54 +70,54 @@ func (r *OutputRelay) Stop() { func (r *OutputRelay) relayLoop() { defer close(r.stopped) - const initialDelay = 1 * time.Second - const maxDelay = 30 * time.Second const maxRetries = 10 - - retryDelay := initialDelay + retryDelay := 1 * time.Second consecutiveFailures := 0 for r.running.Load() { + // Connect if not connected if !(*r.source).IsConnected() { if err := (*r.source).Connect(); err != nil { - consecutiveFailures++ - if consecutiveFailures >= maxRetries { - r.logger.Error().Int("failures", consecutiveFailures).Msg("Max connection retries exceeded, stopping relay") + if consecutiveFailures++; consecutiveFailures >= maxRetries { + r.logger.Error().Int("failures", consecutiveFailures).Msg("Max retries exceeded, stopping relay") return } - r.logger.Debug().Err(err).Int("failures", consecutiveFailures).Dur("retry_delay", retryDelay).Msg("failed to connect, will retry") + r.logger.Debug().Err(err).Int("failures", consecutiveFailures).Msg("Connection failed, retrying") time.Sleep(retryDelay) - retryDelay = min(retryDelay*2, maxDelay) + retryDelay = min(retryDelay*2, 30*time.Second) continue } consecutiveFailures = 0 - retryDelay = initialDelay + retryDelay = 1 * time.Second } + // Read message from source msgType, payload, err := (*r.source).ReadMessage() if err != nil { - if r.running.Load() { - consecutiveFailures++ - if consecutiveFailures >= maxRetries { - r.logger.Error().Int("failures", consecutiveFailures).Msg("Max read retries exceeded, stopping relay") - return - } - r.logger.Warn().Err(err).Int("failures", consecutiveFailures).Msg("read error, reconnecting") - (*r.source).Disconnect() - time.Sleep(retryDelay) - retryDelay = min(retryDelay*2, maxDelay) + if !r.running.Load() { + break } + if consecutiveFailures++; consecutiveFailures >= maxRetries { + r.logger.Error().Int("failures", consecutiveFailures).Msg("Max read retries exceeded, stopping relay") + return + } + r.logger.Warn().Err(err).Int("failures", consecutiveFailures).Msg("Read error, reconnecting") + (*r.source).Disconnect() + time.Sleep(retryDelay) + retryDelay = min(retryDelay*2, 30*time.Second) continue } + // Reset retry state on success consecutiveFailures = 0 - retryDelay = initialDelay + retryDelay = 1 * time.Second + // Write audio sample to WebRTC if msgType == ipcMsgTypeOpus && len(payload) > 0 { r.sample.Data = payload if err := r.audioTrack.WriteSample(r.sample); err != nil { r.framesDropped.Add(1) - r.logger.Warn().Err(err).Msg("failed to write sample to WebRTC") + r.logger.Warn().Err(err).Msg("Failed to write sample to WebRTC") } else { r.framesRelayed.Add(1) } diff --git a/internal/usbgadget/usbgadget.go b/internal/usbgadget/usbgadget.go index ecdbc0db..7dc8c926 100644 --- a/internal/usbgadget/usbgadget.go +++ b/internal/usbgadget/usbgadget.go @@ -204,58 +204,47 @@ func (u *UsbGadget) Close() error { func (u *UsbGadget) CloseHidFiles() { u.log.Debug().Msg("closing HID files") - // Close keyboard HID file - if u.keyboardHidFile != nil { - if err := u.keyboardHidFile.Close(); err != nil { - u.log.Debug().Err(err).Msg("failed to close keyboard HID file") + closeFile := func(file **os.File, name string) { + if *file != nil { + if err := (*file).Close(); err != nil { + u.log.Debug().Err(err).Msgf("failed to close %s HID file", name) + } + *file = nil } - u.keyboardHidFile = nil } - // Close absolute mouse HID file - if u.absMouseHidFile != nil { - if err := u.absMouseHidFile.Close(); err != nil { - u.log.Debug().Err(err).Msg("failed to close absolute mouse HID file") - } - u.absMouseHidFile = nil - } - - // Close relative mouse HID file - if u.relMouseHidFile != nil { - if err := u.relMouseHidFile.Close(); err != nil { - u.log.Debug().Err(err).Msg("failed to close relative mouse HID file") - } - u.relMouseHidFile = nil - } + closeFile(&u.keyboardHidFile, "keyboard") + closeFile(&u.absMouseHidFile, "absolute mouse") + closeFile(&u.relMouseHidFile, "relative mouse") } // PreOpenHidFiles opens all HID files to reduce input latency func (u *UsbGadget) PreOpenHidFiles() { - // Add a small delay to allow USB gadget reconfiguration to complete - // This prevents "no such device or address" errors when trying to open HID files + // Small delay for USB gadget reconfiguration to complete time.Sleep(100 * time.Millisecond) + openHidFile := func(file **os.File, path string, name string) { + if *file == nil { + f, err := os.OpenFile(path, os.O_RDWR, 0666) + if err != nil { + u.log.Debug().Err(err).Msgf("failed to pre-open %s HID file", name) + } else { + *file = f + } + } + } + if u.enabledDevices.Keyboard { if err := u.openKeyboardHidFile(); err != nil { u.log.Debug().Err(err).Msg("failed to pre-open keyboard HID file") } } + if u.enabledDevices.AbsoluteMouse { - if u.absMouseHidFile == nil { - var err error - u.absMouseHidFile, err = os.OpenFile("/dev/hidg1", os.O_RDWR, 0666) - if err != nil { - u.log.Debug().Err(err).Msg("failed to pre-open absolute mouse HID file") - } - } + openHidFile(&u.absMouseHidFile, "/dev/hidg1", "absolute mouse") } + if u.enabledDevices.RelativeMouse { - if u.relMouseHidFile == nil { - var err error - u.relMouseHidFile, err = os.OpenFile("/dev/hidg2", os.O_RDWR, 0666) - if err != nil { - u.log.Debug().Err(err).Msg("failed to pre-open relative mouse HID file") - } - } + openHidFile(&u.relMouseHidFile, "/dev/hidg2", "relative mouse") } } diff --git a/jsonrpc.go b/jsonrpc.go index 465990c1..6dac64c2 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -866,30 +866,30 @@ func rpcGetUsbDevices() (usbgadget.Devices, error) { func updateUsbRelatedConfig(wasUsbAudioEnabled bool) error { ensureConfigLoaded() nowHasUsbAudio := config.UsbDevices != nil && config.UsbDevices.Audio - outputSourceIsUsb := config.AudioOutputSource == "usb" - // must stop input audio before reconfiguring + // Stop audio before reconfiguring USB gadget stopInputAudio() - - // if we're currently sourcing audio from USB, stop the output audio before reconfiguring - if outputSourceIsUsb { + if config.AudioOutputSource == "usb" { stopOutputAudio() } - // Auto-switch to HDMI audio output when USB audio was selected and is now disabled + // Auto-switch to HDMI when USB audio disabled if wasUsbAudioEnabled && !nowHasUsbAudio && config.AudioOutputSource == "usb" { - logger.Info().Msg("USB audio just disabled, automatic switch audio output source to HDMI") + logger.Info().Msg("USB audio disabled, switching output to HDMI") config.AudioOutputSource = "hdmi" } + // Update USB gadget configuration if err := gadget.UpdateGadgetConfig(); err != nil { - return fmt.Errorf("failed to write gadget config: %w", err) + return fmt.Errorf("failed to update gadget config: %w", err) } + // Save configuration if err := SaveConfig(); err != nil { return fmt.Errorf("failed to save config: %w", err) } + // Restart audio if needed if err := startAudio(); err != nil { logger.Warn().Err(err).Msg("Failed to restart audio after USB reconfiguration") }