From ebaec4226a49628ea0ce6e08e4b41480bb9327a7 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 16 Oct 2026 22:45:08 -0700 Subject: The pager glides onto the next clip's own first frame, and onto pictures already decoded — no black slide between. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The clip ahead holds its own first frame: full size, frame 0, drawn from the opening of the file (the bytes the head start put in the cache, read again by range — never more of the file than the head start fetched), so a flick to it glides onto the very picture the player shows first, and the player's frame replaces it with no visible change. A dozen are kept, the oldest let go; the tile's kept frame (480px, at 0.1 s) is made from the same decode when the tile has none yet. Only the next clip, not the ones beyond: their head start is half, and a frame from them would cost bytes. On a connection to be careful with the thumbnail poster stays, as before. Pictures: the slides near the reader are attached from two and a half screens out (was one and a half) and decoded as they are attached, so a flight lands on a picture rather than on a blank that fills in. (Adam, 2026-10-16: "could we actually have the next image or video scroll in while the previous scrolls out? Right now it goes black before the next one scrolls in.") pager-no-poster-test: the next clip holds a blob poster at the clip's full size and shape while the one after holds nothing; after a flick the next plays with its frame under it and the one after now holds its own; Data Saver keeps the thumbnails. pager-eager-test (the byte budget ahead of time) still passes; so do the picture pager and sharpen tests. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp --- tools/pager-no-poster-test.js | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) (limited to 'tools') diff --git a/tools/pager-no-poster-test.js b/tools/pager-no-poster-test.js index 4153bba..0930834 100644 --- a/tools/pager-no-poster-test.js +++ b/tools/pager-no-poster-test.js @@ -1,9 +1,11 @@ -// A clip that plays by itself in the video pager has no poster: every -// poster there is comes 480px wide and from some moment of the clip, so it -// showed soft for a beat and the clip's first frame replaced it — a flash -// on every flick. The slide is the pager's black until the player has its -// first frame. On a connection to be careful with (Data Saver), where a -// clip may never load, the poster stays. +// A clip that plays by itself in the video pager has no thumbnail poster: +// every poster there was came 480px wide and from some moment of the +// clip, so it showed soft for a beat and the clip's first frame replaced +// it — a flash on every flick. What the clip ahead holds instead is its +// own first frame, full size, drawn from the opening of the file: the +// flick glides onto the very picture the player shows first. On a +// connection to be careful with (Data Saver), where a clip may never load, +// the thumbnail poster stays. // Run: bun tools/pager-no-poster-test.js import puppeteer from './puppeteer.js'; import { cacheKeyFor } from '../src/wallet.js'; @@ -26,7 +28,13 @@ const html = await buildHtml({ minify: true, pwa: false }); const server = Bun.serve({ port: 0, fetch: (req) => { const path = new URL(req.url).pathname; const m = /^\/(wide|tall|last)\.webm$/.exec(path); - if (m) return new Response(Bun.file(CLIPS[m[1]]), { headers: { 'content-type': 'video/webm' } }); + if (m) { + // a host that serves ranges, as the real ones do + const f = Bun.file(CLIPS[m[1]]), range = /^bytes=(\d+)-(\d*)$/.exec(req.headers.get('range') || ''); + if (!range) return new Response(f, { headers: { 'content-type': 'video/webm', 'accept-ranges': 'bytes' } }); + const a = +range[1], b = range[2] ? Math.min(+range[2], f.size - 1) : f.size - 1; + return new Response(f.slice(a, b + 1), { status: 206, headers: { 'content-type': 'video/webm', 'accept-ranges': 'bytes', 'content-range': 'bytes ' + a + '-' + b + '/' + f.size, 'content-length': String(b - a + 1) } }); + } if (path === '/still.png') return new Response(png, { headers: { 'content-type': 'image/png' } }); if (path.startsWith('/punks') || path === '/verify-worker.js') return new Response(Bun.file('dist' + path)); return new Response(html, { headers: { 'content-type': 'text/html' } }); @@ -94,10 +102,15 @@ try { let s = await open(); check('the clip on screen plays', s.some((x) => x.here && x.playing), JSON.stringify(s)); check('…with no poster on it', s.filter((x) => x.here).every((x) => !x.poster), JSON.stringify(s.filter((x) => x.here))); - check('…nor on the clips ahead', s.filter((x) => !x.here).every((x) => !x.poster), JSON.stringify(s.filter((x) => !x.here))); + check('…no thumbnail on the clips ahead either', s.filter((x) => !x.here).every((x) => !/still\.png/.test(x.poster || '')), JSON.stringify(s.filter((x) => !x.here))); + check('the next clip holds its own first frame', await waitFor(() => { const v = [...document.querySelectorAll('.mp-slide')].find((x) => x.getAttribute('data-k').endsWith('/tall.webm'))?.querySelector('video.mp-media'); return !!v && /^blob:/.test(v.getAttribute('poster') || ''); }, null, 8000), JSON.stringify(await slides())); + const frame = await page.evaluate(() => new Promise((r) => { const v = [...document.querySelectorAll('.mp-slide')].find((x) => x.getAttribute('data-k').endsWith('/tall.webm')).querySelector('video.mp-media'); const im = new Image(); im.onload = () => r({ w: im.naturalWidth, h: im.naturalHeight }); im.onerror = () => r(null); im.src = v.getAttribute('poster'); })); + check('…full size, the clip\'s own shape', !!frame && frame.w === 180 && frame.h === 320, JSON.stringify(frame)); + check('…and the clip after it, not yet near, holds nothing', s.some((x) => x.clip === 'last.webm' && !x.poster), JSON.stringify(s)); await page.keyboard.press('ArrowDown'); await sleep(900); s = await slides(); - check('the next clip plays, no poster either', s.some((x) => x.here && x.playing && !x.poster && x.clip === 'tall.webm'), JSON.stringify(s)); + check('the next clip plays, its first frame under it', s.some((x) => x.here && x.playing && /^blob:/.test(x.poster || '') && x.clip === 'tall.webm'), JSON.stringify(s)); + check('…and the one after now holds its first frame', await waitFor(() => { const v = [...document.querySelectorAll('.mp-slide')].find((x) => x.getAttribute('data-k').endsWith('/last.webm'))?.querySelector('video.mp-media'); return !!v && /^blob:/.test(v.getAttribute('poster') || ''); }, null, 8000)); await page.keyboard.press('Escape'); await sleep(500); console.log('\n[a careful connection: the poster stays]'); @@ -106,5 +119,5 @@ try { check('the slides carry their posters', s.length >= 3 && s.every((x) => /still\.png$/.test(x.poster || '')), JSON.stringify(s)); check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | ')); } catch (e) { console.error(e); ok = false; } finally { await browser.close(); server.stop(true); } -console.log(ok ? '\n✅ an autoplaying clip starts on its own first frame, no poster' : '\n❌ failed'); +console.log(ok ? '\n✅ an autoplaying clip starts on its own first frame, the one ahead holds it already' : '\n❌ failed'); process.exit(ok ? 0 : 1); -- cgit v1.2.3