fix resuming video playback after background audio only mode.
This commit is contained in:
@@ -826,10 +826,55 @@
|
||||
// with `src=""`, so `loadstart`/`canplay` don't fire reliably and the
|
||||
// canplay-fallback timeout was never armed — audio played while the video
|
||||
// stayed invisible. Any of these callers now reveals it.
|
||||
// Apply the pending background-audio foreground seek, if any. This MUST run
|
||||
// no matter which readiness signal fired — on the Android WebView HLS/MSE path
|
||||
// `canplay` is unreliable and the video is revealed via markMediaReady()
|
||||
// instead, so gating this on handleCanPlay alone meant the seek was silently
|
||||
// dropped and the reloaded stream played from its start (resume "started from
|
||||
// the beginning"). Returns true if a pending seek was consumed.
|
||||
async function applyPendingForegroundSeek(): Promise<boolean> {
|
||||
if (pendingForegroundSeek === null || !videoElement) return false;
|
||||
const seekTo = pendingForegroundSeek;
|
||||
const shouldPlay = pendingForegroundPlay;
|
||||
pendingForegroundSeek = null;
|
||||
pendingForegroundPlay = false;
|
||||
hasPerformedInitialSeek = true;
|
||||
|
||||
const el = videoElement;
|
||||
// currentTime is only honored once the element has metadata (duration/seekable).
|
||||
// If it isn't there yet, defer to loadedmetadata rather than seeking into a
|
||||
// still-empty timeline (which the element clamps back to 0).
|
||||
const doSeek = async () => {
|
||||
try {
|
||||
el.currentTime = seekTo;
|
||||
// Displayed position is absolute: element time + transcode seekOffset.
|
||||
// (Direct stream: seekOffset=0, seekTo=pos. Transcoded: seekOffset=pos,
|
||||
// seekTo=0.) Both yield the correct absolute position.
|
||||
currentTime = seekOffset + seekTo;
|
||||
el.muted = false;
|
||||
el.volume = 1.0;
|
||||
if (shouldPlay) await el.play();
|
||||
} catch (err) {
|
||||
console.error("[VideoPlayer] Failed to resume after background audio:", err);
|
||||
}
|
||||
};
|
||||
|
||||
if (el.readyState >= 1 /* HAVE_METADATA */) {
|
||||
console.log("[VideoPlayer] Applying foreground seek to:", (seekOffset + seekTo).toFixed(1));
|
||||
await doSeek();
|
||||
} else {
|
||||
console.log("[VideoPlayer] Deferring foreground seek until loadedmetadata:", (seekOffset + seekTo).toFixed(1));
|
||||
el.addEventListener("loadedmetadata", () => { void doSeek(); }, { once: true });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function markMediaReady() {
|
||||
if (isMediaReady) return;
|
||||
console.log("[VideoPlayer] Marking media ready");
|
||||
isMediaReady = true;
|
||||
// A handoff return can be revealed here (not via canplay) — apply its seek.
|
||||
void applyPendingForegroundSeek();
|
||||
}
|
||||
|
||||
async function handleCanPlay() {
|
||||
@@ -847,19 +892,7 @@
|
||||
// Returning from background audio: resume the <video> at the position native
|
||||
// audio reached, restoring the prior play/pause state. Takes precedence over
|
||||
// the resume-point seek below (which is for a fresh load, not a handoff).
|
||||
if (pendingForegroundSeek !== null && videoElement) {
|
||||
const seekTo = pendingForegroundSeek;
|
||||
const shouldPlay = pendingForegroundPlay;
|
||||
pendingForegroundSeek = null;
|
||||
pendingForegroundPlay = false;
|
||||
hasPerformedInitialSeek = true;
|
||||
try {
|
||||
videoElement.currentTime = seekTo;
|
||||
currentTime = seekTo;
|
||||
if (shouldPlay) await videoElement.play();
|
||||
} catch (err) {
|
||||
console.error("[VideoPlayer] Failed to resume after background audio:", err);
|
||||
}
|
||||
if (await applyPendingForegroundSeek()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -972,7 +1005,7 @@
|
||||
// Check if video is actually ready despite event not firing
|
||||
if (videoElement.readyState >= 3) { // HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA
|
||||
console.log("[VideoPlayer] Video appears ready (readyState >= 3), forcing media ready state");
|
||||
isMediaReady = true;
|
||||
markMediaReady();
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
@@ -1192,6 +1225,8 @@
|
||||
artist: media.seriesName ?? null,
|
||||
primaryImageTag: media.primaryImageTag ?? null,
|
||||
serverId: media.serverId ?? null,
|
||||
// Real duration so the lockscreen scrubber has a range to draw.
|
||||
durationSeconds: duration > 0 ? duration : null,
|
||||
},
|
||||
pos,
|
||||
);
|
||||
@@ -1216,19 +1251,46 @@
|
||||
const wasPlaying = handoffState.wasPlaying;
|
||||
handoffState = { ...initialHandoffState };
|
||||
try {
|
||||
// Absolute position the native audio reached (base offset applied in Rust).
|
||||
const pos = await commands.playerExitBackgroundAudio();
|
||||
// Reload the video at the returned position. Resetting these re-runs the
|
||||
// HLS init $effect and reveals/seeks the element as on a fresh load.
|
||||
hasPerformedInitialSeek = false;
|
||||
lastAppliedInitialPosition = undefined;
|
||||
seekOffset = 0;
|
||||
console.log("[VideoPlayer] Returning from background audio at:", pos.toFixed(1));
|
||||
|
||||
isMediaReady = false;
|
||||
// Re-point the element at the (unchanged) video stream URL; assigning a new
|
||||
// reference restarts the HLS effect even if the string is identical.
|
||||
currentStreamUrl = streamUrl;
|
||||
// Seek to where native audio left off once the element is ready again.
|
||||
pendingForegroundSeek = pos;
|
||||
// The foreground seek below (pendingForegroundSeek/handleCanPlay) OWNS the
|
||||
// post-handoff position. Keep the initial-position change-effect quiescent:
|
||||
// leaving hasPerformedInitialSeek=true and pinning lastAppliedInitialPosition
|
||||
// to the current prop means the effect sees no "change" and won't fire a
|
||||
// stale seek back to the original resume point (clobbering the handoff pos).
|
||||
hasPerformedInitialSeek = true;
|
||||
lastAppliedInitialPosition = initialPosition;
|
||||
|
||||
pendingForegroundPlay = wasPlaying;
|
||||
|
||||
// Determine the target URL + how the element/offset should be positioned.
|
||||
let targetUrl: string;
|
||||
if (needsTranscoding && onSeek) {
|
||||
// Transcoded HLS can't seek by setting currentTime — the stream must be
|
||||
// rebuilt at the new position (StartTimeTicks). onSeek returns that URL.
|
||||
// The reloaded segment's timeline starts at 0, so seekOffset carries the
|
||||
// absolute base and the element seeks to 0 (handled on canplay).
|
||||
targetUrl = await onSeek(pos, selectedAudioTrackIndex ?? undefined);
|
||||
seekOffset = pos;
|
||||
currentTime = pos;
|
||||
pendingForegroundSeek = 0;
|
||||
} else {
|
||||
// Direct stream: reload the original URL and seek the element to pos.
|
||||
targetUrl = streamUrl;
|
||||
seekOffset = 0;
|
||||
pendingForegroundSeek = pos;
|
||||
}
|
||||
|
||||
// Force the HLS-init $effect to re-run even if the URL string is unchanged:
|
||||
// blank it first, then set it on the next microtask so Svelte sees a real
|
||||
// transition. Without this, assigning the same value is a no-op and the
|
||||
// player stays stuck on the loading spinner (HLS never re-initialises).
|
||||
currentStreamUrl = "";
|
||||
await Promise.resolve();
|
||||
currentStreamUrl = targetUrl;
|
||||
} catch (err) {
|
||||
console.error("[VideoPlayer] Background-audio return failed:", err);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user