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
+42 -20
View File
@@ -1,16 +1,16 @@
use crate::utils::lock::MutexSafe;
use log::{debug, error, info, warn};
use super::backend::{PlayerBackend, PlayerError};
use super::events::{PlayerEventEmitter, PlayerStatusEvent};
use super::media::{MediaItem, MediaSource};
use super::state::PlayerState;
use crate::playback_reporting::{EventThrottler, PlaybackOperation, PlaybackReporter};
use crate::settings::AudioSettings;
use crate::playback_reporting::{PlaybackReporter, EventThrottler, PlaybackOperation};
use crate::utils::conversions::{seconds_to_ticks, volume_to_percent};
use crate::utils::lock::MutexSafe;
use libmpv::Mpv;
use log::{debug, error, info, warn};
use std::process::Command;
use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::Mutex as TokioMutex;
@@ -104,11 +104,17 @@ impl MpvBackend {
// Detect and configure audio output
let audio_driver = detect_audio_system();
info!("[MpvBackend] Configuring audio output driver: {}", audio_driver);
info!(
"[MpvBackend] Configuring audio output driver: {}",
audio_driver
);
mpv.set_property("ao", audio_driver.as_str())
.map_err(|e| PlayerError {
message: format!("Failed to set audio output to '{}': {:?}. Make sure audio system is working.", audio_driver, e),
message: format!(
"Failed to set audio output to '{}': {:?}. Make sure audio system is working.",
audio_driver, e
),
})?;
// Enable verbose logging for audio initialization
@@ -123,10 +129,9 @@ impl MpvBackend {
message: format!("Failed to configure MPV audio-display: {:?}", e),
})?;
mpv.set_property("video", "no")
.map_err(|e| PlayerError {
message: format!("Failed to configure MPV video: {:?}", e),
})?;
mpv.set_property("video", "no").map_err(|e| PlayerError {
message: format!("Failed to configure MPV video: {:?}", e),
})?;
// Set volume to 100% (we'll control via MPV's volume property)
mpv.set_property("volume", 100i64)
@@ -191,7 +196,11 @@ impl MpvBackend {
libmpv::events::Event::PlaybackRestart => {
debug!("[MpvBackend] Playback started/resumed");
let media_id = state.lock_safe().current_media.as_ref().map(|m| m.id.clone());
let media_id = state
.lock_safe()
.current_media
.as_ref()
.map(|m| m.id.clone());
if let Some(emitter) = &event_emitter {
emitter.emit(PlayerStatusEvent::StateChanged {
@@ -203,11 +212,16 @@ impl MpvBackend {
libmpv::events::Event::PropertyChange { name, .. } if name == "pause" => {
// Handle pause state changes
if let Ok(is_paused) = mpv.get_property::<bool>("pause") {
let media_id = state.lock_safe().current_media.as_ref().map(|m| m.id.clone());
let media_id = state
.lock_safe()
.current_media
.as_ref()
.map(|m| m.id.clone());
if let Some(emitter) = &event_emitter {
emitter.emit(PlayerStatusEvent::StateChanged {
state: if is_paused { "paused" } else { "playing" }.to_string(),
state: if is_paused { "paused" } else { "playing" }
.to_string(),
media_id,
});
}
@@ -303,14 +317,18 @@ impl MpvBackend {
}
// Check if we're playing for progress reporting
let is_paused = mpv_for_position.get_property::<bool>("pause").unwrap_or(true);
let is_paused = mpv_for_position
.get_property::<bool>("pause")
.unwrap_or(true);
// Only report progress to server when playing (not paused)
if !is_paused {
// Throttled progress reporting (every 30s)
let jellyfin_id = {
let state = state_for_position.lock_safe();
state.current_media.as_ref()
state
.current_media
.as_ref()
.and_then(|m| m.jellyfin_id().map(|s| s.to_string()))
};
@@ -333,8 +351,14 @@ impl MpvBackend {
};
match reporter_instance.report(operation, true).await {
Ok(_) => debug!("[MpvBackend] Reported progress for {}", item_id_clone),
Err(e) => warn!("[MpvBackend] Failed to report progress: {}", e),
Ok(_) => debug!(
"[MpvBackend] Reported progress for {}",
item_id_clone
),
Err(e) => warn!(
"[MpvBackend] Failed to report progress: {}",
e
),
}
}
});
@@ -468,9 +492,7 @@ impl PlayerBackend for MpvBackend {
}
fn position(&self) -> f64 {
self.mpv
.get_property::<f64>("time-pos")
.unwrap_or(0.0)
self.mpv.get_property::<f64>("time-pos").unwrap_or(0.0)
}
fn duration(&self) -> Option<f64> {