aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-19 22:13:55 -0700
committerAdam Soltys <[email protected]>2026-10-19 22:13:55 -0700
commit1366ef7e517cbede61ac81b133bda8ab9ff74f23 (patch)
treedb7278e2198ccee5a0782abef4db3dbad8d17098 /tools
parenta0829612b8751e85f28d066fbaa0c2308a85fbda (diff)
downloadcoinosv3-1366ef7e517cbede61ac81b133bda8ab9ff74f23.tar.gz
coinosv3-1366ef7e517cbede61ac81b133bda8ab9ff74f23.zip
Videos start the moment a swipe lands, and streams say who's watching.
- The pager's next clip gets a warm player: once its opening is in the cache it reads to its first frame (preload metadata: Chrome stops ~2-4 MB in at real link speeds) and waits paused, so landing on it is only play and sound. coinosPagerStarts() in the console times every landing (swipe to moving picture, warm or cold, host, size). - A stream's "N watching" names who on hover, and a tap shows their faces: NIP-53 presence (kind 10312, as Shosho and zap.stream say it), fresh for half an hour. coinos says it too while a stream is open (every 5 min, with a key held here only, never for your own stream) and takes it back on leaving. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
Diffstat (limited to 'tools')
-rw-r--r--tools/live-stream-test.js11
-rw-r--r--tools/pager-eager-test.js14
2 files changed, 19 insertions, 6 deletions
diff --git a/tools/live-stream-test.js b/tools/live-stream-test.js
index 46fb7e9..ef76043 100644
--- a/tools/live-stream-test.js
+++ b/tools/live-stream-test.js
@@ -55,6 +55,9 @@ const pastPage = finalizeEvent({ kind: 30311, created_at: now - 50000, content:
['d', 'stream-p'], ['title', 'Zelda on Rumble'], ['status', 'ended'], ['streaming', STREAM], ['recording', 'https://rumble.com/v7gt2va-zelda.html'],
['ends', String(now - 50000)], ['p', HOST.pk, '', 'Host'],
] }, SERVICE.sk);
+// who's watching (NIP-53 presence): the fan, just now; someone two hours ago is gone
+const here = finalizeEvent({ kind: 10312, created_at: now - 60, content: '', tags: [['a', A, '', 'root']] }, FAN.sk);
+const longGone = finalizeEvent({ kind: 10312, created_at: now - 7200, content: '', tags: [['a', A, '', 'root']] }, STALE.sk);
const reaction = finalizeEvent({kind:7,created_at:now-10,content:'👍',tags:[['e',chat.id],['p',FAN.pk],['k','1311']]},HOST.sk);
const messageZap = finalizeEvent({kind:9737,created_at:now-9,content:'nice comment',tags:[['e',chat.id],['p',FAN.pk],['P',HOST.pk],['amount','5'],['network','regtest']]},HOST.sk);
// and months of older ones, past the first ask's month: the grid pages back through them
@@ -62,7 +65,7 @@ const OLD = Array.from({ length: 130 }, (_, i) => finalizeEvent({ kind: 30311, c
['d', 'old-' + i], ['title', 'Old stream ' + i], ['status', 'ended'], ['streaming', STREAM], ['recording', STREAM],
['ends', String(now - 31 * 86400 - i * 3600)], ['p', HOST.pk, '', 'Host'],
] }, SERVICE.sk));
-const FIXTURES = [live, stale, past, pastPage, chat, zap, reaction, messageZap, ...OLD];
+const FIXTURES = [live, stale, past, pastPage, chat, zap, reaction, messageZap, here, longGone, ...OLD];
const ended = finalizeEvent({ ...live, id: undefined, sig: undefined, created_at: now - 5, tags: live.tags.map((x) => (x[0] === 'status' ? ['status', 'ended'] : x)) }, SERVICE.sk);
// the site as built (the split build: hls.js arrives as its own chunk)
@@ -206,6 +209,11 @@ try {
// a stream that's on plays through hls.js (kept near the live edge), not Chrome's own player
check('...through hls.js, kept near the live edge', await page.evaluate(() => document.querySelector('video.live-video').currentSrc.startsWith('blob:')));
check('the chat shows what was said', await waitText('hello from the chat', 8000));
+ const who = () => page.evaluate(() => { const x = document.querySelector('.live-page .live-watching'); return x && { text: x.textContent, title: x.getAttribute('title') || '' }; });
+ check('"watching" names who, on hover: the fan and this viewer, not someone long gone', await waitFor(() => (document.querySelector('.live-page .live-watching')?.getAttribute('title') || '').split(', ').length === 2, 8000), JSON.stringify(await who()));
+ check('...this viewer said they\'re here (a 10312 for the stream)', await page.evaluate((a) => window.__published.some((e) => e.kind === 10312 && e.tags.some((t) => t[0] === 'a' && t[1] === a)), A));
+ await page.evaluate(() => document.querySelector('.live-page .live-watching').click());
+ check('...a tap shows their faces', await waitFor(() => document.querySelectorAll('.live-page .live-who .live-who-p').length === 2, 3000));
check('...and the zap, its amount and words', await waitText('zapped 21 sats', 5000) && await waitText('great stream', 1000));
const rowSel = '.live-message[data-key="' + chat.id + '"]';
check('comments use the group-chat bubble and shared composer', await page.$(rowSel + ' .chat-bubble') && await page.$('.live-page coinos-text#msg-draft'));
@@ -253,6 +261,7 @@ try {
console.log('\n[leaving]');
await page.goBack();
check('Back leaves the stream\'s page', await waitFor(() => !document.querySelector('.live-page'), 5000));
+ check('...and takes back this viewer\'s presence (an empty 10312)', await waitFor(() => window.__published.some((e) => e.kind === 10312 && !e.tags.some((t) => t[0] === 'a')), 3000));
check('...and nothing plays on behind it', await page.evaluate(() => [...document.querySelectorAll('video')].every((v) => v.paused || !v.isConnected)));
await page.evaluate(() => document.querySelector('.media-mode[data-mode="all"]').click());
await waitFor(() => !!document.querySelector('.notes-feed .live-card'), 5000);
diff --git a/tools/pager-eager-test.js b/tools/pager-eager-test.js
index 15a5076..01c8dd6 100644
--- a/tools/pager-eager-test.js
+++ b/tools/pager-eager-test.js
@@ -88,7 +88,7 @@ async function run(saveData, slow = false) {
const feedRow = (w) => page.evaluate((x) => { const r = [...document.querySelectorAll('.notes-feed [data-key]')].find((n) => n.textContent.includes(x)); r.querySelector('.note-video-box').scrollIntoView({ block: 'center' }); const v = r.querySelector('video.note-video').getBoundingClientRect(); return { cover: !!r.querySelector('.vid-heavy'), x: v.x + v.width * 0.2, y: v.y + v.height * 0.3 }; }, w);
try {
await page.setViewport({ width: 390, height: 844 });
- pace = slow ? 375000 : 0;
+ pace = slow ? 375000 : 12_500_000; // 3 Mbps, or 100 (a loopback's speed is no link: Chrome reads a warm player's whole file before it can stop)
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);
@@ -123,14 +123,17 @@ async function run(saveData, slow = false) {
const c1 = await slide('c1'), c2 = await slide('c2'), c3 = await slide('c3');
let head1 = 0;
if (!saveData) {
- check('the next clips wait on their pictures, no cover, no player yet', c1 && !c1.src && !c1.cover && c2 && !c2.src, JSON.stringify([c1, c2]));
+ check('the next clip has a warm player: its file, read to its first frame and paused', c1 && c1.src && c1.preload === 'metadata' && !c1.playing && !c1.cover && await page.evaluate(() => { const s = [...document.querySelectorAll('.mp-slide')].find((x) => x.getAttribute('data-k').endsWith('/c1.webm')); return s.querySelector('video.mp-media').readyState >= 2; }), JSON.stringify(c1));
+ check('…the one after waits on its picture, no player yet', c2 && !c2.src && !c2.cover, JSON.stringify(c2));
const hs = heads('c1'), h2 = heads('c2');
head1 = hs.length ? hs[0].end + 1 : 0;
const head2 = h2.length ? h2[0].end + 1 : 0;
// before any measure of the link: two seconds' worth (the second clip ahead, half)
check('the next clip\'s opening is fetched ahead: two seconds of it', hs.length === 1 && hs[0].start === 0 && near(head1, 2 * RATE), `${kb(head1)} (two seconds: ${kb(2 * RATE)})`);
check('…and half that of the one after', h2.length === 1 && near(head2, RATE), `${kb(head2)}`);
- check('nothing more than that ahead of time', sent('c1') <= head1 && sent('c2') <= head2 && sent('c3') === 0, `c1 ${kb(sent('c1'))}, c2 ${kb(sent('c2'))}, c3 ${kb(sent('c3'))} of ${kb(SIZE)} each`);
+ // the warm player reads on from the cached opening to its first frame and a little past, then stops
+ check('ahead of time: the next clip\'s opening and a few MB more for its warm player, the one after its opening only', sent('c1') <= head1 + 4 * 1024 * 1024 && sent('c2') <= head2 && sent('c3') === 0, `c1 ${kb(sent('c1'))}, c2 ${kb(sent('c2'))}, c3 ${kb(sent('c3'))} of ${kb(SIZE)} each`);
+ check('…the warm player took the opening from the cache, not the host', !log.some((e) => e.clip === 'c1' && e.dest === 'video' && e.start === 0), log.filter((e) => e.clip === 'c1').map((e) => e.dest + ':' + e.start).join(' '));
} else {
check('nothing loads ahead of a clip over budget', c1 && !c1.src && sent('c1') === 0, JSON.stringify(c1) + ' ' + kb(sent('c1')));
}
@@ -139,8 +142,9 @@ async function run(saveData, slow = false) {
const t0 = Date.now();
if (!saveData) {
check('swiped to, the next clip plays at once, no cover', await waitFor(() => { const s = [...document.querySelectorAll('.mp-slide')].find((x) => x.getAttribute('data-k').endsWith('/c1.webm')); const v = s.querySelector('video.mp-media'); return !s.querySelector('.vid-heavy') && !v.paused && v.currentTime > 0; }, null, 5000) && (Date.now() - t0) < (slow ? 3000 : 1500), (Date.now() - t0) + ' ms');
- const after = log.slice(before).filter((e) => e.clip === 'c1' && e.dest === 'video');
- check('…its player starting where the head start left off (the opening came from the cache)', after.length >= 1 && after[0].start === head1, 'the player asked for ' + (after.map((e) => e.start + '-').join(', ') || 'nothing') + ', head start ' + head1);
+ await waitFor(() => coinosPagerStarts().starts.some((x) => /c1\.webm$/.test(x.url)), null, 3000);
+ const st = await page.evaluate(() => coinosPagerStarts().starts.find((x) => /c1\.webm$/.test(x.url)));
+ check('…from its warm player, the clock says', st && st.warm && st.ms < 1000, JSON.stringify(st));
await sleep(1500);
const h3 = heads('c3'), head3 = h3.length ? h3[0].end + 1 : 0;
check('…the next one gets its head start', h3.length === 1);