fix: Autoplay now resets time to zero and ignores trigger if episode already started
This commit is contained in:
parent
674c8e5cd0
commit
fa7cb6e908
@ -1,6 +1,6 @@
|
||||
<!-- TRACES: UR-003, UR-005, UR-020, UR-021, UR-026 | DR-010, DR-023, DR-024 -->
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { onMount, onDestroy, untrack } from "svelte";
|
||||
import { commands } from "$lib/api/bindings";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import Hls from "hls.js";
|
||||
@ -20,9 +20,13 @@
|
||||
needsTranscoding?: boolean; // Whether content needs transcoding (HEVC/10-bit) - affects seeking behavior
|
||||
onClose: () => void;
|
||||
onSeek?: (positionSeconds: number, audioStreamIndex?: number) => Promise<string>; // Returns new stream URL for transcoded seeking
|
||||
onReportProgress?: (positionSeconds: number, isPaused: boolean) => void;
|
||||
onReportStart?: (positionSeconds: number) => void;
|
||||
onReportStop?: (positionSeconds: number) => void;
|
||||
// Reporting callbacks pass the played media's id explicitly so a late
|
||||
// reportStop (fired from onDestroy during autoplay navigation) is attributed
|
||||
// to the episode this player actually played, not the next episode whose URL
|
||||
// is already active on the page.
|
||||
onReportProgress?: (positionSeconds: number, isPaused: boolean, reportId?: string) => void;
|
||||
onReportStart?: (positionSeconds: number, reportId?: string) => void;
|
||||
onReportStop?: (positionSeconds: number, reportId?: string) => void;
|
||||
onEnded?: () => void; // Called when video playback ends naturally
|
||||
onNext?: () => void; // Called when user clicks next episode button
|
||||
hasNext?: boolean; // Whether there is a next episode available
|
||||
@ -30,6 +34,12 @@
|
||||
|
||||
let { media, streamUrl, mediaSourceId, initialPosition, needsTranscoding = false, onClose, onSeek, onReportProgress, onReportStart, onReportStop, onEnded, onNext, hasNext = false }: Props = $props();
|
||||
|
||||
// The id this player instance reports progress against. Snapshotted from the
|
||||
// media prop so a late reportStop (e.g. from onDestroy during autoplay
|
||||
// navigation) is always attributed to the episode this player played.
|
||||
// untrack() makes the intent explicit: capture the initial value only.
|
||||
const reportMediaId = untrack(() => media?.id);
|
||||
|
||||
let videoElement: HTMLVideoElement | null = $state(null);
|
||||
let isPlaying = $state(false);
|
||||
let currentTime = $state(0);
|
||||
@ -470,7 +480,7 @@
|
||||
// Report progress every 10 seconds while playing
|
||||
progressInterval = setInterval(() => {
|
||||
if (isPlaying && !isSeeking && onReportProgress) {
|
||||
onReportProgress(currentTime, false);
|
||||
onReportProgress(currentTime, false, reportMediaId);
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
@ -536,7 +546,7 @@
|
||||
|
||||
// Report stop when component is destroyed
|
||||
if (onReportStop && currentTime > 0) {
|
||||
onReportStop(currentTime);
|
||||
onReportStop(currentTime, reportMediaId);
|
||||
}
|
||||
});
|
||||
|
||||
@ -730,7 +740,7 @@
|
||||
startTimeUpdates(); // Start RAF loop for smooth time updates
|
||||
// Report playback start on first play
|
||||
if (!hasReportedStart && onReportStart) {
|
||||
onReportStart(currentTime);
|
||||
onReportStart(currentTime, reportMediaId);
|
||||
hasReportedStart = true;
|
||||
}
|
||||
}
|
||||
@ -740,7 +750,7 @@
|
||||
stopTimeUpdates(); // Stop RAF loop when paused
|
||||
// Report progress when paused
|
||||
if (onReportProgress) {
|
||||
onReportProgress(currentTime, true);
|
||||
onReportProgress(currentTime, true, reportMediaId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -749,7 +759,7 @@
|
||||
stopTimeUpdates(); // Stop RAF loop when ended
|
||||
// Report stop when video ends
|
||||
if (onReportStop) {
|
||||
onReportStop(currentTime);
|
||||
onReportStop(currentTime, reportMediaId);
|
||||
}
|
||||
// Notify parent that video has ended (for next episode popup)
|
||||
if (onEnded) {
|
||||
|
||||
@ -39,6 +39,10 @@ export async function cancelAutoPlay() {
|
||||
/**
|
||||
* Navigate to the next episode via goto().
|
||||
* Uses replaceState to prevent history buildup when auto-advancing.
|
||||
*
|
||||
* Advancing to a next episode always starts that episode from the
|
||||
* beginning, even if it was previously started or watched. The `restart`
|
||||
* query param signals the player page to skip the resume-progress check.
|
||||
*/
|
||||
function navigateToEpisode(episode: MediaItem) {
|
||||
if (isNavigating) {
|
||||
@ -48,7 +52,7 @@ function navigateToEpisode(episode: MediaItem) {
|
||||
isNavigating = true;
|
||||
console.log("[NextEpisode] Navigating to next episode:", episode.id, episode.name);
|
||||
nextEpisode.hidePopup();
|
||||
goto(`/player/${episode.id}`, { replaceState: true }).finally(() => {
|
||||
goto(`/player/${episode.id}?restart=true`, { replaceState: true }).finally(() => {
|
||||
isNavigating = false;
|
||||
});
|
||||
}
|
||||
|
||||
@ -25,6 +25,9 @@
|
||||
const itemId = $derived($page.params.id);
|
||||
const queueParam = $derived($page.url.searchParams.get("queue"));
|
||||
const shuffleParam = $derived($page.url.searchParams.get("shuffle") === "true");
|
||||
// When advancing to a next episode we always start from the beginning,
|
||||
// even if the episode was previously started or watched.
|
||||
const restartParam = $derived($page.url.searchParams.get("restart") === "true");
|
||||
|
||||
// Derive playback context from URL query params
|
||||
const playbackContext = $derived.by(() => {
|
||||
@ -80,9 +83,12 @@
|
||||
// Load when itemId changes (handles both initial load and navigation)
|
||||
$effect(() => {
|
||||
const id = itemId;
|
||||
const restart = restartParam;
|
||||
if (id && id !== loadedItemId) {
|
||||
console.log("[AutoPlay] $effect triggered: loading new item", id, "(was:", loadedItemId, ")");
|
||||
loadAndPlay(id);
|
||||
console.log("[AutoPlay] $effect triggered: loading new item", id, "(was:", loadedItemId, ") restart:", restart);
|
||||
// restart=true (advancing to next episode) forces start-from-beginning,
|
||||
// bypassing the resume-progress check.
|
||||
loadAndPlay(id, restart ? 0 : undefined, restart);
|
||||
}
|
||||
});
|
||||
|
||||
@ -96,7 +102,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
async function loadAndPlay(id: string, startPosition?: number) {
|
||||
async function loadAndPlay(id: string, startPosition?: number, forceRestart = false) {
|
||||
loading = true;
|
||||
error = null;
|
||||
loadedItemId = id;
|
||||
@ -118,9 +124,11 @@
|
||||
}
|
||||
|
||||
// If this track is already playing in the backend, just show the UI
|
||||
// without restarting playback (e.g., when expanding from MiniPlayer)
|
||||
// without restarting playback (e.g., when expanding from MiniPlayer).
|
||||
// forceRestart bypasses this so advancing to the next episode always
|
||||
// restarts from the beginning even if it were already loaded.
|
||||
const alreadyPlayingMedia = get(storeCurrentMedia);
|
||||
if (alreadyPlayingMedia?.id === id && !startPosition) {
|
||||
if (alreadyPlayingMedia?.id === id && !startPosition && !forceRestart) {
|
||||
console.log("loadAndPlay: Track already playing, showing UI without restarting");
|
||||
isVideo = item.type === "Movie" || item.type === "Episode";
|
||||
isPlaying = true;
|
||||
@ -155,11 +163,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Check for saved progress if no start position specified
|
||||
// Check for saved progress if no start position specified.
|
||||
// When forceRestart is set (advancing to a next episode) we always start
|
||||
// from the beginning, skipping the resume check and resume dialog.
|
||||
const userId = auth.getUserId();
|
||||
console.log("Resume check - userId:", userId, "itemId:", id, "startPosition:", startPosition);
|
||||
console.log("Resume check - userId:", userId, "itemId:", id, "startPosition:", startPosition, "forceRestart:", forceRestart);
|
||||
|
||||
if (!startPosition && userId) {
|
||||
if (!startPosition && !forceRestart && userId) {
|
||||
try {
|
||||
const progress = await commands.storageGetPlaybackProgress(userId, id);
|
||||
console.log("Resume check - retrieved progress:", progress);
|
||||
@ -471,23 +481,30 @@
|
||||
}
|
||||
|
||||
// Playback reporting callbacks
|
||||
function handleReportStart(positionSeconds: number) {
|
||||
const id = itemId;
|
||||
//
|
||||
// These receive the reporting item id from the VideoPlayer (its own media.id),
|
||||
// NOT the live URL param (itemId). During autoplay the URL flips to the next
|
||||
// episode before the outgoing VideoPlayer's onDestroy fires its final
|
||||
// reportStop. Keying off itemId would stamp the old episode's near-end
|
||||
// position onto the new episode, making it resume at ~99% (or pop the resume
|
||||
// dialog). The VideoPlayer always knows which media it actually played.
|
||||
function handleReportStart(positionSeconds: number, reportId?: string) {
|
||||
const id = reportId ?? itemId;
|
||||
const context = playbackContext; // playbackContext is a derived value, not a function
|
||||
if (id) {
|
||||
reportPlaybackStart(id, positionSeconds, context.type, context.id);
|
||||
}
|
||||
}
|
||||
|
||||
function handleReportProgress(positionSeconds: number, isPaused: boolean) {
|
||||
const id = itemId;
|
||||
function handleReportProgress(positionSeconds: number, isPaused: boolean, reportId?: string) {
|
||||
const id = reportId ?? itemId;
|
||||
if (id) {
|
||||
reportPlaybackProgress(id, positionSeconds, isPaused);
|
||||
}
|
||||
}
|
||||
|
||||
function handleReportStop(positionSeconds: number) {
|
||||
const id = itemId;
|
||||
function handleReportStop(positionSeconds: number, reportId?: string) {
|
||||
const id = reportId ?? itemId;
|
||||
if (id) {
|
||||
reportPlaybackStopped(id, positionSeconds);
|
||||
}
|
||||
@ -538,8 +555,10 @@
|
||||
|
||||
function handleSkipToNextEpisode() {
|
||||
if (nextEpisode) {
|
||||
// Use replaceState so "close/back" returns to the library, not the previous episode
|
||||
goto(`/player/${nextEpisode.id}`, { replaceState: true });
|
||||
// Use replaceState so "close/back" returns to the library, not the previous episode.
|
||||
// restart=true so advancing to the next episode always starts from the beginning,
|
||||
// even if it was previously started or watched.
|
||||
goto(`/player/${nextEpisode.id}?restart=true`, { replaceState: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user