From d65b9e032af09b1b7355c58575da17e952228ede Mon Sep 17 00:00:00 2001 From: Jonathan Sykes Date: Wed, 7 Oct 2026 16:30:15 +0800 Subject: [PATCH] Prewarm upcoming queue streams --- frontend/app.js | 15 +++++++++++++-- frontend/index.html | 1 + frontend/sw.js | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/frontend/app.js b/frontend/app.js index 8da49bd..c174b25 100755 --- a/frontend/app.js +++ b/frontend/app.js @@ -623,8 +623,9 @@ const API = { // Pre-resolve streams for videos the user is likely to play next (server // /api/streams/warm). Each id is warmed at most once per 20 min per tab. const warmedAt = new Map(); +const knownServerCachedIds = new Set(); function warmStreams(ids) { - if (!WEB || navigator.onLine === false) return; + if (!WEB || navigator.onLine === false || navigator.connection?.saveData) return; const now = Date.now(); for (const id of ids) { if (!id || !/^[A-Za-z0-9_-]{11}$/.test(id) || cachedIds.has(id)) continue; @@ -634,6 +635,14 @@ function warmStreams(ids) { } } +function warmNextQueue() { + if (!window.PrewarmNext || queueIndex < 0) return; + warmStreams(window.PrewarmNext.candidates(queue, queueIndex, data.settings, 2, { + offline: cachedIds, + serverCached: knownServerCachedIds, + })); +} + // Resolve a native file path to a URL the WebView can load (Tauri asset // protocol). Returns null on shells without it. function toAssetUrl(path) { @@ -2332,6 +2341,7 @@ const Player = { if (seq !== this._loadSeq) return; // a newer load took over meanwhile if (!res || !res.ok) throw new Error(res?.error || 'Could not load streams'); current = res.data; + if (current.serverCached && current.meta?.id) knownServerCachedIds.add(current.meta.id); // Merge richer metadata we may already have from the list card. current.meta = { ...videoObj, ...current.meta }; @@ -3014,7 +3024,7 @@ function wirePlayerEvents() { perfMeasure('ytp:tap-to-play', 'ytp:tap'); try { performance.clearMarks('ytp:tap'); } catch { /* ignore */ } } - if (Array.isArray(queue) && queueIndex >= 0) warmStreams(queue.slice(queueIndex + 1, queueIndex + 3).map((x) => x && x.id)); + warmNextQueue(); showSpinner(false); Player.clearBufferGrace(); if (Player.secondary && !el.paused && Player.secondary.paused) { @@ -8043,6 +8053,7 @@ function playFromList(list, index, source = '', { playFull = false } = {}) { unshuffledQueue = null; // Play-in-full keeps the list order — a service set plays as arranged. if (data.settings.shuffle && !playFull) applyShuffle(); + warmNextQueue(); Player.loadVideo(queue[queueIndex]); renderUpNext(); } diff --git a/frontend/index.html b/frontend/index.html index b4de34a..fa61d53 100755 --- a/frontend/index.html +++ b/frontend/index.html @@ -693,6 +693,7 @@ + diff --git a/frontend/sw.js b/frontend/sw.js index 7cfec85..7d584dc 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -124,6 +124,7 @@ const SHELL = [ '/piano.js', '/setlist-import.js', '/car-mode.js', + '/prewarm-next.js', '/car-mode.css', '/lower-third.css', '/settings-sections.css',