Remember and restore last playback position per video

Saves playback position to store on pause and periodically every
10s during playback. Restores position (>1s) when the same video
is loaded again, showing a 'Resumed from 0:42' toast.

Files:
- frontend/app.js: resumePositions in data, save in pause/timeupdate,
  restore in Player.afterLoad, boot merge includes resumePositions
This commit is contained in:
Jonathan Sykes
2026-06-14 14:31:19 +08:00
parent 81aced9bc5
commit ddfa1494c6
5 changed files with 38 additions and 3 deletions

View File

@@ -45,7 +45,7 @@ function toAssetUrl(path) {
}
// ---------- State ----------
let data = { playlists: [], history: [], settings: { quality: 'auto', volume: 1, audioOnly: false, autoPreload: true } };
let data = { playlists: [], history: [], settings: { quality: 'auto', volume: 1, audioOnly: false, autoPreload: true }, resumePositions: {} };
let view = { type: 'search' }; // 'search' | 'history' | 'playlist' | 'settings'
let searchResults = [];
let queue = []; // list of video objects for autoplay
@@ -245,6 +245,19 @@ const Player = {
els.npChannel.textContent = current.meta.channel || '';
updateNowPlayingActions();
markPlayingCard();
// Restore saved playback position
const id = current.meta.id;
if (data.resumePositions[id] && data.resumePositions[id] > 1) {
const saved = data.resumePositions[id];
const restore = () => {
Player.seek(saved);
toast('Resumed from ' + fmtTime(saved));
Player.master.removeEventListener('canplay', restore);
Player.master.removeEventListener('loadedmetadata', restore);
};
Player.master.addEventListener('canplay', restore, { once: true });
Player.master.addEventListener('loadedmetadata', restore, { once: true });
}
},
// Ordered list of qualities to try if playback errors out. Progressive
@@ -459,7 +472,28 @@ function wirePlayerEvents() {
el.addEventListener('waiting', () => { if (masterIs(el)) { showSpinner(true); if (Player.secondary) Player.secondary.pause(); } });
el.addEventListener('playing', () => { if (masterIs(el)) { showSpinner(false); if (Player.secondary && !el.paused) { Player.secondary.currentTime = el.currentTime; Player.secondary.play().catch(() => {}); } } });
el.addEventListener('canplay', () => { if (masterIs(el)) showSpinner(false); });
el.addEventListener('timeupdate', () => { if (masterIs(el)) updateProgress(); });
el.addEventListener('timeupdate', () => {
if (masterIs(el)) {
updateProgress();
// Persist playback position every 10s
if (current && current.meta) {
const t = Player.master.currentTime;
if (t > 5 && Math.floor(t) % 10 === 0) {
data.resumePositions[current.meta.id] = t;
persist();
}
}
}
});
el.addEventListener('pause', () => {
if (masterIs(el) && current && current.meta) {
const t = Player.master.currentTime;
if (t > 1) {
data.resumePositions[current.meta.id] = t;
persist();
}
}
});
el.addEventListener('loadedmetadata', () => { if (masterIs(el)) updateProgress(); });
el.addEventListener('ended', () => { if (masterIs(el)) playNext(); });
el.addEventListener('error', () => {
@@ -1137,6 +1171,7 @@ async function boot() {
data = {
playlists: loaded.playlists || [],
history: loaded.history || [],
resumePositions: loaded.resumePositions || {},
settings: { quality: 'auto', volume: 1, audioOnly: false, autoPreload: true, ...(loaded.settings || {}) },
};
}