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
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:
@@ -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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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 ONE</div>
|
||||
</div>
|
||||
<div class="stage" id="stage-b" style="opacity: 0">
|
||||
<div class="label">PHASE</div>
|
||||
<div class="title" id="title-b">PRORES 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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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 ONE</div>
|
||||
</div>
|
||||
<div class="stage" id="stage-b" style="opacity: 0">
|
||||
<div class="label">CHUNK</div>
|
||||
<div class="title" id="title-b">PHASE 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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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 ONE</div>
|
||||
</div>
|
||||
<div class="stage" id="stage-b" style="opacity: 0">
|
||||
<div class="label">CODEC</div>
|
||||
<div class="title" id="title-b">HEVC 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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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 ONE</div>
|
||||
</div>
|
||||
<div class="stage" id="stage-b" style="opacity: 0">
|
||||
<div class="label">PHASE</div>
|
||||
<div class="title" id="title-b">ALPHA 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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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 ONE</div>
|
||||
</div>
|
||||
<div class="stage" id="stage-b" style="opacity: 0">
|
||||
<div class="label">CHUNK</div>
|
||||
<div class="title" id="title-b">PHASE 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>
|
||||
Reference in New Issue
Block a user