Skip to content

Commit 15e7001

Browse files
committed
refactor(producer): unify render requests
1 parent 20de555 commit 15e7001

10 files changed

Lines changed: 464 additions & 45 deletions

File tree

packages/cli/src/commands/render.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,25 @@ vi.mock("../utils/producer.js", () => ({
7575
producerState.resolveConfigCalls.push(overrides);
7676
return { ...overrides, resolved: true };
7777
}),
78+
createRenderRequest: vi.fn(
79+
(input: {
80+
projectDir: string;
81+
outputPath: string;
82+
engineConfig: unknown;
83+
options: object;
84+
}) => ({
85+
version: 1,
86+
projectDir: input.projectDir,
87+
outputPath: input.outputPath,
88+
options: { ...input.options, engineConfig: input.engineConfig },
89+
}),
90+
),
91+
renderConfigFromRequest: vi.fn(
92+
(request: { options: Record<string, unknown> }, runtime: { logger?: unknown }) => {
93+
const { engineConfig, ...options } = request.options;
94+
return { ...options, producerConfig: engineConfig, logger: runtime.logger };
95+
},
96+
),
7897
createRenderJob: vi.fn((config: Record<string, unknown>) => {
7998
producerState.createdJobs.push(config);
8099
return { config, progress: 100, outcome: "completed", warnings: [] };

packages/cli/src/commands/render.ts

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,8 @@ async function renderDocker(
13821382
bestEffort: options.bestEffort,
13831383
experimentalFastCapture: options.experimentalFastCapture,
13841384
pageNavigationTimeoutMs: options.pageNavigationTimeoutMs,
1385+
protocolTimeoutMs: options.protocolTimeout,
1386+
playerReadyTimeoutMs: options.playerReadyTimeout,
13851387
},
13861388
});
13871389

@@ -1475,33 +1477,38 @@ export async function renderLocal(
14751477
producer.createConsoleLogger?.(options.debug ? "debug" : "info") ?? createNoopProducerLogger(),
14761478
);
14771479

1478-
const job = producer.createRenderJob({
1479-
fps: options.fps,
1480-
quality: options.quality,
1481-
format: options.format,
1482-
gifLoop: options.gifLoop,
1483-
workers: options.workers,
1484-
useGpu: options.gpu,
1485-
logger,
1486-
producerConfig: producer.resolveConfig({
1487-
browserGpuMode: options.browserGpuMode ?? "software",
1488-
...(options.pageNavigationTimeoutMs != null
1489-
? { pageNavigationTimeout: options.pageNavigationTimeoutMs }
1490-
: {}),
1491-
...(options.protocolTimeout != null && { protocolTimeout: options.protocolTimeout }),
1492-
...(options.playerReadyTimeout != null && { playerReadyTimeout: options.playerReadyTimeout }),
1493-
...(options.vp9CpuUsed != null ? { vp9CpuUsed: options.vp9CpuUsed } : {}),
1494-
}),
1495-
hdrMode: options.hdrMode,
1496-
crf: options.crf,
1497-
videoBitrate: options.videoBitrate,
1498-
videoFrameFormat: options.videoFrameFormat,
1499-
variables: options.variables,
1500-
entryFile: options.entryFile,
1501-
outputResolution: options.outputResolution,
1502-
debug: options.debug,
1503-
strictness: options.bestEffort ? "best-effort" : "strict",
1480+
const engineConfig = producer.resolveConfig({
1481+
browserGpuMode: options.browserGpuMode ?? "software",
1482+
...(options.pageNavigationTimeoutMs != null
1483+
? { pageNavigationTimeout: options.pageNavigationTimeoutMs }
1484+
: {}),
1485+
...(options.protocolTimeout != null && { protocolTimeout: options.protocolTimeout }),
1486+
...(options.playerReadyTimeout != null && { playerReadyTimeout: options.playerReadyTimeout }),
1487+
...(options.vp9CpuUsed != null ? { vp9CpuUsed: options.vp9CpuUsed } : {}),
1488+
});
1489+
const request = producer.createRenderRequest({
1490+
projectDir,
1491+
outputPath,
1492+
engineConfig,
1493+
options: {
1494+
fps: options.fps,
1495+
quality: options.quality,
1496+
format: options.format,
1497+
gifLoop: options.gifLoop,
1498+
workers: options.workers,
1499+
useGpu: options.gpu,
1500+
hdrMode: options.hdrMode,
1501+
crf: options.crf,
1502+
videoBitrate: options.videoBitrate,
1503+
videoFrameFormat: options.videoFrameFormat,
1504+
variables: options.variables,
1505+
entryFile: options.entryFile,
1506+
outputResolution: options.outputResolution,
1507+
debug: options.debug,
1508+
strictness: options.bestEffort ? "best-effort" : "strict",
1509+
},
15041510
});
1511+
const job = producer.createRenderJob(producer.renderConfigFromRequest(request, { logger }));
15051512

15061513
const onProgress = options.quiet
15071514
? undefined

packages/cli/src/utils/dockerRunArgs.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,22 @@ describe("buildDockerRunArgs", () => {
329329
expect(args).not.toContain("--browser-timeout");
330330
});
331331

332+
it("forwards protocol and player-ready timeouts without unit conversion", () => {
333+
const args = buildDockerRunArgs({
334+
...FIXED_INPUT,
335+
options: {
336+
...BASE,
337+
protocolTimeoutMs: 240_000,
338+
playerReadyTimeoutMs: 90_000,
339+
},
340+
});
341+
342+
expect(args).toContain("--protocol-timeout");
343+
expect(args[args.indexOf("--protocol-timeout") + 1]).toBe("240000");
344+
expect(args).toContain("--player-ready-timeout");
345+
expect(args[args.indexOf("--player-ready-timeout") + 1]).toBe("90000");
346+
});
347+
332348
it("forwards rational --fps verbatim (NTSC 30000/1001)", () => {
333349
// Regression for the fps fraction-syntax feature: the rational form must
334350
// survive the host → container hop as a single `30000/1001` argument so

packages/cli/src/utils/dockerRunArgs.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ export interface DockerRenderOptions {
6666
* `--browser-timeout` flag).
6767
*/
6868
pageNavigationTimeoutMs?: number;
69+
/** CDP protocol timeout in milliseconds. */
70+
protocolTimeoutMs?: number;
71+
/** Player readiness timeout in milliseconds. */
72+
playerReadyTimeoutMs?: number;
6973
}
7074

7175
/**
@@ -152,5 +156,11 @@ export function buildDockerRunArgs(input: DockerRunArgsInput): string[] {
152156
...(options.pageNavigationTimeoutMs != null
153157
? ["--browser-timeout", String(options.pageNavigationTimeoutMs / 1000)]
154158
: []),
159+
...(options.protocolTimeoutMs != null
160+
? ["--protocol-timeout", String(options.protocolTimeoutMs)]
161+
: []),
162+
...(options.playerReadyTimeoutMs != null
163+
? ["--player-ready-timeout", String(options.playerReadyTimeoutMs)]
164+
: []),
155165
];
156166
}

packages/producer/src/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,19 @@ export {
2323
type RenderPerfSummary,
2424
type ProgressCallback,
2525
} from "./services/renderOrchestrator.js";
26+
export {
27+
RENDER_REQUEST_VERSION,
28+
createRenderRequest,
29+
distributedConfigFromRequest,
30+
parseRenderRequest,
31+
renderConfigFromRequest,
32+
renderRequestFromDistributedConfig,
33+
serializeRenderRequest,
34+
type CreateRenderRequestInput,
35+
type DistributedRenderOptions,
36+
type RenderRequest,
37+
type RenderRequestOptions,
38+
} from "./renderRequest.js";
2639
export {
2740
type BrowserDiagnosticSummary,
2841
type RenderCaptureObservability,
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { afterEach, describe, expect, it } from "vitest";
2+
import { DEFAULT_CONFIG } from "@hyperframes/engine";
3+
import {
4+
createRenderRequest,
5+
distributedConfigFromRequest,
6+
parseRenderRequest,
7+
renderConfigFromRequest,
8+
renderRequestFromDistributedConfig,
9+
serializeRenderRequest,
10+
} from "./renderRequest.js";
11+
12+
const originalForceScreenshot = process.env.PRODUCER_FORCE_SCREENSHOT;
13+
14+
afterEach(() => {
15+
if (originalForceScreenshot === undefined) delete process.env.PRODUCER_FORCE_SCREENSHOT;
16+
else process.env.PRODUCER_FORCE_SCREENSHOT = originalForceScreenshot;
17+
});
18+
19+
function request() {
20+
return createRenderRequest({
21+
projectDir: "/project",
22+
outputPath: "/output/video.mp4",
23+
engineConfig: { ...DEFAULT_CONFIG, protocolTimeout: 123_456 },
24+
options: {
25+
fps: { num: 30, den: 1 },
26+
quality: "high",
27+
format: "mp4",
28+
workers: 3,
29+
useGpu: true,
30+
strictness: "best-effort",
31+
entryFile: "compositions/main.html",
32+
crf: 18,
33+
videoFrameFormat: "png",
34+
hdrMode: "force-sdr",
35+
variables: { title: "Hello", nested: { count: 2 } },
36+
outputResolution: "landscape-4k",
37+
distributed: {
38+
width: 1920,
39+
height: 1080,
40+
codec: "h264",
41+
chunkSize: 120,
42+
maxParallelChunks: 8,
43+
cfr: true,
44+
},
45+
},
46+
});
47+
}
48+
49+
describe("RenderRequest", () => {
50+
it("round-trips through JSON without dropping nested options", () => {
51+
const value = request();
52+
expect(parseRenderRequest(serializeRenderRequest(value))).toEqual(value);
53+
});
54+
55+
it("adapts the same request to local and distributed execution", () => {
56+
const value = request();
57+
const local = renderConfigFromRequest(value);
58+
const distributed = distributedConfigFromRequest(value);
59+
60+
expect(local).toMatchObject({
61+
fps: { num: 30, den: 1 },
62+
quality: "high",
63+
format: "mp4",
64+
variables: value.options.variables,
65+
producerConfig: { protocolTimeout: 123_456 },
66+
});
67+
expect(distributed).toMatchObject({
68+
fps: 30,
69+
width: 1920,
70+
height: 1080,
71+
format: "mp4",
72+
chunkSize: 120,
73+
variables: value.options.variables,
74+
producerConfig: { protocolTimeout: 123_456 },
75+
});
76+
});
77+
78+
it("round-trips distributed adapter fields back into the shared request", () => {
79+
const value = request();
80+
const distributed = distributedConfigFromRequest(value);
81+
const reconstructed = renderRequestFromDistributedConfig({
82+
projectDir: value.projectDir,
83+
outputPath: value.outputPath,
84+
config: distributed,
85+
});
86+
87+
expect(reconstructed.options).toMatchObject({
88+
fps: value.options.fps,
89+
quality: value.options.quality,
90+
format: value.options.format,
91+
entryFile: value.options.entryFile,
92+
variables: value.options.variables,
93+
distributed: value.options.distributed,
94+
});
95+
});
96+
97+
it("snapshots environment-derived engine options once at the boundary", () => {
98+
process.env.PRODUCER_FORCE_SCREENSHOT = "true";
99+
const value = createRenderRequest({
100+
projectDir: "/project",
101+
outputPath: "/output/video.mp4",
102+
options: { fps: { num: 30, den: 1 }, quality: "standard", format: "mp4" },
103+
});
104+
process.env.PRODUCER_FORCE_SCREENSHOT = "false";
105+
106+
expect(value.options.engineConfig.forceScreenshot).toBe(true);
107+
expect(renderConfigFromRequest(value).producerConfig?.forceScreenshot).toBe(true);
108+
});
109+
110+
it("rejects unsupported distributed fps before an adapter launch", () => {
111+
const value = request();
112+
value.options.fps = { num: 30_000, den: 1_001 };
113+
expect(() => distributedConfigFromRequest(value)).toThrow("does not support fps");
114+
});
115+
});

0 commit comments

Comments
 (0)