+ 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
This commit is contained in:
2026-04-03 23:39:52 +02:00
parent bf03c56649
commit 5e3fdd1841

View File

@@ -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<CastDevice<'_>, 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<PlaybackState, ControlPointError> {
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<PlaybackPositionInfo, ControlPointError> {
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<u16, ControlPointError> {
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<bool, ControlPointError> {
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))