aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/messages.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-13 05:10:27 +0000
committerAdam Soltys <[email protected]>2026-10-13 05:10:27 +0000
commit6fdca50f17b1e814bbf6ba1fc1ffcb18001c54fd (patch)
tree6598fdb3a3f870cb3eb76e23eb630b07797d5b5e /src/features/messages.js
parent028dd56084b5a07bbff6d37e5f760ad344d58060 (diff)
downloadcoinosv3-6fdca50f17b1e814bbf6ba1fc1ffcb18001c54fd.tar.gz
coinosv3-6fdca50f17b1e814bbf6ba1fc1ffcb18001c54fd.zip
Media pager: every clip plays when it comes up, and the next two get a half-megabyte head start. Adam: flipping through clips should just play them, large ones too, and load a bit of what's next — not the whole of it. A clip over the size budget (or one whose size the host won't say: most, behind CORS) used to wait in the pager under a tap-to-play cover, and even a small one waited on its size probe (up to 5 s of HEAD) before playing. Now, on an ordinary connection, the pager plays the clip on screen at once, streaming as it goes, and fetches the opening 512 KB of the next two into the browser's HTTP cache with a range request (a host that ignores the range has the rest cancelled); the player, given the file when it's swiped to, takes its opening from the cache and streams on from there (the test's server sees its first request start at byte 524288; playing ~300 ms after the swipe). The next clips get no player of their own beforehand: one given its file, even with preload=metadata, took the whole 34 MB on a fast link. With Data Saver on or a slow link (2g/3g), the pager is careful as before: a clip over budget waits for a tap and nothing loads ahead. tools/pager-eager-test.js (35 MB clips: covered in the feed, played in the pager, 512 KB each ahead and nothing more, the cache reused; Data Saver: nothing ahead, the cover); feed-media-views-test moves its tap-to-play checks onto a slow link and checks the 50 MB clip plays on an ordinary one (its index-last probe check fails on HEAD before this too).
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01KPw21NEpWeTGu6swVASmBt
Diffstat (limited to 'src/features/messages.js')
-rw-r--r--src/features/messages.js45
1 files changed, 41 insertions, 4 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 9cba810..61cdc18 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -6467,6 +6467,8 @@ export function messagesFeature(ctx) {
} catch {}
return 25e6;
}
+ // Data Saver on, or a slow link: the connection to be careful with
+ const frugalData = () => { try { const c = navigator.connection; return !!(c && (c.saveData || /(^|-)2g$|^3g$/.test(c.effectiveType || ''))); } catch { return false; } };
const tappedClips = new Set(); // asked for by hand this session: plays, whatever it costs
function videoHeavy(url) {
if (tappedClips.has(url) || largeVideosOk()) return false;
@@ -12120,18 +12122,32 @@ export function messagesFeature(ctx) {
const m = slide.querySelector('video.mp-media');
if (m && m.getAttribute('src')) { try { m.pause(); m.removeAttribute('src'); m.load(); } catch {} }
}
- // Only the clip on screen loads in full. The next one gets its opening
- // (preload metadata: the index and first frames) if it's within budget;
- // every other clip holds nothing. Sizes are asked for a few slides ahead,
- // so the verdict is in before the reader gets there.
+ // Flipping through clips is asking to watch them: whatever they cost, the
+ // one on screen plays at once (streaming the rest as it goes), and the
+ // next two get a head start: the opening half megabyte of each fetched
+ // ahead into the browser's cache, where the player finds it — but no
+ // more, so a big file isn't pulled down for a clip that may be swiped
+ // past. (Not a player of their own yet: one given its file, even told to
+ // preload only the metadata, takes the whole of it on a fast link.) Unless the connection
+ // is one to be careful with (Data Saver, a slow link). Then only the clip on screen
+ // loads in full, the next gets its opening if it's within budget, and a
+ // clip over budget waits for a tap; sizes are asked for a few slides
+ // ahead, so the verdict is in before the reader gets there. Every other
+ // clip holds nothing.
function pagerNeighbours(slide) {
if (!pager || pager.kind !== 'videos') return;
const slides = [...pager.scroller.children];
const i = slides.indexOf(slide);
+ const eager = !frugalData();
slides.forEach((s, j) => {
if (Math.abs(j - i) <= 2) slidePoster(s);
if (j === i) return;
const v = s.querySelector('video.mp-media');
+ if (eager && (j === i + 1 || j === i + 2)) {
+ pagerRelease(s);
+ headStart(playSrc(v.getAttribute('data-src')));
+ return;
+ }
if (j === i + 1) {
const url = v.getAttribute('data-src');
probeVideo(url).then(() => {
@@ -12143,11 +12159,32 @@ export function messagesFeature(ctx) {
});
for (const s of slides.slice(i + 2, i + 4)) probeVideo(s.querySelector('video.mp-media').getAttribute('data-src'));
}
+ // 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.
+ // A host that ignores the range sends the whole file: the opening is
+ // read, the rest cancelled. A host that doesn't allow the request (no
+ // CORS) gets none — its player still has its metadata and first frame.
+ const HEAD_START = 512 * 1024;
+ const headStarted = new Set();
+ function headStart(url) {
+ if (!url || url.startsWith('blob:') || headStarted.has(url) || frugalData()) return;
+ headStarted.add(url);
+ if (headStarted.size > 300) headStarted.delete(headStarted.values().next().value);
+ fetch(url, { headers: { Range: 'bytes=0-' + (HEAD_START - 1) } }).then(async (r) => {
+ const reader = r.ok && r.body ? r.body.getReader() : null;
+ if (!reader) return;
+ let got = 0;
+ while (got < HEAD_START) { const { done, value } = await reader.read(); if (done) return; got += value.length; }
+ reader.cancel().catch(() => {});
+ }).catch(() => {});
+ }
function playSlide(slide) {
const v = slide.querySelector('video.mp-media');
if (!v) return;
pagerNeighbours(slide);
const url = v.getAttribute('data-src');
+ if (!frugalData()) { startSlide(slide); return; } // no waiting on its size: it plays
probeVideo(url).then(() => {
if (!pager || pager.cur !== slide) return;
if (videoHeavy(url)) {