Changeset e276d03 in Klonkt


Ignore:
Timestamp:
07/16/2026 12:20:51 PM (8 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
226b563
Parents:
a85f539
git-author:
Robin <roboburr@…> (07/12/2026 10:31:19 PM)
git-committer:
Robin <roboburr@…> (07/16/2026 12:20:51 PM)
Message:

Fix: remote video thumbnails for moov-at-end files (Loops.video)

The poster-frame path fetched only the first 4MB, which decodes only a
faststart mp4. Loops.video and phone exports put the moov atom at the end,
so the head lacked it and ffmpeg produced no thumbnail (looked like a
'long video' regression). Now fetch the whole file when content-length is
within a 64MB cap (works regardless of moov position), falling back to an
8MB head only when the size is unknown or very large. The post-fetch 12MB
cap is scoped to images so it no longer rejects valid video buffers.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ThumbnailService.js

    ra85f539 re276d03  
    193193    if (!ct.startsWith('image/') && !isVideo) return null;
    194194    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.
     195      // Remote video → poster frame (feed/tile posters). A bounded head (first 4MB) only decodes
     196      // a faststart mp4 (moov atom up front). Many platforms (Loops.video, phone exports) put the
     197      // moov atom at the END, so a head-only fetch fails → no thumbnail. So: if the file is small
     198      // enough (content-length within VIDEO_CAP) grab it WHOLE — that works regardless of moov
     199      // position. Only when the size is unknown or very large do we fall back to a bounded head
     200      // (best-effort; a large moov-at-end file still yields null → the route's 302 fallback).
     201      const VIDEO_CAP = 64 * 1024 * 1024; // 64MB — covers short-form clips incl. moov-at-end
     202      const clen = parseInt(r.headers.get('content-length') || '0', 10);
    199203      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;
     204      let rv;
     205      if (clen && clen <= VIDEO_CAP) {
     206        rv = await safeFetch(url);
     207        if (!rv.ok) return null;
     208      } else {
     209        rv = await safeFetch(url, { headers: { Range: 'bytes=0-8388607' } }); // 8MB head fallback
     210        if (!rv.ok && rv.status !== 206) return null;
     211      }
    202212      buf = Buffer.from(await rv.arrayBuffer());
    203       if (!buf.length) return null;
     213      if (!buf.length || buf.length > VIDEO_CAP) return null;
    204214    } else {
    205215      if (parseInt(r.headers.get('content-length') || '0', 10) > 12 * 1024 * 1024) return null;
     
    210220    return null;
    211221  }
    212   if (buf.length > 12 * 1024 * 1024) return null;
     222  if (!isVideo && buf.length > 12 * 1024 * 1024) return null; // video already capped at VIDEO_CAP
    213223  // ffmpeg-static can't decode an animated WebP (the doomed downscale just logs an error), and a
    214224  // flattened GIF/animated WebP loses its motion → skip it and let the route serve the ORIGINAL
Note: See TracChangeset for help on using the changeset viewer.