fix(cli): extract snapshot video frames from remote http(s) srcs

`hyperframes snapshot` works around Chrome-headless's inability to seek
`<video>` elements by extracting a frame via FFmpeg and injecting it as an
overlay. That path only resolved `<video src>` to a project-LOCAL file and
skipped everything else — so a composition whose embedded `<video>` points
at a remote http(s) URL (e.g. an S3-hosted clip embedded by an upstream
agent) rendered as a blank box in every snapshot, while `render` (which
plays the element in-browser) showed it fine.

Add a remote fallback: when the src doesn't resolve to a project-local file
but is an http(s) URL, pass the absolute URL straight to FFmpeg (it reads
http(s) input directly). Local-first is preserved (fast, sandboxed); the
existing 30s extract timeout bounds remote fetches.

Verified on a real composition: remote-src snapshot was blank, local-src
rendered; `ffmpeg -ss N -i <https-url> -frames:v 1` extracts in ~0.5s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
xuanru
2026-06-30 21:37:40 +00:00
co-authored by Claude Opus 4.8
parent 5915590b06
commit 1f377f732b
+12 -5
View File
@@ -355,23 +355,30 @@ async function captureSnapshots(
const updates: Array<{ videoId: string; dataUri: string }> = [];
for (const v of active) {
let filePath: string | null = null;
// Resolve the <video> src to an FFmpeg input. Prefer a project-local
// file (fast, sandboxed); fall back to the absolute http(s) URL for
// remote assets (e.g. an S3-hosted clip embedded by an upstream agent)
// — FFmpeg reads http(s) input directly, and Chrome-headless can't seek
// it either, so without this those videos render blank in snapshots.
let ffmpegInput: string | null = null;
try {
const url = new URL(v.src);
const decodedPath = decodeURIComponent(url.pathname).replace(/^\//, "");
const candidate = resolve(projectDir, decodedPath);
const rel = relative(projectDir, candidate);
if (!rel.startsWith("..") && !isAbsolute(rel) && existsSync(candidate)) {
filePath = candidate;
ffmpegInput = candidate;
} else if (url.protocol === "http:" || url.protocol === "https:") {
ffmpegInput = url.href;
}
} catch {
/* unresolvable src (e.g. blob:, data:) — skip */
}
if (!filePath) continue;
if (!ffmpegInput) continue;
const png = await extractVideoFrameToBuffer(
filePath,
ffmpegInput,
Math.max(0, v.relTime),
await shouldUseVp9AlphaDecoder(filePath),
await shouldUseVp9AlphaDecoder(ffmpegInput),
);
if (!png) continue;
updates.push({