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:
vicrodh committed 2026-01-26 09:41:11 -06:00
1 parent 003468a28e
commit 71ae2f92c1
6 files changed
+201 -6

No files matched your search

+9 -1
View File
@@ -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
+75
View File
@@ -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
+1 -1
View File
@@ -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;
+1
View File
@@ -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,