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
+30 -9
View File
@@ -1,3 +1,4 @@
use super::media::MediaItem;
/**
* Media Session Management
*
@@ -7,10 +8,8 @@
*
* See docs/architecture/01-rust-backend.md for the state machine diagram.
*/
use log::info;
use serde::{Deserialize, Serialize};
use super::media::MediaItem;
/// Media session type tracking the high-level playback context
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, PartialEq)]
@@ -98,7 +97,10 @@ impl MediaSessionManager {
/// Start an audio session with a queue
/// Transitions: Idle → Audio(active), Any → Audio(active)
pub fn start_audio_session(&mut self, first_item: MediaItem) {
info!("[MediaSession] Starting audio session: {}", first_item.title);
info!(
"[MediaSession] Starting audio session: {}",
first_item.title
);
self.current = MediaSessionType::Audio {
last_item: Some(first_item),
is_active: true,
@@ -107,7 +109,11 @@ impl MediaSessionManager {
/// Update audio session with new track (during playback)
pub fn update_audio_track(&mut self, item: MediaItem) {
if let MediaSessionType::Audio { last_item, is_active } = &mut self.current {
if let MediaSessionType::Audio {
last_item,
is_active,
} = &mut self.current
{
info!("[MediaSession] Updating audio track: {}", item.title);
*last_item = Some(item);
*is_active = true;
@@ -171,8 +177,14 @@ impl MediaSessionManager {
/// Advance to next episode in TV session
pub fn tv_session_next_episode(&mut self, next_item: MediaItem) {
if let MediaSessionType::TvShow { item, is_active, .. } = &mut self.current {
info!("[MediaSession] Advancing to next episode: {}", next_item.title);
if let MediaSessionType::TvShow {
item, is_active, ..
} = &mut self.current
{
info!(
"[MediaSession] Advancing to next episode: {}",
next_item.title
);
*item = next_item;
*is_active = true;
}
@@ -294,7 +306,10 @@ mod tests {
assert!(matches!(
manager.current(),
MediaSessionType::Audio { is_active: true, .. }
MediaSessionType::Audio {
is_active: true,
..
}
));
assert!(manager.should_show_miniplayer());
@@ -307,7 +322,10 @@ mod tests {
manager.audio_session_inactive();
assert!(matches!(
manager.current(),
MediaSessionType::Audio { is_active: false, .. }
MediaSessionType::Audio {
is_active: false,
..
}
));
assert!(manager.should_show_miniplayer()); // Still shows!
@@ -330,7 +348,10 @@ mod tests {
assert!(matches!(
manager.current(),
MediaSessionType::Movie { is_active: true, .. }
MediaSessionType::Movie {
is_active: true,
..
}
));
assert!(manager.should_show_video_player());