feat(cast): add position tracking and auto-advance for Chromecast
- Add cast_get_position command for Chromecast media position - Add CastPositionInfo struct with position, duration, player_state, idle_reason - Extend position polling to work with both DLNA and Chromecast - Detect track end on Chromecast via IDLE state with FINISHED reason - Unify polling logic for both protocols cpvsc
This commit is contained in:
1 parent
5a2eba97d9
commit
1384a1d276
6 files changed
+142
-28
No files matched your search
@@ -9,7 +9,7 @@ use std::time::Duration;
|
||||
|
||||
use crate::cast::device::CastDeviceConnection;
|
||||
use crate::cast::errors::CastError;
|
||||
use crate::cast::{CastStatus, MediaMetadata};
|
||||
use crate::cast::{CastStatus, MediaMetadata, CastPositionInfo};
|
||||
|
||||
/// Commands sent to the Chromecast thread
|
||||
pub enum CastCommand {
|
||||
@@ -24,6 +24,9 @@ pub enum CastCommand {
|
||||
GetStatus {
|
||||
reply: Sender<Result<CastStatus, CastError>>,
|
||||
},
|
||||
GetMediaPosition {
|
||||
reply: Sender<Result<CastPositionInfo, CastError>>,
|
||||
},
|
||||
LoadMedia {
|
||||
url: String,
|
||||
content_type: String,
|
||||
@@ -188,6 +191,17 @@ impl ChromecastHandle {
|
||||
.recv()
|
||||
.map_err(|_| CastError::Connection("Thread response error".to_string()))?
|
||||
}
|
||||
|
||||
/// Get media position for seekbar updates
|
||||
pub fn get_media_position(&self) -> Result<CastPositionInfo, CastError> {
|
||||
let (reply_tx, reply_rx) = mpsc::channel();
|
||||
self.sender
|
||||
.send(CastCommand::GetMediaPosition { reply: reply_tx })
|
||||
.map_err(|_| CastError::Connection("Thread communication error".to_string()))?;
|
||||
reply_rx
|
||||
.recv()
|
||||
.map_err(|_| CastError::Connection("Thread response error".to_string()))?
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ChromecastHandle {
|
||||
@@ -248,6 +262,14 @@ fn chromecast_thread_main(receiver: Receiver<CastCommand>) {
|
||||
let _ = reply.send(result);
|
||||
}
|
||||
|
||||
CastCommand::GetMediaPosition { reply } => {
|
||||
let result = match connection.as_mut() {
|
||||
Some(conn) => conn.get_media_position(),
|
||||
None => Err(CastError::NotConnected),
|
||||
};
|
||||
let _ = reply.send(result);
|
||||
}
|
||||
|
||||
CastCommand::LoadMedia {
|
||||
url,
|
||||
content_type,
|
||||
|
||||
@@ -7,7 +7,7 @@ use tokio::sync::Mutex;
|
||||
use crate::api::models::Quality;
|
||||
use crate::AppState;
|
||||
use crate::cast::{
|
||||
CastError, CastStatus, DeviceDiscovery, DiscoveredDevice, MediaMetadata, MediaServer,
|
||||
CastError, CastStatus, CastPositionInfo, DeviceDiscovery, DiscoveredDevice, MediaMetadata, MediaServer,
|
||||
};
|
||||
use crate::cast::chromecast_thread::ChromecastHandle;
|
||||
use crate::library::{AudioFormat, LibraryState};
|
||||
@@ -110,6 +110,11 @@ pub async fn cast_get_status(state: State<'_, CastState>) -> Result<CastStatus,
|
||||
state.chromecast.get_status().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn cast_get_position(state: State<'_, CastState>) -> Result<CastPositionInfo, String> {
|
||||
state.chromecast.get_media_position().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// === Playback ===
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use rust_cast::CastDevice;
|
||||
use rust_cast::channels::media::{Image, Media, Metadata, MusicTrackMediaMetadata, StreamType};
|
||||
use rust_cast::channels::media::{Image, Media, Metadata, MusicTrackMediaMetadata, StreamType, PlayerState, IdleReason};
|
||||
use rust_cast::channels::receiver::{CastDeviceApp, Status as ReceiverStatus};
|
||||
|
||||
use crate::cast::CastError;
|
||||
@@ -20,6 +20,15 @@ pub struct MediaMetadata {
|
||||
pub duration_secs: Option<u64>,
|
||||
}
|
||||
|
||||
/// Media position info for seekbar updates
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CastPositionInfo {
|
||||
pub position_secs: f64,
|
||||
pub duration_secs: f64,
|
||||
pub player_state: String,
|
||||
pub idle_reason: Option<String>,
|
||||
}
|
||||
|
||||
/// Device status for frontend
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct CastStatus {
|
||||
@@ -208,6 +217,56 @@ impl CastDeviceConnection {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get current media position for seekbar updates
|
||||
pub fn get_media_position(&mut self) -> Result<CastPositionInfo, CastError> {
|
||||
let session = self.session.as_ref().ok_or(CastError::NotConnected)?;
|
||||
let destination = &session.transport_id;
|
||||
|
||||
let status = self
|
||||
.device
|
||||
.media
|
||||
.get_status(destination.as_str(), None)
|
||||
.map_err(|e| CastError::Media(e.to_string()))?;
|
||||
|
||||
// Get the first media entry (current playing item)
|
||||
if let Some(entry) = status.entries.first() {
|
||||
let player_state = match entry.player_state {
|
||||
PlayerState::Idle => "IDLE",
|
||||
PlayerState::Playing => "PLAYING",
|
||||
PlayerState::Paused => "PAUSED",
|
||||
PlayerState::Buffering => "BUFFERING",
|
||||
};
|
||||
|
||||
let idle_reason = entry.idle_reason.as_ref().map(|r| match r {
|
||||
IdleReason::Cancelled => "CANCELLED".to_string(),
|
||||
IdleReason::Interrupted => "INTERRUPTED".to_string(),
|
||||
IdleReason::Finished => "FINISHED".to_string(),
|
||||
IdleReason::Error => "ERROR".to_string(),
|
||||
});
|
||||
|
||||
let duration = entry.media.as_ref()
|
||||
.and_then(|m| m.duration)
|
||||
.unwrap_or(0.0) as f64;
|
||||
|
||||
let position = entry.current_time.unwrap_or(0.0) as f64;
|
||||
|
||||
Ok(CastPositionInfo {
|
||||
position_secs: position,
|
||||
duration_secs: duration,
|
||||
player_state: player_state.to_string(),
|
||||
idle_reason,
|
||||
})
|
||||
} else {
|
||||
// No media playing
|
||||
Ok(CastPositionInfo {
|
||||
position_secs: 0.0,
|
||||
duration_secs: 0.0,
|
||||
player_state: "IDLE".to_string(),
|
||||
idle_reason: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_session(&mut self) -> Result<(), CastError> {
|
||||
if self.session.is_some() {
|
||||
return Ok(());
|
||||
|
||||
@@ -10,7 +10,7 @@ pub mod airplay;
|
||||
pub mod dlna;
|
||||
|
||||
pub use commands::CastState;
|
||||
pub use device::{CastDeviceConnection, CastStatus, MediaMetadata};
|
||||
pub use device::{CastDeviceConnection, CastStatus, MediaMetadata, CastPositionInfo};
|
||||
pub use discovery::{DeviceDiscovery, DiscoveredDevice};
|
||||
pub use errors::CastError;
|
||||
pub use media_server::MediaServer;
|
||||
|
||||
@@ -619,6 +619,7 @@ pub fn run() {
|
||||
cast::commands::cast_connect,
|
||||
cast::commands::cast_disconnect,
|
||||
cast::commands::cast_get_status,
|
||||
cast::commands::cast_get_position,
|
||||
cast::commands::cast_play_track,
|
||||
cast::commands::cast_play_local_track,
|
||||
cast::commands::cast_play,
|
||||
|
||||
Reference in new issue
Block a user