diff --git a/server/media.js b/server/media.js index 4c40fcf..b1ccde3 100644 --- a/server/media.js +++ b/server/media.js @@ -151,10 +151,30 @@ function transcode(id, done) { }); } +// Poster frame, generated to a temp then renamed so a reader never sees a half-written JPEG (the /thumbs +// handler and this can both target the same file). Warming it at upload means the chat bubble shows the +// poster immediately instead of a blank tile while ffmpeg runs on the first view. +function ensureThumb(id) { + const thumb = path.join(UPLOADS_DIR, id + '.thumb.jpg'); + const src = path.join(UPLOADS_DIR, id); + if (!src.startsWith(UPLOADS_DIR)) return; + if (fs.existsSync(thumb)) return; + fs.stat(src, (e) => { + if (e) return; + const tmp = thumb + '.part'; + execFile('ffmpeg', ['-y', '-ss', '0.5', '-i', src, '-frames:v', '1', '-vf', 'scale=480:-2', '-q:v', '4', tmp], + { timeout: 15000 }, (err) => { + if (err) { try { fs.unlinkSync(tmp); } catch (_) {} return; } + try { fs.renameSync(tmp, thumb); } catch (_) { try { fs.unlinkSync(tmp); } catch (__) {} } + }); + }); +} + // Queue a freshly uploaded (or first-played) video. Cheap and idempotent: safe to call on every // /stream hit, which is also how pre-existing uploads get backfilled. function ensureWebRendition(id, mime) { if (!/^video\//.test(mime || '')) return; + ensureThumb(id); // warm the poster so the bubble isn't blank if (pending.has(id) || failed.has(id) || hasWebRendition(id)) return; pending.add(id); queue.push(id); diff --git a/server/public/home.html b/server/public/home.html index db08e02..0861912 100644 --- a/server/public/home.html +++ b/server/public/home.html @@ -1197,7 +1197,7 @@
-