+ 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:
@@ -16,28 +16,28 @@ use std::thread::JoinHandle;
|
|||||||
|
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::DeviceIdentity;
|
|
||||||
use crate::discovery::chromecast_discovery::{
|
use crate::discovery::chromecast_discovery::{
|
||||||
extract_host_from_location, extract_port_from_location,
|
extract_host_from_location, extract_port_from_location,
|
||||||
};
|
};
|
||||||
use crate::errors::ControlPointError;
|
use crate::errors::ControlPointError;
|
||||||
use crate::model::{PlaybackState, RendererInfo};
|
use crate::model::{PlaybackState, RendererInfo};
|
||||||
use crate::music_renderer::RendererFromMediaRendererInfo;
|
|
||||||
use crate::music_renderer::capabilities::{
|
use crate::music_renderer::capabilities::{
|
||||||
PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, QueueTransportControl, RendererBackend,
|
PlaybackPosition, PlaybackPositionInfo, PlaybackStatus, QueueTransportControl, RendererBackend,
|
||||||
TransportControl, VolumeControl,
|
TransportControl, VolumeControl,
|
||||||
};
|
};
|
||||||
use crate::music_renderer::musicrenderer::MusicRendererBackend;
|
use crate::music_renderer::musicrenderer::MusicRendererBackend;
|
||||||
use crate::music_renderer::time_utils::{format_hhmmss_f64, parse_hhmmss_strict};
|
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::queue::{EnqueueMode, MusicQueue, PlaybackItem, QueueBackend, QueueSnapshot};
|
||||||
|
use crate::DeviceIdentity;
|
||||||
|
|
||||||
use rust_cast::{
|
use rust_cast::{
|
||||||
CastDevice, ChannelMessage,
|
|
||||||
channels::{
|
channels::{
|
||||||
heartbeat::HeartbeatResponse,
|
heartbeat::HeartbeatResponse,
|
||||||
media::{Media, PlayerState as CastPlayerState, StreamType},
|
media::{Media, PlayerState as CastPlayerState, StreamType},
|
||||||
receiver::CastDeviceApp,
|
receiver::CastDeviceApp,
|
||||||
},
|
},
|
||||||
|
CastDevice, ChannelMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_DESTINATION_ID: &str = "receiver-0";
|
const DEFAULT_DESTINATION_ID: &str = "receiver-0";
|
||||||
@@ -172,6 +172,29 @@ impl ChromecastRenderer {
|
|||||||
pub fn is_continuous_stream(&self) -> bool {
|
pub fn is_continuous_stream(&self) -> bool {
|
||||||
*self.continuous_stream.lock().unwrap()
|
*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 {
|
impl TransportControl for ChromecastRenderer {
|
||||||
@@ -231,7 +254,27 @@ impl TransportControl for ChromecastRenderer {
|
|||||||
let handle = std::thread::spawn(move || {
|
let handle = std::thread::spawn(move || {
|
||||||
tracing::info!("Play thread starting for URI: {}", uri);
|
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,
|
Ok(d) => d,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to connect in play thread: {}", e);
|
tracing::error!("Failed to connect in play thread: {}", e);
|
||||||
@@ -334,7 +377,7 @@ impl TransportControl for ChromecastRenderer {
|
|||||||
fn play(&self) -> Result<(), ControlPointError> {
|
fn play(&self) -> Result<(), ControlPointError> {
|
||||||
debug!("ChromecastRenderer: play()");
|
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
|
// Get receiver status to find the active app
|
||||||
let status = device.receiver.get_status().map_err(|e| {
|
let status = device.receiver.get_status().map_err(|e| {
|
||||||
@@ -379,7 +422,7 @@ impl TransportControl for ChromecastRenderer {
|
|||||||
fn pause(&self) -> Result<(), ControlPointError> {
|
fn pause(&self) -> Result<(), ControlPointError> {
|
||||||
debug!("ChromecastRenderer: pause()");
|
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| {
|
let status = device.receiver.get_status().map_err(|e| {
|
||||||
ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", 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.
|
// If a new play_uri() is called, it will properly wait for this thread.
|
||||||
|
|
||||||
// Also send stop command to the device
|
// 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| {
|
let status = device.receiver.get_status().map_err(|e| {
|
||||||
ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", 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 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| {
|
let status = device.receiver.get_status().map_err(|e| {
|
||||||
ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e))
|
ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", e))
|
||||||
@@ -519,7 +562,7 @@ impl TransportControl for ChromecastRenderer {
|
|||||||
|
|
||||||
impl PlaybackStatus for ChromecastRenderer {
|
impl PlaybackStatus for ChromecastRenderer {
|
||||||
fn playback_state(&self) -> Result<PlaybackState, ControlPointError> {
|
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
|
// Get receiver status to find the active app
|
||||||
let status = device.receiver.get_status().map_err(|e| {
|
let status = device.receiver.get_status().map_err(|e| {
|
||||||
@@ -560,7 +603,7 @@ impl PlaybackStatus for ChromecastRenderer {
|
|||||||
|
|
||||||
impl PlaybackPosition for ChromecastRenderer {
|
impl PlaybackPosition for ChromecastRenderer {
|
||||||
fn playback_position(&self) -> Result<PlaybackPositionInfo, ControlPointError> {
|
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
|
// Get receiver status to find the active app
|
||||||
let status = device.receiver.get_status().map_err(|e| {
|
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 {
|
impl VolumeControl for ChromecastRenderer {
|
||||||
fn volume(&self) -> Result<u16, ControlPointError> {
|
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| {
|
let status = device.receiver.get_status().map_err(|e| {
|
||||||
ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", 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> {
|
fn set_volume(&self, volume: u16) -> Result<(), ControlPointError> {
|
||||||
debug!("ChromecastRenderer: set_volume({})", volume);
|
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;
|
let level = (volume as f32) / 100.0;
|
||||||
device.receiver.set_volume(level).map_err(|e| {
|
device.receiver.set_volume(level).map_err(|e| {
|
||||||
@@ -707,7 +750,7 @@ impl VolumeControl for ChromecastRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn mute(&self) -> Result<bool, ControlPointError> {
|
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| {
|
let status = device.receiver.get_status().map_err(|e| {
|
||||||
ControlPointError::ChromecastError(format!("Failed to get receiver status: {}", 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> {
|
fn set_mute(&self, mute: bool) -> Result<(), ControlPointError> {
|
||||||
debug!("ChromecastRenderer: set_mute({})", mute);
|
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| {
|
device.receiver.set_volume(mute).map_err(|e| {
|
||||||
ControlPointError::ChromecastError(format!("Failed to set mute: {}", e))
|
ControlPointError::ChromecastError(format!("Failed to set mute: {}", e))
|
||||||
|
|||||||
Reference in New Issue
Block a user