aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-18 06:05:25 -0700
committerAdam Soltys <[email protected]>2026-10-18 06:05:25 -0700
commit4807f138feba04a60292886a196d614e2254810b (patch)
treed9e7ee76fc61381ea9c5916050ed0d9f1553da73 /tools
parentb9297719d294779a2ea14c77a67975308bace65c (diff)
downloadcoinosv3-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.js6
-rw-r--r--tools/video-hls-play-test.js26
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),