feat(dlna): add position tracking and seekbar support
DLNA POSITION TRACKING: - Added GetPositionInfo SOAP action to query current playback position - Added GetTransportInfo SOAP action to get transport state (PLAYING/PAUSED/STOPPED) - New dlna_get_position command returns position, duration, and transport state - Added parse_time_string() helper for HH:MM:SS format FRONTEND INTEGRATION: - castStore: Added position polling (1s interval) when connected to DLNA - castStore: New getCastPosition() for UI components - castStore: Automatic polling start/stop on track play/disconnect - playerStore: Subscribes to castStore for position updates - playerStore: Updates currentTime from cast position when casting This enables the seekbar to display accurate position during DLNA playback. Seek functionality was already implemented (dlna_seek command). Files modified: - src-tauri/src/cast/dlna/device.rs - src-tauri/src/cast/dlna/commands.rs - src-tauri/src/cast/dlna/mod.rs - src-tauri/src/lib.rs - src/lib/stores/castStore.ts - src/lib/stores/playerStore.ts
This commit is contained in:
1 parent
003468a28e
commit
71ae2f92c1
6 files changed
+201
-6
No files matched your search
@@ -7,7 +7,7 @@ use tokio::sync::Mutex;
|
||||
use crate::api::models::Quality;
|
||||
use crate::AppState;
|
||||
use crate::cast::dlna::{
|
||||
DiscoveredDlnaDevice, DlnaConnection, DlnaDiscovery, DlnaError, DlnaMetadata, DlnaStatus,
|
||||
DiscoveredDlnaDevice, DlnaConnection, DlnaDiscovery, DlnaError, DlnaMetadata, DlnaPositionInfo, DlnaStatus,
|
||||
};
|
||||
use crate::cast::MediaServer;
|
||||
|
||||
@@ -101,6 +101,14 @@ pub async fn dlna_get_status(state: State<'_, DlnaState>) -> Result<DlnaStatus,
|
||||
Ok(conn.get_status())
|
||||
}
|
||||
|
||||
/// Get current playback position from DLNA device
|
||||
#[tauri::command]
|
||||
pub async fn dlna_get_position(state: State<'_, DlnaState>) -> Result<DlnaPositionInfo, String> {
|
||||
let connection = state.connection.lock().await;
|
||||
let conn = connection.as_ref().ok_or_else(|| "Not connected".to_string())?;
|
||||
conn.get_position_info().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// === Playback ===
|
||||
|
||||
/// Play a Qobuz track on the DLNA device
|
||||
|
||||
@@ -17,6 +17,14 @@ pub struct DlnaMetadata {
|
||||
pub duration_secs: Option<u64>,
|
||||
}
|
||||
|
||||
/// DLNA playback position info
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DlnaPositionInfo {
|
||||
pub position_secs: u64,
|
||||
pub duration_secs: u64,
|
||||
pub transport_state: String, // PLAYING, PAUSED_PLAYBACK, STOPPED, etc.
|
||||
}
|
||||
|
||||
/// DLNA device status
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DlnaStatus {
|
||||
@@ -265,6 +273,73 @@ impl DlnaConnection {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get current playback position and transport state
|
||||
pub async fn get_position_info(&self) -> Result<DlnaPositionInfo, DlnaError> {
|
||||
if !self.connected {
|
||||
return Err(DlnaError::NotConnected);
|
||||
}
|
||||
|
||||
let av_service = self.av_transport_service.as_ref()
|
||||
.ok_or_else(|| DlnaError::Playback("Device has no AVTransport service".to_string()))?;
|
||||
|
||||
// Get position info
|
||||
let position_response = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
av_service.action(&self.device_url, "GetPositionInfo", "<InstanceID>0</InstanceID>")
|
||||
)
|
||||
.await
|
||||
.map_err(|_| DlnaError::Playback("GetPositionInfo timed out".to_string()))?
|
||||
.map_err(|e| DlnaError::Playback(e.to_string()))?;
|
||||
|
||||
// Get transport state
|
||||
let transport_response = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
av_service.action(&self.device_url, "GetTransportInfo", "<InstanceID>0</InstanceID>")
|
||||
)
|
||||
.await
|
||||
.map_err(|_| DlnaError::Playback("GetTransportInfo timed out".to_string()))?
|
||||
.map_err(|e| DlnaError::Playback(e.to_string()))?;
|
||||
|
||||
// Parse RelTime (position) - format: "HH:MM:SS" or "H:MM:SS"
|
||||
let rel_time = position_response.get("RelTime")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("0:00:00");
|
||||
let position_secs = parse_time_string(rel_time);
|
||||
|
||||
// Parse TrackDuration - format: "HH:MM:SS"
|
||||
let track_duration = position_response.get("TrackDuration")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("0:00:00");
|
||||
let duration_secs = parse_time_string(track_duration);
|
||||
|
||||
// Get transport state (PLAYING, PAUSED_PLAYBACK, STOPPED, etc.)
|
||||
let transport_state = transport_response.get("CurrentTransportState")
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| "UNKNOWN".to_string());
|
||||
|
||||
Ok(DlnaPositionInfo {
|
||||
position_secs,
|
||||
duration_secs,
|
||||
transport_state,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Parse time string "HH:MM:SS" or "H:MM:SS" to seconds
|
||||
fn parse_time_string(time: &str) -> u64 {
|
||||
let parts: Vec<&str> = time.split(':').collect();
|
||||
if parts.len() != 3 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let hours: u64 = parts[0].parse().unwrap_or(0);
|
||||
let minutes: u64 = parts[1].parse().unwrap_or(0);
|
||||
let seconds: u64 = parts[2].split('.').next()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
hours * 3600 + minutes * 60 + seconds
|
||||
}
|
||||
|
||||
/// Build DIDL-Lite metadata for a track
|
||||
|
||||
@@ -6,6 +6,6 @@ pub mod discovery;
|
||||
pub mod errors;
|
||||
|
||||
pub use commands::DlnaState;
|
||||
pub use device::{DlnaConnection, DlnaMetadata, DlnaStatus};
|
||||
pub use device::{DlnaConnection, DlnaMetadata, DlnaPositionInfo, DlnaStatus};
|
||||
pub use discovery::{DiscoveredDlnaDevice, DlnaDiscovery};
|
||||
pub use errors::DlnaError;
|
||||
@@ -633,6 +633,7 @@ pub fn run() {
|
||||
cast::dlna::commands::dlna_connect,
|
||||
cast::dlna::commands::dlna_disconnect,
|
||||
cast::dlna::commands::dlna_get_status,
|
||||
cast::dlna::commands::dlna_get_position,
|
||||
cast::dlna::commands::dlna_play_track,
|
||||
cast::dlna::commands::dlna_load_media,
|
||||
cast::dlna::commands::dlna_play,
|
||||
|
||||
Reference in new issue
Block a user