fix resuming video playback after background audio only mode.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user