From 69aed50012f2703d99d199f894ec598b7d6b775c Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Mon, 7 Sep 2026 06:41:54 +0200 Subject: [PATCH] Keep a replay out of the playback that took its place --- Yattee/Services/Player/PlayerService.swift | 32 ++++++++++++++++++---- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/Yattee/Services/Player/PlayerService.swift b/Yattee/Services/Player/PlayerService.swift index 5faa776b..d8448cb9 100644 --- a/Yattee/Services/Player/PlayerService.swift +++ b/Yattee/Services/Player/PlayerService.swift @@ -151,6 +151,14 @@ final class PlayerService { /// Used to prevent saving progress when switching to next video after natural completion. private var videoEndedNaturally = false + /// Bumped every time the playback identity changes hands: a load claiming + /// the player, or a teardown clearing it. Work that suspends mid-playback + /// captures it and compares on the way back, so a continuation belonging to + /// a watch that is over cannot act on the one that replaced it — including + /// when the replacement is the very same video, which an ID check alone + /// cannot tell apart. + private var playbackGeneration = 0 + /// Current playback task - cancelled when video is closed or new video is opened. private var currentPlayTask: Task? @@ -273,6 +281,9 @@ final class PlayerService { // a second time. watchHistorySyncService?.beginSessionIfNeeded(video: video) + // This load is the playback from here on + playbackGeneration += 1 + // Reset flag for the new video videoEndedNaturally = false @@ -690,14 +701,16 @@ final class PlayerService { // leave a timer saving progress for an ended video. videoEndedNaturally = false + let generation = playbackGeneration await seek(to: 0) // The seek is awaited, and the player can have moved on while it ran: - // stopped, or already playing something else. Only the replay that is - // still the current watch may resume playback and arm the save timer — - // otherwise this would unpause a video the user paused, or time saves - // for a watch that is not this one. - guard let video, state.currentVideo?.id == video.id else { return } + // stopped, playing something else, or stopped and given this same video + // a fresh start — which the id alone cannot tell from our own watch. + // Only the replay that is still the current playback may resume and arm + // the save timer — otherwise this would unpause a video the user paused + // or is still loading, or time saves for a watch that is not this one. + guard let video, playbackGeneration == generation, state.currentVideo?.id == video.id else { return } resume() startProgressSaveTimer() @@ -1132,6 +1145,9 @@ final class PlayerService { // Cancel any previous play task before starting a new one currentPlayTask?.cancel() + // The opening owns the player from here, before the load task even runs + playbackGeneration += 1 + // Set video info before expanding so the sheet animates with content visible state.setPlaybackState(.loading) state.setCurrentVideo(video, stream: nil) @@ -1176,6 +1192,9 @@ final class PlayerService { // Cancel any previous play task before starting a new one currentPlayTask?.cancel() + // The opening owns the player from here, before the load task even runs + playbackGeneration += 1 + // Set video info before expanding so the sheet animates with content visible state.setPlaybackState(.loading) state.setCurrentVideo(video, stream: stream, audioStream: audioStream) @@ -2979,6 +2998,9 @@ final class PlayerService { } private func cleanup() { + // Whatever was playing is gone from here on + playbackGeneration += 1 + // Clean up temp subtitle files for current video if let currentVideo = state.currentVideo { cleanupTempSubtitles(for: currentVideo.id.id)