Skip to content
Open
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
19 changes: 19 additions & 0 deletions packages/cli/src/commands/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,25 @@ vi.mock("../utils/producer.js", () => ({
producerState.resolveConfigCalls.push(overrides);
return { ...overrides, resolved: true };
}),
createRenderRequest: vi.fn(
(input: {
projectDir: string;
outputPath: string;
engineConfig: unknown;
options: object;
}) => ({
version: 1,
projectDir: input.projectDir,
outputPath: input.outputPath,
options: { ...input.options, engineConfig: input.engineConfig },
}),
),
renderConfigFromRequest: vi.fn(
(request: { options: Record<string, unknown> }, runtime: { logger?: unknown }) => {
const { engineConfig, ...options } = request.options;
return { ...options, producerConfig: engineConfig, logger: runtime.logger };
},
),
createRenderJob: vi.fn((config: Record<string, unknown>) => {
producerState.createdJobs.push(config);
return { config, progress: 100, outcome: "completed", warnings: [] };
Expand Down
59 changes: 33 additions & 26 deletions packages/cli/src/commands/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1386,6 +1386,8 @@ async function renderDocker(
bestEffort: options.bestEffort,
experimentalFastCapture: options.experimentalFastCapture,
pageNavigationTimeoutMs: options.pageNavigationTimeoutMs,
protocolTimeoutMs: options.protocolTimeout,
playerReadyTimeoutMs: options.playerReadyTimeout,
},
});

Expand Down Expand Up @@ -1479,33 +1481,38 @@ export async function renderLocal(
producer.createConsoleLogger?.(options.debug ? "debug" : "info") ?? createNoopProducerLogger(),
);

const job = producer.createRenderJob({
fps: options.fps,
quality: options.quality,
format: options.format,
gifLoop: options.gifLoop,
workers: options.workers,
useGpu: options.gpu,
logger,
producerConfig: producer.resolveConfig({
browserGpuMode: options.browserGpuMode ?? "software",
...(options.pageNavigationTimeoutMs != null
? { pageNavigationTimeout: options.pageNavigationTimeoutMs }
: {}),
...(options.protocolTimeout != null && { protocolTimeout: options.protocolTimeout }),
...(options.playerReadyTimeout != null && { playerReadyTimeout: options.playerReadyTimeout }),
...(options.vp9CpuUsed != null ? { vp9CpuUsed: options.vp9CpuUsed } : {}),
}),
hdrMode: options.hdrMode,
crf: options.crf,
videoBitrate: options.videoBitrate,
videoFrameFormat: options.videoFrameFormat,
variables: options.variables,
entryFile: options.entryFile,
outputResolution: options.outputResolution,
debug: options.debug,
strictness: options.bestEffort === false ? "strict" : "best-effort",
const engineConfig = producer.resolveConfig({
browserGpuMode: options.browserGpuMode ?? "software",
...(options.pageNavigationTimeoutMs != null
? { pageNavigationTimeout: options.pageNavigationTimeoutMs }
: {}),
...(options.protocolTimeout != null && { protocolTimeout: options.protocolTimeout }),
...(options.playerReadyTimeout != null && { playerReadyTimeout: options.playerReadyTimeout }),
...(options.vp9CpuUsed != null ? { vp9CpuUsed: options.vp9CpuUsed } : {}),
});
const request = producer.createRenderRequest({
projectDir,
outputPath,
engineConfig,
options: {
fps: options.fps,
quality: options.quality,
format: options.format,
gifLoop: options.gifLoop,
workers: options.workers,
useGpu: options.gpu,
hdrMode: options.hdrMode,
crf: options.crf,
videoBitrate: options.videoBitrate,
videoFrameFormat: options.videoFrameFormat,
variables: options.variables,
entryFile: options.entryFile,
outputResolution: options.outputResolution,
debug: options.debug,
strictness: options.bestEffort === false ? "strict" : "best-effort",
},
});
const job = producer.createRenderJob(producer.renderConfigFromRequest(request, { logger }));

const onProgress = options.quiet
? undefined
Expand Down
16 changes: 16 additions & 0 deletions packages/cli/src/utils/dockerRunArgs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,22 @@ describe("buildDockerRunArgs", () => {
expect(args).not.toContain("--browser-timeout");
});

it("forwards protocol and player-ready timeouts without unit conversion", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: {
...BASE,
protocolTimeoutMs: 240_000,
playerReadyTimeoutMs: 90_000,
},
});

expect(args).toContain("--protocol-timeout");
expect(args[args.indexOf("--protocol-timeout") + 1]).toBe("240000");
expect(args).toContain("--player-ready-timeout");
expect(args[args.indexOf("--player-ready-timeout") + 1]).toBe("90000");
});

