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:
@@ -7,14 +7,14 @@
|
||||
|
||||
use crate::utils::lock::{MutexSafe, RwLockSafe};
|
||||
use log::{debug, info, warn};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::jellyfin::JellyfinClient;
|
||||
use crate::player::PlayerEventEmitter;
|
||||
use crate::playback_mode::{PlaybackMode, PlaybackModeManager};
|
||||
use crate::player::PlayerEventEmitter;
|
||||
|
||||
/// Hint for adjusting poll frequency based on UI state
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -103,10 +103,8 @@ impl SessionPollerManager {
|
||||
|
||||
while is_running.load(Ordering::Relaxed) {
|
||||
// Calculate poll interval based on mode and hint
|
||||
let new_interval = Self::calculate_interval(
|
||||
&mode_manager.get_mode(),
|
||||
*hint.read_safe(),
|
||||
);
|
||||
let new_interval =
|
||||
Self::calculate_interval(&mode_manager.get_mode(), *hint.read_safe());
|
||||
|
||||
interval_ms.store(new_interval, Ordering::Relaxed);
|
||||
|
||||
@@ -158,9 +156,7 @@ impl SessionPollerManager {
|
||||
}
|
||||
|
||||
if let Some(em) = emitter.lock_safe().as_ref() {
|
||||
em.emit(crate::player::PlayerStatusEvent::SessionsUpdated {
|
||||
sessions,
|
||||
});
|
||||
em.emit(crate::player::PlayerStatusEvent::SessionsUpdated { sessions });
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -201,10 +197,7 @@ impl SessionPollerManager {
|
||||
/// album, duration and position to the media notification. Silently does
|
||||
/// nothing if the session isn't found or has no now-playing item (e.g. the
|
||||
/// remote stopped) - the next state change will refresh it.
|
||||
fn push_remote_lockscreen(
|
||||
sessions: &[crate::jellyfin::client::SessionInfo],
|
||||
session_id: &str,
|
||||
) {
|
||||
fn push_remote_lockscreen(sessions: &[crate::jellyfin::client::SessionInfo], session_id: &str) {
|
||||
// 100ns Jellyfin ticks -> milliseconds.
|
||||
const TICKS_PER_MS: i64 = 10_000;
|
||||
|
||||
@@ -251,7 +244,10 @@ impl SessionPollerManager {
|
||||
};
|
||||
|
||||
if let Err(e) = crate::player::update_lockscreen_metadata(&meta) {
|
||||
warn!("[SessionPoller] Failed to update lockscreen metadata: {}", e);
|
||||
warn!(
|
||||
"[SessionPoller] Failed to update lockscreen metadata: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +258,7 @@ impl SessionPollerManager {
|
||||
PollingHint::CastDiscovery => 15000, // Slow discovery
|
||||
PollingHint::Normal => {
|
||||
match mode {
|
||||
PlaybackMode::Remote { .. } => 2000, // Fast in remote mode
|
||||
PlaybackMode::Remote { .. } => 2000, // Fast in remote mode
|
||||
PlaybackMode::Local | PlaybackMode::Idle => 10000, // Default
|
||||
}
|
||||
}
|
||||
@@ -271,7 +267,10 @@ impl SessionPollerManager {
|
||||
|
||||
/// Manually trigger a poll (for frontend refresh button)
|
||||
pub async fn poll_now(&self) -> Result<Vec<crate::jellyfin::client::SessionInfo>, String> {
|
||||
let client = self.jellyfin_client.lock_safe().clone()
|
||||
let client = self
|
||||
.jellyfin_client
|
||||
.lock_safe()
|
||||
.clone()
|
||||
.ok_or("Jellyfin client not configured")?;
|
||||
|
||||
client.get_sessions().await
|
||||
@@ -302,7 +301,9 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
SessionPollerManager::calculate_interval(
|
||||
&PlaybackMode::Remote { session_id: "test".to_string() },
|
||||
&PlaybackMode::Remote {
|
||||
session_id: "test".to_string()
|
||||
},
|
||||
PollingHint::CastActive
|
||||
),
|
||||
500
|
||||
@@ -310,16 +311,24 @@ mod tests {
|
||||
|
||||
// CastDiscovery hint should always be 15s regardless of mode
|
||||
assert_eq!(
|
||||
SessionPollerManager::calculate_interval(&PlaybackMode::Idle, PollingHint::CastDiscovery),
|
||||
15000
|
||||
);
|
||||
assert_eq!(
|
||||
SessionPollerManager::calculate_interval(&PlaybackMode::Local, PollingHint::CastDiscovery),
|
||||
SessionPollerManager::calculate_interval(
|
||||
&PlaybackMode::Idle,
|
||||
PollingHint::CastDiscovery
|
||||
),
|
||||
15000
|
||||
);
|
||||
assert_eq!(
|
||||
SessionPollerManager::calculate_interval(
|
||||
&PlaybackMode::Remote { session_id: "test".to_string() },
|
||||
&PlaybackMode::Local,
|
||||
PollingHint::CastDiscovery
|
||||
),
|
||||
15000
|
||||
);
|
||||
assert_eq!(
|
||||
SessionPollerManager::calculate_interval(
|
||||
&PlaybackMode::Remote {
|
||||
session_id: "test".to_string()
|
||||
},
|
||||
PollingHint::CastDiscovery
|
||||
),
|
||||
15000
|
||||
@@ -338,7 +347,9 @@ mod tests {
|
||||
// Remote mode -> 2s
|
||||
assert_eq!(
|
||||
SessionPollerManager::calculate_interval(
|
||||
&PlaybackMode::Remote { session_id: "test".to_string() },
|
||||
&PlaybackMode::Remote {
|
||||
session_id: "test".to_string()
|
||||
},
|
||||
PollingHint::Normal
|
||||
),
|
||||
2000
|
||||
|
||||
Reference in New Issue
Block a user