Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 36 additions & 15 deletions packages/producer/src/regression-harness-distributed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ export interface RunDistributedSimulatedInput {
/** From the fixture's renderConfig — must pass `checkDistributedSupport`. */
fps: 24 | 30 | 60;
format: "mp4" | "mov" | "png-sequence";
/**
* Codec for `format: "mp4"`. Defaults to `"h264"`; pass `"h265"` to
* exercise the libx265 closed-GOP path. Ignored for non-mp4 formats —
* `plan()` throws if codec is passed with a non-mp4 format.
*/
codec?: "h264" | "h265";
/** Optional chunkSize override; defaults to the plan's 240. */
chunkSize?: number;
/** Optional maxParallelChunks override; defaults to the plan's 16. */
Expand Down Expand Up @@ -147,23 +153,38 @@ export async function runDistributedSimulatedRender(
mkdirSync(planDir, { recursive: true });
mkdirSync(chunksDir, { recursive: true });

// Step A: plan.
// Step A: plan. `codec` is only forwarded when the format actually
// accepts it — `plan()` throws if codec is set for a non-mp4 format,
// and a caller passing `format: "mov", codec: undefined` would still
// surface that field in the resulting object. We omit it conditionally
// to keep the off-path planDir identical to pre-codec-knob output.
const planResult = await plan(
input.projectDir,
{
fps: input.fps,
// Required-by-type but overridden by the composition's own attrs;
// see docstring above. Any positive integer works.
width: 1920,
height: 1080,
format: input.format,
chunkSize: input.chunkSize,
maxParallelChunks: input.maxParallelChunks,
// Force the SDR path explicitly — `auto` would still resolve to
// force-sdr in distributed mode, but pinning it here keeps the
// harness's behavior independent of any future auto-detect changes.
hdrMode: "force-sdr",
},
input.format === "mp4"
? {
fps: input.fps,
width: 1920,
height: 1080,
format: "mp4",
codec: input.codec,
chunkSize: input.chunkSize,
maxParallelChunks: input.maxParallelChunks,
hdrMode: "force-sdr",
}
: {
fps: input.fps,
// Required-by-type but overridden by the composition's own attrs;
// see docstring above. Any positive integer works.
width: 1920,
height: 1080,
format: input.format,
chunkSize: input.chunkSize,
maxParallelChunks: input.maxParallelChunks,
// Force the SDR path explicitly — `auto` would still resolve to
// force-sdr in distributed mode, but pinning it here keeps the
// harness's behavior independent of any future auto-detect changes.
hdrMode: "force-sdr",
},
planDir,
);

Expand Down
31 changes: 31 additions & 0 deletions packages/producer/src/regression-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ type TestMetadata = {
* time; the in-process renderer accepts it.
*/
format?: "mp4" | "webm" | "mov" | "png-sequence";
/**
* Codec selection for `format: "mp4"`, forwarded to
* `DistributedRenderConfig.codec`. The in-process renderer doesn't take
* a codec hint — for the baseline it always picks the format's default
* (h264 for mp4 SDR), so `codec: "h265"` is exercised exclusively in
* `--mode=distributed-simulated`. The PSNR comparison against the
* baseline therefore measures "h265 chunked + concat" ≈ "h264 single-
* pass" rather than byte equality. Fixtures asserting a tighter
* contract should explicitly pin a higher `minPsnr`.
*/
codec?: "h264" | "h265";
workers?: number; // Optional: auto-calculates if omitted
/** Force HDR in the harness; omitted/false preserves historical SDR-only test behavior. */
hdr?: boolean;
Expand Down Expand Up @@ -250,6 +261,25 @@ function validateMetadata(meta: unknown): TestMetadata {
"meta.json: 'renderConfig.format' must be 'mp4', 'webm', 'mov', or 'png-sequence' (or omit for mp4)",
);
}
if (rc.codec !== undefined && rc.codec !== "h264" && rc.codec !== "h265") {
throw new Error(
"meta.json: 'renderConfig.codec' must be 'h264' or 'h265' (or omit for the format's default)",
);
}
// Normalize the implicit default before comparing so a fixture that
// omits `format` (which defaults to "mp4" everywhere downstream) doesn't
// get accidentally treated as "format is missing, so codec is illegal."
// The previous formulation `rc.format !== undefined && rc.format !== "mp4"`
// worked but relied on the reader knowing the default; this reads the
// intent more directly.
const effectiveFormat = (rc.format as string | undefined) ?? "mp4";
if (rc.codec !== undefined && effectiveFormat !== "mp4") {
throw new Error(
`meta.json: 'renderConfig.codec' is only valid for format='mp4' (got format=${JSON.stringify(
rc.format,
)})`,
);
}
if (rc.workers !== undefined) {
if (typeof rc.workers !== "number" || rc.workers < 1) {
throw new Error("meta.json: 'renderConfig.workers' must be >= 1 (or omit to auto-calculate)");
Expand Down Expand Up @@ -822,6 +852,7 @@ async function runTestSuite(
renderedOutputPath,
fps: fpsNum,
format: outputFormat as "mp4" | "mov" | "png-sequence",
codec: suite.meta.renderConfig.codec,
chunkSize: suite.meta.renderConfig.chunkSize,
maxParallelChunks: suite.meta.renderConfig.maxParallelChunks,
variables: suite.meta.renderConfig.variables,
Expand Down
18 changes: 18 additions & 0 deletions packages/producer/tests/distributed/mp4-h265-sdr/meta.json
Original file line number Diff line number Diff line change
@@ -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
}
}
121 changes: 121 additions & 0 deletions packages/producer/tests/distributed/mp4-h265-sdr/output/compiled.html

Large diffs are not rendered by default.

Git LFS file not shown
115 changes: 115 additions & 0 deletions packages/producer/tests/distributed/mp4-h265-sdr/src/index.html
Original file line number Diff line number Diff line change
@@ -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>

Check warning

Code scanning / CodeQL

Inclusion of functionality from an untrusted source Medium test

Script loaded from content delivery network with no integrity check.
Comment thread
jrusso1020 marked this conversation as resolved.
Dismissed
<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>
Loading