Background-audio handoff for video + repository/player refactor

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.
This commit is contained in:
2026-07-22 21:52:07 +02:00
parent 4e6ab017d4
commit 3fbf6afdbc
72 changed files with 6728 additions and 2338 deletions
+80 -16
View File
@@ -1,5 +1,7 @@
//! 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`.
@@ -17,22 +19,36 @@ pub async fn remote_play_on_session(
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] 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()
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?;
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())
Err(
"Jellyfin client not configured - please restart the app or log out and log back in"
.to_string(),
)
}
}
@@ -44,11 +60,19 @@ pub async fn remote_send_command(
session_id: String,
command: String,
) -> Result<(), String> {
log::info!("[RemoteSession] Sending command '{}' to session {}", command, session_id);
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()
controller
.jellyfin_client()
.lock()
.map_err(|e| e.to_string())?
.clone()
};
if let Some(client) = client_opt {
@@ -68,11 +92,19 @@ pub async fn remote_session_seek(
session_id: String,
position_ticks: i64,
) -> Result<(), String> {
log::info!("[RemoteSession] Seeking to {} ticks on session {}", position_ticks, session_id);
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()
controller
.jellyfin_client()
.lock()
.map_err(|e| e.to_string())?
.clone()
};
if let Some(client) = client_opt {
@@ -92,11 +124,19 @@ pub async fn remote_session_set_volume(
session_id: String,
volume: i32,
) -> Result<(), String> {
log::info!("[RemoteSession] Setting volume to {} on session {}", volume, session_id);
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()
controller
.jellyfin_client()
.lock()
.map_err(|e| e.to_string())?
.clone()
};
if let Some(client) = client_opt {
@@ -119,7 +159,11 @@ pub async fn remote_session_toggle_mute(
let client_opt = {
let controller = player.0.lock().await;
controller.jellyfin_client().lock().map_err(|e| e.to_string())?.clone()
controller
.jellyfin_client()
.lock()
.map_err(|e| e.to_string())?
.clone()
};
if let Some(client) = client_opt {
@@ -145,7 +189,11 @@ pub async fn lms_get_sync_groups(
) -> Result<Vec<LmsSyncGroup>, String> {
let client_opt = {
let controller = player.0.lock().await;
controller.jellyfin_client().lock().map_err(|e| e.to_string())?.clone()
controller
.jellyfin_client()
.lock()
.map_err(|e| e.to_string())?
.clone()
};
if let Some(client) = client_opt {
@@ -164,11 +212,19 @@ pub async fn lms_create_sync_group(
master_mac: String,
slave_macs: Vec<String>,
) -> Result<(), String> {
log::info!("[LmsSync] Fusing zones: master={}, slaves={:?}", master_mac, slave_macs);
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()
controller
.jellyfin_client()
.lock()
.map_err(|e| e.to_string())?
.clone()
};
if let Some(client) = client_opt {
@@ -189,7 +245,11 @@ pub async fn lms_unsync_player(
let client_opt = {
let controller = player.0.lock().await;
controller.jellyfin_client().lock().map_err(|e| e.to_string())?.clone()
controller
.jellyfin_client()
.lock()
.map_err(|e| e.to_string())?
.clone()
};
if let Some(client) = client_opt {
@@ -210,7 +270,11 @@ pub async fn lms_dissolve_sync_group(
let client_opt = {
let controller = player.0.lock().await;
controller.jellyfin_client().lock().map_err(|e| e.to_string())?.clone()
controller
.jellyfin_client()
.lock()
.map_err(|e| e.to_string())?
.clone()
};
if let Some(client) = client_opt {