diff options
| author | Adam Soltys <[email protected]> | 2026-10-16 23:03:55 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-16 23:03:55 -0700 |
| commit | d718fcebcf40672f856805d69cbb2a84296919c6 (patch) | |
| tree | 526eed3cf0e72a13c3db86294b4c5d95e2a4e7eb /src | |
| parent | 8deede7162ae195934cdee6074d1443c5af3c803 (diff) | |
| download | coinosv3-d718fcebcf40672f856805d69cbb2a84296919c6.tar.gz coinosv3-d718fcebcf40672f856805d69cbb2a84296919c6.zip | |
The video pager's slides near the reader hold their clip's first frame — the server's, full size — so no slide comes up black.
Some slides were black until the player had its frame, then the picture appeared all at once (Adam, 2026-10-16: "i'm still seeing mostly black between some videos and then they suddenly appear when it starts playing"). The frame drawn in the browser (ebaec42) needed the next clip's cached opening, a file led by its index and a host that allows the read — and only the next clip had one at all: not the one before (its player let go on leaving), not a flight's landing, not the clip two along.
vmedia gains /vmedia/first.jpg?u=<clip>: frame 0 itself, longest side 1280 (the player's size), any host, any layout, kept and cached at the CDN like the stills. The pager now gives every slide within three of the reader, the clips ahead and the slide landed on (not the one the pager opened on mid-clip) their clip's first frame: the server's, fetched and decoded aside and put on the slide when ready, so the flick glides onto the very picture the player shows first; a flight's landing slide fetches its frame as the flight sets off. Where the server has none the next clip's frame is drawn here as before, and a clip beyond keeps the post's still — soft, but not black — replaced by a first frame when one comes; never a soft still under the clip on screen. Data Saver is unchanged (posters at build).
pager-first-frame-test (https clips, vmedia answered by interception): the next clip holds the server's frame, asked once; a clip the server has none for keeps the still, then has its frame drawn here once it is the clip ahead; the clip left behind holds its frame for the swipe back and plays again on it. pager-no-poster, pager-eager, pager-seek-hide, pager-swipe-fly, video-tile-poster, video-fallback, pager-controls still pass. vmedia deployed to cs first (first frames of live clips: 47–144 KB, 0.4–1.7 s to make).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/features/messages.js | 57 |
1 files changed, 53 insertions, 4 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 1371b04..ce578e4 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -7781,6 +7781,9 @@ export function messagesFeature(ctx) { const vmediaOk = (url) => /^https:\/\//i.test(url || ''); // v=4: stills past a dark opening frame (a new URL, so the CDN's old black ones aren't served) const serverThumb = (url) => (vmediaOk(url) ? VMEDIA + 'thumb.jpg?v=4&u=' + encodeURIComponent(url) : undefined); + // the clip's very first frame at full size (the pager's slides ahead hold it) + const serverFirst = (url) => (vmediaOk(url) ? VMEDIA + 'first.jpg?u=' + encodeURIComponent(url) : undefined); + const serverFirstFailed = new Set(); // clips the server had no first frame for (this session) const h264Of = (url) => VMEDIA + 'h264.mp4?u=' + encodeURIComponent(url); const noPicture = new Set(), h264Failed = new Set(); const serverThumbFailed = new Set(); // clips vmedia had no picture for (this session) @@ -12819,14 +12822,13 @@ export function messagesFeature(ctx) { slides.forEach((s, j) => { const k = ahead.indexOf(s); if (Math.abs(j - i) <= 2 || k >= 0) slidePoster(s); + // the slides around the reader (and the one landed on) hold their clip's first frame + if (eager && (Math.abs(j - i) <= 3 || k >= 0) && !(j === i && pager.at && slide.getAttribute('data-k') === pager.start)) slideFrame(s, k); if (j === i) return; const v = s.querySelector('video.mp-media'); if (eager && k >= 0) { pagerRelease(s); - const src = playSrc(v.getAttribute('data-src')); - headStart(src, k === 0 ? 1 : 0.5); - // the next clip holds its first frame: the flick glides onto it - if (k === 0 && !v.getAttribute('poster')) pagerPoster(src).then((p) => { if (p && pager && s.isConnected && !v.getAttribute('poster')) v.setAttribute('poster', p); }); + headStart(playSrc(v.getAttribute('data-src')), k === 0 ? 1 : 0.5); return; } if (k === 0) { @@ -12840,6 +12842,51 @@ export function messagesFeature(ctx) { }); for (const s of slides.slice(i + 2, i + 4)) probeVideo(s.querySelector('video.mp-media').getAttribute('data-src')); } + // A slide near the reader holds its clip's first frame as its poster, so + // a flick glides onto the very picture the player shows first and the + // player takes over with no visible change — and a slide left behind + // (its player let go) keeps it for the swipe back. Some slides came up + // black until the player had its frame: the frame drawn here needed the + // next clip's cached opening, a file led by its index and a host that + // allows the read, and only the next clip had one at all — not the one + // before, not a flight's landing (Adam, 2026-10-16: "i'm still seeing + // mostly black between some videos and then they suddenly appear when + // 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. + 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; + 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; + const im = new Image(); + im.decoding = 'async'; + const ok = () => { v._frameBusy = false; put(first, 'first'); }; + im.onload = () => (im.decode ? im.decode().then(ok, ok) : ok()); + im.onerror = () => { v._frameBusy = false; serverFirstFailed.add(url); slideFrame(s, k); }; + im.src = first; + return; + } + 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; + } + 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 // is answered from there as far as it goes, and streams on from the host. @@ -13074,6 +13121,8 @@ export function messagesFeature(ctx) { const dest = pagerAt(pager.scroller, to, true); const screens = Math.abs(dest - pager.scroller.scrollTop) / Math.max(1, pager.scroller.clientHeight); pager.flying = true; + const land = pager.kind === 'videos' && !frugalData() && rowSlide(to); // the landing's first frame, fetched on the way + if (land) slideFrame(land, -1); pagerGlide(pager.scroller, 'y', dest, vel, { ms: screens > 1.05 ? Math.min(most, 100 + pace * screens) : 0, onDone: () => { if (!pager) return; pager.flying = false; const s = pagerHere(); if (s && s !== pager.cur) pagerArrive(s); }, |
