fix(player): resume from saved position and protect it across keep-alive

Clicking a video from history (or any cached watch page) restarted it from 0
instead of resuming, and history bars showed stale positions. Four shared-path
bugs, all engine-agnostic:

- The resume position was computed but never applied: Shaka's load(uri, startTime)
  doesn't perform the initial seek for lazily-fetched segment indexes, so playback
  began at 0. Apply the resume explicitly with a runtime seek once load() resolves.

- initialSeekComplete (gates progress saving until the resume seek lands) was never
  reset per-load. On a reactivated player it stayed true from the previous play, so
  a timeupdate at currentTime=0 during rebuild churn overwrote the saved position
  before the resume read ran. Reset it at the start of loadVideo.

- Leaving a watch page (destroy) empties the media element -> currentTime snaps to
  0 and a stray timeupdate fires while initialSeekComplete is still true, clobbering
  the saved position. Gate the save on destroying as well.

- HistoryPage: re-read watch_history in onActivated so progress bars reflect the
  current saved position instead of a stale first-mount snapshot. Kept off onMounted
  to avoid double-loading (both fire on first keep-alive mount).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
LogicalKarma
2026-05-28 23:19:48 +03:00
parent de44e80a5c
commit 174ca1a072
2 changed files with 21 additions and 17 deletions

View File

@@ -61,7 +61,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onActivated, onDeactivated } from "vue"; import { ref, onActivated, onDeactivated } from "vue";
import VideoItem from "./VideoItem.vue"; import VideoItem from "./VideoItem.vue";
import SortingSelector from "./SortingSelector.vue"; import SortingSelector from "./SortingSelector.vue";
import ExportHistoryModal from "./ExportHistoryModal.vue"; import ExportHistoryModal from "./ExportHistoryModal.vue";
@@ -73,8 +73,8 @@ let currentVideoCount = 0;
const videoStep = 100; const videoStep = 100;
const videosStore = []; const videosStore = [];
const videos = ref([]); const videos = ref([]);
const autoDeleteHistory = ref(false); const autoDeleteHistory = ref(getPreferenceBoolean("autoDeleteWatchHistory", false));
const autoDeleteDelayHours = ref("24"); const autoDeleteDelayHours = ref(getPreferenceString("autoDeleteWatchHistoryDelayHours", "24"));
const showExportModal = ref(false); const showExportModal = ref(false);
const showImportModal = ref(false); const showImportModal = ref(false);
@@ -109,11 +109,12 @@ function onChange() {
setPreference("autoDeleteWatchHistoryDelayHours", autoDeleteDelayHours.value); setPreference("autoDeleteWatchHistoryDelayHours", autoDeleteDelayHours.value);
} }
onMounted(() => { function loadHistory() {
autoDeleteHistory.value = getPreferenceBoolean("autoDeleteWatchHistory", false); videosStore.length = 0;
autoDeleteDelayHours.value = getPreferenceString("autoDeleteWatchHistoryDelayHours", "24"); currentVideoCount = 0;
videos.value = [];
(async () => { return (async () => {
if (window.db && getPreferenceBoolean("watchHistory", false)) { if (window.db && getPreferenceBoolean("watchHistory", false)) {
var tx = window.db.transaction("watch_history", "readwrite"); var tx = window.db.transaction("watch_history", "readwrite");
var store = tx.objectStore("watch_history"); var store = tx.objectStore("watch_history");
@@ -148,10 +149,11 @@ onMounted(() => {
})().then(() => { })().then(() => {
loadMoreVideos(); loadMoreVideos();
}); });
}); }
onActivated(() => { onActivated(() => {
document.title = "Watch History - Piped"; document.title = "Watch History - Piped";
loadHistory();
window.addEventListener("scroll", handleScroll); window.addEventListener("scroll", handleScroll);
}); });

View File

@@ -326,7 +326,7 @@ async function updateProgressDatabase(time) {
if (new Date().getTime() - lastUpdate.value < 500) return; if (new Date().getTime() - lastUpdate.value < 500) return;
lastUpdate.value = new Date().getTime(); lastUpdate.value = new Date().getTime();
if (!initialSeekComplete.value || !props.video.id || !window.db) return; if (!initialSeekComplete.value || destroying.value || !props.video.id || !window.db) return;
var tx = window.db.transaction("watch_history", "readwrite"); var tx = window.db.transaction("watch_history", "readwrite");
var store = tx.objectStore("watch_history"); var store = tx.objectStore("watch_history");
@@ -448,7 +448,6 @@ async function setPlayerAttrs(localPlayer, el, uri, mime, shaka) {
if (time) { if (time) {
startTime = parseTimeParam(time); startTime = parseTimeParam(time);
initialSeekComplete.value = true;
} else if (window.db && getPreferenceBoolean("watchHistory", false)) { } else if (window.db && getPreferenceBoolean("watchHistory", false)) {
await new Promise(resolve => { await new Promise(resolve => {
var tx = window.db.transaction("watch_history", "readonly"); var tx = window.db.transaction("watch_history", "readonly");
@@ -464,18 +463,19 @@ async function setPlayerAttrs(localPlayer, el, uri, mime, shaka) {
} }
resolve(); resolve();
}; };
tx.oncomplete = () => {
initialSeekComplete.value = true;
};
}); });
} else {
initialSeekComplete.value = true;
} }
playerInstance playerInstance
.load(uri, startTime, mime) .load(uri, null, mime)
.then(async () => { .then(async () => {
// Player.load()'s startTime arg does not reliably perform the
// initial seek; apply it here. See shaka-project/shaka-player#6241.
if (startTime > 0) {
el.currentTime = startTime;
await new Promise(resolve => el.addEventListener("seeked", resolve, { once: true }));
}
initialSeekComplete.value = true;
let lang = "en"; let lang = "en";
const prefLang = getPreferenceString("hl", "en").substr(0, 2); const prefLang = getPreferenceString("hl", "en").substr(0, 2);
const audioTracks = playerInstance.getAudioTracks(); const audioTracks = playerInstance.getAudioTracks();
@@ -573,6 +573,8 @@ async function setPlayerAttrs(localPlayer, el, uri, mime, shaka) {
} }
async function loadVideo() { async function loadVideo() {
initialSeekComplete.value = false;
updateSponsors(); updateSponsors();
const el = videoEl.value; const el = videoEl.value;