fix(player): lockscreen skip scrubs instead of advancing in background audio
onSkipToNext/onSkipToPrevious forwarded a bare next/previous to Rust, which always advanced the queue. Correct for music, wrong for a video whose audio is running through a background-audio handoff (UR-040): pressing skip to re-hear a line jumped to the next episode instead of scrubbing. resolve_skip_action in player/seek.rs maps the command to Advance or SeekTo, and is_background_audio_active() is the whole test — the handoff exists only for video, and an episode played through it reports MediaType::Audio, so media type cannot distinguish the case. Forward 30s, back 10s, both clamped to [0, duration] so a skip near either end cannot seek negative or read as EOF and advance. Routed through the same spawn-then-seek_absolute path as the scrubber, because a handoff seek re-opens the stream and must not run under the blocking lock (DR-159). Kotlin keeps sending the opaque command; it only gains FAST_FORWARD/ REWIND in the PlaybackStateCompat so the system stops drawing skip arrows for a control that scrubs. The remote-volume action block is deliberately untouched: the handoff never applies to cast sessions, where skip really does mean advance. Tests written first and watched fail (left: Advance, right: SeekTo). 706 Rust tests pass, clippy 0, coverage 90%.
This commit is contained in:
+47
-2
@@ -314,6 +314,10 @@ use download::DownloadManager;
|
||||
use jellyfin::{HttpClient, HttpConfig};
|
||||
#[cfg(target_os = "android")]
|
||||
use playback_mode::PlaybackModeManager;
|
||||
// Only the Android MediaSessionHandler resolves lockscreen skips; on other
|
||||
// targets this would be an unused import.
|
||||
#[cfg(target_os = "android")]
|
||||
use player::seek::{resolve_skip_action, SkipAction};
|
||||
use player::{MediaSessionManager, PlayerBackend, PlayerController, TauriEventEmitter};
|
||||
// NullBackend is used both for platforms without a native backend AND as a graceful
|
||||
// fallback when a native backend (MPV/ExoPlayer) fails to initialize, so the app can
|
||||
@@ -449,14 +453,55 @@ impl MediaSessionHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip means different things depending on what is actually playing, so
|
||||
// the decision belongs here rather than in the Kotlin that drew the
|
||||
// button: music advances the queue, while a video whose audio is running
|
||||
// through a background-audio handoff scrubs instead (UR-040). Routed
|
||||
// through the same spawn-and-seek path as "seek:" above, because
|
||||
// `seek_absolute` rebuilds the stream during a handoff and must not run
|
||||
// under the blocking lock (DR-159).
|
||||
//
|
||||
// TRACES: UR-040, UR-006 | DR-201
|
||||
if command == "next" || command == "previous" {
|
||||
let is_next = command == "next";
|
||||
let player = self.player.clone();
|
||||
tokio::spawn(async move {
|
||||
let controller = player.lock().await;
|
||||
let action = resolve_skip_action(
|
||||
is_next,
|
||||
controller.is_background_audio_active(),
|
||||
controller.position(),
|
||||
controller.duration(),
|
||||
);
|
||||
let label = if is_next { "next" } else { "previous" };
|
||||
let result: Result<(), String> = match action {
|
||||
SkipAction::Advance => if is_next {
|
||||
controller.next()
|
||||
} else {
|
||||
controller.previous()
|
||||
}
|
||||
.map_err(|e| e.to_string()),
|
||||
SkipAction::SeekTo(position) => {
|
||||
info!(
|
||||
"[MediaSession] Background audio: '{}' scrubs to {:.1}s",
|
||||
label, position
|
||||
);
|
||||
controller.seek_absolute(position).await
|
||||
}
|
||||
};
|
||||
if let Err(e) = result {
|
||||
error!("[MediaSession] Skip '{}' failed: {}", label, e);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Use blocking_lock since this is called from a non-async JNI callback
|
||||
let controller = self.player.blocking_lock();
|
||||
|
||||
let result = match command {
|
||||
"play" => controller.play(),
|
||||
"pause" => controller.pause(),
|
||||
"next" => controller.next(),
|
||||
"previous" => controller.previous(),
|
||||
"stop" => controller.stop(),
|
||||
_ => {
|
||||
warn!("[MediaSession] Unknown command: {}", command);
|
||||
|
||||
Reference in New Issue
Block a user