diff options
Diffstat (limited to 'vmedia/server.js')
| -rw-r--r-- | vmedia/server.js | 32 |
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); |
