From 5e3fdd184114379ed430f9512a207654b0ec4d00 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Fri, 3 Apr 2026 23:39:52 +0200 Subject: [PATCH] + retry logic with exponential backoff for Chromecast connections - Add `connect_with_retry()` method to handle connection failures with exponential backoff (200ms, 400ms) - Replace all direct `connect_to_device()` calls with either the new method or an inlined retry loop (for thread-local context) - Improve robustness of Chromecast operations by automatically reconnecting on transient failures --- .../src/music_renderer/chromecast_renderer.rs | 71 +++++++++++++++---- 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/pmocontrol/src/music_renderer/chromecast_renderer.rs b/pmocontrol/src/music_renderer/chromecast_renderer.rs index cfb484ea..685024c8 100644 --- a/pmocontrol/src/music_renderer/chromecast_renderer.rs +++ b/pmocontrol/src/music_renderer/chromecast_renderer.rs @@ -16,28 +16,28 @@ use std::thread::JoinHandle; use tracing::debug; -use crate::DeviceIdentity; use crate::discovery::chromecast_discovery::{ extract_host_from_location, extract_port_from_location, }; use crate::errors::ControlPointError; use crate::model::{PlaybackState, RendererInfo}; -use crate::music_renderer::RendererFromMediaRendererInfo; use crate::music_renderer::capabilities::{ PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, QueueTransportControl, RendererBackend, TransportControl, VolumeControl, }; use crate::music_renderer::musicrenderer::MusicRendererBackend; use crate::music_renderer::time_utils::{format_hhmmss_f64, parse_hhmmss_strict}; +use crate::music_renderer::RendererFromMediaRendererInfo; use crate::queue::{EnqueueMode, MusicQueue, PlaybackItem, QueueBackend, QueueSnapshot}; +use crate::DeviceIdentity; use rust_cast::{ - CastDevice, ChannelMessage, channels::{ heartbeat::HeartbeatResponse, media::{Media, PlayerState as CastPlayerState, StreamType}, receiver::CastDeviceApp, }, + CastDevice, ChannelMessage, }; const DEFAULT_DESTINATION_ID: &str = "receiver-0"; @@ -172,6 +172,29 @@ impl ChromecastRenderer { pub fn is_continuous_stream(&self) -> bool { *self.continuous_stream.lock().unwrap() } + + /// Connect to the device with retry on connection failures. + /// Uses exponential backoff: 200ms, 400ms, 800ms + fn connect_with_retry(&self) -> Result, ControlPointError> { + // Try up to 3 times with exponential backoff + for attempt in 0..3 { + match connect_to_device(&self.host, self.port) { + Ok(device) => return Ok(device), + Err(e) if attempt < 2 => { + let delay = 200 * 2u64.pow(attempt); + tracing::warn!( + "Chromecast connection failed (attempt {}/3), retrying in {}ms: {}", + attempt + 1, + delay, + e + ); + std::thread::sleep(std::time::Duration::from_millis(delay)); + } + Err(e) => return Err(e), + } + } + unreachable!() + } } impl TransportControl for ChromecastRenderer { @@ -231,7 +254,27 @@ impl TransportControl for ChromecastRenderer { let handle = std::thread::spawn(move || { tracing::info!("Play thread starting for URI: {}", uri); - let device = match connect_to_device(&host, port) { + // Connect with retry (inlined for thread context) + let device = + (|| { + for attempt in 0..3 { + match connect_to_device(&host, port) { + Ok(d) => return Ok(d), + Err(e) if attempt < 2 => { + let delay = 200 * 2u64.pow(attempt); + tracing::warn!( + "Chromecast connection failed (attempt {}/3), retrying in {}ms: {}", + attempt + 1, delay, e + ); + std::thread::sleep(std::time::Duration::from_millis(delay)); + } + Err(e) => return Err(e), + } + } + unreachable!() + })(); + + let device = match device { Ok(d) => d, Err(e) => { tracing::error!("Failed to connect in play thread: {}", e); @@ -334,7 +377,7 @@ impl TransportControl for ChromecastRenderer { fn play(&self) -> Result<(), ControlPointError> { debug!("ChromecastRenderer: play()"); - let device = connect_to_device(&self.host, self.port)?; + let device = self.connect_with_retry()?; // Get receiver status to find the active app let status = device.receiver.get_status().map_err(|e| { @@ -379,7 +422,7 @@ impl TransportControl for ChromecastRenderer { fn pause(&self) -> Result<(), ControlPointError> { debug!("ChromecastRenderer: pause()"); - let device = connect_to_device(&self.host, self.port)?; + let device = self.connect_with_retry()?; let status = device.receiver.get_status().map_err(|e| { ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)) @@ -430,7 +473,7 @@ impl TransportControl for ChromecastRenderer { // If a new play_uri() is called, it will properly wait for this thread. // Also send stop command to the device - let device = connect_to_device(&self.host, self.port)?; + let device = self.connect_with_retry()?; let status = device.receiver.get_status().map_err(|e| { ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)) @@ -473,7 +516,7 @@ impl TransportControl for ChromecastRenderer { let total_seconds = parse_hhmmss_strict(hhmmss)? as f32; - let device = connect_to_device(&self.host, self.port)?; + let device = self.connect_with_retry()?; let status = device.receiver.get_status().map_err(|e| { ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)) @@ -519,7 +562,7 @@ impl TransportControl for ChromecastRenderer { impl PlaybackStatus for ChromecastRenderer { fn playback_state(&self) -> Result { - let device = connect_to_device(&self.host, self.port)?; + let device = self.connect_with_retry()?; // Get receiver status to find the active app let status = device.receiver.get_status().map_err(|e| { @@ -560,7 +603,7 @@ impl PlaybackStatus for ChromecastRenderer { impl PlaybackPosition for ChromecastRenderer { fn playback_position(&self) -> Result { - let device = connect_to_device(&self.host, self.port)?; + let device = self.connect_with_retry()?; // Get receiver status to find the active app let status = device.receiver.get_status().map_err(|e| { @@ -680,7 +723,7 @@ fn detect_content_type_from_meta(uri: &str, meta: &str) -> String { impl VolumeControl for ChromecastRenderer { fn volume(&self) -> Result { - let device = connect_to_device(&self.host, self.port)?; + let device = self.connect_with_retry()?; let status = device.receiver.get_status().map_err(|e| { ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)) @@ -696,7 +739,7 @@ impl VolumeControl for ChromecastRenderer { fn set_volume(&self, volume: u16) -> Result<(), ControlPointError> { debug!("ChromecastRenderer: set_volume({})", volume); - let device = connect_to_device(&self.host, self.port)?; + let device = self.connect_with_retry()?; let level = (volume as f32) / 100.0; device.receiver.set_volume(level).map_err(|e| { @@ -707,7 +750,7 @@ impl VolumeControl for ChromecastRenderer { } fn mute(&self) -> Result { - let device = connect_to_device(&self.host, self.port)?; + let device = self.connect_with_retry()?; let status = device.receiver.get_status().map_err(|e| { ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e)) @@ -719,7 +762,7 @@ impl VolumeControl for ChromecastRenderer { fn set_mute(&self, mute: bool) -> Result<(), ControlPointError> { debug!("ChromecastRenderer: set_mute({})", mute); - let device = connect_to_device(&self.host, self.port)?; + let device = self.connect_with_retry()?; device.receiver.set_volume(mute).map_err(|e| { ControlPointError::ChromecastError(format!("Failed to set mute: {}", e))