fix resuming video playback after background audio only mode.
Traceability Validation / Check Requirement Traces (pull_request) Failing after 3h14m1s
🏗️ Build and Test JellyTau / Android Compile Check (pull_request) Has been cancelled
🏗️ Build and Test JellyTau / Run Tests (pull_request) Has been cancelled

This commit is contained in:
2026-07-22 22:28:07 +02:00
parent 3fbf6afdbc
commit acf1bb200d
7 changed files with 249 additions and 39 deletions
+51 -11
View File
@@ -57,6 +57,18 @@ pub struct MediaSessionManagerWrapper(pub Mutex<MediaSessionManager>);
/// @req: DR-048 - Video settings (auto-play toggle, countdown duration)
pub struct VideoSettingsWrapper(pub Mutex<VideoSettings>);
/// Base offset (seconds) for the active background-audio handoff.
///
/// The audio-only stream is requested with `StartTimeTicks` = the handoff
/// position, so the server makes that point the stream's zero. ExoPlayer then
/// reports position RELATIVE to that zero. To convert back to an absolute
/// position on exit (so the video resumes where the audio actually reached), we
/// add this stored base to the native player's reported position.
///
/// TRACES: UR-040 | DR-052
#[derive(Default)]
pub struct BackgroundAudioOffset(pub Mutex<f64>);
/// Response for player state queries
#[derive(specta::Type, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
@@ -184,6 +196,11 @@ pub struct PlayItemRequest {
pub primary_image_tag: Option<String>,
#[serde(default)]
pub server_id: Option<String>,
/// Total media duration (seconds). Threaded through the background-audio
/// handoff so the lockscreen MediaSession advertises a real duration — a
/// zero-duration session renders no scrubber, even with ACTION_SEEK_TO set.
#[serde(default)]
pub duration_seconds: Option<f64>,
}
/// Queue context for remote transfer - what type of queue is this?
@@ -555,6 +572,7 @@ pub async fn player_play_item(
pub async fn player_enter_background_audio(
player: State<'_, PlayerStateWrapper>,
session: State<'_, MediaSessionManagerWrapper>,
bg_offset: State<'_, BackgroundAudioOffset>,
item: PlayItemRequest,
position_seconds: f64,
) -> Result<PlayerStatus, String> {
@@ -579,7 +597,8 @@ pub async fn player_enter_background_audio(
primary_image_tag: item.primary_image_tag.clone(),
item_type: None,
playlist_id: None,
duration: None,
// Carry the real duration so the lockscreen MediaSession can draw a scrubber.
duration: item.duration_seconds,
artwork_url: None,
media_type: MediaType::Audio,
source: MediaSource::Remote {
@@ -600,16 +619,23 @@ pub async fn player_enter_background_audio(
session_mgr.start_audio_session(media_item.clone());
}
// Remember where the video was: the audio stream's zero == this position
// (the URL was built with StartTimeTicks=position_seconds), so on exit we add
// this base to the native player's relative position to get the absolute one.
*bg_offset.0.lock().map_err(|e| e.to_string())? = position_seconds.max(0.0);
// Same base offset drives the lockscreen scrubber: ExoPlayer reports position
// relative to the stream's StartTimeTicks zero, but the metadata duration is
// absolute, so shift the reported position back to absolute for the scrubber.
let _ = crate::player::set_lockscreen_position_offset(position_seconds.max(0.0));
let controller = player.0.lock().await;
controller
.play_item(media_item)
.map_err(|e| e.to_string())?;
// Resume at the handoff position.
if position_seconds > 0.0 {
controller
.seek(position_seconds)
.map_err(|e| e.to_string())?;
}
// NOTE: do NOT seek here. The audio-only URL already starts at the handoff
// position via StartTimeTicks; the stream's timeline begins at 0 == that
// point, so an extra seek(position_seconds) would jump PAST the content.
controller.emit_queue_changed();
if let Some(emitter) = controller.event_emitter() {
@@ -634,17 +660,31 @@ pub async fn player_enter_background_audio(
#[specta::specta]
pub async fn player_exit_background_audio(
player: State<'_, PlayerStateWrapper>,
bg_offset: State<'_, BackgroundAudioOffset>,
) -> Result<f64, String> {
// The base offset (handoff position) + native player's relative position =
// the absolute position to resume the video at. Read/reset the base first.
let base = {
let mut off = bg_offset.0.lock().map_err(|e| e.to_string())?;
let b = *off;
*off = 0.0;
b
};
// Back to foreground playback: the lockscreen scrubber is absolute again.
let _ = crate::player::set_lockscreen_position_offset(0.0);
let controller = player.0.lock().await;
// Capture position into a `let` BEFORE stop() — never hold work across a lock
// re-entrant call (deadlock discipline, CLAUDE.md).
let position = controller.position();
let relative = controller.position();
controller.stop().map_err(|e| e.to_string())?;
let absolute = base + relative;
info!(
"player_exit_background_audio: returning position {:.1}s",
position
"player_exit_background_audio: base={:.1}s + relative={:.1}s = {:.1}s",
base, relative, absolute
);
Ok(position)
Ok(absolute)
}
/// Play a queue of media items