* Extract a single frame from a video file at `timeSeconds` via FFmpeg. * Used to work around Chrome-headless's inability to reliably seek * elements during snapshot capture.
( videoPath: string, timeSeconds: number, useVp9AlphaDecoder = false, )
| 58 | * <video> elements during snapshot capture. |
| 59 | */ |
| 60 | async function extractVideoFrameToBuffer( |
| 61 | videoPath: string, |
| 62 | timeSeconds: number, |
| 63 | useVp9AlphaDecoder = false, |
| 64 | ): Promise<Buffer | null> { |
| 65 | const tmp = mkdtempSync(join(tmpdir(), "hf-snapshot-frame-")); |
| 66 | const outPath = join(tmp, "frame.png"); |
| 67 | try { |
| 68 | const ffmpegPath = findFFmpeg(); |
| 69 | if (!ffmpegPath) return null; |
| 70 | const result = await new Promise<{ code: number | null; stderr: string; timedOut: boolean }>( |
| 71 | (resolvePromise) => { |
| 72 | // `-ss` before `-i` performs a fast keyframe seek; adequate for snapshot accuracy |
| 73 | // (±1 frame) and orders of magnitude faster than the decode-and-scan alternative. |
| 74 | const args = ["-hide_banner", "-loglevel", "error"]; |
| 75 | if (useVp9AlphaDecoder) { |
| 76 | args.push("-c:v", "libvpx-vp9"); |
| 77 | } |
| 78 | args.push( |
| 79 | "-ss", |
| 80 | String(Math.max(0, timeSeconds)), |
| 81 | "-i", |
| 82 | videoPath, |
| 83 | "-frames:v", |
| 84 | "1", |
| 85 | "-q:v", |
| 86 | "2", |
| 87 | "-y", |
| 88 | outPath, |
| 89 | ); |
| 90 | const ff = spawn(ffmpegPath, args); |
| 91 | let stderr = ""; |
| 92 | let timedOut = false; |
| 93 | const timer = setTimeout(() => { |
| 94 | timedOut = true; |
| 95 | ff.kill("SIGTERM"); |
| 96 | }, FFMPEG_EXTRACT_TIMEOUT_MS); |
| 97 | ff.stderr.on("data", (d: Buffer) => { |
| 98 | stderr += d.toString(); |
| 99 | }); |
| 100 | ff.on("close", (code) => { |
| 101 | clearTimeout(timer); |
| 102 | resolvePromise({ code, stderr, timedOut }); |
| 103 | }); |
| 104 | ff.on("error", () => { |
| 105 | clearTimeout(timer); |
| 106 | resolvePromise({ code: null, stderr: "ffmpeg spawn failed", timedOut }); |
| 107 | }); |
| 108 | }, |
| 109 | ); |
| 110 | if (result.code !== 0 || result.timedOut || !existsSync(outPath)) return null; |
| 111 | return readFileSync(outPath); |
| 112 | } finally { |
| 113 | try { |
| 114 | rmSync(tmp, { recursive: true, force: true }); |
| 115 | } catch { |
| 116 | /* best-effort */ |
| 117 | } |
no test coverage detected