diff options
| author | Adam Soltys <[email protected]> | 2026-10-17 05:12:10 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-17 05:12:10 -0700 |
| commit | 90e1fade3b5041a3db3353569ef49ecff441fee3 (patch) | |
| tree | 278d7863682494a6f910aeac51b481f44b4727f6 | |
| parent | fa3ce973a66a2044c8cc9ac383928d70eb32e3d0 (diff) | |
| download | coinosv3-90e1fade3b5041a3db3353569ef49ecff441fee3.tar.gz coinosv3-90e1fade3b5041a3db3353569ef49ecff441fee3.zip | |
The video pager never puts up a stand-in still: a clip joins the carousel only once its own first frame is on its slide.
Where the server had no first frame for a clip, its slide showed the post's still (a kept frame or the post's picture) and swapped it for the clip's first frame when one came — often a different picture, and the swap jarred (Adam, 2026-10-17: "i don't want to show posters and then switch to the first frame because sometimes the poster and first frame were not the same and that was jarring. could we just always wait until we have a non-black frame before including the video in the pager?"). Now such a clip gets no picture at all and waits out of the carousel until its first frame is drawn from its cached opening as the clip ahead; one with no frame to be had stays out. The clip tapped to open the pager is unchanged.
tools/pager-first-frame-test.js: the clip without a server frame has no stand-in and waits out of the carousel, then gets its drawn frame. pager-ready-gate, pager-no-poster, pager-eager, pager-loading and pager-swipe-fly pass.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
| -rw-r--r-- | src/features/messages.js | 16 | ||||
| -rw-r--r-- | tools/pager-first-frame-test.js | 10 |
2 files changed, 14 insertions, 12 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 326c420..0ea7520 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -12893,21 +12893,23 @@ export function messagesFeature(ctx) { // it starts playing"). The server's first frame (vmedia first.jpg: frame // 0 at full size, any host, any layout, cached at the CDN) is the first // choice, fetched and decoded aside and put on the slide when it is - // ready; the frame drawn here is the fallback for the next clip; a clip - // with neither keeps the still there is (a kept frame, the post's - // picture), soft but not black, replaced by a first frame when one comes. + // ready; the frame drawn here is the fallback for the next clip. A clip + // with neither gets no picture at all — and so stays out of the carousel + // (pagerReveal): the post's own still, put up meanwhile, was often not + // the clip's first frame, and the swap from one to the other jarred + // (Adam, 2026-10-17: "i don't want to show posters and then switch to + // the first frame … could we just always wait until we have a non-black + // frame before including the video in the pager?"). function slideFrame(s, k) { const v = s.querySelector('video.mp-media'); if (!v || v._frame === 'first' || v._frameBusy) return; const url = v.getAttribute('data-src'); const put = (p, kind) => { if (!p || !pager || !s.isConnected || v._frame === 'first') return false; - if (kind === 'soft' && (v._frame || pager.cur === s)) return false; // never a soft still under the clip on screen v.setAttribute('poster', p); v._frame = kind; pagerReveal(); return true; }; - const soft = () => put(videoThumbs.get(url) || posterOf(s._ev, url), 'soft'); const first = serverFirst(url); if (first && !serverFirstFailed.has(url)) { v._frameBusy = true; @@ -12926,10 +12928,8 @@ export function messagesFeature(ctx) { if (k === 0 && !v._grabbed) { // the next clip: its opening is in the cache, the frame comes from there v._grabbed = true; v._frameBusy = true; - pagerPoster(playSrc(url)).then((p) => { v._frameBusy = false; if (!put(p, 'first')) soft(); }); - return; + pagerPoster(playSrc(url)).then((p) => { v._frameBusy = false; put(p, 'first'); }); } - if (first) soft(); // (the server had none: the still, if any, rather than black) } // The opening of a clip about to be reached, fetched into the browser's // HTTP cache with a range request: the player's own request for the file diff --git a/tools/pager-first-frame-test.js b/tools/pager-first-frame-test.js index f88a074..6507607 100644 --- a/tools/pager-first-frame-test.js +++ b/tools/pager-first-frame-test.js @@ -2,8 +2,8 @@ // before the reader gets there: the server's (vmedia first.jpg — frame 0 at // full size, any host), put on the slide once fetched and decoded; where the // server has none, the next clip's frame is drawn here from its cached -// opening, and a clip beyond keeps the post's still rather than black, until -// a first frame comes. The slide left behind keeps its frame for the swipe +// opening, and a clip beyond gets no picture (and stays out of the +// carousel) until a first frame comes — never the post's still first. The slide left behind keeps its frame for the swipe // back. Run: bun tools/pager-first-frame-test.js import puppeteer from './puppeteer.js'; import { cacheKeyFor } from '../src/wallet.js'; @@ -106,13 +106,15 @@ try { console.log('\n[the slides ahead hold their first frames]'); check('the next clip holds the server\'s first frame', await waitPoster('tall', 'server'), JSON.stringify(await slides())); check('…the server was asked for it once', asked.filter((x) => x === 'first:tall').length === 1, asked.join(' ')); - check('the clip after, which the server has no frame for, keeps the post\'s still', await waitPoster('last', 'still'), JSON.stringify(await slides())); + await sleep(1500); + check('the clip after, which the server has no frame for, gets no stand-in still', (await posterOf('last')) === 'none', JSON.stringify(await slides())); + check('…and waits out of the carousel', await page.evaluate(() => [...document.querySelectorAll('.mp-group')].find((g) => /last\.webm$/.test(g.querySelector('.mp-slide').getAttribute('data-k'))).classList.contains('mp-wait'))); check('…no thumbnail route was tried for any of them', !asked.some((x) => /thumb\.jpg/.test(x)), asked.join(' ')); await page.keyboard.press('ArrowDown'); check('on to the next: it plays', await waitFor(() => { const s = document.querySelector('.mp-slide.mp-cur'); return !!s && s.getAttribute('data-k').endsWith('/tall.webm') && !s.querySelector('video').paused; }, null, 6000), JSON.stringify(await slides())); check('…the server\'s frame still under it', (await posterOf('tall')) === 'server', String(await posterOf('tall'))); check('the one left behind holds its first frame for the swipe back', await waitPoster('wide', 'server'), JSON.stringify(await slides())); - check('the next, now the clip ahead, has its frame drawn here in place of the still', await waitPoster('last', 'drawn', 12000), JSON.stringify(await slides())); + check('the next, now the clip ahead, has its frame drawn here ', await waitPoster('last', 'drawn', 12000), JSON.stringify(await slides())); check('…the server asked only once more for it, not again and again', asked.filter((x) => x === 'first:last').length === 1, asked.join(' ')); await page.keyboard.press('ArrowUp'); check('back on the first: it plays again, its frame under it', await waitFor(() => { const s = document.querySelector('.mp-slide.mp-cur'); return !!s && s.getAttribute('data-k').endsWith('/wide.webm') && !s.querySelector('video').paused && /first\.jpg/.test(s.querySelector('video').getAttribute('poster') || ''); }, null, 6000), JSON.stringify(await slides())); |
