From b562b57bb44e380d2e073a2a16d297c5c2f728d1 Mon Sep 17 00:00:00 2001 From: zwitschi Date: Fri, 24 Jul 2026 14:49:36 +0200 Subject: [PATCH] implement video prefetching for improved playback performance --- static/js/app.js | 90 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 84 insertions(+), 6 deletions(-) diff --git a/static/js/app.js b/static/js/app.js index aee332d..f9158b5 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -50,6 +50,70 @@ let sortMode = "oldest"; let current = null; + // ---- Prefetch helpers ---- + + // Track already-prefetched URLs so we don't re-fetch + var prefetchedUrls = {}; + + /** + * Prefetch a video URL using a element. + * The browser downloads the resource in the background at low priority + * and caches it for instant playback later. + */ + function prefetchVideo(url) { + if (!url || prefetchedUrls[url]) return; + prefetchedUrls[url] = true; + var link = document.createElement("link"); + link.rel = "prefetch"; + link.as = "video"; + link.href = url; + document.head.appendChild(link); + } + + /** + * After the current video starts playing, prefetch: + * 1. All sibling camera angles for the same timestamp + * 2. The next clip in the same camera's timeline + */ + function schedulePrefetch(clip) { + // Use requestIdleCallback to run after the browser has handled the + // current frame (i.e. the video has started loading). Fall back to a + // short setTimeout if requestIdleCallback isn't available. + var doPrefetch = function () { + // 1. Sibling camera angles (same timestamp, different cameras) + var group = byTimestamp[clip.timestamp] || {}; + Object.keys(group).forEach(function (key) { + if (key !== clip.cam) { + prefetchVideo(group[key].url); + } + }); + + // 2. Next clip in the same camera's timeline + var sameCam = clips + .filter(function (c) { + return c.cam === clip.cam; + }) + .sort(function (a, b) { + if (sortMode === "name") return a.filename.localeCompare(b.filename); + var da = tsToDate(a.timestamp), + db = tsToDate(b.timestamp); + return (da ? da.getTime() : 0) - (db ? db.getTime() : 0); + }); + var idx = sameCam.findIndex(function (c) { + return c.timestamp === clip.timestamp && c.filename === clip.filename; + }); + if (idx > -1 && sameCam[idx + 1]) { + prefetchVideo(sameCam[idx + 1].url); + } + }; + + if (typeof requestIdleCallback === "function") { + requestIdleCallback(doPrefetch, { timeout: 2000 }); + } else { + setTimeout(doPrefetch, 100); + } + } + function tsToDate(ts) { const m = /^(\d{4})-(\d{2})-(\d{2})[_ ](\d{2})-(\d{2})-(\d{2})/.exec( ts || "", @@ -197,7 +261,24 @@ current = clip; player.src = clip.url; player.load(); - player.play().catch(function () {}); + + // Start playback first, prefetch only after the video is actually playing + var prefetchDone = false; + function onPlaying() { + if (prefetchDone) return; + prefetchDone = true; + player.removeEventListener("playing", onPlaying); + schedulePrefetch(clip); + } + player.addEventListener("playing", onPlaying); + player.play().catch(function () { + // Even if autoplay is blocked, still prefetch for when the user clicks play + if (!prefetchDone) { + prefetchDone = true; + player.removeEventListener("playing", onPlaying); + schedulePrefetch(clip); + } + }); placeholder.hidden = true; badge.classList.add("visible"); @@ -356,10 +437,7 @@ c.filename === current.filename ); }); - i = - e.key === "j" - ? Math.min(list.length - 1, i + 1) - : Math.max(0, i - 1); + i = e.key === "j" ? Math.min(list.length - 1, i + 1) : Math.max(0, i - 1); if (list[i]) playClip(list[i]); } }); @@ -391,4 +469,4 @@ }); render(); -})(); \ No newline at end of file +})();