Changeset e67828e in Klonkt


Ignore:
Timestamp:
07/01/2026 10:38:02 PM (2 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
0877771
Parents:
5e52448
Message:

fix(media): poster frames for remote videos (News feed + Circle tiles)

A remote video (Loops/PeerTube/Mastodon attachment) rendered as a black box until
played: the image proxy refused non-image content, so the tile/feed poster 302'd to
the raw mp4 and never displayed. The proxy now extracts a poster frame from remote
video too — via a bounded range fetch (first 4MB, enough for a faststart mp4's first
frame; moov-at-end files fall back to the old 302). The News feed's <video> gets a
poster so long videos show a frame instead of black. (>30s keeping controls instead
of autoplaying is by design — that was the reported "didn't load like the others".)

  • src/services/ThumbnailService.js — getRemoteThumbnail accepts video/* via a 4MB ranged fetch + single-frame extract; animated-buf guard scoped to images.
  • src/views/pages/news.ejs — feed videos carry poster=thumb(url, 1280).
  • CHANGELOG(.nl/.de).md — under Fixed.

Closes prutfolio-src-k1q.

Co-Authored-By: Claude <noreply@…>

Files:
5 edited

Legend:

Unmodified
Added
Removed
  • CHANGELOG.de.md

    r5e52448 re67828e  
    4242
    4343### Behoben
     44- **Entfernte Videos zeigen ein Vorschaubild.** Ein Video im News-Feed oder auf einer Zirkel-Kachel
     45  (z.B. von Loops oder PeerTube) erschien als schwarze Fläche, bis man auf Abspielen drückte; jetzt
     46  gibt es ein echtes Posterbild. (Längere Videos behalten bewusst ihre Steuerung — nur Clips unter
     47  30 Sekunden laufen automatisch wie ein GIF.)
    4448- **Erwähnungen, Hashtags und Links in Klammern funktionieren jetzt.** Eine Erwähnung wie
    4549  `(@benutzer@server)`, ein `(#hashtag)` oder eine URL in Klammern föderierte als reiner Text —
  • CHANGELOG.md

    r5e52448 re67828e  
    3737
    3838### Fixed
     39- **Remote videos show a preview frame.** A video in the News feed or a Circle tile (e.g. from
     40  Loops or PeerTube) used to appear as a black box until you pressed play; it now shows a real
     41  poster frame. (Longer videos keep their player controls by design — only clips under 30 seconds
     42  autoplay like a GIF.)
    3943- **Mentions, hashtags and links inside brackets now work.** A mention like `(@user@server)`, a
    4044  `(#hashtag)` or a bracketed URL federated as plain text — and the mentioned person was never
  • CHANGELOG.nl.md

    r5e52448 re67828e  
    4040
    4141### Opgelost
     42- **Remote video's tonen een preview-frame.** Een video in de News-feed of op een Cirkel-tegel
     43  (bv. van Loops of PeerTube) verscheen als zwart vlak tot je op afspelen drukte; er staat nu een
     44  echt poster-frame. (Langere video's houden bewust hun bediening — alleen clips onder de 30
     45  seconden spelen automatisch als een GIF.)
    4246- **Vermeldingen, hashtags en links tussen haakjes werken nu.** Een vermelding als
    4347  `(@gebruiker@server)`, een `(#hashtag)` of een URL tussen haakjes federeerde als platte tekst —
  • src/services/ThumbnailService.js

    r5e52448 re67828e  
    185185  if (fs.existsSync(cached)) return cached;
    186186
    187   let buf;
     187  let buf, isVideo = false;
    188188  try {
    189189    const r = await safeFetch(url);
    190190    if (!r.ok) return null;
    191     if (!(r.headers.get('content-type') || '').startsWith('image/')) return null;
    192     if (parseInt(r.headers.get('content-length') || '0', 10) > 12 * 1024 * 1024) return null;
    193     buf = Buffer.from(await r.arrayBuffer());
     191    const ct = r.headers.get('content-type') || '';
     192    isVideo = ct.startsWith('video/');
     193    if (!ct.startsWith('image/') && !isVideo) return null;
     194    if (isVideo) {
     195      // Remote video → poster frame (feed/tile posters). Don't buffer the whole file: re-fetch
     196      // a bounded head (first 4MB) — enough for ffmpeg to decode the first frame of a faststart
     197      // mp4 (the web-streaming norm). A moov-at-end file just fails → null → the route's 302
     198      // fallback, same as before this path existed.
     199      try { if (r.body && r.body.cancel) r.body.cancel(); } catch { /* ignore */ }
     200      const rv = await safeFetch(url, { headers: { Range: 'bytes=0-4194303' } });
     201      if (!rv.ok && rv.status !== 206) return null;
     202      buf = Buffer.from(await rv.arrayBuffer());
     203      if (!buf.length) return null;
     204    } else {
     205      if (parseInt(r.headers.get('content-length') || '0', 10) > 12 * 1024 * 1024) return null;
     206      buf = Buffer.from(await r.arrayBuffer());
     207    }
    194208  } catch (e) {
    195209    console.warn('[thumb-remote] fetch failed for', url, '-', e.message);
     
    199213  // ffmpeg-static can't decode an animated WebP (the doomed downscale just logs an error), and a
    200214  // flattened GIF/animated WebP loses its motion → skip it and let the route serve the ORIGINAL
    201   // (keeps the animation; mirrors the local path's isAnimatedSrc guard).
    202   if (isAnimatedBuf(buf)) return null;
     215  // (keeps the animation; mirrors the local path's isAnimatedSrc guard). Video heads skip this
     216  // (they're not webp/gif) and go straight to the single-frame extract.
     217  if (!isVideo && isAnimatedBuf(buf)) return null;
    203218
    204219  await fs.promises.mkdir(path.dirname(cached), { recursive: true });
  • src/views/pages/news.ejs

    r5e52448 re67828e  
    122122            </div>
    123123          <% } %>
    124           <% vids.forEach(function(m){ %><video class="tl-media-video" src="<%= m.url %>" controls preload="metadata" playsinline></video><% }); %>
     124          <% vids.forEach(function(m){ %><video class="tl-media-video" src="<%= m.url %>" poster="<%= thumb(m.url, 1280) %>" controls preload="metadata" playsinline></video><% }); %>
    125125          <% /* A Klonkt audio post carries an embed player (embedHtml/embedUrl) that already
    126126                covers these tracks, so don't ALSO render the raw Audio attachments as bare
Note: See TracChangeset for help on using the changeset viewer.