1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
// Opening a clip from the Videos grid: the clip hosts are preconnected,
// and the player starts loading when the finger comes down on a tile —
// the pager adopts that very element, so it's moving sooner after the tap.
// A press that isn't followed by a tap (a scroll) lets its player go.
// Run: bun tools/grid-early-player-test.js
//
// (adapted from pager-eager-test:)
// Flipping through clips in the pager is asking to watch them: a clip plays
// at once whatever it costs — even one over the size budget, which in the
// feed waits under a tap-to-play cover — and the next two get a head start:
// their opening fetched into the cache, no more of a big file than that, and
// the player takes it from there. How much: a couple of seconds' worth of
// the clip on a link well ahead of its bitrate (the second clip ahead, half
// that); on a link slower than the clip, more — to the 4 MB cap. With Data
// Saver on, the pager is careful again: such a clip waits for a tap and
// nothing loads ahead.
import puppeteer from './puppeteer.js';
import { cacheKeyFor } from '../src/wallet.js';
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import { buildHtml } from '../build.js';
import { existsSync, statSync } from 'node:fs';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let ok = true;
const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; };
// small clips with sound (a tone), ~2 MB each: under the 25 MB a warm player is primed up to
const BIG = '/tmp/coinos-prime-small.webm';
if (!existsSync(BIG)) Bun.spawnSync(['ffmpeg', '-y', '-loglevel', 'error', '-f', 'lavfi', '-i', 'testsrc=s=180x320:r=15', '-f', 'lavfi', '-i', 'sine=f=440', '-t', '50', '-c:v', 'libvpx', '-b:v', '300k', '-c:a', 'libvorbis', BIG]);
const SIZE = statSync(BIG).size;
const FILE = new Uint8Array(await Bun.file(BIG).arrayBuffer());
const NAMES = ['c0', 'c1', 'c2', 'c3', 'c4'];
const SK = generateSecretKey(), AUTHOR = getPublicKey(SK);
const now = Math.floor(Date.now() / 1000);
const post = (content, ago) => finalizeEvent({ kind: 1, created_at: now - ago, tags: [], content }, SK);
const html = await buildHtml({ minify: true, pwa: false });
// every request for a clip's bytes: who asked (the player, or a fetch), the
// range, and the bytes actually sent (pulled by the reader, in 64 KB pieces)
let log = [];
let pace = 0; // bytes a second the server sends a clip at (0: as fast as it can) — a slow link
const server = Bun.serve({ port: 0, idleTimeout: 60, fetch: (req) => {
const path = new URL(req.url).pathname;
const m = /^\/(c\d)\.webm$/.exec(path);
if (m) {
const headers = { 'access-control-allow-origin': '*', 'access-control-expose-headers': 'content-range, content-length', 'content-type': 'video/webm', 'accept-ranges': 'bytes', etag: '"' + m[1] + '-v1"', 'last-modified': 'Mon, 05 Oct 2026 00:00:00 GMT', 'cache-control': 'public, max-age=3600' };
if (req.method === 'HEAD') return new Response(null, { headers: { ...headers, 'content-length': String(SIZE) } });
const rg = /bytes=(\d+)-(\d*)/.exec(req.headers.get('range') || '');
const start = rg ? +rg[1] : 0, end = rg && rg[2] ? Math.min(+rg[2], SIZE - 1) : SIZE - 1;
const entry = { clip: m[1], dest: req.headers.get('sec-fetch-dest') || '', start, end, sent: 0 };
log.push(entry);
let at = start;
const t0 = Date.now();
const body = new ReadableStream({ async pull(ctl) {
if (at > end) { ctl.close(); return; }
if (pace) { const due = t0 + ((at - start) / pace) * 1000; if (due > Date.now()) await new Promise((r) => setTimeout(r, due - Date.now())); }
const n = Math.min(65536, end - at + 1);
ctl.enqueue(FILE.subarray(at, at + n)); at += n; entry.sent += n;
} }, { highWaterMark: 0 });
return new Response(body, { status: rg ? 206 : 200, headers: { ...headers, 'content-length': String(end - start + 1), ...(rg ? { 'content-range': `bytes ${start}-${end}/${SIZE}` } : {}) } });
}
if (path.startsWith('/punks') || path === '/verify-worker.js') return new Response(Bun.file('dist' + path));
return new Response(html, { headers: { 'content-type': 'text/html' } });
} });
const U = (n) => server.url.origin.replace('localhost', '127.0.0.1') + '/' + n + '.webm'; // another origin than the page's: one to preconnect to
const seeded = [
// each says its size and length (as Primal's and others' imeta do): a bitrate of SIZE / 50 bytes a second
...NAMES.map((n, i) => finalizeEvent({ kind: 1, created_at: now - 100 - i * 100, tags: [['imeta', 'url ' + U(n), 'm video/webm', 'size ' + SIZE, 'duration 50', 'dim 180x320']], content: 'clip ' + n + ' here ' + U(n) }, SK)),
...Array.from({ length: 6 }, (_, i) => post('filler ' + i + ' ' + 'lorem ipsum dolor sit amet. '.repeat(5), 900 + i * 60)),
];
const sent = (clip, dest) => log.filter((e) => e.clip === clip && (!dest || e.dest === dest)).reduce((a, e) => a + e.sent, 0);
const kb = (n) => Math.round(n / 1024) + ' KB';
const RATE = SIZE / 50, HEAD_MAX = 4 * 1024 * 1024;
const heads = (clip) => log.filter((e) => e.clip === clip && e.dest === 'empty');
const near = (a, b) => Math.abs(a - b) <= b * 0.02 + 1;
const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox', '--autoplay-policy=no-user-gesture-required'] });
async function run() {
log = [];
const ctx = await browser.createBrowserContext();
const page = await ctx.newPage();
await page.evaluateOnNewDocument(() => {
const Real = window.WebSocket;
window.WebSocket = function (...a) { const ws = new Real(...a); const send = ws.send.bind(ws); ws.send = (d) => { try { const m = JSON.parse(d); if (m[0] === 'REQ') { setTimeout(() => ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['EOSE', m[1]]) })), 0); return; } if (m[0] === 'EVENT') return; } catch {} return send(d); }; return ws; };
window.WebSocket.prototype = Real.prototype; Object.assign(window.WebSocket, Real);
});
const errs = [];
page.on('pageerror', (e) => errs.push(String(e)));
const click = (t) => page.evaluate((x) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(x)); if (e) { e.click(); return true; } return false; }, t);
const waitText = async (x, ms = 25000) => { for (let i = 0; i < ms / 250; i++) { if ((await page.evaluate(() => document.body.innerText)).toLowerCase().includes(x)) return true; await sleep(250); } return false; };
const waitFor = async (fn, arg, ms = 10000) => { for (let i = 0; i < ms / 100; i++) { if (await page.evaluate(fn, arg)) return true; await sleep(100); } return false; };
const vid = (n) => page.evaluate((n) => { const s = [...document.querySelectorAll('.mp-slide')].find((x) => x.getAttribute('data-k').endsWith('/' + n + '.webm')); const v = s && s.querySelector('video.mp-media'); return v ? { src: !!v.getAttribute('src'), paused: v.paused, t: v.currentTime, volume: v.volume, muted: v.muted } : null; }, n);
try {
await page.setViewport({ width: 390, height: 844 });
pace = 12_500_000;
await page.goto(server.url.href, { waitUntil: 'domcontentloaded' });
await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest'));
await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400);
await click('create a new wallet'); await sleep(600);
await page.waitForSelector('.words .w .t');
const mn = (await page.$$eval('.words .w .t', (l) => l.map((e) => e.textContent.trim()))).join(' ');
await click('skip verification');
await waitText('receive', 20000);
const base = cacheKeyFor(mn + '\n');
await page.evaluate(([k, pk, ns]) => {
localStorage.setItem(k + ':follows', JSON.stringify({ tags: [['p', pk]], c: '', at: Math.floor(Date.now() / 1000) }));
localStorage.setItem(k + ':feedNotes', JSON.stringify(ns));
localStorage.setItem(k + ':profiles', JSON.stringify({ [pk]: { name: 'Prime Poster', t: Date.now() } }));
localStorage.setItem('btc-wallet-feed:' + k, 'following');
}, [base, AUTHOR, seeded]);
await page.reload({ waitUntil: 'domcontentloaded' });
await waitText('receive', 20000);
await page.evaluate(() => document.querySelector('.app-nav-button[aria-label="Feed"]').click());
await page.waitForSelector('.media-mode[data-mode="videos"]', { timeout: 20000 });
await page.evaluate(() => document.querySelector('.media-mode[data-mode="videos"]').click());
check('the Videos grid shows the clips', await waitFor(() => document.querySelectorAll('.media-tile.video').length >= 3, null, 15000));
check('…their host is connected to ahead', await page.evaluate((o) => [...document.querySelectorAll('link[rel=preconnect]')].some((l) => l.href.startsWith(o)), server.url.origin.replace('localhost', '127.0.0.1')));
// a press with no tap after it (the start of a scroll): its player is let go
await page.evaluate(() => document.querySelectorAll('.media-tile.video')[1].dispatchEvent(new PointerEvent('pointerdown', { bubbles: true })));
await sleep(600);
const pressed = await page.evaluate(() => { const v = document.querySelector('body > video[data-early]'); return v && { src: v.getAttribute('src'), rs: v.readyState }; });
check('a press starts the clip loading before any tap: its player has its picture', pressed && /c1\.webm$/.test(pressed.src) && pressed.rs >= 2, JSON.stringify(pressed));
await sleep(2500);
check('…and let go when no tap follows', await page.evaluate(() => !document.querySelector('body > video[data-early]')));
// a press, then the tap: the pager plays the very player the press made
await page.evaluate(() => { const t = document.querySelectorAll('.media-tile.video')[0]; t.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true })); setTimeout(() => t.click(), 120); });
check('the pager opens', await waitFor(() => !!document.querySelector('.media-pager.videos'), null, 4000));
check('…on the player the press started, adopted', await waitFor(() => { const v = document.querySelector('.mp-cur video.mp-media'); return v && v.dataset.early === '1' && !v.paused && v.currentTime > 0; }, null, 8000), JSON.stringify(await page.evaluate(() => { const v = document.querySelector('.mp-cur video.mp-media'); return v && { early: v.dataset.early, paused: v.paused, t: v.currentTime }; })));
check('…and only one player was ever made for it', await page.evaluate(() => document.querySelectorAll('.media-pager video.mp-media[data-early]').length) === 1);
check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | '));
} finally { await ctx.close(); }
}
try { await run(); } finally { await browser.close(); server.stop(true); }
console.log(ok ? '\n✅ a clip opened from the grid starts loading at the press' : '\n❌ failed');
process.exit(ok ? 0 : 1);
|