From 34e366964ee8eb916db5218cdaaba74c94e87a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Matczuk?= Date: Fri, 24 Jul 2026 14:03:04 +0200 Subject: [PATCH] linux-rust: preserve the active A2DP profile Switching between A2DP variants recreates the PipeWire sink and can stop the stream that triggered activation. Read the card's active profile and leave any existing a2dp-sink profile unchanged so playback keeps the codec negotiated by the audio stack. --- linux-rust/src/media_controller.rs | 31 +++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/linux-rust/src/media_controller.rs b/linux-rust/src/media_controller.rs index 5bd68b1a2..e39944f7c 100644 --- a/linux-rust/src/media_controller.rs +++ b/linux-rust/src/media_controller.rs @@ -29,6 +29,7 @@ struct OwnedCardInfo { index: u32, proplist: Proplist, profiles: Vec, + active_profile: Option, } #[derive(Clone, Debug)] @@ -327,6 +328,19 @@ impl MediaController { } } + // If the card is already on an A2DP profile, leave it alone. Switching + // profiles (even between two a2dp-sink variants) tears down and + // recreates the PipeWire sink, which kills the very stream whose start + // triggered this call -- players like Chromium react by pausing, so + // every resume immediately pauses itself again. + if let Some(idx) = current_device_index + && let Some(active) = self.get_active_profile(idx).await + && active.starts_with("a2dp-sink") + { + info!("A2DP profile {} already active, not switching", active); + return; + } + if !self.is_a2dp_profile_available().await { warn!("A2DP profile not available, attempting to restart WirePlumber"); if self.restart_wire_plumber().await { @@ -575,6 +589,17 @@ impl MediaController { String::new() } + async fn get_active_profile(&self, card_index: u32) -> Option { + tokio::task::spawn_blocking(move || { + get_card_info_list_sync() + .iter() + .find(|c| c.index == card_index) + .and_then(|card| card.active_profile.clone()) + }) + .await + .unwrap_or(None) + } + async fn is_profile_available(&self, card_index: u32, profile: &str) -> bool { let profile_name = profile.to_string(); tokio::task::spawn_blocking(move || { @@ -924,6 +949,10 @@ fn get_card_info_list_sync() -> Vec { index: item.index, proplist: item.proplist.clone(), profiles, + active_profile: item + .active_profile + .as_ref() + .and_then(|p| p.name.as_ref().map(|n| n.to_string())), }); } } @@ -1079,4 +1108,4 @@ async fn get_sink_name_by_mac(mac: &str) -> Option { }) .await .unwrap_or(None) -} \ No newline at end of file +}