aboutsummaryrefslogtreecommitdiffstats
path: root/vmedia/server.js
diff options
context:
space:
mode:
Diffstat (limited to 'vmedia/server.js')
-rw-r--r--vmedia/server.js32
1 files changed, 29 insertions, 3 deletions
diff --git a/vmedia/server.js b/vmedia/server.js
index b2f5bae..0f11200 100644
--- a/vmedia/server.js
+++ b/vmedia/server.js
@@ -4,6 +4,9 @@
// /vmedia/thumb.jpg?u=<clip> a still from the clip's first half second,
// 480px wide — the poster a post's imeta didn't give. A 4K or 134 MB
// clip waits for a tap, so without one the feed showed a black box.
+// /vmedia/first.jpg?u=<clip> the clip's very first frame, at its size
+// (longest side 1280): the very picture a player shows first, so the
+// video pager's slide ahead holds it before the player has it.
// /vmedia/h264.mp4?u=<clip> the clip as H.264/AAC at 720p, for a browser
// that can't decode the original's picture (an iPhone's HEVC on Chrome
// for Linux plays its sound over black). The first ask streams the
@@ -74,6 +77,9 @@ export const thumbArgs = (src, at, lift = false) => ['-hide_banner', '-loglevel'
// that fades in from black has a black frame at half a second
'-frames:v', '1', '-vf', "scale='min(480,iw)':-2," + (lift ? 'normalize=strength=0.7:smoothing=0,' : '') + "signalstats,metadata=mode=print:key=lavfi.signalstats.YAVG:file='pipe\\:2'",
'-q:v', '5', '-f', 'image2', '-c:v', 'mjpeg', 'pipe:1'];
+// frame 0 itself, as big as the player shows it (the pager's slide ahead)
+export const firstArgs = (src) => ['-hide_banner', '-loglevel', 'error', ...NET, '-i', src,
+ '-frames:v', '1', '-vf', SCALE_720, '-q:v', '3', '-f', 'image2', '-c:v', 'mjpeg', 'pipe:1'];
export const clipArgs = (src, out) => ['-hide_banner', '-loglevel', 'error', '-y', ...NET, '-i', src,
'-map', '0:v:0', '-map', '0:a:0?', '-t', String(MAX_SECONDS), '-vf', SCALE_720,
'-c:v', 'libx264', '-preset', 'veryfast', '-crf', '26', '-profile:v', 'high', '-pix_fmt', 'yuv420p', '-threads', '4',
@@ -131,6 +137,26 @@ async function makeThumb(src, key) {
failed.set(key, Date.now());
return null;
}
+// The very first frame, full size, for the video pager: the slide ahead
+// holds the picture its player will show first, so a flick glides onto it
+// and the player takes over with no visible change. Dark is dark: this one
+// is meant to match the player, not to show what the clip is.
+async function makeFirst(src, key) {
+ const out = `${THUMBS}/${key}-f.jpg`;
+ const { p, done } = run(firstArgs(src), 25_000);
+ const [buf] = await Promise.all([new Response(p.stdout).arrayBuffer(), done]);
+ if (p.exitCode === 0 && buf.byteLength > 500) { await Bun.write(out, buf); return out; }
+ failed.set(key, Date.now());
+ return null;
+}
+const FIRST_V = 1;
+async function first(src) {
+ const key = keyOf('f' + FIRST_V + '|' + src), out = `${THUMBS}/${key}-f.jpg`;
+ if (existsSync(out)) { touch(out); return out; }
+ if (failedLately(key)) return null;
+ if (!thumbJobs.has(key)) thumbJobs.set(key, thumbSlots(() => makeFirst(src, key)).finally(() => thumbJobs.delete(key)));
+ return thumbJobs.get(key);
+}
const THUMB_V = 4; // v4: past a dark opening frame (stills made before are made again)
async function thumb(src) {
const key = keyOf('v' + THUMB_V + '|' + src), out = `${THUMBS}/${key}.jpg`;
@@ -349,7 +375,7 @@ export async function handle(req) {
const u = new URL(req.url);
if (!['GET', 'HEAD'].includes(req.method)) return nope(405, 'GET only');
const route = u.pathname.replace(/^\/vmedia/, '');
- if (route !== '/thumb.jpg' && route !== '/h264.mp4' && route !== '/pic.webp') return nope(404, 'Not found');
+ if (route !== '/thumb.jpg' && route !== '/first.jpg' && route !== '/h264.mp4' && route !== '/pic.webp') return nope(404, 'Not found');
const src = await sourceOk(u.searchParams.get('u') || '');
if (!src) return nope(400, 'Not a public https URL');
const ip = req.headers.get('cf-connecting-ip') || req.headers.get('x-real-ip') || 'unknown';
@@ -357,8 +383,8 @@ export async function handle(req) {
const shape = u.searchParams.get('s') || 'col';
return SHAPES[shape] ? picResponse(req, src, shape, ip) : nope(400, 'Unknown shape');
}
- if (route === '/thumb.jpg') {
- const p = await thumb(src);
+ if (route === '/thumb.jpg' || route === '/first.jpg') {
+ const p = await (route === '/first.jpg' ? first(src) : thumb(src));
return p ? fileResponse(req, p, 'image/jpeg') : nope(404, 'No picture');
}
return clipResponse(req, src, ip);