Skip to content

Commit 4d5b383

Browse files
vanceingallsclaude
andcommitted
chore(ci): add fast-capture video validation workflow
Renders a video composition baseline-vs-fast on a native amd64 Linux runner and asserts the fast (drawElementImage) output matches via PSNR — validating the BeginFrame paint path captures video, which couldn't be checked locally (macOS has no BeginFrame; Docker-on-rosetta hung). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7599a75 commit 4d5b383

3 files changed

Lines changed: 127 additions & 0 deletions

File tree

.fallowrc.jsonc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"packages/producer/src/runtime-conformance.ts",
1515
"packages/producer/src/benchmark.ts",
1616
"packages/producer/scripts/generate-font-data.ts",
17+
"packages/producer/scripts/validate-fast-video.ts",
1718
"packages/cli/scripts/generate-font-data.ts",
1819
"packages/engine/scripts/test-fitTextFontSize-browser.ts",
1920
"packages/aws-lambda/scripts/*.ts",
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Validates the experimental fast-capture (drawElementImage) VIDEO path on a
2+
# native amd64 Linux runner — where chrome-headless-shell's per-frame BeginFrame
3+
# drives a real paint each frame, so drawElementImage's snapshot is fresh and
4+
# video captures correctly. This is the one part of the feature that could not be
5+
# validated locally (macOS has no BeginFrame; Docker-on-rosetta hung).
6+
# See docs/fast-capture-limitations.md (Limitation 2).
7+
#
8+
# Manual trigger: Actions → "Fast-capture video validation" → Run workflow.
9+
name: Fast-capture video validation
10+
11+
on:
12+
workflow_dispatch:
13+
inputs:
14+
composition:
15+
description: Test composition to render (must contain <video>)
16+
default: sub-composition-video
17+
min_psnr:
18+
description: Min fast-vs-baseline PSNR (dB) to pass
19+
default: "25"
20+
21+
jobs:
22+
validate:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@v4
26+
27+
- name: Set up Docker Buildx
28+
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
29+
30+
- name: Build test Docker image (cached)
31+
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
32+
with:
33+
context: .
34+
file: Dockerfile.test
35+
load: true
36+
tags: hyperframes-producer:test
37+
cache-from: type=gha,scope=regression-test-image
38+
cache-to: type=gha,mode=max,scope=regression-test-image
39+
40+
- name: Validate fast-capture video (drawElement + BeginFrame)
41+
run: |
42+
docker run --rm \
43+
--security-opt seccomp=unconfined \
44+
--shm-size=4g \
45+
-e PRODUCER_VALIDATE_COMP='${{ inputs.composition }}' \
46+
-e PRODUCER_VALIDATE_MIN_PSNR='${{ inputs.min_psnr }}' \
47+
--workdir /app/packages/producer \
48+
--entrypoint bunx \
49+
hyperframes-producer:test tsx scripts/validate-fast-video.ts
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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

Comments
 (0)