From e12a3dfa4fe174d773375633b55e6ad315c0b7ea Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Mon, 23 Feb 2026 15:59:29 +0100 Subject: [PATCH] [fix](trx-server): re-enumerate audio device on each stream recovery cycle After ALSA POLLERR the existing cpal Device handle can be stale, causing repeated stream build failures with no path to recovery. Re-acquire host and device inside the outer recreation loop so each attempt gets a fresh handle. Device-not-found is now a warn+retry rather than a fatal error, allowing recovery from transient USB audio device disappearances. Co-authored-by: Claude Sonnet 4.6 --- src/trx-server/src/audio.rs | 100 +++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 30 deletions(-) diff --git a/src/trx-server/src/audio.rs b/src/trx-server/src/audio.rs index b3a86f6..371ca79 100644 --- a/src/trx-server/src/audio.rs +++ b/src/trx-server/src/audio.rs @@ -259,21 +259,6 @@ fn run_capture( use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use std::sync::mpsc::{RecvTimeoutError, TryRecvError as StdTryRecvError}; - let host = cpal::default_host(); - let device = if let Some(ref name) = device_name { - host.input_devices()? - .find(|d| d.name().map(|n| n == *name).unwrap_or(false)) - .ok_or_else(|| format!("audio input device '{}' not found", name))? - } else { - host.default_input_device() - .ok_or("no default audio input device")? - }; - - info!( - "Audio capture: using device '{}'", - device.name().unwrap_or_else(|_| "unknown".into()) - ); - let config = cpal::StreamConfig { channels, sample_rate: cpal::SampleRate(sample_rate), @@ -304,6 +289,41 @@ fn run_capture( let mut capturing = false; loop { + // Re-enumerate the device on every recovery cycle: after POLLERR the + // existing device handle can be stale (especially for USB audio). + let host = cpal::default_host(); + let device = if let Some(ref name) = device_name { + match host.input_devices() { + Ok(mut devs) => { + match devs.find(|d| d.name().map(|n| n == *name).unwrap_or(false)) { + Some(d) => d, + None => { + warn!("Audio capture: device '{}' not found, retrying", name); + std::thread::sleep(AUDIO_STREAM_RECOVERY_DELAY); + continue; + } + } + } + Err(e) => { + warn!("Audio capture: failed to enumerate devices, retrying: {}", e); + std::thread::sleep(AUDIO_STREAM_RECOVERY_DELAY); + continue; + } + } + } else { + match host.default_input_device() { + Some(d) => d, + None => { + warn!("Audio capture: no default input device, retrying"); + std::thread::sleep(AUDIO_STREAM_RECOVERY_DELAY); + continue; + } + } + }; + info!( + "Audio capture: using device '{}'", + device.name().unwrap_or_else(|_| "unknown".into()) + ); let (sample_tx, sample_rx) = std::sync::mpsc::sync_channel::>(64); let (stream_err_tx, stream_err_rx) = std::sync::mpsc::sync_channel::<()>(1); let stream_failed = Arc::new(AtomicBool::new(false)); @@ -443,21 +463,6 @@ fn run_playback( use std::sync::mpsc::TryRecvError as StdTryRecvError; use tokio::sync::mpsc::error::TryRecvError as TokioTryRecvError; - let host = cpal::default_host(); - let device = if let Some(ref name) = device_name { - host.output_devices()? - .find(|d| d.name().map(|n| n == *name).unwrap_or(false)) - .ok_or_else(|| format!("audio output device '{}' not found", name))? - } else { - host.default_output_device() - .ok_or("no default audio output device")? - }; - - info!( - "Audio playback: using device '{}'", - device.name().unwrap_or_else(|_| "unknown".into()) - ); - let config = cpal::StreamConfig { channels, sample_rate: cpal::SampleRate(sample_rate), @@ -489,6 +494,41 @@ fn run_playback( let mut channel_closed = false; loop { + // Re-enumerate the device on every recovery cycle: after POLLERR the + // existing device handle can be stale (especially for USB audio). + let host = cpal::default_host(); + let device = if let Some(ref name) = device_name { + match host.output_devices() { + Ok(mut devs) => { + match devs.find(|d| d.name().map(|n| n == *name).unwrap_or(false)) { + Some(d) => d, + None => { + warn!("Audio playback: device '{}' not found, retrying", name); + std::thread::sleep(AUDIO_STREAM_RECOVERY_DELAY); + continue; + } + } + } + Err(e) => { + warn!("Audio playback: failed to enumerate devices, retrying: {}", e); + std::thread::sleep(AUDIO_STREAM_RECOVERY_DELAY); + continue; + } + } + } else { + match host.default_output_device() { + Some(d) => d, + None => { + warn!("Audio playback: no default output device, retrying"); + std::thread::sleep(AUDIO_STREAM_RECOVERY_DELAY); + continue; + } + } + }; + info!( + "Audio playback: using device '{}'", + device.name().unwrap_or_else(|_| "unknown".into()) + ); let (stream_err_tx, stream_err_rx) = std::sync::mpsc::sync_channel::<()>(1); let stream_failed = Arc::new(AtomicBool::new(false)); let stream = match device.build_output_stream(