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
+24 -17
View File
@@ -149,7 +149,10 @@ impl QueueManager {
}
let insert_index = match position {
AddPosition::Next => self.current_index.map(|i| i + 1).unwrap_or(self.items.len()),
AddPosition::Next => self
.current_index
.map(|i| i + 1)
.unwrap_or(self.items.len()),
AddPosition::End => self.items.len(),
};
@@ -167,10 +170,7 @@ impl QueueManager {
// Regenerate shuffle order if shuffle is on
if self.shuffle {
self.shuffle_order = self.generate_shuffle_order(
self.items.len(),
self.current_index,
);
self.shuffle_order = self.generate_shuffle_order(self.items.len(), self.current_index);
}
}
@@ -199,7 +199,8 @@ impl QueueManager {
// Update shuffle order
if self.shuffle {
self.shuffle_order = self.shuffle_order
self.shuffle_order = self
.shuffle_order
.iter()
.filter(|&&i| i != index)
.map(|&i| if i > index { i - 1 } else { i })
@@ -239,7 +240,10 @@ impl QueueManager {
} else if self.repeat == RepeatMode::All {
0
} else {
log::debug!("[Queue] next() at end of queue (index {}), no next track", current);
log::debug!(
"[Queue] next() at end of queue (index {}), no next track",
current
);
return None;
}
};
@@ -269,8 +273,11 @@ impl QueueManager {
if let Some(prev) = self.history.pop() {
// Safety check: ensure the history entry is valid
if prev >= self.items.len() {
log::warn!("[Queue] Invalid history entry {} (queue has {} items), clearing history",
prev, self.items.len());
log::warn!(
"[Queue] Invalid history entry {} (queue has {} items), clearing history",
prev,
self.items.len()
);
self.history.clear();
return None;
}
@@ -334,10 +341,7 @@ impl QueueManager {
self.shuffle = !self.shuffle;
if self.shuffle && !self.items.is_empty() {
self.shuffle_order = self.generate_shuffle_order(
self.items.len(),
self.current_index,
);
self.shuffle_order = self.generate_shuffle_order(self.items.len(), self.current_index);
} else {
self.shuffle_order.clear();
}
@@ -371,7 +375,8 @@ impl QueueManager {
true
} else if self.shuffle {
let pos = self.shuffle_order.iter().position(|&i| i == current);
pos.map(|p| p + 1 < self.shuffle_order.len()).unwrap_or(false)
pos.map(|p| p + 1 < self.shuffle_order.len())
.unwrap_or(false)
} else {
current + 1 < self.items.len()
}
@@ -473,8 +478,7 @@ impl QueueManager {
// Update shuffle order if shuffle is on
if self.shuffle && !self.shuffle_order.is_empty() {
// Regenerate shuffle order to maintain consistency
self.shuffle_order =
self.generate_shuffle_order(self.items.len(), self.current_index);
self.shuffle_order = self.generate_shuffle_order(self.items.len(), self.current_index);
}
true
@@ -486,7 +490,10 @@ impl QueueManager {
if let Some(current_index) = self.current_index {
if let Some(item) = self.items.get_mut(current_index) {
// Only update if it's a Remote source
if let MediaSource::Remote { jellyfin_item_id, .. } = &item.source {
if let MediaSource::Remote {
jellyfin_item_id, ..
} = &item.source
{
item.source = MediaSource::Remote {
stream_url: new_url,
jellyfin_item_id: jellyfin_item_id.clone(),