Simplify audio configuration and error handling

- Replace helper function in getAudioConfig with explicit validation
- Consolidate audio default application in LoadConfig
- Streamline relay retry logic with inline conditions
- Extract closeFile and openHidFile helpers in USB gadget
- Simplify setPendingInputTrack pointer handling
- Improve error handling clarity in startAudio and updateUsbRelatedConfig
- Clean up processInputPacket mutex usage
This commit is contained in:
Alex P 2025-11-21 00:54:32 +02:00
parent 3ed663b4d1
commit 5f7c90649a
6 changed files with 105 additions and 133 deletions

View File

@ -4,14 +4,11 @@
set -e set -e
# Sudo wrapper function # Sudo wrapper function
SUDO_PATH=$(which sudo 2>/dev/null || echo "")
function use_sudo() { 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 else
"$@" sudo -E "$@"
fi fi
} }

100
audio.go
View File

@ -49,49 +49,47 @@ func initAudio() {
func getAudioConfig() audio.AudioConfig { func getAudioConfig() audio.AudioConfig {
cfg := audio.DefaultAudioConfig() cfg := audio.DefaultAudioConfig()
// Helper to validate numeric ranges and return sanitized values // Apply bitrate (64-256 kbps)
// Returns (value, true) if valid, (0, false) if invalid if config.AudioBitrate >= 64 && config.AudioBitrate <= 256 {
validateAndApply := func(value int, min int, max int, paramName string) (int, bool) { cfg.Bitrate = uint16(config.AudioBitrate)
if value >= min && value <= max { } else if config.AudioBitrate != 0 {
return value, true audioLogger.Warn().Int("bitrate", config.AudioBitrate).Msg("Invalid audio bitrate, using default")
}
if value != 0 {
audioLogger.Warn().Int(paramName, value).Msgf("Invalid %s, using default", paramName)
}
return 0, false
} }
// Validate and apply bitrate // Apply complexity (0-10)
if bitrate, valid := validateAndApply(config.AudioBitrate, 64, 256, "audio bitrate"); valid { if config.AudioComplexity >= 0 && config.AudioComplexity <= 10 {
cfg.Bitrate = uint16(bitrate) 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 // Apply buffer periods (2-24)
if complexity, valid := validateAndApply(config.AudioComplexity, 0, 10, "audio complexity"); valid { if config.AudioBufferPeriods >= 2 && config.AudioBufferPeriods <= 24 {
cfg.Complexity = uint8(complexity) 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.DTXEnabled = config.AudioDTXEnabled
cfg.FECEnabled = config.AudioFECEnabled 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 return cfg
} }
@ -120,13 +118,17 @@ func startAudio() error {
inputErr = startInputAudioUnderMutex(getAlsaDevice("usb")) inputErr = startInputAudioUnderMutex(getAlsaDevice("usb"))
} }
if outputErr != nil && inputErr != nil { // Simplified error handling - both errors are worth reporting
return fmt.Errorf("audio start failed - output: %w, input: %v", outputErr, inputErr) 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 nil
return outputErr
}
return inputErr
} }
func startOutputAudioUnderMutex(alsaOutputDevice string) error { func startOutputAudioUnderMutex(alsaOutputDevice string) error {
@ -250,9 +252,8 @@ func setAudioTrack(audioTrack *webrtc.TrackLocalStaticSample) {
} }
func setPendingInputTrack(track *webrtc.TrackRemote) { func setPendingInputTrack(track *webrtc.TrackRemote) {
trackID := new(string) trackID := track.ID()
*trackID = track.ID() currentInputTrack.Store(&trackID)
currentInputTrack.Store(trackID)
go handleInputTrackForSession(track) go handleInputTrackForSession(track)
} }
@ -397,22 +398,11 @@ func handleInputTrackForSession(track *webrtc.TrackRemote) {
// processInputPacket handles writing audio data to the input source // processInputPacket handles writing audio data to the input source
func processInputPacket(opusData []byte) error { func processInputPacket(opusData []byte) error {
// Early check to avoid mutex acquisition if source is nil
if inputSource.Load() == nil {
return nil
}
inputSourceMutex.Lock() inputSourceMutex.Lock()
defer inputSourceMutex.Unlock() defer inputSourceMutex.Unlock()
// Reload source inside mutex to ensure we have the currently active source
source := inputSource.Load() source := inputSource.Load()
if source == nil { if source == nil || *source == nil {
return nil
}
// Defensive null check - ensure dereferenced pointer is valid
if *source == nil {
return nil return nil
} }

View File

@ -290,6 +290,7 @@ func LoadConfig() {
loadedConfig.JigglerConfig = getDefaultConfig().JigglerConfig loadedConfig.JigglerConfig = getDefaultConfig().JigglerConfig
} }
// Apply audio defaults for new configs
if loadedConfig.AudioBitrate == 0 { if loadedConfig.AudioBitrate == 0 {
defaults := getDefaultConfig() defaults := getDefaultConfig()
loadedConfig.AudioBitrate = defaults.AudioBitrate loadedConfig.AudioBitrate = defaults.AudioBitrate
@ -297,13 +298,8 @@ func LoadConfig() {
loadedConfig.AudioDTXEnabled = defaults.AudioDTXEnabled loadedConfig.AudioDTXEnabled = defaults.AudioDTXEnabled
loadedConfig.AudioFECEnabled = defaults.AudioFECEnabled loadedConfig.AudioFECEnabled = defaults.AudioFECEnabled
loadedConfig.AudioBufferPeriods = defaults.AudioBufferPeriods loadedConfig.AudioBufferPeriods = defaults.AudioBufferPeriods
} loadedConfig.AudioSampleRate = defaults.AudioSampleRate
loadedConfig.AudioPacketLossPerc = defaults.AudioPacketLossPerc
if loadedConfig.AudioSampleRate == 0 {
loadedConfig.AudioSampleRate = getDefaultConfig().AudioSampleRate
}
if loadedConfig.AudioPacketLossPerc == 0 {
loadedConfig.AudioPacketLossPerc = getDefaultConfig().AudioPacketLossPerc
} }
// fixup old keyboard layout value // fixup old keyboard layout value

View File

@ -70,54 +70,54 @@ func (r *OutputRelay) Stop() {
func (r *OutputRelay) relayLoop() { func (r *OutputRelay) relayLoop() {
defer close(r.stopped) defer close(r.stopped)
const initialDelay = 1 * time.Second
const maxDelay = 30 * time.Second
const maxRetries = 10 const maxRetries = 10
retryDelay := 1 * time.Second
retryDelay := initialDelay
consecutiveFailures := 0 consecutiveFailures := 0
for r.running.Load() { for r.running.Load() {
// Connect if not connected
if !(*r.source).IsConnected() { if !(*r.source).IsConnected() {
if err := (*r.source).Connect(); err != nil { if err := (*r.source).Connect(); err != nil {
consecutiveFailures++ if consecutiveFailures++; consecutiveFailures >= maxRetries {
if consecutiveFailures >= maxRetries { r.logger.Error().Int("failures", consecutiveFailures).Msg("Max retries exceeded, stopping relay")
r.logger.Error().Int("failures", consecutiveFailures).Msg("Max connection retries exceeded, stopping relay")
return 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) time.Sleep(retryDelay)
retryDelay = min(retryDelay*2, maxDelay) retryDelay = min(retryDelay*2, 30*time.Second)
continue continue
} }
consecutiveFailures = 0 consecutiveFailures = 0
retryDelay = initialDelay retryDelay = 1 * time.Second
} }
// Read message from source
msgType, payload, err := (*r.source).ReadMessage() msgType, payload, err := (*r.source).ReadMessage()
if err != nil { if err != nil {
if r.running.Load() { if !r.running.Load() {
consecutiveFailures++ break
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 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 continue
} }
// Reset retry state on success
consecutiveFailures = 0 consecutiveFailures = 0
retryDelay = initialDelay retryDelay = 1 * time.Second
// Write audio sample to WebRTC
if msgType == ipcMsgTypeOpus && len(payload) > 0 { if msgType == ipcMsgTypeOpus && len(payload) > 0 {
r.sample.Data = payload r.sample.Data = payload
if err := r.audioTrack.WriteSample(r.sample); err != nil { if err := r.audioTrack.WriteSample(r.sample); err != nil {
r.framesDropped.Add(1) 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 { } else {
r.framesRelayed.Add(1) r.framesRelayed.Add(1)
} }

View File

@ -204,58 +204,47 @@ func (u *UsbGadget) Close() error {
func (u *UsbGadget) CloseHidFiles() { func (u *UsbGadget) CloseHidFiles() {
u.log.Debug().Msg("closing HID files") u.log.Debug().Msg("closing HID files")
// Close keyboard HID file closeFile := func(file **os.File, name string) {
if u.keyboardHidFile != nil { if *file != nil {
if err := u.keyboardHidFile.Close(); err != nil { if err := (*file).Close(); err != nil {
u.log.Debug().Err(err).Msg("failed to close keyboard HID file") u.log.Debug().Err(err).Msgf("failed to close %s HID file", name)
}
*file = nil
} }
u.keyboardHidFile = nil
} }
// Close absolute mouse HID file closeFile(&u.keyboardHidFile, "keyboard")
if u.absMouseHidFile != nil { closeFile(&u.absMouseHidFile, "absolute mouse")
if err := u.absMouseHidFile.Close(); err != nil { closeFile(&u.relMouseHidFile, "relative mouse")
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
}
} }
// PreOpenHidFiles opens all HID files to reduce input latency // PreOpenHidFiles opens all HID files to reduce input latency
func (u *UsbGadget) PreOpenHidFiles() { func (u *UsbGadget) PreOpenHidFiles() {
// Add a small delay to allow USB gadget reconfiguration to complete // Small delay for USB gadget reconfiguration to complete
// This prevents "no such device or address" errors when trying to open HID files
time.Sleep(100 * time.Millisecond) 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 u.enabledDevices.Keyboard {
if err := u.openKeyboardHidFile(); err != nil { if err := u.openKeyboardHidFile(); err != nil {
u.log.Debug().Err(err).Msg("failed to pre-open keyboard HID file") u.log.Debug().Err(err).Msg("failed to pre-open keyboard HID file")
} }
} }
if u.enabledDevices.AbsoluteMouse { if u.enabledDevices.AbsoluteMouse {
if u.absMouseHidFile == nil { openHidFile(&u.absMouseHidFile, "/dev/hidg1", "absolute mouse")
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")
}
}
} }
if u.enabledDevices.RelativeMouse { if u.enabledDevices.RelativeMouse {
if u.relMouseHidFile == nil { openHidFile(&u.relMouseHidFile, "/dev/hidg2", "relative mouse")
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")
}
}
} }
} }

View File

@ -866,30 +866,30 @@ func rpcGetUsbDevices() (usbgadget.Devices, error) {
func updateUsbRelatedConfig(wasUsbAudioEnabled bool) error { func updateUsbRelatedConfig(wasUsbAudioEnabled bool) error {
ensureConfigLoaded() ensureConfigLoaded()
nowHasUsbAudio := config.UsbDevices != nil && config.UsbDevices.Audio nowHasUsbAudio := config.UsbDevices != nil && config.UsbDevices.Audio
outputSourceIsUsb := config.AudioOutputSource == "usb"
// must stop input audio before reconfiguring // Stop audio before reconfiguring USB gadget
stopInputAudio() stopInputAudio()
if config.AudioOutputSource == "usb" {
// if we're currently sourcing audio from USB, stop the output audio before reconfiguring
if outputSourceIsUsb {
stopOutputAudio() 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" { 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" config.AudioOutputSource = "hdmi"
} }
// Update USB gadget configuration
if err := gadget.UpdateGadgetConfig(); err != nil { 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 { if err := SaveConfig(); err != nil {
return fmt.Errorf("failed to save config: %w", err) return fmt.Errorf("failed to save config: %w", err)
} }
// Restart audio if needed
if err := startAudio(); err != nil { if err := startAudio(); err != nil {
logger.Warn().Err(err).Msg("Failed to restart audio after USB reconfiguration") logger.Warn().Err(err).Msg("Failed to restart audio after USB reconfiguration")
} }