aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/features/messages.js57
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); },