mirror of https://github.com/jetkvm/kvm.git
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:
parent
3ed663b4d1
commit
5f7c90649a
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
100
audio.go
100
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
|
||||
}
|
||||
|
||||
|
|
|
|||
10
config.go
10
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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
jsonrpc.go
16
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")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue