Merge branch 'codex/prewarm-next'
# Conflicts: # frontend/index.html
This commit is contained in:
@@ -623,8 +623,9 @@ const API = {
|
|||||||
// Pre-resolve streams for videos the user is likely to play next (server
|
// 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.
|
// /api/streams/warm). Each id is warmed at most once per 20 min per tab.
|
||||||
const warmedAt = new Map();
|
const warmedAt = new Map();
|
||||||
|
const knownServerCachedIds = new Set();
|
||||||
function warmStreams(ids) {
|
function warmStreams(ids) {
|
||||||
if (!WEB || navigator.onLine === false) return;
|
if (!WEB || navigator.onLine === false || navigator.connection?.saveData) return;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
if (!id || !/^[A-Za-z0-9_-]{11}$/.test(id) || cachedIds.has(id)) continue;
|
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
|
// Resolve a native file path to a URL the WebView can load (Tauri asset
|
||||||
// protocol). Returns null on shells without it.
|
// protocol). Returns null on shells without it.
|
||||||
function toAssetUrl(path) {
|
function toAssetUrl(path) {
|
||||||
@@ -2332,6 +2341,7 @@ const Player = {
|
|||||||
if (seq !== this._loadSeq) return; // a newer load took over meanwhile
|
if (seq !== this._loadSeq) return; // a newer load took over meanwhile
|
||||||
if (!res || !res.ok) throw new Error(res?.error || 'Could not load streams');
|
if (!res || !res.ok) throw new Error(res?.error || 'Could not load streams');
|
||||||
current = res.data;
|
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.
|
// Merge richer metadata we may already have from the list card.
|
||||||
current.meta = { ...videoObj, ...current.meta };
|
current.meta = { ...videoObj, ...current.meta };
|
||||||
|
|
||||||
@@ -3014,7 +3024,7 @@ function wirePlayerEvents() {
|
|||||||
perfMeasure('ytp:tap-to-play', 'ytp:tap');
|
perfMeasure('ytp:tap-to-play', 'ytp:tap');
|
||||||
try { performance.clearMarks('ytp:tap'); } catch { /* ignore */ }
|
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);
|
showSpinner(false);
|
||||||
Player.clearBufferGrace();
|
Player.clearBufferGrace();
|
||||||
if (Player.secondary && !el.paused && Player.secondary.paused) {
|
if (Player.secondary && !el.paused && Player.secondary.paused) {
|
||||||
@@ -8044,6 +8054,7 @@ function playFromList(list, index, source = '', { playFull = false } = {}) {
|
|||||||
unshuffledQueue = null;
|
unshuffledQueue = null;
|
||||||
// Play-in-full keeps the list order — a service set plays as arranged.
|
// Play-in-full keeps the list order — a service set plays as arranged.
|
||||||
if (data.settings.shuffle && !playFull) applyShuffle();
|
if (data.settings.shuffle && !playFull) applyShuffle();
|
||||||
|
warmNextQueue();
|
||||||
Player.loadVideo(queue[queueIndex]);
|
Player.loadVideo(queue[queueIndex]);
|
||||||
renderUpNext();
|
renderUpNext();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -694,6 +694,7 @@
|
|||||||
<script src="setlist-import.js"></script>
|
<script src="setlist-import.js"></script>
|
||||||
<script src="car-mode.js"></script>
|
<script src="car-mode.js"></script>
|
||||||
<script src="visible-timer.js"></script>
|
<script src="visible-timer.js"></script>
|
||||||
|
<script src="prewarm-next.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
37
frontend/prewarm-next.js
Normal file
37
frontend/prewarm-next.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* Pure queue candidate selection for stream prewarming. */
|
||||||
|
(function (root, factory) {
|
||||||
|
if (typeof module === 'object' && module.exports) module.exports = factory();
|
||||||
|
else root.PrewarmNext = factory();
|
||||||
|
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function candidates(queue, index, state = {}, n = 2, known = {}) {
|
||||||
|
if (!Array.isArray(queue) || !queue.length || !Number.isInteger(index) || index < 0 || index >= queue.length) return [];
|
||||||
|
if (state.loopOne) return [];
|
||||||
|
const count = Math.max(0, Math.floor(Number(n) || 0));
|
||||||
|
if (!count) return [];
|
||||||
|
const repeatAll = state.repeatMode === 'all' || state.repeatAll === true;
|
||||||
|
const offline = new Set(known.offline || []);
|
||||||
|
const serverCached = new Set(known.serverCached || []);
|
||||||
|
const current = queue[index];
|
||||||
|
const currentId = typeof current === 'string' ? current : current && current.id;
|
||||||
|
const seen = new Set(currentId ? [currentId] : []);
|
||||||
|
const ids = [];
|
||||||
|
for (let step = 1; step < queue.length + (repeatAll ? 1 : 0) && ids.length < count; step++) {
|
||||||
|
let at = index + step;
|
||||||
|
if (at >= queue.length) {
|
||||||
|
if (!repeatAll) break;
|
||||||
|
at %= queue.length;
|
||||||
|
}
|
||||||
|
const item = queue[at];
|
||||||
|
const id = typeof item === 'string' ? item : item && item.id;
|
||||||
|
if (!id || seen.has(id)) continue;
|
||||||
|
seen.add(id);
|
||||||
|
if (offline.has(id) || serverCached.has(id) || (item && item.serverCached)) continue;
|
||||||
|
ids.push(id);
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { candidates };
|
||||||
|
});
|
||||||
26
frontend/prewarm-next.test.js
Normal file
26
frontend/prewarm-next.test.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
'use strict';
|
||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { candidates } = require('./prewarm-next.js');
|
||||||
|
|
||||||
|
const q = ['a', 'b', 'c', 'd'].map(id => ({ id }));
|
||||||
|
|
||||||
|
test('returns the next n queue ids in existing order', () => {
|
||||||
|
assert.deepEqual(candidates(q, 0, {}, 2), ['b', 'c']);
|
||||||
|
});
|
||||||
|
test('repeat all wraps and does not duplicate ids', () => {
|
||||||
|
assert.deepEqual(candidates(q, 3, { repeatMode: 'all' }, 2), ['a', 'b']);
|
||||||
|
assert.deepEqual(candidates([{ id: 'x' }, { id: 'x' }], 0, { repeatMode: 'all' }, 3), []);
|
||||||
|
});
|
||||||
|
test('repeat off stops at the end and loop one has no candidates', () => {
|
||||||
|
assert.deepEqual(candidates(q, 3, {}, 2), []);
|
||||||
|
assert.deepEqual(candidates(q, 0, { loopOne: true, repeatMode: 'all' }, 2), []);
|
||||||
|
});
|
||||||
|
test('skips offline and known server cached ids and deduplicates', () => {
|
||||||
|
assert.deepEqual(candidates(q, 0, {}, 3, { offline: ['b'], serverCached: ['c'] }), ['d']);
|
||||||
|
assert.deepEqual(candidates([{ id: 'a' }, { id: 'b', serverCached: true }, { id: 'c' }], 0, {}, 2), ['c']);
|
||||||
|
});
|
||||||
|
test('shuffle follows the queue order selected before advance', () => {
|
||||||
|
const shuffled = ['a', 'd', 'b', 'c'].map(id => ({ id }));
|
||||||
|
assert.deepEqual(candidates(shuffled, 0, { shuffle: true }, 2), ['d', 'b']);
|
||||||
|
});
|
||||||
@@ -125,6 +125,7 @@ const SHELL = [
|
|||||||
'/piano.js',
|
'/piano.js',
|
||||||
'/setlist-import.js',
|
'/setlist-import.js',
|
||||||
'/car-mode.js',
|
'/car-mode.js',
|
||||||
|
'/prewarm-next.js',
|
||||||
'/car-mode.css',
|
'/car-mode.css',
|
||||||
'/lower-third.css',
|
'/lower-third.css',
|
||||||
'/settings-sections.css',
|
'/settings-sections.css',
|
||||||
|
|||||||
Reference in New Issue
Block a user