diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml index 62e2bc503..e077fd951 100644 --- a/.github/workflows/regression.yml +++ b/.github/workflows/regression.yml @@ -64,7 +64,7 @@ jobs: - shard: shard-2 args: "style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets" - shard: shard-3 - args: "style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr" + args: "style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9" - shard: shard-4 args: "style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr" - shard: shard-5 diff --git a/packages/aws-lambda/src/sdk/validateConfig.ts b/packages/aws-lambda/src/sdk/validateConfig.ts index f959bcab1..dcda6c002 100644 --- a/packages/aws-lambda/src/sdk/validateConfig.ts +++ b/packages/aws-lambda/src/sdk/validateConfig.ts @@ -12,8 +12,9 @@ * * The check is deliberately narrow — it covers the *shape* errors any * caller could have surfaced with `tsc` if they passed a literal, plus - * the documented `webm`/`force-hdr` rejections from §5.3 of the - * distributed-rendering plan. Anything deeper (font availability, plan + * the `force-hdr` rejection (HDR mp4 isn't supported in distributed + * mode). webm was previously rejected here too; v0.7+ supports it via + * closed-GOP concat-copy. Anything deeper (font availability, plan * size cap, GPU mode at runtime) needs the actual planner. */ diff --git a/packages/producer/src/regression-harness-distributed.test.ts b/packages/producer/src/regression-harness-distributed.test.ts index a036aba36..6de6de6be 100644 --- a/packages/producer/src/regression-harness-distributed.test.ts +++ b/packages/producer/src/regression-harness-distributed.test.ts @@ -70,12 +70,9 @@ describe("checkDistributedSupport()", () => { } }); - it("rejects format=webm", () => { + it("accepts format=webm (distributed-supported via closed-GOP concat-copy)", () => { const result = checkDistributedSupport({ fps: { num: 30, den: 1 }, format: "webm" }); - expect(result.supported).toBe(false); - if (!result.supported) { - expect(result.reason).toMatch(/webm/); - } + expect(result.supported).toBe(true); }); it("rejects hdr=true", () => { diff --git a/packages/producer/src/regression-harness-distributed.ts b/packages/producer/src/regression-harness-distributed.ts index 449bced29..5283b31f1 100644 --- a/packages/producer/src/regression-harness-distributed.ts +++ b/packages/producer/src/regression-harness-distributed.ts @@ -25,9 +25,9 @@ * capture jitter, so the harness can't use it as a per-test gate. * * Not every fixture can run in distributed-simulated mode. Distributed mode - * refuses webm, HDR mp4, NTSC framerates, and non-{24,30,60} fps at plan - * time. Fixtures that don't meet the constraints are skipped — the harness - * logs the reason and the fixture is treated as "passed (skipped)" in + * refuses HDR mp4, NTSC framerates, and non-{24,30,60} fps at plan time. + * Fixtures that don't meet the constraints are skipped — the harness logs + * the reason and the fixture is treated as "passed (skipped)" in * distributed-simulated mode. */ @@ -67,15 +67,13 @@ export type DistributedSupportResult = { supported: true } | { supported: false; /** * Decide whether a fixture's `renderConfig` is one the distributed pipeline - * can actually run. The four hard gates: + * can actually run. Two hard gates: * * - fps must be `{ num: 24|30|60, den: 1 }`. `DistributedRenderConfig.fps` * accepts only the three integer values, and rationals like * `{ num: 30000, den: 1001 }` (NTSC) trip the type system at the call * site. We surface this gate in code rather than only in TS so the * harness can skip the fixture cleanly instead of throwing. - * - format must not be `webm`. `plan()` refuses webm with - * `FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED`. * - hdr must not be `true`. Distributed mode is SDR-only at v1. * * Callers that want the structured reason can read it off the returned @@ -99,13 +97,6 @@ export function checkDistributedSupport(renderConfig: { reason: `fps ${fpsNum} not in {24, 30, 60} (DistributedRenderConfig.fps is a closed set)`, }; } - const format = renderConfig.format ?? "mp4"; - if (format === "webm") { - return { - supported: false, - reason: "format=webm refused in distributed mode (VP9+matroska concat-copy is unstable)", - }; - } if (renderConfig.hdr === true) { return { supported: false, @@ -129,7 +120,7 @@ export interface RunDistributedSimulatedInput { renderedOutputPath: string; /** From the fixture's renderConfig — must pass `checkDistributedSupport`. */ fps: 24 | 30 | 60; - format: "mp4" | "mov" | "png-sequence"; + format: "mp4" | "mov" | "png-sequence" | "webm"; /** * Codec for `format: "mp4"`. Defaults to `"h264"`; pass `"h265"` to * exercise the libx265 closed-GOP path. Ignored for non-mp4 formats — diff --git a/packages/producer/src/regression-harness-lambda-local-types.ts b/packages/producer/src/regression-harness-lambda-local-types.ts index bb021951a..54d11c25f 100644 --- a/packages/producer/src/regression-harness-lambda-local-types.ts +++ b/packages/producer/src/regression-harness-lambda-local-types.ts @@ -26,7 +26,7 @@ export interface RunLambdaLocalInput { */ width: number; height: number; - format: "mp4" | "mov" | "png-sequence"; + format: "mp4" | "mov" | "png-sequence" | "webm"; codec?: "h264" | "h265"; chunkSize?: number; maxParallelChunks?: number; diff --git a/packages/producer/src/regression-harness.ts b/packages/producer/src/regression-harness.ts index b3cef543a..19d5b8f3e 100644 --- a/packages/producer/src/regression-harness.ts +++ b/packages/producer/src/regression-harness.ts @@ -94,8 +94,8 @@ type TestMetadata = { * single video file — the harness branches its comparison logic * accordingly (per-frame byte equality instead of PSNR). `"mov"` and * `"webm"` are encoded video containers that share the PSNR path with - * `"mp4"`. `"webm"` is rejected by the distributed pipeline at plan - * time; the in-process renderer accepts it. + * `"mp4"`. Distributed mode supports all four — webm goes through + * libvpx-vp9 with closed-GOP concat-copy. */ format?: "mp4" | "webm" | "mov" | "png-sequence"; /** @@ -163,7 +163,7 @@ type TestResult = { passed: boolean; /** * Set when `--mode=distributed-simulated` skips a fixture that the - * distributed pipeline can't run (webm, HDR, NTSC fps, fps∉{24,30,60}). + * distributed pipeline can't run (HDR, NTSC fps, fps∉{24,30,60}). * `passed` is `true` for skipped fixtures — skipping is a clean outcome, * not a failure — but the summary distinguishes them. */ @@ -939,19 +939,16 @@ async function runTestSuite( result.skipped = { reason: support.reason }; return result; } - // `checkDistributedSupport` already narrowed fps to {24,30,60} and - // rejected webm; the cast surfaces that guarantee to TS. + // `checkDistributedSupport` already narrowed fps to {24,30,60}; the + // cast surfaces that guarantee to TS. webm is now distributed- + // supported via closed-GOP concat-copy, so the format passes through. const fpsNum = suite.meta.renderConfig.fps.num as 24 | 30 | 60; const distributedInput = { projectDir: tempSrcDir, tempRoot, renderedOutputPath, fps: fpsNum, - // `runDistributedSimulatedRender` / `runLambdaLocalRender`'s - // `format` parameter accepts the distributed-supported set; - // the harness type allows `"webm"` too but - // `checkDistributedSupport` rejected that above. Narrow. - format: outputFormat as "mp4" | "mov" | "png-sequence", + format: outputFormat, codec: suite.meta.renderConfig.codec, chunkSize: suite.meta.renderConfig.chunkSize, maxParallelChunks: suite.meta.renderConfig.maxParallelChunks, diff --git a/packages/producer/tests/distributed/webm-vp9/meta.json b/packages/producer/tests/distributed/webm-vp9/meta.json new file mode 100644 index 000000000..5f67e58f5 --- /dev/null +++ b/packages/producer/tests/distributed/webm-vp9/meta.json @@ -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 2) 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 + } +} diff --git a/packages/producer/tests/distributed/webm-vp9/output/compiled.html b/packages/producer/tests/distributed/webm-vp9/output/compiled.html new file mode 100644 index 000000000..c2f95979e --- /dev/null +++ b/packages/producer/tests/distributed/webm-vp9/output/compiled.html @@ -0,0 +1,165 @@ + + + + + + webm VP9 distributed fixture + + + + +
+
+
CHUNK
+
PHASE ONE
+
+
+
CHUNK
+
PHASE TWO
+
+ + + + + + +
+ + + + diff --git a/packages/producer/tests/distributed/webm-vp9/output/output.webm b/packages/producer/tests/distributed/webm-vp9/output/output.webm new file mode 100644 index 000000000..d541643af --- /dev/null +++ b/packages/producer/tests/distributed/webm-vp9/output/output.webm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b98efde6993524eaec419b2a7d3c37f33dcc84602bbd89cfcb1eea41cc125506 +size 78674 diff --git a/packages/producer/tests/distributed/webm-vp9/src/index.html b/packages/producer/tests/distributed/webm-vp9/src/index.html new file mode 100644 index 000000000..e73c0db59 --- /dev/null +++ b/packages/producer/tests/distributed/webm-vp9/src/index.html @@ -0,0 +1,131 @@ + + + + + + webm VP9 distributed fixture + + + + +
+
+
CHUNK
+
PHASE ONE
+
+
+
CHUNK
+
PHASE TWO
+
+ + + + + + +
+ + + +