Hand video playback off to a native audio-only stream when the app is backgrounded or locked, with no on-device video decode (UR-040). Adds player_enter/exit_background_audio commands, an audio-only stream URL for video items across the repository layer, and the frontend handoff state machine wired into VideoPlayer. Includes accompanying repository/offline/player refactoring and regenerates the traceability matrix.
286 lines
7.8 KiB
Rust
286 lines
7.8 KiB
Rust
//! Remote Jellyfin session control commands (casting to another device).
|
|
//!
|
|
//! TRACES: UR-010, UR-046 | IR-012, IR-028, JA-022, JA-023, JA-025, JA-026 | DR-037, DR-058
|
|
//!
|
|
//! These thin command adapters forward control actions to the active Jellyfin
|
|
//! session via the player's configured `JellyfinClient`.
|
|
|
|
use tauri::State;
|
|
|
|
use super::PlayerStateWrapper;
|
|
use crate::jellyfin::client::LmsSyncGroup;
|
|
|
|
/// Play items on a remote Jellyfin session (casting)
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub async fn remote_play_on_session(
|
|
player: State<'_, PlayerStateWrapper>,
|
|
session_id: String,
|
|
item_ids: Vec<String>,
|
|
start_index: usize,
|
|
) -> Result<(), String> {
|
|
log::info!(
|
|
"[RemoteSession] Playing {} items on session {} (start index: {})",
|
|
item_ids.len(),
|
|
session_id,
|
|
start_index
|
|
);
|
|
log::info!("[RemoteSession] Item IDs: {:?}", item_ids);
|
|
|
|
let client_opt = {
|
|
let controller = player.0.lock().await;
|
|
controller
|
|
.jellyfin_client()
|
|
.lock()
|
|
.map_err(|e| e.to_string())?
|
|
.clone()
|
|
};
|
|
|
|
if let Some(client) = client_opt {
|
|
log::info!("[RemoteSession] Jellyfin client IS configured, calling play_on_session");
|
|
client
|
|
.play_on_session(session_id, item_ids, start_index, None)
|
|
.await?;
|
|
log::info!("[RemoteSession] Successfully started playback on remote session");
|
|
Ok(())
|
|
} else {
|
|
log::error!("[RemoteSession] Jellyfin client is NOT configured! User needs to log out/in or restart app");
|
|
Err(
|
|
"Jellyfin client not configured - please restart the app or log out and log back in"
|
|
.to_string(),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Send a playback command to a remote session
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub async fn remote_send_command(
|
|
player: State<'_, PlayerStateWrapper>,
|
|
session_id: String,
|
|
command: String,
|
|
) -> Result<(), String> {
|
|
log::info!(
|
|
"[RemoteSession] Sending command '{}' to session {}",
|
|
command,
|
|
session_id
|
|
);
|
|
|
|
let client_opt = {
|
|
let controller = player.0.lock().await;
|
|
controller
|
|
.jellyfin_client()
|
|
.lock()
|
|
.map_err(|e| e.to_string())?
|
|
.clone()
|
|
};
|
|
|
|
if let Some(client) = client_opt {
|
|
client.send_session_command(session_id, &command).await?;
|
|
log::info!("[RemoteSession] Command sent successfully");
|
|
Ok(())
|
|
} else {
|
|
Err("Jellyfin client not configured".to_string())
|
|
}
|
|
}
|
|
|
|
/// Seek on a remote session
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub async fn remote_session_seek(
|
|
player: State<'_, PlayerStateWrapper>,
|
|
session_id: String,
|
|
position_ticks: i64,
|
|
) -> Result<(), String> {
|
|
log::info!(
|
|
"[RemoteSession] Seeking to {} ticks on session {}",
|
|
position_ticks,
|
|
session_id
|
|
);
|
|
|
|
let client_opt = {
|
|
let controller = player.0.lock().await;
|
|
controller
|
|
.jellyfin_client()
|
|
.lock()
|
|
.map_err(|e| e.to_string())?
|
|
.clone()
|
|
};
|
|
|
|
if let Some(client) = client_opt {
|
|
client.session_seek(session_id, position_ticks).await?;
|
|
log::info!("[RemoteSession] Seek successful");
|
|
Ok(())
|
|
} else {
|
|
Err("Jellyfin client not configured".to_string())
|
|
}
|
|
}
|
|
|
|
/// Set volume on a remote session
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub async fn remote_session_set_volume(
|
|
player: State<'_, PlayerStateWrapper>,
|
|
session_id: String,
|
|
volume: i32,
|
|
) -> Result<(), String> {
|
|
log::info!(
|
|
"[RemoteSession] Setting volume to {} on session {}",
|
|
volume,
|
|
session_id
|
|
);
|
|
|
|
let client_opt = {
|
|
let controller = player.0.lock().await;
|
|
controller
|
|
.jellyfin_client()
|
|
.lock()
|
|
.map_err(|e| e.to_string())?
|
|
.clone()
|
|
};
|
|
|
|
if let Some(client) = client_opt {
|
|
client.session_set_volume(session_id, volume).await?;
|
|
log::info!("[RemoteSession] Volume set successfully");
|
|
Ok(())
|
|
} else {
|
|
Err("Jellyfin client not configured".to_string())
|
|
}
|
|
}
|
|
|
|
/// Toggle mute on a remote session
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub async fn remote_session_toggle_mute(
|
|
player: State<'_, PlayerStateWrapper>,
|
|
session_id: String,
|
|
) -> Result<(), String> {
|
|
log::info!("[RemoteSession] Toggling mute on session {}", session_id);
|
|
|
|
let client_opt = {
|
|
let controller = player.0.lock().await;
|
|
controller
|
|
.jellyfin_client()
|
|
.lock()
|
|
.map_err(|e| e.to_string())?
|
|
.clone()
|
|
};
|
|
|
|
if let Some(client) = client_opt {
|
|
client.session_toggle_mute(session_id).await?;
|
|
log::info!("[RemoteSession] Mute toggled successfully");
|
|
Ok(())
|
|
} else {
|
|
Err("Jellyfin client not configured".to_string())
|
|
}
|
|
}
|
|
|
|
// --- JellyLMS multi-room sync groups (fuse / unfuse LMS zones) --------------
|
|
//
|
|
// The frontend addresses LMS players by MAC address, which it derives from a
|
|
// session's device id (`lms-{mac}`). These commands forward to the JellyLMS
|
|
// plugin REST API via the configured JellyfinClient.
|
|
|
|
/// List current LMS sync groups.
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub async fn lms_get_sync_groups(
|
|
player: State<'_, PlayerStateWrapper>,
|
|
) -> Result<Vec<LmsSyncGroup>, String> {
|
|
let client_opt = {
|
|
let controller = player.0.lock().await;
|
|
controller
|
|
.jellyfin_client()
|
|
.lock()
|
|
.map_err(|e| e.to_string())?
|
|
.clone()
|
|
};
|
|
|
|
if let Some(client) = client_opt {
|
|
client.lms_get_sync_groups().await
|
|
} else {
|
|
Err("Jellyfin client not configured".to_string())
|
|
}
|
|
}
|
|
|
|
/// Fuse LMS zones into a sync group. `master_mac` keeps playing and the
|
|
/// `slave_macs` zones join it in sync.
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub async fn lms_create_sync_group(
|
|
player: State<'_, PlayerStateWrapper>,
|
|
master_mac: String,
|
|
slave_macs: Vec<String>,
|
|
) -> Result<(), String> {
|
|
log::info!(
|
|
"[LmsSync] Fusing zones: master={}, slaves={:?}",
|
|
master_mac,
|
|
slave_macs
|
|
);
|
|
|
|
let client_opt = {
|
|
let controller = player.0.lock().await;
|
|
controller
|
|
.jellyfin_client()
|
|
.lock()
|
|
.map_err(|e| e.to_string())?
|
|
.clone()
|
|
};
|
|
|
|
if let Some(client) = client_opt {
|
|
client.lms_create_sync_group(&master_mac, slave_macs).await
|
|
} else {
|
|
Err("Jellyfin client not configured".to_string())
|
|
}
|
|
}
|
|
|
|
/// Remove a single LMS zone from its sync group (decouple one player).
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub async fn lms_unsync_player(
|
|
player: State<'_, PlayerStateWrapper>,
|
|
mac: String,
|
|
) -> Result<(), String> {
|
|
log::info!("[LmsSync] Decoupling zone {}", mac);
|
|
|
|
let client_opt = {
|
|
let controller = player.0.lock().await;
|
|
controller
|
|
.jellyfin_client()
|
|
.lock()
|
|
.map_err(|e| e.to_string())?
|
|
.clone()
|
|
};
|
|
|
|
if let Some(client) = client_opt {
|
|
client.lms_unsync_player(&mac).await
|
|
} else {
|
|
Err("Jellyfin client not configured".to_string())
|
|
}
|
|
}
|
|
|
|
/// Dissolve an entire LMS sync group, identified by its master's MAC.
|
|
#[tauri::command]
|
|
#[specta::specta]
|
|
pub async fn lms_dissolve_sync_group(
|
|
player: State<'_, PlayerStateWrapper>,
|
|
master_mac: String,
|
|
) -> Result<(), String> {
|
|
log::info!("[LmsSync] Dissolving group with master {}", master_mac);
|
|
|
|
let client_opt = {
|
|
let controller = player.0.lock().await;
|
|
controller
|
|
.jellyfin_client()
|
|
.lock()
|
|
.map_err(|e| e.to_string())?
|
|
.clone()
|
|
};
|
|
|
|
if let Some(client) = client_opt {
|
|
client.lms_dissolve_sync_group(&master_mac).await
|
|
} else {
|
|
Err("Jellyfin client not configured".to_string())
|
|
}
|
|
}
|