chore: import upstream snapshot with attribution
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:35 +08:00
commit 85453da49f
4031 changed files with 710987 additions and 0 deletions
@@ -0,0 +1,482 @@
/**
* Smoke test for the WebM (VP9) distributed concat-copy path.
*
* Asserts that `buildEncoderArgs(..., { codec: "vp9",
* lockGopForChunkConcat: true, gopSize: N })` produces VP9 chunk files
* that `ffmpeg -f concat -c copy` can stitch into a single playable
* WebM.
*
* Uses direct ffmpeg invocation instead of `plan() / renderChunk() /
* assemble()` so the contract this test pins is exactly the encoder-arg
* surface — independent of plan-time validation, file servers, browser
* capture, and the rest of the distributed-pipeline stack.
*
* Each chunk + concat-copy + ffprobe verification surfaces its failure
* fingerprint in the error message so a regression-driven concat-copy
* failure (alt-ref reaching across a seam, libvpx bumping its default
* cpu-used, etc.) can be diagnosed without re-running locally.
*/
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { buildEncoderArgs } from "@hyperframes/engine";
const FPS = 30;
const TOTAL_FRAMES = 60;
const CHUNK_SIZE = 15;
const CHUNK_COUNT = TOTAL_FRAMES / CHUNK_SIZE; // 4
const WIDTH = 320;
const HEIGHT = 240;
let runRoot: string;
let framesDir: string;
let chunkDir: string;
let concatListPath: string;
let outputPath: string;
let frameGenStderr = "";
interface FfmpegResult {
exitCode: number | null;
stderr: string;
stdout: string;
}
function runFfmpegSync(args: string[]): FfmpegResult {
const result = spawnSync("ffmpeg", args, { encoding: "utf8" });
return {
exitCode: result.status,
stderr: result.stderr ?? "",
stdout: result.stdout ?? "",
};
}
function runFfprobeSync(args: string[]): FfmpegResult {
const result = spawnSync("ffprobe", args, { encoding: "utf8" });
return {
exitCode: result.status,
stderr: result.stderr ?? "",
stdout: result.stdout ?? "",
};
}
beforeAll(() => {
runRoot = mkdtempSync(join(tmpdir(), "hf-webm-concat-smoke-"));
framesDir = join(runRoot, "frames");
chunkDir = join(runRoot, "chunks");
mkdirSync(framesDir, { recursive: true });
mkdirSync(chunkDir, { recursive: true });
concatListPath = join(runRoot, "concat-list.txt");
outputPath = join(runRoot, "output.webm");
// Generate 60 PNG frames using lavfi testsrc2 (animated counter / color
// bars — easy to eyeball for seam errors if a human inspects the output).
// Each frame is a real image; we use a frame sequence rather than a single
// mp4 source so the per-chunk encode is a pure image2 → VP9 pass with no
// intermediate decode.
const frameGen = runFfmpegSync([
"-hide_banner",
"-y",
"-f",
"lavfi",
"-i",
`testsrc2=s=${WIDTH}x${HEIGHT}:r=${FPS}:d=${TOTAL_FRAMES / FPS}`,
"-frames:v",
String(TOTAL_FRAMES),
join(framesDir, "frame_%04d.png"),
]);
frameGenStderr = frameGen.stderr;
if (frameGen.exitCode !== 0) {
throw new Error(
`[smoke setup] frame generation failed (exit ${frameGen.exitCode}): ${frameGen.stderr.slice(-400)}`,
);
}
});
afterAll(() => {
rmSync(runRoot, { recursive: true, force: true });
});
describe("webm VP9 concat-copy smoke", () => {
it("generates 60 source PNG frames", () => {
// Sanity check — if testsrc2 frame generation broke, downstream
// failures would be miscategorized as concat-copy errors.
const firstFrame = join(framesDir, "frame_0001.png");
const lastFrame = join(framesDir, `frame_${String(TOTAL_FRAMES).padStart(4, "0")}.png`);
expect(existsSync(firstFrame)).toBe(true);
expect(existsSync(lastFrame)).toBe(true);
expect(frameGenStderr).toBeDefined();
});
it("encodes 4 VP9 chunks with closed-GOP args from buildEncoderArgs", () => {
// The contract this test asserts: buildEncoderArgs with
// lockGopForChunkConcat=true + codec=vp9 + gopSize=chunkSize produces
// VP9 chunks whose first frame is an independently-decodable keyframe
// and whose alt-ref behavior doesn't reach back across chunk seams.
//
// Use the exact args buildEncoderArgs returns. We only swap the input
// args (image2 input range per chunk) — the encoder args (everything
// after `-r <fps>`) are byte-identical to what a real renderChunk()
// call would invoke.
for (let chunkIdx = 0; chunkIdx < CHUNK_COUNT; chunkIdx++) {
const startNumber = chunkIdx * CHUNK_SIZE + 1; // image2 frame numbers are 1-based
const chunkPath = join(chunkDir, `chunk_${String(chunkIdx).padStart(4, "0")}.webm`);
const inputArgs = [
"-framerate",
String(FPS),
"-start_number",
String(startNumber),
"-i",
join(framesDir, "frame_%04d.png"),
"-frames:v",
String(CHUNK_SIZE),
];
const args = buildEncoderArgs(
{
fps: { num: FPS, den: 1 },
width: WIDTH,
height: HEIGHT,
codec: "vp9",
preset: "good",
quality: 32,
pixelFormat: "yuv420p",
lockGopForChunkConcat: true,
gopSize: CHUNK_SIZE,
},
inputArgs,
chunkPath,
);
const result = runFfmpegSync(["-hide_banner", "-loglevel", "error", ...args]);
if (result.exitCode !== 0) {
throw new Error(
`[smoke chunk ${chunkIdx}] VP9 encode failed (exit ${result.exitCode}):\n` +
`args: ${JSON.stringify(args)}\n` +
`stderr: ${result.stderr.slice(-1000)}`,
);
}
expect(existsSync(chunkPath)).toBe(true);
expect(statSync(chunkPath).size).toBeGreaterThan(0);
}
});
it("concat-copies the 4 chunks into a single WebM", () => {
const lines: string[] = [];
for (let chunkIdx = 0; chunkIdx < CHUNK_COUNT; chunkIdx++) {
const chunkPath = join(chunkDir, `chunk_${String(chunkIdx).padStart(4, "0")}.webm`);
lines.push(`file '${chunkPath.replace(/'/g, "'\\''")}'`);
}
writeFileSync(concatListPath, `${lines.join("\n")}\n`, "utf-8");
const result = runFfmpegSync([
"-hide_banner",
"-loglevel",
"error",
"-f",
"concat",
"-safe",
"0",
"-i",
concatListPath,
"-c",
"copy",
"-y",
outputPath,
]);
// Surface ffmpeg's full stderr in the assertion message — a broken
// concat-copy fails with something specific ("Non-monotonous DTS",
// "missing keyframe at chunk 2", matroska/webm cluster errors) that
// the message above wouldn't disambiguate.
if (result.exitCode !== 0) {
throw new Error(
`[smoke concat-copy] failed (exit ${result.exitCode}). ` +
`Failure fingerprint: ${result.stderr.slice(-1000)}`,
);
}
expect(existsSync(outputPath)).toBe(true);
expect(statSync(outputPath).size).toBeGreaterThan(0);
});
it("ffprobe -show_streams reports a single playable VP9 stream", () => {
// First verification — the output file is structurally a valid WebM
// with one video stream encoded as VP9. A broken concat-copy can
// produce a file whose container parses but whose stream metadata is
// corrupted (no codec ID, zero duration, broken pixel format).
const result = runFfprobeSync([
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=codec_name,width,height,pix_fmt,r_frame_rate",
"-of",
"default=noprint_wrappers=1",
outputPath,
]);
if (result.exitCode !== 0) {
throw new Error(
`[smoke ffprobe] -show_streams failed (exit ${result.exitCode}). ` +
`This means concat-copy produced a structurally broken WebM. ` +
`Failure fingerprint: ${result.stderr.slice(-1000)}`,
);
}
expect(result.stdout).toMatch(/codec_name=vp9/);
expect(result.stdout).toMatch(new RegExp(`width=${WIDTH}`));
expect(result.stdout).toMatch(new RegExp(`height=${HEIGHT}`));
});
it("ffmpeg -i ... -f null - decodes the concat'd WebM without errors", () => {
// Second verification — the bitstream actually decodes end-to-end.
// A WebM whose containers parse but whose VP9 frames reference
// non-existent alt-ref frames (because alt-ref crossed a chunk
// seam) will fail here with "Reference frame not found" or
// "Invalid frame" errors.
const result = runFfmpegSync([
"-hide_banner",
"-v",
"error",
"-i",
outputPath,
"-f",
"null",
"-",
]);
if (result.exitCode !== 0 || result.stderr.length > 0) {
throw new Error(
`[smoke decode-test] ffmpeg -f null - reported decode errors ` +
`(exit ${result.exitCode}). This means concat-copy seams produce ` +
`invalid VP9 references ` +
`Failure fingerprint: ${result.stderr.slice(-1000) || "(no stderr; check exit code)"}`,
);
}
});
it("ffprobe -count_frames matches the sum of chunk frames", () => {
// Third verification — playable frame count equals what we encoded.
// A broken concat-copy can produce a file that decodes "without
// errors" up to the first bad seam and then silently truncates,
// leaving fewer frames than expected.
const result = runFfprobeSync([
"-v",
"error",
"-select_streams",
"v:0",
"-count_frames",
"-show_entries",
"stream=nb_read_frames",
"-of",
"default=noprint_wrappers=1:nokey=1",
outputPath,
]);
if (result.exitCode !== 0) {
throw new Error(
`[smoke ffprobe count_frames] failed (exit ${result.exitCode}): ` +
`${result.stderr.slice(-1000)}`,
);
}
const nbFrames = Number.parseInt(result.stdout.trim(), 10);
if (!Number.isFinite(nbFrames) || nbFrames !== TOTAL_FRAMES) {
throw new Error(
`[smoke ffprobe count_frames] expected ${TOTAL_FRAMES} frames, got ${result.stdout.trim()} ` +
`— concat-copy dropped frames at one or more chunk seams.`,
);
}
expect(nbFrames).toBe(TOTAL_FRAMES);
});
});
describe("webm VP9 concat-copy smoke (yuva420p alpha)", () => {
// The wired-up distributed webm path uses yuva420p. This block proves
// (a) the closed-GOP args + alpha pixel format don't break concat-copy
// at the bitstream level, and (b) the alpha plane round-trips with
// real spatial content — catching the failure mode where the encoder
// accepted yuva420p input but dropped the alpha sub-stream silently.
// The source frames carry a per-pixel alpha gradient so the encoder
// cannot treat the alpha plane as uniform/redundant and drop it.
it("encode + concat-copy + decode round-trip works for yuva420p", () => {
const alphaRoot = mkdtempSync(join(tmpdir(), "hf-webm-concat-smoke-alpha-"));
try {
const alphaFramesDir = join(alphaRoot, "frames");
const alphaChunkDir = join(alphaRoot, "chunks");
mkdirSync(alphaFramesDir, { recursive: true });
mkdirSync(alphaChunkDir, { recursive: true });
const alphaConcatListPath = join(alphaRoot, "concat-list.txt");
const alphaOutputPath = join(alphaRoot, "output.webm");
// `geq=a='X*255/W'` writes a horizontal alpha gradient on top of
// the testsrc2 RGB. `testsrc2 + format=rgba` alone produced
// uniformly-opaque alpha and libvpx-vp9 silently downgraded the
// output to yuv420p, masking any alpha-pipeline bug — the
// gradient ensures the encoder has spatially-varying alpha to
// preserve.
const frameGen = runFfmpegSync([
"-hide_banner",
"-y",
"-f",
"lavfi",
"-i",
`testsrc2=s=${WIDTH}x${HEIGHT}:r=${FPS}:d=${TOTAL_FRAMES / FPS}`,
"-vf",
"format=rgba,geq=r='r(X,Y)':g='g(X,Y)':b='b(X,Y)':a='X*255/W'",
"-frames:v",
String(TOTAL_FRAMES),
join(alphaFramesDir, "frame_%04d.png"),
]);
if (frameGen.exitCode !== 0) {
throw new Error(
`[alpha smoke setup] frame generation failed: ${frameGen.stderr.slice(-400)}`,
);
}
const chunkPaths: string[] = [];
for (let chunkIdx = 0; chunkIdx < CHUNK_COUNT; chunkIdx++) {
const startNumber = chunkIdx * CHUNK_SIZE + 1;
const chunkPath = join(alphaChunkDir, `chunk_${String(chunkIdx).padStart(4, "0")}.webm`);
chunkPaths.push(chunkPath);
const args = buildEncoderArgs(
{
fps: { num: FPS, den: 1 },
width: WIDTH,
height: HEIGHT,
codec: "vp9",
preset: "good",
quality: 32,
pixelFormat: "yuva420p",
lockGopForChunkConcat: true,
gopSize: CHUNK_SIZE,
},
[
"-framerate",
String(FPS),
"-start_number",
String(startNumber),
"-i",
join(alphaFramesDir, "frame_%04d.png"),
"-frames:v",
String(CHUNK_SIZE),
],
chunkPath,
);
const result = runFfmpegSync(["-hide_banner", "-loglevel", "error", ...args]);
if (result.exitCode !== 0) {
throw new Error(
`[alpha smoke chunk ${chunkIdx}] yuva420p VP9 encode failed: ${result.stderr.slice(-1000)}`,
);
}
}
writeFileSync(
alphaConcatListPath,
`${chunkPaths.map((p) => `file '${p.replace(/'/g, "'\\''")}'`).join("\n")}\n`,
"utf-8",
);
const concatResult = runFfmpegSync([
"-hide_banner",
"-loglevel",
"error",
"-f",
"concat",
"-safe",
"0",
"-i",
alphaConcatListPath,
"-c",
"copy",
"-y",
alphaOutputPath,
]);
if (concatResult.exitCode !== 0) {
throw new Error(`[alpha smoke concat-copy] failed: ${concatResult.stderr.slice(-1000)}`);
}
// Decode-test gates only on exit code — `-v error` ffmpeg builds
// can emit non-fatal stderr (DTS warnings, container-quirk notes)
// and we don't want the test to flake on chatty stderr in a
// future libavformat upgrade.
const decodeResult = runFfmpegSync([
"-hide_banner",
"-v",
"error",
"-i",
alphaOutputPath,
"-f",
"null",
"-",
]);
if (decodeResult.exitCode !== 0) {
throw new Error(
`[alpha smoke decode-test] failed (exit ${decodeResult.exitCode}): ` +
`${decodeResult.stderr.slice(-1000) || "(no stderr)"}`,
);
}
// libvpx-vp9 stores the alpha plane as a Matroska `BlockAdditional`
// sidecar, NOT in the main stream's `pix_fmt` — `ffprobe` always
// reports `pix_fmt=yuv420p` for VP9-with-alpha. The right signal
// is the stream-level `TAG:ALPHA_MODE=1` tag the encoder writes
// when `-metadata:s:v:0 alpha_mode=1` is set on yuva420p input.
const probeResult = runFfprobeSync([
"-v",
"error",
"-select_streams",
"v:0",
"-show_streams",
alphaOutputPath,
]);
expect(probeResult.exitCode).toBe(0);
expect(probeResult.stdout).toMatch(/codec_name=vp9/);
expect(probeResult.stdout).toMatch(/ALPHA_MODE=1/);
// Decode the alpha plane and check it has spatially-varying
// content — catches the case where the encoder accepted yuva420p
// input but dropped the alpha sub-stream silently (a uniform
// alpha plane would mask any plan-time bug like a misconfigured
// `needsAlpha` gate). The horizontal gradient source produces
// YMIN ≈ 0 / YMAX ≈ 255 on the alpha plane; uniform alpha would
// give YMIN == YMAX. Spread > 100 cleanly rejects the bad case.
//
// `-c:v libvpx-vp9` before `-i` is load-bearing: ffmpeg's default
// VP9 decoder strips the BlockAdditional alpha track when
// decoding to non-rgba pixel formats; forcing the libvpx-vp9
// decoder + `-pix_fmt rgba` is how the alpha plane comes back.
const statsResult = runFfmpegSync([
"-hide_banner",
"-v",
"error",
"-c:v",
"libvpx-vp9",
"-i",
alphaOutputPath,
"-pix_fmt",
"rgba",
"-vf",
"extractplanes=a,signalstats,metadata=mode=print:file=-",
"-f",
"null",
"-",
]);
if (statsResult.exitCode !== 0) {
throw new Error(
`[alpha smoke signalstats] failed (exit ${statsResult.exitCode}): ` +
`${statsResult.stderr.slice(-500)}`,
);
}
const yminMatch = statsResult.stdout.match(/lavfi\.signalstats\.YMIN=(\d+)/);
const ymaxMatch = statsResult.stdout.match(/lavfi\.signalstats\.YMAX=(\d+)/);
if (!yminMatch || !ymaxMatch) {
throw new Error(
`[alpha smoke signalstats] could not parse YMIN/YMAX from output: ` +
`${statsResult.stdout.slice(0, 500)}`,
);
}
const ymin = Number.parseInt(yminMatch[1], 10);
const ymax = Number.parseInt(ymaxMatch[1], 10);
expect(ymax - ymin).toBeGreaterThan(100);
expect(statSync(alphaOutputPath).size).toBeGreaterThan(0);
} finally {
rmSync(alphaRoot, { recursive: true, force: true });
}
});
});
@@ -0,0 +1,71 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>chunk-boundary: anime.js</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/animejs@4.0.2/lib/anime.iife.min.js"></script>
<style>
body,
html {
margin: 0;
padding: 0;
width: 320px;
height: 180px;
background: #0f172a;
overflow: hidden;
}
#main-comp {
position: relative;
width: 320px;
height: 180px;
}
#box {
position: absolute;
left: 0;
top: 70px;
width: 40px;
height: 40px;
background: #ec4899;
border-radius: 6px;
}
</style>
</head>
<body>
<div
id="main-comp"
data-composition-id="main-comp"
data-width="320"
data-height="180"
data-start="0"
data-duration="2"
>
<div id="box"></div>
</div>
<script>
window.__hfAnime = window.__hfAnime || [];
const tl = anime.createTimeline({ autoplay: false });
tl.add(
"#box",
{ translateX: [0, 280], rotate: [0, 360], duration: 2000, ease: "linear" },
0,
);
window.__hfAnime.push({
seek: function (globalTimeMs) {
tl.seek(globalTimeMs);
},
pause: function () {
tl.pause();
},
play: function () {
tl.play();
},
});
const dur = gsap.timeline({ paused: true });
window.__timelines = window.__timelines || {};
window.__timelines["main-comp"] = dur;
dur.to({}, { duration: 2 }, 0);
</script>
</body>
</html>
@@ -0,0 +1,67 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>chunk-boundary: CSS @keyframes</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<style>
body,
html {
margin: 0;
padding: 0;
width: 320px;
height: 180px;
background: #0f172a;
overflow: hidden;
}
#main-comp {
position: relative;
width: 320px;
height: 180px;
}
@keyframes slide {
from {
transform: translateX(0px) rotate(0deg);
}
to {
transform: translateX(280px) rotate(360deg);
}
}
#box {
position: absolute;
left: 0;
top: 70px;
width: 40px;
height: 40px;
background: #f97316;
border-radius: 6px;
animation: slide 2s linear forwards;
}
</style>
</head>
<body>
<div
id="main-comp"
data-composition-id="main-comp"
data-width="320"
data-height="180"
data-start="0"
data-duration="2"
>
<div id="box"></div>
</div>
<script>
// CSS animation is seek-driven by the HyperFrames CSS adapter — at
// each frame the runtime sets `animation-delay` so the keyframe
// playhead lands on the right point. A boundary regression here
// would show up as box position drift at frames 15, 30, 45.
// The empty GSAP timeline is just to satisfy the composition's
// duration-driver requirement.
const tl = gsap.timeline({ paused: true });
window.__timelines = window.__timelines || {};
window.__timelines["main-comp"] = tl;
tl.to({}, { duration: 2 }, 0);
</script>
</body>
</html>
@@ -0,0 +1,13 @@
{
"name": "Distributed: CSS var() font-family",
"description": "Composition that declares font-family via CSS custom properties (var(--ui-font), var(--display-font)). Exercises the fix for FONT_FETCH_FAILED when the font resolver encounters var() expressions in fail-closed distributed renders.",
"tags": ["distributed", "mp4", "h264", "sdr", "fonts", "css-variables"],
"minPsnr": 30,
"maxFrameFailures": 0,
"renderConfig": {
"fps": 30,
"chunkSize": 15
}
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7e9a156b6ee3a7dd157ec0c44a337e7e9f5e4c7bbb80159e8f2b0b901554deeb
size 71779
@@ -0,0 +1,74 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>CSS var() font-family regression fixture</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Montserrat:wght@400;700;900&display=swap");
:root {
--ui-font: "Inter";
--display-font: "Montserrat";
}
body,
html {
margin: 0;
padding: 0;
width: 640px;
height: 360px;
background: #1a1a2e;
overflow: hidden;
font-family: var(--ui-font), sans-serif;
}
#main-comp {
position: relative;
width: 640px;
height: 360px;
}
.heading {
position: absolute;
top: 30%;
left: 50%;
transform: translateX(-50%);
font-family: var(--display-font), sans-serif;
font-size: 48px;
font-weight: 900;
color: #e94560;
white-space: nowrap;
opacity: 0;
}
.body-text {
position: absolute;
top: 60%;
left: 50%;
transform: translateX(-50%);
font-family: var(--ui-font), sans-serif;
font-size: 18px;
font-weight: 400;
color: #c4c4c4;
white-space: nowrap;
opacity: 0;
}
</style>
</head>
<body>
<div id="main-comp" data-composition-id="css-var-fonts" data-width="640" data-height="360" data-start="0">
<div class="heading">CSS Variable Fonts</div>
<div class="body-text">var(--ui-font) and var(--display-font)</div>
</div>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline();
tl.to(".heading", { opacity: 1, duration: 0.5 });
tl.to(".body-text", { opacity: 1, duration: 0.5 }, "+=0.2");
tl.to({}, { duration: 1 });
window.__timelines["css-var-fonts"] = tl;
</script>
</body>
</html>
@@ -0,0 +1,57 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>chunk-boundary: GSAP</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<style>
body,
html {
margin: 0;
padding: 0;
width: 320px;
height: 180px;
background: #0f172a;
overflow: hidden;
}
#main-comp {
position: relative;
width: 320px;
height: 180px;
}
#box {
position: absolute;
left: 0;
top: 70px;
width: 40px;
height: 40px;
background: #6366f1;
border-radius: 6px;
}
</style>
</head>
<body>
<div
id="main-comp"
data-composition-id="main-comp"
data-width="320"
data-height="180"
data-start="0"
data-duration="2"
>
<div id="box"></div>
</div>
<script>
// Single GSAP timeline tween that straddles every chunk seam at
// chunkSize=15 (frames 15, 30, 45). Box translates from x=0 to x=280
// continuously across the 2s duration. Identical state on every
// frame regardless of chunk count.
const tl = gsap.timeline({ paused: true });
window.__timelines = window.__timelines || {};
window.__timelines["main-comp"] = tl;
tl.to("#box", { x: 280, duration: 2, ease: "none" }, 0);
tl.to("#box", { rotation: 360, duration: 2, ease: "none" }, 0);
</script>
</body>
</html>
@@ -0,0 +1,129 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>chunk-boundary: Lottie</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bodymovin/5.12.2/lottie.min.js"></script>
<style>
body,
html {
margin: 0;
padding: 0;
width: 320px;
height: 180px;
background: #0f172a;
overflow: hidden;
}
#main-comp {
position: relative;
width: 320px;
height: 180px;
}
#lottie-host {
position: absolute;
inset: 0;
}
</style>
</head>
<body>
<div
id="main-comp"
data-composition-id="main-comp"
data-width="320"
data-height="180"
data-start="0"
data-duration="2"
>
<div id="lottie-host"></div>
</div>
<script>
// Minimal Lottie JSON: 60-frame composition (30fps × 2s) with a
// single rectangle layer animating its position from x=20 to x=280
// linearly. The chunk-boundary test asserts N=1 == N=4 byte-identical
// — any Lottie sub-frame interpolation drift across seams trips it.
const lottieJson = {
v: "5.7.0",
fr: 30,
ip: 0,
op: 60,
w: 320,
h: 180,
nm: "boundary",
ddd: 0,
assets: [],
layers: [
{
ddd: 0,
ind: 1,
ty: 4,
nm: "rect",
sr: 1,
ks: {
o: { a: 0, k: 100 },
p: {
a: 1,
k: [
{
i: { x: [1], y: [1] },
o: { x: [0], y: [0] },
t: 0,
s: [20, 90],
},
{ t: 60, s: [280, 90] },
],
},
r: {
a: 1,
k: [
{
i: { x: [1], y: [1] },
o: { x: [0], y: [0] },
t: 0,
s: [0],
},
{ t: 60, s: [360] },
],
},
s: { a: 0, k: [100, 100, 100] },
a: { a: 0, k: [0, 0, 0] },
},
shapes: [
{
ty: "rc",
p: { a: 0, k: [0, 0] },
s: { a: 0, k: [40, 40] },
r: { a: 0, k: 6 },
},
{
ty: "fl",
c: { a: 0, k: [0.337, 0.408, 0.941, 1] },
o: { a: 0, k: 100 },
},
],
ip: 0,
op: 60,
st: 0,
bm: 0,
},
],
};
const anim = lottie.loadAnimation({
container: document.getElementById("lottie-host"),
renderer: "svg",
loop: false,
autoplay: false,
animationData: lottieJson,
});
window.__hfLottie = window.__hfLottie || [];
window.__hfLottie.push(anim);
const dur = gsap.timeline({ paused: true });
window.__timelines = window.__timelines || {};
window.__timelines["main-comp"] = dur;
dur.to({}, { duration: 2 }, 0);
</script>
</body>
</html>
@@ -0,0 +1,17 @@
{
"name": "Distributed: mov ProRes",
"description": "60-frame composition (2s @ 30fps) with transparent background, text, and a rotating SVG icon. Output is mov + ProRes 4444 with alpha. ProRes is intra-only (every frame is a keyframe), so concat-copy at assemble time is structurally trivial — this fixture pins the orthogonal contract: that `-c copy` across N=4 chunks preserves QuickTime atoms (moov, mvhd, trak, mdia) intact so the assembled file plays in editors that consume mov directly (Premiere, FCPX, DaVinci).",
"tags": ["distributed", "mov", "prores", "alpha"],
"minPsnr": 30,
"maxFrameFailures": 0,
"minAudioCorrelation": 0.9,
"maxAudioLagWindows": 120,
"renderConfig": {
"fps": 30,
"format": "mov",
"chunkSize": 15
}
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:697925f9de650010084992df21e9fa8225bb7ac9ae5df185b9596d64a9d0c8f2
size 3848214
@@ -0,0 +1,109 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>mov ProRes distributed fixture</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap");
body,
html {
margin: 0;
padding: 0;
width: 640px;
height: 360px;
/* Transparent background so the mov's alpha channel carries real data. */
background: transparent;
overflow: hidden;
font-family: "Space Mono", monospace;
}
#main-comp {
position: relative;
width: 640px;
height: 360px;
}
.label {
position: absolute;
top: 22%;
left: 50%;
transform: translateX(-50%);
font-size: 18px;
letter-spacing: 4px;
color: #94a3b8;
text-transform: uppercase;
}
.title {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-family: "Space Mono", monospace;
font-size: 56px;
font-weight: 700;
color: #6366f1;
white-space: nowrap;
}
.stage {
position: absolute;
inset: 0;
}
.icon {
position: absolute;
bottom: 18%;
left: 50%;
transform: translate(-50%, 0);
width: 48px;
height: 48px;
}
</style>
</head>
<body>
<div
id="main-comp"
data-composition-id="main-comp"
data-width="640"
data-height="360"
data-start="0"
data-duration="2"
>
<div class="stage" id="stage-a">
<div class="label">PHASE</div>
<div class="title" id="title-a">PRORES&nbsp;ONE</div>
</div>
<div class="stage" id="stage-b" style="opacity: 0">
<div class="label">PHASE</div>
<div class="title" id="title-b">PRORES&nbsp;TWO</div>
</div>
<svg class="icon" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" id="icon">
<circle cx="24" cy="24" r="18" fill="none" stroke="#6366f1" stroke-width="4" />
<circle cx="24" cy="24" r="6" fill="#6366f1" />
</svg>
</div>
<script>
// Same chunk-seam stressors as the mp4 siblings: a crossfade straddling
// the frame-30 seam (0.9-1.1s = frames 27-33) and a continuous icon
// rotation across every chunk boundary. The point with ProRes is the
// QuickTime atom contract — frame-level continuity is structurally
// satisfied because ProRes is intra-only.
const tl = gsap.timeline({ paused: true });
window.__timelines = window.__timelines || {};
window.__timelines["main-comp"] = tl;
const stageA = document.getElementById("stage-a");
const stageB = document.getElementById("stage-b");
const icon = document.getElementById("icon");
tl.to(stageA, { opacity: 0, duration: 0.2, ease: "none" }, 0.9);
tl.to(stageB, { opacity: 1, duration: 0.2, ease: "none" }, 0.9);
tl.to(icon, { rotation: 360, duration: 2, ease: "none" }, 0);
</script>
</body>
</html>
@@ -0,0 +1,16 @@
{
"name": "Distributed: mp4 H.264 SDR",
"description": "60-frame composition (2s @ 30fps) with text, a crossfade transition, and a small inline-SVG image. renderConfig.chunkSize=15 produces exactly N=4 chunks, exercising libx264's closed-GOP + concat-copy contract end-to-end.",
"tags": ["distributed", "mp4", "h264", "sdr"],
"minPsnr": 30,
"maxFrameFailures": 0,
"minAudioCorrelation": 0.9,
"maxAudioLagWindows": 120,
"renderConfig": {
"fps": 30,
"chunkSize": 15
}
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6c6137ec9cb1117e09d8da79b0dc18d160bdc750d5311e3752c353046ccac7f2
size 62079
@@ -0,0 +1,121 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>mp4 H.264 SDR distributed fixture</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap");
body,
html {
margin: 0;
padding: 0;
width: 640px;
height: 360px;
background: #0f172a;
overflow: hidden;
font-family: "Space Mono", monospace;
}
#main-comp {
position: relative;
width: 640px;
height: 360px;
}
.stage {
position: absolute;
inset: 0;
}
.label {
position: absolute;
top: 22%;
left: 50%;
transform: translateX(-50%);
font-size: 18px;
letter-spacing: 4px;
color: #94a3b8;
text-transform: uppercase;
}
.title {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-family: "Space Mono", monospace;
font-size: 56px;
font-weight: 700;
color: #6366f1;
white-space: nowrap;
}
.icon {
position: absolute;
bottom: 18%;
left: 50%;
transform: translate(-50%, 0);
width: 48px;
height: 48px;
}
</style>
</head>
<body>
<div
id="main-comp"
data-composition-id="main-comp"
data-width="640"
data-height="360"
data-start="0"
data-duration="2"
>
<div class="stage" id="stage-a">
<div class="label">CHUNK</div>
<div class="title" id="title-a">PHASE&nbsp;ONE</div>
</div>
<div class="stage" id="stage-b" style="opacity: 0">
<div class="label">CHUNK</div>
<div class="title" id="title-b">PHASE&nbsp;TWO</div>
</div>
<!-- Small inline SVG icon; no external image fetch required. -->
<svg class="icon" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" id="icon">
<circle cx="24" cy="24" r="18" fill="none" stroke="#6366f1" stroke-width="4" />
<circle cx="24" cy="24" r="6" fill="#6366f1" />
</svg>
<!--
No audio element on purpose: AAC frame quantization pads a 2-second
silent track past 2.0s of container time, which extends format.duration
past nb_frames / fps and trips the harness PSNR sampler at the very
last checkpoint. The chunk-boundary contracts this fixture pins are
video-only; omitting audio keeps container duration == 2.0s exactly.
-->
</div>
<script>
// Build a single timeline pinned to the composition's wall-clock so
// every frame is fully determined by the seek position. Chunk
// boundaries at frames {15, 30, 45} sit inside the crossfade window
// (frames 27-33 = 0.9s-1.1s) and the icon rotation — both of which
// therefore exercise per-chunk state continuity.
const tl = gsap.timeline({ paused: true });
window.__timelines = window.__timelines || {};
window.__timelines["main-comp"] = tl;
const stageA = document.getElementById("stage-a");
const stageB = document.getElementById("stage-b");
const icon = document.getElementById("icon");
// Crossfade A → B straddling the frame-30 chunk seam.
tl.to(stageA, { opacity: 0, duration: 0.2, ease: "none" }, 0.9);
tl.to(stageB, { opacity: 1, duration: 0.2, ease: "none" }, 0.9);
// Continuous icon rotation — the absolute angle at any time must match
// across chunk boundaries, so a state-keeping regression in the engine's
// virtual clock would show up as a rotation discontinuity at frame 15,
// 30, or 45.
tl.to(icon, { rotation: 360, duration: 2, ease: "none" }, 0);
</script>
</body>
</html>
@@ -0,0 +1,18 @@
{
"name": "Distributed: mp4 H.265 SDR",
"description": "60-frame composition (2s @ 30fps) with text, a crossfade transition, and a small rotating SVG icon. renderConfig.format=mp4 + codec=h265 routes the distributed pipeline through libx265 with closed-GOP keyint params (min-keyint=N:scenecut=0:open-gop=0:repeat-headers=1). The in-process baseline is rendered as h264 because the in-process renderer doesn't expose a codec hint — the harness's PSNR comparison therefore measures 'libx265 chunked + concat' against 'libx264 single-pass', catching gross codec failures while accepting normal cross-codec PSNR drift (~30-35 dB at standard quality).",
"tags": ["distributed", "mp4", "h265", "hevc", "sdr"],
"minPsnr": 30,
"maxFrameFailures": 0,
"minAudioCorrelation": 0.9,
"maxAudioLagWindows": 120,
"renderConfig": {
"fps": 30,
"format": "mp4",
"codec": "h265",
"chunkSize": 15
}
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c62e51a616e24c4ba16504a8508bafa58866bd3dabc68cbf32076a49eabb53bd
size 59699
@@ -0,0 +1,115 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>mp4 H.265 SDR distributed fixture</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap");
body,
html {
margin: 0;
padding: 0;
width: 640px;
height: 360px;
background: #0f172a;
overflow: hidden;
font-family: "Space Mono", monospace;
}
#main-comp {
position: relative;
width: 640px;
height: 360px;
}
.label {
position: absolute;
top: 22%;
left: 50%;
transform: translateX(-50%);
font-size: 18px;
letter-spacing: 4px;
color: #94a3b8;
text-transform: uppercase;
}
.title {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-family: "Space Mono", monospace;
font-size: 56px;
font-weight: 700;
color: #6366f1;
white-space: nowrap;
}
.stage {
position: absolute;
inset: 0;
}
.icon {
position: absolute;
bottom: 18%;
left: 50%;
transform: translate(-50%, 0);
width: 48px;
height: 48px;
}
</style>
</head>
<body>
<div
id="main-comp"
data-composition-id="main-comp"
data-width="640"
data-height="360"
data-start="0"
data-duration="2"
>
<div class="stage" id="stage-a">
<div class="label">CODEC</div>
<div class="title" id="title-a">HEVC&nbsp;ONE</div>
</div>
<div class="stage" id="stage-b" style="opacity: 0">
<div class="label">CODEC</div>
<div class="title" id="title-b">HEVC&nbsp;TWO</div>
</div>
<svg class="icon" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" id="icon">
<circle cx="24" cy="24" r="18" fill="none" stroke="#6366f1" stroke-width="4" />
<circle cx="24" cy="24" r="6" fill="#6366f1" />
</svg>
<!--
No audio element on purpose: AAC frame quantization pads a 2-second
silent track past 2.0s of container time, which extends format.duration
past nb_frames / fps and trips the harness PSNR sampler at the very
last checkpoint. The chunk-boundary contracts this fixture pins are
video-only; omitting audio keeps container duration == 2.0s exactly.
(Same rationale as mp4-h264-sdr.)
-->
</div>
<script>
// Same chunk-seam stressors as the mp4-h264-sdr sibling: crossfade
// straddles the frame-30 seam (0.9-1.1s = frames 27-33); continuous
// icon rotation makes per-frame angle a pure function of seek-position
// so virtual-clock drift surfaces as discontinuity at frame 15/30/45.
const tl = gsap.timeline({ paused: true });
window.__timelines = window.__timelines || {};
window.__timelines["main-comp"] = tl;
const stageA = document.getElementById("stage-a");
const stageB = document.getElementById("stage-b");
const icon = document.getElementById("icon");
tl.to(stageA, { opacity: 0, duration: 0.2, ease: "none" }, 0.9);
tl.to(stageB, { opacity: 1, duration: 0.2, ease: "none" }, 0.9);
tl.to(icon, { rotation: 360, duration: 2, ease: "none" }, 0);
</script>
</body>
</html>
@@ -0,0 +1,17 @@
{
"name": "Distributed: png-sequence",
"description": "60-frame composition (2s @ 30fps) with transparent background, text, and a rotating SVG icon. Output is a directory of zero-padded RGBA PNGs; the assemble path merges chunk frame directories rather than concat-copying mp4 files. renderConfig.chunkSize=15 produces N=4 chunks, exercising the per-frame state continuity across chunk seams that distinguishes the assemble path's directory-merge from mp4's concat-copy. maxFrameFailures=0 makes this a strict byte-identity gate; the upstream byte sources are Chrome's CDP screenshot output and libpng's deflate, so a Chromium or zlib bump in Dockerfile.test will produce identical pixels but different bytes — the failure mode on a Chrome version bump is 'regenerate baselines via docker:test:update', not 'investigate regression'.",
"tags": ["distributed", "png-sequence", "alpha"],
"minPsnr": 30,
"maxFrameFailures": 0,
"minAudioCorrelation": 0.9,
"maxAudioLagWindows": 120,
"renderConfig": {
"fps": 30,
"format": "png-sequence",
"chunkSize": 15
}
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4933d5dd2abc677fa661d5e94b673f26fed05b7921b252d0a65da0e17f51824f
size 7997
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9195c93140f142bfbded3a1f5758141f87bcb11eaa8ec8350a37eef7c0902fe4
size 9205
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e8a5d321bcbf9edff83068102f14ff4ea599034d1a97a33b684c5c33f384fa73
size 9261
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3daa0e36f959ce465b04db4180733ed9c309bdc13851a3caedd0261d004401f9
size 9272
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bef89116d6825d572d9c655e8a01cb0e391087e1175d70ececc90ee6941b148f
size 9249
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1100827ceda13433b600ea226331eb3f8a1e9f73517d73381e626302d5e3b675
size 9322
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bcd7d85837277d51b52804bb0761c93ac49d2a5d0d1e11d1c411e1f4386d4f7b
size 9302
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fc9826a6adbbf40cca08779aa9c6e8564e1d39902eb4c052c517d0574324d699
size 9371
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6fa8146dcc47b6a78a5b55db9289693772aaa2c366ff7ee124b7053155af058c
size 9385
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d0c8bf011435e3e229f615e081d2933fca5c8937505465a8481582ac46de94cb
size 9285
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3baf13eab4045857adc8aa75bf1cc4fe4fa829c50ef6c857eab0bd3c28a45293
size 9303
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b71dfee4cd91267f4d6ea65a2d6c846dd5ac5689cfe8781a90ded5c10ca50afa
size 9236
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:737a38a778b722d280ec11986650c57e72466245687db964222bbe47a085aea5
size 9265
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b3bc25a8bcf17103040f6bec9642e09f155d3115397c6ba2b59dd5d60314bf92
size 9286
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bf033c619debdcaa970ef0a32e67001ee3d2caee6df54d671f23003008ca7ef7
size 9204
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4933d5dd2abc677fa661d5e94b673f26fed05b7921b252d0a65da0e17f51824f
size 7997
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9195c93140f142bfbded3a1f5758141f87bcb11eaa8ec8350a37eef7c0902fe4
size 9205
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e8a5d321bcbf9edff83068102f14ff4ea599034d1a97a33b684c5c33f384fa73
size 9261
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3daa0e36f959ce465b04db4180733ed9c309bdc13851a3caedd0261d004401f9
size 9272
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bef89116d6825d572d9c655e8a01cb0e391087e1175d70ececc90ee6941b148f
size 9249
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1100827ceda13433b600ea226331eb3f8a1e9f73517d73381e626302d5e3b675
size 9322
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3de351c6f5e536ea58347cddb373b7521eb146558ed058a2609cd3e59e46bd62
size 9301
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fc9826a6adbbf40cca08779aa9c6e8564e1d39902eb4c052c517d0574324d699
size 9371
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:90b58af819c992b79c43b9681028f5f9f2467253c36a16dbb927e77fbad27bc1
size 9389
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:49e4a510bfe9c2a983cdc93a9760ade16e8da6818ac75b21eff3eea17218f3f8
size 9285
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3baf13eab4045857adc8aa75bf1cc4fe4fa829c50ef6c857eab0bd3c28a45293
size 9303
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b71dfee4cd91267f4d6ea65a2d6c846dd5ac5689cfe8781a90ded5c10ca50afa
size 9236
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:737a38a778b722d280ec11986650c57e72466245687db964222bbe47a085aea5
size 9265
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:40e724c558e8e1597943ec672224cd2e18ca8a5100fad3f153bca8bca0df4969
size 11311
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:94e3e8ce6cbffa74557fcc76ecc561471d3d49ecdb3591a0fe01b8e14013fe5a
size 11391
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:48602bb09bff3d79210729565bdb524488149574cae55eeda53671eae88e01dc
size 10344
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:389ae5df4c313fbe65043e3edc8e1d1a64d58a195c006c60c089164700cb12de
size 11471
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d9552049a6b5c263b350d98d86a3f1e9ba474a0cba8af2d8899903c3a2b401d0
size 11467
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7b00cd02037d594c4576f0413d61d6a88de62cca131de314d002dbf33c6292aa
size 10046
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:46d60ddd91ec1d7185aaeb6686d6add1aed7d81aae53612ac2edbf6a7d93c345
size 10031
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c5e13cd286a3233563cdbeb4ba5b1b2bab04b4cb29bac5c93e5702744d11c088
size 10110
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:633d22d5463e34c3fb2c6335b249467cb9807a04980d05eb0bbca6d82f203b76
size 10093
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f0f2982c19feb426c3d813a1e552d8d89f10b244744cb2664c31404d918e5471
size 10156
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6019e56d9876d24e67ee8f3458f9d2073e8af0be9852876ea3e377c79b24fae6
size 10164
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:69839d2376bb82be36e1ebb4b3250c2b8d8e554285008c1ae30b6bc1d52f1752
size 10070
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9f1d1e98f9d5a24ce456a3a13c4b47d83f602a3c3e1a61c72726335d62e14657
size 10093
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bc31fc2b24e5a97e8ffbf87e8428354adb35eec131693ddd171f8485017477c4
size 10020
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:48a14b5afb08bd5770f8b462df9ee5077837ca0c0235b74c94bc158e97e0519c
size 10040
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4de5c8e9be3371102aef78373ae26173c6cd90014a4ea38a948650b3631644f6
size 10070
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:093dd3500e5d1897547fc58b9555692f678a4a9fb85b01963ebf0035c59ac36c
size 9990
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8315981ec4add959e8e7807cf5d80e02b1f2c9d87f4606bb0ecf3d593e4d72db
size 8828
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3bd6e235efe18a6f3d2f17a77706dbb3b5418fcc936908f5f7ff7649c0bfc757
size 9988
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6d989e45af9527ca8402746a8b141a5c4133eac643c933c0f0d3ca9ddd37950a
size 10043
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7b00cd02037d594c4576f0413d61d6a88de62cca131de314d002dbf33c6292aa
size 10046
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:46d60ddd91ec1d7185aaeb6686d6add1aed7d81aae53612ac2edbf6a7d93c345
size 10031
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c5e13cd286a3233563cdbeb4ba5b1b2bab04b4cb29bac5c93e5702744d11c088
size 10110
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f648a1174c6d3cd13a721bc0ccc144e89d0df5fed15eeaec1a67f6d5f55377b0
size 10097
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f0f2982c19feb426c3d813a1e552d8d89f10b244744cb2664c31404d918e5471
size 10156
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6019e56d9876d24e67ee8f3458f9d2073e8af0be9852876ea3e377c79b24fae6
size 10164
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:69839d2376bb82be36e1ebb4b3250c2b8d8e554285008c1ae30b6bc1d52f1752
size 10070
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9f1d1e98f9d5a24ce456a3a13c4b47d83f602a3c3e1a61c72726335d62e14657
size 10093
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bc31fc2b24e5a97e8ffbf87e8428354adb35eec131693ddd171f8485017477c4
size 10020
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:48a14b5afb08bd5770f8b462df9ee5077837ca0c0235b74c94bc158e97e0519c
size 10040
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4de5c8e9be3371102aef78373ae26173c6cd90014a4ea38a948650b3631644f6
size 10070
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:093dd3500e5d1897547fc58b9555692f678a4a9fb85b01963ebf0035c59ac36c
size 9990
@@ -0,0 +1,109 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>png-sequence distributed fixture</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap");
body,
html {
margin: 0;
padding: 0;
width: 640px;
height: 360px;
/* Transparent background so the png-sequence's alpha channel actually carries information. */
background: transparent;
overflow: hidden;
font-family: "Space Mono", monospace;
}
#main-comp {
position: relative;
width: 640px;
height: 360px;
}
.label {
position: absolute;
top: 22%;
left: 50%;
transform: translateX(-50%);
font-size: 18px;
letter-spacing: 4px;
color: #94a3b8;
text-transform: uppercase;
}
.title {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-family: "Space Mono", monospace;
font-size: 56px;
font-weight: 700;
color: #6366f1;
white-space: nowrap;
}
.stage {
position: absolute;
inset: 0;
}
.icon {
position: absolute;
bottom: 18%;
left: 50%;
transform: translate(-50%, 0);
width: 48px;
height: 48px;
}
</style>
</head>
<body>
<div
id="main-comp"
data-composition-id="main-comp"
data-width="640"
data-height="360"
data-start="0"
data-duration="2"
>
<div class="stage" id="stage-a">
<div class="label">PHASE</div>
<div class="title" id="title-a">ALPHA&nbsp;ONE</div>
</div>
<div class="stage" id="stage-b" style="opacity: 0">
<div class="label">PHASE</div>
<div class="title" id="title-b">ALPHA&nbsp;TWO</div>
</div>
<svg class="icon" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" id="icon">
<circle cx="24" cy="24" r="18" fill="none" stroke="#6366f1" stroke-width="4" />
<circle cx="24" cy="24" r="6" fill="#6366f1" />
</svg>
</div>
<script>
// Same chunk-boundary stressors as the mp4-h264-sdr fixture: a
// crossfade straddling frame 30 and a continuous rotation across
// every chunk seam. A png-sequence regression at a seam would show
// up as a sudden brightness step or rotation discontinuity in the
// boundary frame.
const tl = gsap.timeline({ paused: true });
window.__timelines = window.__timelines || {};
window.__timelines["main-comp"] = tl;
const stageA = document.getElementById("stage-a");
const stageB = document.getElementById("stage-b");
const icon = document.getElementById("icon");
tl.to(stageA, { opacity: 0, duration: 0.2, ease: "none" }, 0.9);
tl.to(stageB, { opacity: 1, duration: 0.2, ease: "none" }, 0.9);
tl.to(icon, { rotation: 360, duration: 2, ease: "none" }, 0);
</script>
</body>
</html>
@@ -0,0 +1,16 @@
{
"name": "Distributed: Three.js WebGL boundary (setTimeout-deferred)",
"description": "Same Three.js purple cube as three-boundary, but the entire GSAP timeline and Three.js setup is deferred via setTimeout(fn, 0). Exercises the maybePublishRenderReady race where __hfTimelinesBuilding starts false at DOMContentLoaded then flips true after init.ts has already run. Without the hf-timelines-built re-registration fix (#1279), this composition times out with 'Composition has zero duration'.",
"tags": ["distributed", "threejs", "webgl", "deferred"],
"minPsnr": 25,
"maxFrameFailures": 0,
"minAudioCorrelation": 0.9,
"maxAudioLagWindows": 120,
"renderConfig": {
"fps": 10,
"chunkSize": 10
}
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cc8c784064e83a62324b4968ef71d3f9c5e151ccf9f3e96a725660bb6c9bb3b9
size 22114
@@ -0,0 +1,93 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>chunk-boundary: Three.js (setTimeout-deferred)</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<style>
body,
html {
margin: 0;
padding: 0;
width: 320px;
height: 180px;
background: #0f172a;
overflow: hidden;
}
#main-comp {
position: relative;
width: 320px;
height: 180px;
}
canvas {
display: block;
width: 320px;
height: 180px;
}
</style>
</head>
<body>
<div
id="main-comp"
data-composition-id="main-comp"
data-width="320"
data-height="180"
data-start="0"
data-duration="2"
></div>
<script>
// Exercises the setTimeout-deferred timeline registration pattern from
// issue #1260. The entire GSAP timeline + Three.js setup is deferred
// past DOMContentLoaded via setTimeout(fn, 0), which triggers the
// maybePublishRenderReady race fixed in this PR.
setTimeout(function () {
var canvas = document.createElement("canvas");
canvas.width = 320;
canvas.height = 180;
document.getElementById("main-comp").appendChild(canvas);
var scene = new THREE.Scene();
scene.background = new THREE.Color(0x0f172a);
var camera = new THREE.PerspectiveCamera(45, 320 / 180, 0.1, 50);
camera.position.set(0, 0, 4);
var renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
});
renderer.setSize(320, 180, false);
var geo = new THREE.BoxGeometry(1, 1, 1);
var mat = new THREE.MeshStandardMaterial({ color: 0xa855f7 });
var cube = new THREE.Mesh(geo, mat);
scene.add(cube);
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
var light = new THREE.DirectionalLight(0xffffff, 0.7);
light.position.set(2, 2, 3);
scene.add(light);
window.__hfThreeTime = 0;
function renderFrame() {
var t = window.__hfThreeTime || 0;
cube.rotation.x = t * Math.PI;
cube.rotation.y = t * Math.PI * 0.5;
renderer.render(scene, camera);
}
window.addEventListener("hf-seek", function (e) {
if (e && e.detail && typeof e.detail.time === "number") {
window.__hfThreeTime = e.detail.time;
}
renderFrame();
});
renderFrame();
var dur = gsap.timeline({ paused: true });
window.__timelines = window.__timelines || {};
window.__timelines["main-comp"] = dur;
dur.to({}, { duration: 2, onUpdate: renderFrame }, 0);
}, 0);
</script>
</body>
</html>
@@ -0,0 +1,16 @@
{
"name": "Distributed: Three.js WebGL boundary",
"description": "2s composition (20 frames @ 10fps) with a Three.js purple cube on a dark background. Validates that the GSAP batching proxy correctly handles onUpdate callbacks in to() vars and that CDP screenshot capture composites WebGL canvas layers.",
"tags": ["distributed", "threejs", "webgl"],
"minPsnr": 25,
"maxFrameFailures": 0,
"minAudioCorrelation": 0.9,
"maxAudioLagWindows": 120,
"renderConfig": {
"fps": 10,
"chunkSize": 10
}
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cc8c784064e83a62324b4968ef71d3f9c5e151ccf9f3e96a725660bb6c9bb3b9
size 22114
@@ -0,0 +1,90 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>chunk-boundary: Three.js</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<style>
body,
html {
margin: 0;
padding: 0;
width: 320px;
height: 180px;
background: #0f172a;
overflow: hidden;
}
#main-comp {
position: relative;
width: 320px;
height: 180px;
}
canvas {
display: block;
width: 320px;
height: 180px;
}
</style>
</head>
<body>
<div
id="main-comp"
data-composition-id="main-comp"
data-width="320"
data-height="180"
data-start="0"
data-duration="2"
></div>
<script>
// Minimal Three.js scene driven by window.__hfThreeTime. The runtime
// sets this per frame; the render loop reads it and derives all
// rotation/position from time-in-seconds so output is fully
// determined by the seek position.
const canvas = document.createElement("canvas");
canvas.width = 320;
canvas.height = 180;
document.getElementById("main-comp").appendChild(canvas);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0f172a);
const camera = new THREE.PerspectiveCamera(45, 320 / 180, 0.1, 50);
camera.position.set(0, 0, 4);
const renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
renderer.setSize(320, 180, false);
const geo = new THREE.BoxGeometry(1, 1, 1);
const mat = new THREE.MeshStandardMaterial({ color: 0xa855f7 });
const cube = new THREE.Mesh(geo, mat);
scene.add(cube);
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
const light = new THREE.DirectionalLight(0xffffff, 0.7);
light.position.set(2, 2, 3);
scene.add(light);
window.__hfThreeTime = 0;
function renderFrame() {
const t = window.__hfThreeTime || 0;
cube.rotation.x = t * Math.PI;
cube.rotation.y = t * Math.PI * 0.5;
renderer.render(scene, camera);
}
// Listen for the runtime's `hf-seek` event in case the engine pushes
// a seek before our render loop polls __hfThreeTime.
window.addEventListener("hf-seek", (e) => {
if (e && e.detail && typeof e.detail.time === "number") {
window.__hfThreeTime = e.detail.time;
}
renderFrame();
});
renderFrame();
const dur = gsap.timeline({ paused: true });
window.__timelines = window.__timelines || {};
window.__timelines["main-comp"] = dur;
dur.to({}, { duration: 2, onUpdate: renderFrame }, 0);
</script>
</body>
</html>
@@ -0,0 +1,67 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>chunk-boundary: WAAPI</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<style>
body,
html {
margin: 0;
padding: 0;
width: 320px;
height: 180px;
background: #0f172a;
overflow: hidden;
}
#main-comp {
position: relative;
width: 320px;
height: 180px;
}
#box {
position: absolute;
left: 0;
top: 70px;
width: 40px;
height: 40px;
background: #22c55e;
border-radius: 6px;
}
</style>
</head>
<body>
<div
id="main-comp"
data-composition-id="main-comp"
data-width="320"
data-height="180"
data-start="0"
data-duration="2"
>
<div id="box"></div>
</div>
<script>
// Element.animate() / KeyframeEffect — the WAAPI surface. The
// HyperFrames runtime drives the animation's currentTime each frame.
// A boundary regression would manifest as box position discontinuity
// at chunk seam frames (15, 30, 45).
const box = document.getElementById("box");
const anim = box.animate(
[
{ transform: "translateX(0px) rotate(0deg)" },
{ transform: "translateX(280px) rotate(360deg)" },
],
{ duration: 2000, fill: "forwards", easing: "linear" },
);
anim.pause();
// The empty GSAP timeline owns the composition's duration; WAAPI is
// the actual animation under test.
const tl = gsap.timeline({ paused: true });
window.__timelines = window.__timelines || {};
window.__timelines["main-comp"] = tl;
tl.to({}, { duration: 2 }, 0);
</script>
</body>
</html>
@@ -0,0 +1,17 @@
{
"name": "Distributed: webm VP9",
"description": "60-frame composition (2s @ 30fps) with text and a small rotating SVG icon, rendered to webm (VP9 + yuva420p). renderConfig.format=webm routes the distributed pipeline through libvpx-vp9 with closed-GOP keyint params (-g N -keyint_min N -auto-alt-ref 0 -cpu-used 4 by default) so per-chunk VP9 output can be losslessly stitched with `ffmpeg -f concat -c copy`. The in-process baseline renders to webm too (codec=vp9, pixelFormat=yuva420p), so the harness's PSNR comparison measures 'libvpx-vp9 chunked + concat' against 'libvpx-vp9 single-pass'. Closed-GOP forces more keyframes than open-GOP, which inflates per-chunk bitrate at constant CRF; PSNR threshold is set at 30 dB to absorb the resulting cross-mode drift without masking gross regressions.",
"tags": ["distributed", "webm", "vp9", "sdr"],
"minPsnr": 30,
"maxFrameFailures": 0,
"minAudioCorrelation": 0.9,
"maxAudioLagWindows": 120,
"renderConfig": {
"fps": 30,
"format": "webm",
"chunkSize": 15
}
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:44d40fa4cd520eb59966ada4ef3eff70f5125fa580408dfd488e10ec883cd0b3
size 78674
@@ -0,0 +1,129 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>webm VP9 distributed fixture</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<style>
@import url("https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap");
body,
html {
margin: 0;
padding: 0;
width: 640px;
height: 360px;
/* Transparent background — webm + yuva420p output preserves alpha,
* which is the format's reason for existing. The renderer's
* `initTransparentBackground` helper enforces this in production.
*/
background: transparent;
overflow: hidden;
font-family: "Space Mono", monospace;
}
#main-comp {
position: relative;
width: 640px;
height: 360px;
}
.stage {
position: absolute;
inset: 0;
}
.label {
position: absolute;
top: 22%;
left: 50%;
transform: translateX(-50%);
font-size: 18px;
letter-spacing: 4px;
color: #94a3b8;
text-transform: uppercase;
}
.title {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-family: "Space Mono", monospace;
font-size: 56px;
font-weight: 700;
color: #6366f1;
white-space: nowrap;
}
.icon {
position: absolute;
bottom: 18%;
left: 50%;
transform: translate(-50%, 0);
width: 48px;
height: 48px;
}
</style>
</head>
<body>
<div
id="main-comp"
data-composition-id="main-comp"
data-width="640"
data-height="360"
data-start="0"
data-duration="2"
>
<div class="stage" id="stage-a">
<div class="label">CHUNK</div>
<div class="title" id="title-a">PHASE&nbsp;ONE</div>
</div>
<div class="stage" id="stage-b" style="opacity: 0">
<div class="label">CHUNK</div>
<div class="title" id="title-b">PHASE&nbsp;TWO</div>
</div>
<!-- Small inline SVG icon; no external image fetch required. -->
<svg class="icon" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" id="icon">
<circle cx="24" cy="24" r="18" fill="none" stroke="#6366f1" stroke-width="4" />
<circle cx="24" cy="24" r="6" fill="#6366f1" />
</svg>
<!--
No audio element on purpose. Opus frame quantization at 20ms grain
pads a 2-second silent track past 2.0s of container time, which
extends the muxed webm's duration past nb_frames/fps and trips the
harness PSNR sampler at the very last checkpoint. Omitting audio
keeps container duration == 2.0s exactly.
-->
</div>
<script>
// Build a single timeline pinned to the composition's wall-clock so
// every frame is fully determined by the seek position. Chunk
// boundaries at frames {15, 30, 45} sit inside the crossfade window
// (frames 27-33 = 0.9s-1.1s) and the icon rotation — both of which
// therefore exercise per-chunk state continuity AND VP9 closed-GOP
// alpha encoding (`-auto-alt-ref 0` so chunk seams remain
// independently decodable).
const tl = gsap.timeline({ paused: true });
window.__timelines = window.__timelines || {};
window.__timelines["main-comp"] = tl;
const stageA = document.getElementById("stage-a");
const stageB = document.getElementById("stage-b");
const icon = document.getElementById("icon");
// Crossfade A → B straddling the frame-30 chunk seam. The opacity
// animation goes through the alpha plane in yuva420p output, which
// is exactly what we want to test under chunked-concat conditions.
tl.to(stageA, { opacity: 0, duration: 0.2, ease: "none" }, 0.9);
tl.to(stageB, { opacity: 1, duration: 0.2, ease: "none" }, 0.9);
// Continuous icon rotation — the absolute angle at any time must
// match across chunk boundaries, so a state-keeping regression in
// the engine's virtual clock would show up as a rotation
// discontinuity at frame 15, 30, or 45.
tl.to(icon, { rotation: 360, duration: 2, ease: "none" }, 0);
</script>
</body>
</html>