Skip to content

Commit 2269a04

Browse files
jrusso1020claude
andcommitted
test(producer): add webm-vp9 distributed regression fixture
PR 8.3 of the WebM distributed-rendering plan (v1.5 backlog #1; see DISTRIBUTED-RENDERING-PLAN.md §7.2). End-to-end regression coverage for the webm distributed path PRs 8.1 and 8.2 wired up. Adds packages/producer/tests/distributed/webm-vp9/ matching the mp4-h264-sdr fixture pattern: a 2-second composition (60 frames @ 30fps) with text, a crossfade across the frame-30 chunk seam, and a continuous icon rotation — exercises chunk-boundary continuity for both display contents and VP9 closed-GOP alpha encoding. `chunkSize: 15` produces 4 chunks so 3 seams are tested, and the crossfade straddles the middle seam to surface alpha-plane discontinuities introduced by alt-ref drift. Baseline regenerated inside Dockerfile.test via `bun run --cwd packages/producer docker:test:update webm-vp9`. Runs in: - in-process mode: byte-identical match against baseline ✓ - distributed-simulated mode: PSNR 56.88-63.49 dB across 100 checkpoints, well above the 30 dB threshold ✓ Wiring updates required to let webm flow through the harness: - regression-harness-distributed.ts: - checkDistributedSupport() no longer rejects webm. HDR mp4 + NTSC fps + non-{24,30,60} fps remain rejected. - RunDistributedSimulatedInput.format widened to include webm. - Docstring + comments updated. - regression-harness-distributed.test.ts: webm-rejection test replaced with "accepts format=webm" test. - regression-harness.ts: the now-incorrect format cast at the distributed-input call site is dropped; comment about why webm was excluded is replaced with "webm is now distributed-supported". - regression-harness-lambda-local-types.ts: RunLambdaLocalInput.format widened to include webm so lambda-local mode can also exercise webm fixtures end-to-end. - aws-lambda webm support (Path A through the Lambda handler): - formatExtension.ts: DistributedFormat gains "webm" → ".webm" case. - events.ts: RenderChunkEvent / AssembleEvent / PlanLambdaResult Format widened to include webm. - sdk/validateConfig.ts: ALLOWED_FORMATS gains "webm". - handler.ts: downloadChunkObjects format param widened. The Lambda handler delegates to the producer's assemble() primitive which PR 8.2 already taught to handle webm (concat-copy + applyFaststart no-op + muxVideoWithAudio with libopus); no Lambda-side rendering changes are needed beyond the type/validation surfaces above. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f8ffe9f commit 2269a04

12 files changed

Lines changed: 338 additions & 35 deletions

packages/aws-lambda/src/events.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export interface RenderChunkEvent {
6565
/** S3 URI prefix where the chunk output should be uploaded (`s3://bucket/{prefix}/`). */
6666
ChunkOutputS3Prefix: string;
6767
/** Output container format from the plan's encoder.json; drives file vs frame-dir handling. */
68-
Format: "mp4" | "mov" | "png-sequence";
68+
Format: "mp4" | "mov" | "png-sequence" | "webm";
6969
}
7070

7171
/** Activity C: fetch planDir + all chunks + audio, assemble, upload final. */
@@ -80,7 +80,7 @@ export interface AssembleEvent {
8080
/** Final output S3 URI (`s3://bucket/key.mp4`). */
8181
OutputS3Uri: string;
8282
/** Output container format; drives file vs frame-dir handling. */
83-
Format: "mp4" | "mov" | "png-sequence";
83+
Format: "mp4" | "mov" | "png-sequence" | "webm";
8484
}
8585

8686
/**
@@ -106,7 +106,7 @@ export interface PlanLambdaResult {
106106
Fps: 24 | 30 | 60;
107107
Width: number;
108108
Height: number;
109-
Format: "mp4" | "mov" | "png-sequence";
109+
Format: "mp4" | "mov" | "png-sequence" | "webm";
110110
HasAudio: boolean;
111111
AudioS3Uri: string | null;
112112
FfmpegVersion: string;

packages/aws-lambda/src/formatExtension.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@
66
* looks like vs a png-sequence.
77
*/
88

9-
export type DistributedFormat = "mp4" | "mov" | "png-sequence";
9+
export type DistributedFormat = "mp4" | "mov" | "png-sequence" | "webm";
1010

1111
export function formatExtension(format: DistributedFormat): string {
1212
switch (format) {
1313
case "mp4":
1414
return ".mp4";
1515
case "mov":
1616
return ".mov";
17+
case "webm":
18+
return ".webm";
1719
case "png-sequence":
1820
return "";
1921
default: {

packages/aws-lambda/src/handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ async function downloadChunkObjects(
433433
s3: S3Client,
434434
uris: string[],
435435
workDir: string,
436-
format: "mp4" | "mov" | "png-sequence",
436+
format: "mp4" | "mov" | "png-sequence" | "webm",
437437
): Promise<string[]> {
438438
const chunksDir = join(workDir, "chunks");
439439
mkdirSync(chunksDir, { recursive: true });

packages/aws-lambda/src/sdk/validateConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class InvalidConfigError extends Error {
3131
}
3232

3333
const ALLOWED_FPS = [24, 30, 60] as const;
34-
const ALLOWED_FORMATS = ["mp4", "mov", "png-sequence"] as const;
34+
const ALLOWED_FORMATS = ["mp4", "mov", "png-sequence", "webm"] as const;
3535
const ALLOWED_CODECS = ["h264", "h265"] as const;
3636
const ALLOWED_QUALITIES = ["draft", "standard", "high"] as const;
3737
const ALLOWED_RUNTIME_CAPS = ["lambda", "temporal", "cloud-run-job", "k8s-job", "none"] as const;

packages/producer/src/regression-harness-distributed.test.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,9 @@ describe("checkDistributedSupport()", () => {
7070
}
7171
});
7272

73-
it("rejects format=webm", () => {
73+
it("accepts format=webm (distributed-supported via closed-GOP concat-copy)", () => {
7474
const result = checkDistributedSupport({ fps: { num: 30, den: 1 }, format: "webm" });
75-
expect(result.supported).toBe(false);
76-
if (!result.supported) {
77-
expect(result.reason).toMatch(/webm/);
78-
}
75+
expect(result.supported).toBe(true);
7976
});
8077

8178
it("rejects hdr=true", () => {

packages/producer/src/regression-harness-distributed.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
* capture jitter, so the harness can't use it as a per-test gate.
2626
*
2727
* Not every fixture can run in distributed-simulated mode. Distributed mode
28-
* refuses webm, HDR mp4, NTSC framerates, and non-{24,30,60} fps at plan
29-
* time. Fixtures that don't meet the constraints are skipped — the harness
30-
* logs the reason and the fixture is treated as "passed (skipped)" in
28+
* refuses HDR mp4, NTSC framerates, and non-{24,30,60} fps at plan time.
29+
* Fixtures that don't meet the constraints are skipped — the harness logs
30+
* the reason and the fixture is treated as "passed (skipped)" in
3131
* distributed-simulated mode.
3232
*/
3333

@@ -74,8 +74,6 @@ export type DistributedSupportResult = { supported: true } | { supported: false;
7474
* `{ num: 30000, den: 1001 }` (NTSC) trip the type system at the call
7575
* site. We surface this gate in code rather than only in TS so the
7676
* harness can skip the fixture cleanly instead of throwing.
77-
* - format must not be `webm`. `plan()` refuses webm with
78-
* `FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED`.
7977
* - hdr must not be `true`. Distributed mode is SDR-only at v1.
8078
*
8179
* Callers that want the structured reason can read it off the returned
@@ -99,13 +97,6 @@ export function checkDistributedSupport(renderConfig: {
9997
reason: `fps ${fpsNum} not in {24, 30, 60} (DistributedRenderConfig.fps is a closed set)`,
10098
};
10199
}
102-
const format = renderConfig.format ?? "mp4";
103-
if (format === "webm") {
104-
return {
105-
supported: false,
106-
reason: "format=webm refused in distributed mode (VP9+matroska concat-copy is unstable)",
107-
};
108-
}
109100
if (renderConfig.hdr === true) {
110101
return {
111102
supported: false,
@@ -129,7 +120,7 @@ export interface RunDistributedSimulatedInput {
129120
renderedOutputPath: string;
130121
/** From the fixture's renderConfig — must pass `checkDistributedSupport`. */
131122
fps: 24 | 30 | 60;
132-
format: "mp4" | "mov" | "png-sequence";
123+
format: "mp4" | "mov" | "png-sequence" | "webm";
133124
/**
134125
* Codec for `format: "mp4"`. Defaults to `"h264"`; pass `"h265"` to
135126
* exercise the libx265 closed-GOP path. Ignored for non-mp4 formats —

packages/producer/src/regression-harness-lambda-local-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export interface RunLambdaLocalInput {
2626
*/
2727
width: number;
2828
height: number;
29-
format: "mp4" | "mov" | "png-sequence";
29+
format: "mp4" | "mov" | "png-sequence" | "webm";
3030
codec?: "h264" | "h265";
3131
chunkSize?: number;
3232
maxParallelChunks?: number;

packages/producer/src/regression-harness.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ type TestMetadata = {
9494
* single video file — the harness branches its comparison logic
9595
* accordingly (per-frame byte equality instead of PSNR). `"mov"` and
9696
* `"webm"` are encoded video containers that share the PSNR path with
97-
* `"mp4"`. `"webm"` is rejected by the distributed pipeline at plan
98-
* time; the in-process renderer accepts it.
97+
* `"mp4"`. Distributed mode supports all four — webm goes through
98+
* libvpx-vp9 with closed-GOP concat-copy.
9999
*/
100100
format?: "mp4" | "webm" | "mov" | "png-sequence";
101101
/**
@@ -163,7 +163,7 @@ type TestResult = {
163163
passed: boolean;
164164
/**
165165
* Set when `--mode=distributed-simulated` skips a fixture that the
166-
* distributed pipeline can't run (webm, HDR, NTSC fps, fps∉{24,30,60}).
166+
* distributed pipeline can't run (HDR, NTSC fps, fps∉{24,30,60}).
167167
* `passed` is `true` for skipped fixtures — skipping is a clean outcome,
168168
* not a failure — but the summary distinguishes them.
169169
*/
@@ -939,19 +939,16 @@ async function runTestSuite(
939939
result.skipped = { reason: support.reason };
940940
return result;
941941
}
942-
// `checkDistributedSupport` already narrowed fps to {24,30,60} and
943-
// rejected webm; the cast surfaces that guarantee to TS.
942+
// `checkDistributedSupport` already narrowed fps to {24,30,60}; the
943+
// cast surfaces that guarantee to TS. webm is now distributed-
944+
// supported via closed-GOP concat-copy, so the format passes through.
944945
const fpsNum = suite.meta.renderConfig.fps.num as 24 | 30 | 60;
945946
const distributedInput = {
946947
projectDir: tempSrcDir,
947948
tempRoot,
948949
renderedOutputPath,
949950
fps: fpsNum,
950-
// `runDistributedSimulatedRender` / `runLambdaLocalRender`'s
951-
// `format` parameter accepts the distributed-supported set;
952-
// the harness type allows `"webm"` too but
953-
// `checkDistributedSupport` rejected that above. Narrow.
954-
format: outputFormat as "mp4" | "mov" | "png-sequence",
951+
format: outputFormat,
955952
codec: suite.meta.renderConfig.codec,
956953
chunkSize: suite.meta.renderConfig.chunkSize,
957954
maxParallelChunks: suite.meta.renderConfig.maxParallelChunks,
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "Distributed: webm VP9",
3+
"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.",
4+
"tags": ["distributed", "webm", "vp9", "sdr"],
5+
6+
"minPsnr": 30,
7+
"maxFrameFailures": 0,
8+
9+
"minAudioCorrelation": 0.9,
10+
"maxAudioLagWindows": 120,
11+
12+
"renderConfig": {
13+
"fps": 30,
14+
"format": "webm",
15+
"chunkSize": 15
16+
}
17+
}

packages/producer/tests/distributed/webm-vp9/output/compiled.html

Lines changed: 165 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)