Files
jellytau/src-tauri/src/commands/player/remote.rs
T
dtourolle 3fbf6afdbc 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.
2026-07-22 21:52:07 +02:00

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())
}
}