diff options
| author | Adam Soltys <[email protected]> | 2026-10-18 06:05:25 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-18 06:05:25 -0700 |
| commit | 4807f138feba04a60292886a196d614e2254810b (patch) | |
| tree | d9e7ee76fc61381ea9c5916050ed0d9f1553da73 /tools | |
| parent | b9297719d294779a2ea14c77a67975308bace65c (diff) | |
| download | coinosv3-4807f138feba04a60292886a196d614e2254810b.tar.gz coinosv3-4807f138feba04a60292886a196d614e2254810b.zip | |
Clips too big to play as they are (past 1080p, over the data budget, unmeasurable) stream as HLS from vmedia and play by themselves instead of waiting behind a tap-to-play cover; with Data Saver or a slow link the cover stays.
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/video-fallback-test.js | 6 | ||||
| -rw-r--r-- | tools/video-hls-play-test.js | 26 |
2 files changed, 24 insertions, 8 deletions
diff --git a/tools/video-fallback-test.js b/tools/video-fallback-test.js index 85250c0..93726e1 100644 --- a/tools/video-fallback-test.js +++ b/tools/video-fallback-test.js @@ -37,6 +37,12 @@ const server = Bun.serve({ port: 0, fetch: (req) => { } }); const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox', '--autoplay-policy=no-user-gesture-required'] }); const page = await browser.newPage(); +// a browser without HLS of its own (Firefox): a clip too big to play as it +// is waits for a tap here, rather than streaming (video-hls-play-test) +await page.evaluateOnNewDocument(() => { + const can = HTMLMediaElement.prototype.canPlayType; + HTMLMediaElement.prototype.canPlayType = function (t) { return /mpegurl/i.test(t) ? '' : can.call(this, t); }; +}); await page.setRequestInterception(true); const asked = []; // [kind, clip url] page.on('request', (r) => { diff --git a/tools/video-hls-play-test.js b/tools/video-hls-play-test.js index 5bf6809..f1aea1c 100644 --- a/tools/video-hls-play-test.js +++ b/tools/video-hls-play-test.js @@ -1,5 +1,6 @@ // A long clip in the feed plays as HLS from vmedia (a rendition the -// connection carries, in pieces made as watched), a short one as it is, +// connection carries, in pieces made as watched), and so does one too big +// to play as it is (past 1080p: it used to wait for a tap); a short one as it is, // and a post linking an .m3u8 stream plays that. vmedia runs here for real, // on a local copy of the clip. // Run: bun tools/video-hls-play-test.js @@ -22,9 +23,13 @@ const SHORT = ff(DIR + '/coinos-hls-short.mp4', ['-c:v', 'libx264']); Bun.spawnSync(['ffmpeg', '-y', '-loglevel', 'error', '-f', 'lavfi', '-i', 'testsrc=size=320x240:rate=10', '-f', 'lavfi', '-i', 'sine=frequency=440', '-t', '100', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-movflags', '+faststart', DIR + '/coinos-hls-long.mp4']); const LONG = readFileSync(DIR + '/coinos-hls-long.mp4'); +// short but past 1080p: it used to wait for a tap with its cost on it +Bun.spawnSync(['ffmpeg', '-y', '-loglevel', 'error', '-f', 'lavfi', '-i', 'testsrc=size=1440x1440:rate=10', '-f', 'lavfi', '-i', 'sine=frequency=440', + '-t', '6', '-c:v', 'libx264', '-preset', 'ultrafast', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-movflags', '+faststart', DIR + '/coinos-hls-big.mp4']); +const BIG = readFileSync(DIR + '/coinos-hls-big.mp4'); // vmedia itself, reading the clip from here -const files = Bun.serve({ port: 0, hostname: '127.0.0.1', fetch: () => new Response(LONG) }); -const LOCAL_SRC = `http://127.0.0.1:${files.port}/long.mp4`; +const files = Bun.serve({ port: 0, hostname: '127.0.0.1', fetch: (req) => new Response(new URL(req.url).pathname === '/big.mp4' ? BIG : LONG) }); +const local = (src) => src.replace('https://media.test/', `http://127.0.0.1:${files.port}/`); process.env.VMEDIA_LOCAL = '1'; process.env.DATA = DIR + '/coinos-hls-vmedia'; const vm = await import('../vmedia/server.js'); @@ -34,8 +39,8 @@ const JPEG = Bun.spawnSync(['ffmpeg', '-loglevel', 'error', '-f', 'lavfi', '-i', 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 CLIP = { long: 'https://media.test/long.mp4', short: 'https://media.test/short.mp4', stream: 'https://media.test/show/master.m3u8' }; -const seeded = [post('lecture ' + CLIP.long, 100), post('quick ' + CLIP.short, 200), post('episode ' + CLIP.stream, 300)]; +const CLIP = { long: 'https://media.test/long.mp4', short: 'https://media.test/short.mp4', stream: 'https://media.test/show/master.m3u8', big: 'https://media.test/big.mp4' }; +const seeded = [post('lecture ' + CLIP.long, 100), post('quick ' + CLIP.short, 200), post('episode ' + CLIP.stream, 300), post('square ' + CLIP.big, 400)]; const html = await buildHtml({ minify: true, pwa: false }); const server = Bun.serve({ port: 0, fetch: (req) => { @@ -49,7 +54,7 @@ await page.setRequestInterception(true); const asked = []; // [kind, clip url] // vmedia's answers, from the real thing; the clip's host serves the files const vmedia = async (u) => { - if (u.pathname === '/vmedia/hls.m3u8' && u.searchParams.get('u') === CLIP.long) u.searchParams.set('u', LOCAL_SRC); + if (u.pathname === '/vmedia/hls.m3u8') u.searchParams.set('u', local(u.searchParams.get('u'))); const res = await vm.handle(new Request('http://127.0.0.1' + u.pathname + u.search)); return { status: res.status, contentType: res.headers.get('content-type') || 'text/plain', body: Buffer.from(await res.arrayBuffer()), headers: { 'access-control-allow-origin': '*' } }; }; @@ -64,7 +69,7 @@ page.on('request', async (r) => { asked.push([r.resourceType(), r.url()]); // the stream a post links: vmedia's of the long clip, served from its host if (u.pathname === '/show/master.m3u8') { const x = new URL('https://v3.coinos.io/vmedia/hls.m3u8'); x.searchParams.set('u', CLIP.long); return r.respond(await vmedia(x)); } - return r.respond({ status: 200, contentType: 'video/mp4', body: u.pathname === '/long.mp4' ? LONG : SHORT, headers: { 'access-control-allow-origin': '*' } }); + return r.respond({ status: 200, contentType: 'video/mp4', body: u.pathname === '/long.mp4' ? LONG : u.pathname === '/big.mp4' ? BIG : SHORT, headers: { 'access-control-allow-origin': '*' } }); } r.continue(); }); @@ -99,7 +104,7 @@ try { await page.reload({ waitUntil: 'domcontentloaded' }); await waitText('receive', 20000); await page.evaluate(() => document.querySelector('.app-nav-button[aria-label="Feed"]').click()); - check('the three clips are in the feed', await waitFor(() => document.querySelectorAll('.note-video-box video').length >= 3, null, 20000), + check('the four clips are in the feed', await waitFor(() => document.querySelectorAll('.note-video-box video').length >= 4, null, 20000), String(await page.evaluate(() => document.querySelectorAll('.note-video-box video').length))); console.log('\n[a long clip]'); @@ -108,6 +113,11 @@ try { JSON.stringify(await player('lecture'))); check('...in its pieces', asked.some(([k, p]) => k === 'hls' && /\.ts$/.test(p)), JSON.stringify(asked.filter(([k]) => k === 'hls').slice(0, 4))); + console.log('\n[a short clip past 1080p]'); + await page.evaluate(new Function(`${box('square')}.scrollIntoView({ block: 'center' })`)); + check('plays by itself as HLS, no tap-to-play cover', await waitFor(new Function(`const b = ${box('square')}, v = b?.querySelector('video'); return v && v.getAttribute('src').includes('/vmedia/hls.m3u8') && v.videoWidth > 0 && v.currentTime > 0.3 && !b.querySelector('.vid-heavy');`), null, 20000), + JSON.stringify(await player('square'))); + console.log('\n[a short clip]'); await show('quick'); check('plays the original', await waitFor(new Function(`const v = ${box('quick')}?.querySelector('video'); return v && v.getAttribute('src') === '${CLIP.short}' && v.videoWidth > 0;`), null, 15000), |
