From 11b08c1669bc04dd3d00bcd7e7e991a2207e583a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 11 Jul 2026 15:32:27 +0200 Subject: [PATCH] fix(pipewire): only auto-reroute default devices --- CHANGELOG.md | 1 + src/error.rs | 6 ++++++ src/host/pipewire/device.rs | 12 ++++++++---- src/host/pipewire/stream.rs | 22 +++++++++++++--------- src/traits.rs | 16 ++++++++++++++++ 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7530eeb8a..a089170b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Timestamps now stay monotonic across device and graph changes. - **ALSA**: A nonzero but sub-millisecond stream timeout is no longer treated as a non-blocking poll. +- **PipeWire**: Streams for a specific device no longer auto-reroute if it disappears. - **visionOS**: The CoreAudio backend now builds. - **WASAPI**: Reported buffer sizes are no longer off by one frame. - **WASAPI**: `Stream::drop`, `play`, and `pause` no longer panic when the device is lost. diff --git a/src/error.rs b/src/error.rs index 59791ccea..ffff4fb9c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,6 +14,12 @@ pub enum ErrorKind { /// The active audio route changed and the stream was automatically rerouted. /// The stream remains active and no rebuild is required. + /// + /// Only fires for a stream built from the default device, on backends that support following + /// it. A stream built from a specific device does not follow a replacement; if that device + /// disappears, it reports [`DeviceNotAvailable`] instead. + /// + /// [`DeviceNotAvailable`]: ErrorKind::DeviceNotAvailable DeviceChanged, /// The requested audio device is not available. diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index a0c374532..e097b526e 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -399,6 +399,10 @@ impl DeviceTrait for Device { last_quantum: last_quantum_clone, start, connect_automatically: device.connect_automatically.load(Ordering::Relaxed), + is_default_device: matches!( + device.class(), + Class::DefaultSink | Class::DefaultInput | Class::DefaultOutput + ), }, data_callback, error_callback, @@ -423,7 +427,6 @@ impl DeviceTrait for Device { error_callback, pending_device_changed, invalidated, - is_default_device, } = stream_data; let default_monitor = if let Some(key) = device.default_metadata_key() { @@ -446,7 +449,6 @@ impl DeviceTrait for Device { } else { None }; - is_default_device.store(default_monitor.is_some(), Ordering::Relaxed); let stream_clone = stream.clone(); let mainloop_rc1 = mainloop.clone(); let error_callback_cmd = error_callback.clone(); @@ -575,6 +577,10 @@ impl DeviceTrait for Device { last_quantum: last_quantum_clone, start, connect_automatically: device.connect_automatically.load(Ordering::Relaxed), + is_default_device: matches!( + device.class(), + Class::DefaultSink | Class::DefaultInput | Class::DefaultOutput + ), }, data_callback, error_callback, @@ -599,7 +605,6 @@ impl DeviceTrait for Device { error_callback, pending_device_changed, invalidated, - is_default_device, } = stream_data; let default_monitor = if let Some(key) = device.default_metadata_key() { @@ -622,7 +627,6 @@ impl DeviceTrait for Device { } else { None }; - is_default_device.store(default_monitor.is_some(), Ordering::Relaxed); let stream_clone = stream.clone(); let mainloop_rc1 = mainloop.clone(); let error_callback_cmd = error_callback.clone(); diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 94b3cbc18..5a19255be 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -225,7 +225,7 @@ pub struct UserData { format: AudioInfoRaw, last_quantum: Arc, start: Instant, - is_default_device: Arc, + is_default_device: bool, has_connected: bool, invalidated: Arc, pending_device_changed: Arc, @@ -253,7 +253,7 @@ impl UserData { StreamState::Unconnected => { // Let the metadata monitor fire for default-device streams if self.has_connected - && !self.is_default_device.load(Ordering::Relaxed) + && !self.is_default_device && !self.invalidated.swap(true, Ordering::Relaxed) { emit_error( @@ -376,7 +376,6 @@ pub struct StreamData { pub error_callback: ErrorCallbackArc, pub pending_device_changed: Arc, pub invalidated: Arc, - pub is_default_device: Arc, } /// Fallback timestamp using elapsed time since stream creation. @@ -525,6 +524,7 @@ pub struct ConnectParams { pub last_quantum: Arc, pub start: Instant, pub connect_automatically: bool, + pub is_default_device: bool, } pub fn connect_output( @@ -543,6 +543,7 @@ where last_quantum, start, connect_automatically, + is_default_device, } = params; let mainloop = MainLoopRc::new(None)?; @@ -553,7 +554,6 @@ where let invalidated = Arc::new(AtomicBool::new(false)); let pending_device_changed = Arc::new(AtomicBool::new(false)); - let is_default_device = Arc::new(AtomicBool::new(false)); let core_monitor = { let invalidated_core = invalidated.clone(); @@ -582,7 +582,7 @@ where last_quantum, start, invalidated: invalidated.clone(), - is_default_device: is_default_device.clone(), + is_default_device, has_connected: false, pending_device_changed: pending_device_changed.clone(), #[cfg(feature = "realtime")] @@ -756,6 +756,9 @@ where if connect_automatically { flags |= StreamFlags::AUTOCONNECT; } + if !is_default_device { + flags |= StreamFlags::DONT_RECONNECT; + } stream.connect(Direction::Output, None, flags, &mut params)?; @@ -769,7 +772,6 @@ where error_callback: error_callback_out, pending_device_changed, invalidated, - is_default_device, }) } @@ -789,6 +791,7 @@ where last_quantum, start, connect_automatically, + is_default_device, } = params; let mainloop = MainLoopRc::new(None)?; @@ -799,7 +802,6 @@ where let invalidated = Arc::new(AtomicBool::new(false)); let pending_device_changed = Arc::new(AtomicBool::new(false)); - let is_default_device = Arc::new(AtomicBool::new(false)); let core_monitor = { let invalidated_core = invalidated.clone(); @@ -828,7 +830,7 @@ where last_quantum, start, invalidated: invalidated.clone(), - is_default_device: is_default_device.clone(), + is_default_device, has_connected: false, pending_device_changed: pending_device_changed.clone(), #[cfg(feature = "realtime")] @@ -985,6 +987,9 @@ where if connect_automatically { flags |= StreamFlags::AUTOCONNECT; } + if !is_default_device { + flags |= StreamFlags::DONT_RECONNECT; + } stream.connect(Direction::Input, None, flags, &mut params)?; @@ -998,6 +1003,5 @@ where error_callback: error_callback_out, pending_device_changed, invalidated, - is_default_device, }) } diff --git a/src/traits.rs b/src/traits.rs index 4491ad979..5ef3b554e 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -73,11 +73,27 @@ pub trait HostTrait { /// The default input audio device on the system. /// /// Returns `None` if no input device is available. + /// + /// Some backends reroute a stream built from this device to the new default device when it + /// changes; capture continues there, and [`ErrorKind::DeviceChanged`] is reported. Other + /// backends report [`ErrorKind::DeviceNotAvailable`] instead, and the caller must rebuild the + /// stream. + /// + /// [`ErrorKind::DeviceChanged`]: crate::ErrorKind::DeviceChanged + /// [`ErrorKind::DeviceNotAvailable`]: crate::ErrorKind::DeviceNotAvailable fn default_input_device(&self) -> Option; /// The default output audio device on the system. /// /// Returns `None` if no output device is available. + /// + /// Some backends reroute a stream built from this device to the new default device when it + /// changes; playback continues there, and [`ErrorKind::DeviceChanged`] is reported. Other + /// backends report [`ErrorKind::DeviceNotAvailable`] instead, and the caller must rebuild the + /// stream. + /// + /// [`ErrorKind::DeviceChanged`]: crate::ErrorKind::DeviceChanged + /// [`ErrorKind::DeviceNotAvailable`]: crate::ErrorKind::DeviceNotAvailable fn default_output_device(&self) -> Option; /// An iterator yielding all `Device`s currently available to the system that support one or more