|
| 1 | +/** |
| 2 | + * Validate the fast-capture (drawElementImage) VIDEO path on real Linux. |
| 3 | + * |
| 4 | + * drawElementImage draws a snapshot taken at the paint event; capturing video |
| 5 | + * needs a fresh per-frame paint. On Linux headless-shell that paint comes from |
| 6 | + * the per-frame HeadlessExperimental.beginFrame — so video should capture |
| 7 | + * correctly there (see docs/fast-capture-limitations.md, Limitation 2). This |
| 8 | + * could not be validated under Docker-on-rosetta (renders hung); this script is |
| 9 | + * meant to run on a native amd64 Linux runner inside Dockerfile.test. |
| 10 | + * |
| 11 | + * Renders a video composition twice — baseline (screenshot) and fast |
| 12 | + * (drawElement) — and asserts the fast output matches the baseline (PSNR above |
| 13 | + * threshold), proving the video was captured and not dropped to black. |
| 14 | + * |
| 15 | + * PRODUCER_VALIDATE_COMP=sub-composition-video \ |
| 16 | + * bunx tsx scripts/validate-fast-video.ts |
| 17 | + * |
| 18 | + * Exit 0 = fast video matches baseline; exit 1 = regression (black/stale video). |
| 19 | + */ |
| 20 | +import { execFileSync } from "node:child_process"; |
| 21 | +import { mkdtempSync } from "node:fs"; |
| 22 | +import { tmpdir } from "node:os"; |
| 23 | +import { join, resolve } from "node:path"; |
| 24 | +import { createRenderJob, executeRenderJob } from "../src/index.js"; |
| 25 | + |
| 26 | +const COMP = process.env.PRODUCER_VALIDATE_COMP ?? "sub-composition-video"; |
| 27 | +const MIN_PSNR = Number.parseFloat(process.env.PRODUCER_VALIDATE_MIN_PSNR ?? "25"); |
| 28 | +const work = mkdtempSync(join(tmpdir(), "fastvideo-")); |
| 29 | + |
| 30 | +process.env.PRODUCER_ENABLE_BROWSER_POOL = "false"; |
| 31 | + |
| 32 | +async function render(mode: "baseline" | "fast", out: string): Promise<void> { |
| 33 | + process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE = mode === "fast" ? "true" : "false"; |
| 34 | + const job = createRenderJob({ |
| 35 | + fps: 30, |
| 36 | + quality: "high", |
| 37 | + format: "mp4", |
| 38 | + workers: 1, |
| 39 | + useGpu: false, |
| 40 | + hdrMode: "force-sdr", |
| 41 | + }); |
| 42 | + await executeRenderJob(job, resolve("tests", COMP, "src"), out); |
| 43 | +} |
| 44 | + |
| 45 | +function psnr(a: string, b: string): number { |
| 46 | + const out = execFileSync( |
| 47 | + "bash", |
| 48 | + ["-c", `ffmpeg -y -i "${a}" -i "${b}" -lavfi psnr -f null - 2>&1`], |
| 49 | + { encoding: "utf8" }, |
| 50 | + ); |
| 51 | + const m = out.match(/average:(\S+)/); |
| 52 | + if (!m) throw new Error(`ffmpeg psnr produced no average:\n${out}`); |
| 53 | + return m[1] === "inf" ? Number.POSITIVE_INFINITY : Number.parseFloat(m[1]); |
| 54 | +} |
| 55 | + |
| 56 | +async function main(): Promise<void> { |
| 57 | + const baseline = join(work, "baseline.mp4"); |
| 58 | + const fast = join(work, "fast.mp4"); |
| 59 | + console.log(`[validate-fast-video] comp=${COMP} minPsnr=${MIN_PSNR}`); |
| 60 | + await render("baseline", baseline); |
| 61 | + await render("fast", fast); |
| 62 | + const db = psnr(baseline, fast); |
| 63 | + console.log(`[validate-fast-video] fast-vs-baseline PSNR = ${db} dB`); |
| 64 | + if (db < MIN_PSNR) { |
| 65 | + console.error( |
| 66 | + `[validate-fast-video] FAIL — ${db} dB < ${MIN_PSNR} dB. Fast capture dropped video ` + |
| 67 | + `(stale/black snapshot). The Linux BeginFrame paint path is not capturing video.`, |
| 68 | + ); |
| 69 | + process.exit(1); |
| 70 | + } |
| 71 | + console.log("[validate-fast-video] PASS — fast video matches baseline."); |
| 72 | +} |
| 73 | + |
| 74 | +main().catch((e) => { |
| 75 | + console.error(e); |
| 76 | + process.exit(1); |
| 77 | +}); |
0 commit comments