Warm the stream cache for likely next plays
This commit is contained in:
@@ -327,6 +327,20 @@ const API = {
|
||||
: call('cache.clear', 'cache_clear', {}),
|
||||
};
|
||||
|
||||
// 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();
|
||||
function warmStreams(ids) {
|
||||
if (!WEB || navigator.onLine === false) return;
|
||||
const now = Date.now();
|
||||
for (const id of ids) {
|
||||
if (!id || !/^[A-Za-z0-9_-]{11}$/.test(id) || cachedIds.has(id)) continue;
|
||||
if (now - (warmedAt.get(id) || 0) < 20 * 60_000) continue;
|
||||
warmedAt.set(id, now);
|
||||
fetch(`/api/streams/warm?v=${encodeURIComponent(id)}`, { priority: 'low' }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve a native file path to a URL the WebView can load (Tauri asset
|
||||
// protocol). Returns null on shells without it.
|
||||
function toAssetUrl(path) {
|
||||
@@ -2303,6 +2317,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));
|
||||
showSpinner(false);
|
||||
Player.clearBufferGrace();
|
||||
if (Player.secondary && !el.paused && Player.secondary.paused) {
|
||||
@@ -7795,6 +7810,7 @@ function renderCard(v, index, list) {
|
||||
card.insertBefore(noteBtn, menuBtn);
|
||||
}
|
||||
|
||||
card.addEventListener('pointerdown', () => warmStreams([v.id]), { passive: true });
|
||||
card.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.card-menu') || e.target.closest('.card-del') || e.target.closest('.card-note-btn') || e.target.closest('.card-channel.link')) return;
|
||||
if (selectMode) { toggleSelectCard(v.id); return; }
|
||||
@@ -8625,6 +8641,7 @@ function wireUI() {
|
||||
searchAttempted = true;
|
||||
renderList();
|
||||
RecentSearches.cacheResults(q, results);
|
||||
warmStreams(results.slice(0, 3).map((r) => r.id));
|
||||
perfMeasure('ytp:search', 'ytp:search-start');
|
||||
} catch (err) {
|
||||
if (mySeq !== searchSeq) return;
|
||||
|
||||
@@ -12,7 +12,7 @@ green, app boots with no JS errors, P2P on by default, offline boot works).
|
||||
| 002 | 002-compress-and-etag-shell-bd459c | Serve the app shell gzip/brotli-compressed with ETags | done | Serve the app shell gzip/brotli-compressed with ETags | app.js 426 KB → 94 KB (br) |
|
||||
| 003 | 003-self-host-fonts-89466b | Self-host the three web fonts and drop the render-blocking Google Fonts CSS | done | Self-host the three web fonts and drop the render-blocking Google Fonts CSS | needs network once; CSP font-src fix |
|
||||
| 004 | 004-coalesce-stream-resolves-a92d40 | Coalesce concurrent resolveStreams calls for the same video | done | Coalesce concurrent resolveStreams calls for the same video | |
|
||||
| 005 | 005-warm-streams-on-intent-47b3d3 | Warm the stream cache for likely next plays | in-progress | | cold play 7.5 s → cached 1.2 s |
|
||||
| 005 | 005-warm-streams-on-intent-47b3d3 | Warm the stream cache for likely next plays | done | Warm the stream cache for likely next plays | cold play 7.5 s → cached 1.2 s |
|
||||
| 006 | 006-innertube-search-48066b | Answer searches from YouTube InnerTube directly with yt-dlp fallback | queued | | search 4–5 s → ~0.8 s |
|
||||
| 007 | 007-ytdlp-worker-045800 | Keep one long-lived yt-dlp worker process instead of spawning per call | queued | | ~1 s per yt-dlp call |
|
||||
| 008 | 008-p2p-schema-and-config-127966 | Add P2P tables, config flags and db helpers | queued | | P2P ON, malware scan OFF by default |
|
||||
|
||||
@@ -109,3 +109,10 @@ Output ONLY the following, no other prose:
|
||||
3. `Findings:` — max 10 lines.
|
||||
|
||||
Do not commit. Do not push. Do not touch files outside the Steps.
|
||||
|
||||
## Execution log
|
||||
|
||||
- Executor: in-session Agent (haiku). Attempts: 1. Fix rounds: 0.
|
||||
- Orchestrator re-ran Verification: `node --check` ok, `SERVER_OK`, `/api/streams/warm` returns 204 for a bad id and a valid id, 52 frontend tests pass, `warmStreams` appears 4 times in app.js.
|
||||
- Executor Findings (verbatim): All steps completed successfully. The /api/streams/warm endpoint returns 204 for both invalid IDs and valid YouTube IDs as expected. The warmStreams client function correctly validates IDs against the regex pattern and rate-limits warming to 20 minutes per ID. All 52 unit tests pass with zero failures. No deviations from the plan.
|
||||
- Orchestrator note: the executor started the server from the repo root, leaving an untracked `public` symlink and `data/` database at the root; deleted before commit (not part of the plan).
|
||||
@@ -547,6 +547,21 @@ function pickFormat(formats, { formatId, wantAudio, wantHeight }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// GET /api/streams/warm?v=<id> — fire-and-forget: resolve streams into
|
||||
// streamCache so the real /api/streams a moment later is instant. Bounded
|
||||
// so a scrolling user can't queue dozens of yt-dlp processes.
|
||||
const WARM_MAX = 2;
|
||||
let warmActive = 0;
|
||||
app.get('/api/streams/warm', async (c) => {
|
||||
const id = (c.req.query('v') || '').trim();
|
||||
if (!/^[A-Za-z0-9_-]{11}$/.test(id)) return c.body(null, 204);
|
||||
if (warmActive >= WARM_MAX) return c.body(null, 204);
|
||||
try { if (await media.getReady(id)) return c.body(null, 204); } catch { /* fall through */ }
|
||||
warmActive++;
|
||||
resolveStreams(id).catch(() => {}).finally(() => { warmActive--; });
|
||||
return c.body(null, 204);
|
||||
});
|
||||
|
||||
// GET /api/streams?v=<videoId> — meta + proxied audio/quality URLs.
|
||||
app.get('/api/streams', async (c) => {
|
||||
const videoId = (c.req.query('v') || '').replace(/[/\\:?<>|*"]/g, '').trim();
|
||||
|
||||
Reference in New Issue
Block a user