it("forwards rational --fps verbatim (NTSC 30000/1001)", () => {
// Regression for the fps fraction-syntax feature: the rational form must
// survive the host → container hop as a single `30000/1001` argument so
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/utils/dockerRunArgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ export interface DockerRenderOptions {
* `--browser-timeout` flag).
*/
pageNavigationTimeoutMs?: number;
/** CDP protocol timeout in milliseconds. */
protocolTimeoutMs?: number;
/** Player readiness timeout in milliseconds. */
playerReadyTimeoutMs?: number;
}

/**
Expand Down Expand Up @@ -154,5 +158,11 @@ export function buildDockerRunArgs(input: DockerRunArgsInput): string[] {
...(options.pageNavigationTimeoutMs != null
? ["--browser-timeout", String(options.pageNavigationTimeoutMs / 1000)]
: []),
...(options.protocolTimeoutMs != null
? ["--protocol-timeout", String(options.protocolTimeoutMs)]
: []),
...(options.playerReadyTimeoutMs != null
? ["--player-ready-timeout", String(options.playerReadyTimeoutMs)]
: []),
];
}
13 changes: 13 additions & 0 deletions packages/producer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ export {
type RenderPerfSummary,
type ProgressCallback,
} from "./services/renderOrchestrator.js";
export {
RENDER_REQUEST_VERSION,
createRenderRequest,
distributedConfigFromRequest,
parseRenderRequest,
renderConfigFromRequest,
renderRequestFromDistributedConfig,
serializeRenderRequest,
type CreateRenderRequestInput,
type DistributedRenderOptions,
type RenderRequest,
type RenderRequestOptions,
} from "./renderRequest.js";
export {
type BrowserDiagnosticSummary,
type RenderCaptureObservability,
Expand Down
115 changes: 115 additions & 0 deletions packages/producer/src/renderRequest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { afterEach, describe, expect, it } from "vitest";
import { DEFAULT_CONFIG } from "@hyperframes/engine";
import {
createRenderRequest,
distributedConfigFromRequest,
parseRenderRequest,
renderConfigFromRequest,
renderRequestFromDistributedConfig,
serializeRenderRequest,
} from "./renderRequest.js";

const originalForceScreenshot = process.env.PRODUCER_FORCE_SCREENSHOT;

afterEach(() => {
if (originalForceScreenshot === undefined) delete process.env.PRODUCER_FORCE_SCREENSHOT;
else process.env.PRODUCER_FORCE_SCREENSHOT = originalForceScreenshot;
});

function request() {
return createRenderRequest({
projectDir: "/project",
outputPath: "/output/video.mp4",
engineConfig: { ...DEFAULT_CONFIG, protocolTimeout: 123_456 },
options: {
fps: { num: 30, den: 1 },
quality: "high",
format: "mp4",
workers: 3,
useGpu: true,
strictness: "best-effort",
entryFile: "compositions/main.html",
crf: 18,
videoFrameFormat: "png",
hdrMode: "force-sdr",
variables: { title: "Hello", nested: { count: 2 } },
outputResolution: "landscape-4k",
distributed: {
width: 1920,
height: 1080,
codec: "h264",
chunkSize: 120,
maxParallelChunks: 8,
cfr: true,
},
},
});
}

describe("RenderRequest", () => {
it("round-trips through JSON without dropping nested options", () => {
const value = request();
expect(parseRenderRequest(serializeRenderRequest(value))).toEqual(value);
});

it("adapts the same request to local and distributed execution", () => {
const value = request();
const local = renderConfigFromRequest(value);
const distributed = distributedConfigFromRequest(value);

expect(local).toMatchObject({
fps: { num: 30, den: 1 },
quality: "high",
format: "mp4",
variables: value.options.variables,
producerConfig: { protocolTimeout: 123_456 },
});
expect(distributed).toMatchObject({
fps: 30,
width: 1920,
height: 1080,
format: "mp4",
chunkSize: 120,
variables: value.options.variables,
producerConfig: { protocolTimeout: 123_456 },
});
});

it("round-trips distributed adapter fields back into the shared request", () => {
const value = request();
const distributed = distributedConfigFromRequest(value);
const reconstructed = renderRequestFromDistributedConfig({
projectDir: value.projectDir,
outputPath: value.outputPath,
config: distributed,
});

expect(reconstructed.options).toMatchObject({
fps: value.options.fps,
quality: value.options.quality,
format: value.options.format,
entryFile: value.options.entryFile,
variables: value.options.variables,
distributed: value.options.distributed,
});
});

it("snapshots environment-derived engine options once at the boundary", () => {
process.env.PRODUCER_FORCE_SCREENSHOT = "true";
const value = createRenderRequest({
projectDir: "/project",
outputPath: "/output/video.mp4",
options: { fps: { num: 30, den: 1 }, quality: "standard", format: "mp4" },
});
process.env.PRODUCER_FORCE_SCREENSHOT = "false";

expect(value.options.engineConfig.forceScreenshot).toBe(true);
expect(renderConfigFromRequest(value).producerConfig?.forceScreenshot).toBe(true);
});

it("rejects unsupported distributed fps before an adapter launch", () => {
const value = request();
value.options.fps = { num: 30_000, den: 1_001 };
expect(() => distributedConfigFromRequest(value)).toThrow("does not support fps");
});
});
Loading
Loading