mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
refactor(producer): unify render requests
This commit is contained in:
@@ -87,6 +87,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: [] };
|
||||
|
||||
@@ -1494,6 +1494,8 @@ async function renderDocker(
|
||||
bestEffort: options.bestEffort,
|
||||
experimentalFastCapture: options.experimentalFastCapture,
|
||||
pageNavigationTimeoutMs: options.pageNavigationTimeoutMs,
|
||||
protocolTimeoutMs: options.protocolTimeout,
|
||||
playerReadyTimeoutMs: options.playerReadyTimeout,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1629,34 +1631,39 @@ 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,
|
||||
outputResolutionAspectAgnostic: options.outputResolutionAspectAgnostic,
|
||||
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,
|
||||
outputResolutionAspectAgnostic: options.outputResolutionAspectAgnostic,
|
||||
debug: options.debug,
|
||||
strictness: options.bestEffort === false ? "strict" : "best-effort",
|
||||
},
|
||||
});
|
||||
const job = producer.createRenderJob(producer.renderConfigFromRequest(request, { logger }));
|
||||
|
||||
const onProgress = options.quiet
|
||||
? undefined
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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)]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
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",
|
||||
outputResolutionAspectAgnostic: true,
|
||||
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,
|
||||
outputResolutionAspectAgnostic: true,
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,230 @@
|
||||
import { resolveConfig, type EngineConfig, type VideoFrameFormat } from "@hyperframes/engine";
|
||||
import type { CanvasResolution, Fps } from "@hyperframes/core";
|
||||
import type { ProducerLogger } from "./logger.js";
|
||||
import type { RenderConfig } from "./services/renderOrchestrator.js";
|
||||
import type { DistributedRenderConfig } from "./services/distributed/plan.js";
|
||||
import type { SerializableDistributedRenderConfig } from "./services/distributed/renderConfigValidation.js";
|
||||
|
||||
export const RENDER_REQUEST_VERSION = 1 as const;
|
||||
|
||||
export interface DistributedRenderOptions {
|
||||
width: number;
|
||||
height: number;
|
||||
codec?: "h264" | "h265";
|
||||
chunkSize?: number;
|
||||
maxParallelChunks?: number;
|
||||
targetChunkFrames?: number;
|
||||
runtimeCap?: DistributedRenderConfig["runtimeCap"];
|
||||
rejectOnSystemFonts?: boolean;
|
||||
failClosedFontFetch?: boolean;
|
||||
cfr?: boolean;
|
||||
planDirSizeLimitBytes?: number;
|
||||
}
|
||||
|
||||
/** JSON-safe render intent shared by local, Docker, server and cloud adapters. */
|
||||
export interface RenderRequestOptions {
|
||||
fps: Fps;
|
||||
quality: "draft" | "standard" | "high";
|
||||
format: NonNullable<RenderConfig["format"]>;
|
||||
gifLoop?: number;
|
||||
workers?: number;
|
||||
useGpu?: boolean;
|
||||
debug?: boolean;
|
||||
strictness?: RenderConfig["strictness"];
|
||||
entryFile?: string;
|
||||
crf?: number;
|
||||
videoBitrate?: string;
|
||||
videoFrameFormat?: VideoFrameFormat;
|
||||
hdrMode?: RenderConfig["hdrMode"];
|
||||
variables?: Record<string, unknown>;
|
||||
outputResolution?: CanvasResolution;
|
||||
outputResolutionAspectAgnostic?: boolean;
|
||||
engineConfig: EngineConfig;
|
||||
distributed?: DistributedRenderOptions;
|
||||
}
|
||||
|
||||
export interface RenderRequest {
|
||||
version: typeof RENDER_REQUEST_VERSION;
|
||||
projectDir: string;
|
||||
outputPath: string;
|
||||
options: RenderRequestOptions;
|
||||
}
|
||||
|
||||
export interface CreateRenderRequestInput {
|
||||
projectDir: string;
|
||||
outputPath: string;
|
||||
options: Omit<RenderRequestOptions, "engineConfig">;
|
||||
engineConfig?: EngineConfig;
|
||||
engineOverrides?: Partial<EngineConfig>;
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function assertPositiveFps(options: Record<string, unknown>): void {
|
||||
const fps = options.fps;
|
||||
if (
|
||||
!isPlainObject(fps) ||
|
||||
!Number.isInteger(fps.num) ||
|
||||
!Number.isInteger(fps.den) ||
|
||||
(fps.num as number) <= 0 ||
|
||||
(fps.den as number) <= 0
|
||||
) {
|
||||
throw new Error("Render request fps must be a positive rational");
|
||||
}
|
||||
}
|
||||
|
||||
function assertRequestOptions(options: unknown): asserts options is RenderRequestOptions {
|
||||
if (!isPlainObject(options)) throw new Error("Render request options must be an object");
|
||||
assertPositiveFps(options);
|
||||
if (!["draft", "standard", "high"].includes(String(options.quality))) {
|
||||
throw new Error("Render request quality is invalid");
|
||||
}
|
||||
if (!["mp4", "webm", "mov", "png-sequence", "gif"].includes(String(options.format))) {
|
||||
throw new Error("Render request format is invalid");
|
||||
}
|
||||
if (!isPlainObject(options.engineConfig)) {
|
||||
throw new Error("Render request must contain a resolved engineConfig snapshot");
|
||||
}
|
||||
if (options.variables !== undefined && !isPlainObject(options.variables)) {
|
||||
throw new Error("Render request variables must be a JSON object");
|
||||
}
|
||||
}
|
||||
|
||||
function assertNonEmptyPath(value: unknown, field: "projectDir" | "outputPath"): void {
|
||||
if (typeof value !== "string" || value.length === 0) {
|
||||
throw new Error(`Render request ${field} must be a non-empty string`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertRenderRequest(value: unknown): asserts value is RenderRequest {
|
||||
if (!isPlainObject(value) || value.version !== RENDER_REQUEST_VERSION) {
|
||||
const version = isPlainObject(value) ? value.version : undefined;
|
||||
throw new Error(`Unsupported render request version: ${String(version)}`);
|
||||
}
|
||||
assertNonEmptyPath(value.projectDir, "projectDir");
|
||||
assertNonEmptyPath(value.outputPath, "outputPath");
|
||||
assertRequestOptions(value.options);
|
||||
}
|
||||
|
||||
export function parseRenderRequest(serialized: string | unknown): RenderRequest {
|
||||
const value = typeof serialized === "string" ? JSON.parse(serialized) : serialized;
|
||||
assertRenderRequest(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
export function serializeRenderRequest(request: RenderRequest): string {
|
||||
assertRenderRequest(request);
|
||||
return JSON.stringify(request);
|
||||
}
|
||||
|
||||
export function createRenderRequest(input: CreateRenderRequestInput): RenderRequest {
|
||||
const request = {
|
||||
version: RENDER_REQUEST_VERSION,
|
||||
projectDir: input.projectDir,
|
||||
outputPath: input.outputPath,
|
||||
options: {
|
||||
...input.options,
|
||||
engineConfig: input.engineConfig ?? resolveConfig(input.engineOverrides),
|
||||
},
|
||||
} satisfies RenderRequest;
|
||||
// A JSON round-trip both proves serializability and detaches caller-owned
|
||||
// variables/config objects before asynchronous adapters receive them.
|
||||
return parseRenderRequest(JSON.stringify(request));
|
||||
}
|
||||
|
||||
export function renderConfigFromRequest(
|
||||
request: RenderRequest,
|
||||
runtime: { logger?: ProducerLogger } = {},
|
||||
): RenderConfig {
|
||||
const { engineConfig, distributed: _distributed, ...options } = request.options;
|
||||
return {
|
||||
...options,
|
||||
producerConfig: engineConfig,
|
||||
logger: runtime.logger,
|
||||
};
|
||||
}
|
||||
|
||||
function distributedFps(fps: Fps): 24 | 30 | 60 {
|
||||
if (fps.den === 1 && (fps.num === 24 || fps.num === 30 || fps.num === 60)) return fps.num;
|
||||
throw new Error(`Distributed render does not support fps ${fps.num}/${fps.den}`);
|
||||
}
|
||||
|
||||
export function distributedConfigFromRequest(
|
||||
request: RenderRequest,
|
||||
runtime: { logger?: ProducerLogger; abortSignal?: AbortSignal } = {},
|
||||
): DistributedRenderConfig {
|
||||
const options = request.options;
|
||||
const distributed = options.distributed;
|
||||
if (!distributed) throw new Error("Render request is missing distributed options");
|
||||
if (options.format === "gif") throw new Error("Distributed render does not support gif");
|
||||
if (options.hdrMode === "force-hdr") {
|
||||
throw new Error("Distributed render does not support force-hdr");
|
||||
}
|
||||
return {
|
||||
fps: distributedFps(options.fps),
|
||||
width: distributed.width,
|
||||
height: distributed.height,
|
||||
format: options.format,
|
||||
codec: distributed.codec,
|
||||
quality: options.quality,
|
||||
crf: options.crf,
|
||||
bitrate: options.videoBitrate,
|
||||
videoFrameFormat: options.videoFrameFormat,
|
||||
outputResolution: options.outputResolution,
|
||||
chunkSize: distributed.chunkSize,
|
||||
maxParallelChunks: distributed.maxParallelChunks,
|
||||
targetChunkFrames: distributed.targetChunkFrames,
|
||||
runtimeCap: distributed.runtimeCap,
|
||||
rejectOnSystemFonts: distributed.rejectOnSystemFonts,
|
||||
failClosedFontFetch: distributed.failClosedFontFetch,
|
||||
hdrMode: options.hdrMode === "auto" ? "auto" : "force-sdr",
|
||||
cfr: distributed.cfr,
|
||||
logger: runtime.logger,
|
||||
producerConfig: options.engineConfig,
|
||||
engineConfig: options.engineConfig,
|
||||
entryFile: options.entryFile,
|
||||
abortSignal: runtime.abortSignal,
|
||||
planDirSizeLimitBytes: distributed.planDirSizeLimitBytes,
|
||||
variables: options.variables,
|
||||
};
|
||||
}
|
||||
|
||||
export function renderRequestFromDistributedConfig(input: {
|
||||
projectDir: string;
|
||||
outputPath: string;
|
||||
config: SerializableDistributedRenderConfig;
|
||||
}): RenderRequest {
|
||||
const { config } = input;
|
||||
return createRenderRequest({
|
||||
projectDir: input.projectDir,
|
||||
outputPath: input.outputPath,
|
||||
engineConfig: config.engineConfig ?? resolveConfig(),
|
||||
options: {
|
||||
fps: { num: config.fps, den: 1 },
|
||||
quality: config.quality ?? "standard",
|
||||
format: config.format,
|
||||
crf: config.crf,
|
||||
videoBitrate: config.bitrate,
|
||||
videoFrameFormat: config.videoFrameFormat,
|
||||
outputResolution: config.outputResolution,
|
||||
hdrMode: config.hdrMode ?? "force-sdr",
|
||||
entryFile: config.entryFile,
|
||||
variables: config.variables,
|
||||
distributed: {
|
||||
width: config.width,
|
||||
height: config.height,
|
||||
codec: config.codec,
|
||||
chunkSize: config.chunkSize,
|
||||
maxParallelChunks: config.maxParallelChunks,
|
||||
targetChunkFrames: config.targetChunkFrames,
|
||||
runtimeCap: config.runtimeCap,
|
||||
rejectOnSystemFonts: config.rejectOnSystemFonts,
|
||||
failClosedFontFetch: config.failClosedFontFetch,
|
||||
cfr: config.cfr,
|
||||
planDirSizeLimitBytes: config.planDirSizeLimitBytes,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
isAspectAgnosticResolutionAlias,
|
||||
type CanvasResolution,
|
||||
} from "@hyperframes/core";
|
||||
import { createRenderRequest, renderConfigFromRequest } from "./renderRequest.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -221,22 +222,26 @@ function parseRenderOverrides(body: Record<string, unknown>): {
|
||||
* the sync (`render`) and streaming (`render-stream`) handlers so the field
|
||||
* set — including `variables` and `outputResolution` — stays in one place.
|
||||
*/
|
||||
function buildRenderJobConfig(input: RenderInput, log: ProducerLogger) {
|
||||
return {
|
||||
fps: input.fps,
|
||||
quality: input.quality,
|
||||
format: input.format,
|
||||
workers: input.workers,
|
||||
useGpu: input.useGpu,
|
||||
debug: input.debug,
|
||||
strictness: input.strictness,
|
||||
entryFile: input.entryFile,
|
||||
variables: input.variables,
|
||||
outputResolution: input.outputResolution,
|
||||
outputResolutionAspectAgnostic: input.outputResolutionAspectAgnostic,
|
||||
videoFrameFormat: input.videoFrameFormat,
|
||||
logger: log,
|
||||
};
|
||||
function buildRenderJobConfig(input: RenderInput, outputPath: string, log: ProducerLogger) {
|
||||
const request = createRenderRequest({
|
||||
projectDir: input.projectDir,
|
||||
outputPath,
|
||||
options: {
|
||||
fps: input.fps,
|
||||
quality: input.quality,
|
||||
format: input.format ?? "mp4",
|
||||
workers: input.workers,
|
||||
useGpu: input.useGpu,
|
||||
debug: input.debug,
|
||||
strictness: input.strictness,
|
||||
entryFile: input.entryFile,
|
||||
variables: input.variables,
|
||||
outputResolution: input.outputResolution,
|
||||
outputResolutionAspectAgnostic: input.outputResolutionAspectAgnostic,
|
||||
videoFrameFormat: input.videoFrameFormat,
|
||||
},
|
||||
});
|
||||
return renderConfigFromRequest(request, { logger: log });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -642,7 +647,7 @@ export function createRenderHandlers(options: HandlerOptions = {}): RenderHandle
|
||||
quality: input.quality,
|
||||
});
|
||||
|
||||
const job = createRenderJob(buildRenderJobConfig(input, log));
|
||||
const job = createRenderJob(buildRenderJobConfig(input, absoluteOutputPath, log));
|
||||
|
||||
try {
|
||||
await executeRenderJob(
|
||||
@@ -718,7 +723,7 @@ export function createRenderHandlers(options: HandlerOptions = {}): RenderHandle
|
||||
|
||||
log.info("render-stream started", { requestId, projectDir: input.projectDir });
|
||||
|
||||
const job = createRenderJob(buildRenderJobConfig(input, log));
|
||||
const job = createRenderJob(buildRenderJobConfig(input, absoluteOutputPath, log));
|
||||
const abortController = new AbortController();
|
||||
const onRequestAbort = () =>
|
||||
abortController.abort(new RenderCancelledError("request_aborted"));
|
||||
|
||||
@@ -206,6 +206,8 @@ export interface DistributedRenderConfig {
|
||||
cfr?: boolean;
|
||||
|
||||
logger?: ProducerLogger;
|
||||
/** JSON-safe engine snapshot carried across cloud/process boundaries. */
|
||||
engineConfig?: EngineConfig;
|
||||
/** Optional engine config override (env vars are not read when provided). */
|
||||
producerConfig?: EngineConfig;
|
||||
/** Entry HTML file relative to `projectDir`. Defaults to `"index.html"`. */
|
||||
@@ -755,7 +757,7 @@ export async function plan(
|
||||
}
|
||||
};
|
||||
const cfg: EngineConfig = {
|
||||
...(config.producerConfig ?? resolveConfig()),
|
||||
...(config.producerConfig ?? config.engineConfig ?? resolveConfig()),
|
||||
browserGpuMode: "software",
|
||||
forceScreenshot: false,
|
||||
};
|
||||
@@ -775,7 +777,7 @@ export async function plan(
|
||||
strictness: config.strictness,
|
||||
entryFile: config.entryFile ?? "index.html",
|
||||
logger: config.logger,
|
||||
producerConfig: config.producerConfig,
|
||||
producerConfig: cfg,
|
||||
});
|
||||
const entryFile = config.entryFile ?? "index.html";
|
||||
const htmlPath = join(projectDir, entryFile);
|
||||
|
||||
@@ -203,6 +203,9 @@ export function validateDistributedRenderConfig(
|
||||
if (config.variables !== undefined) {
|
||||
validateVariablesPayload(config.variables);
|
||||
}
|
||||
if (config.engineConfig !== undefined) {
|
||||
walkVariables(config.engineConfig, "config.engineConfig", new WeakSet());
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user