Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/packages/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1346,6 +1346,7 @@ Quality and file size:
| `--video-frame-format` | `auto` | How source video frames are extracted: `auto`, `jpg`, `png`. Use `png` for UI recordings, screen captures, and other colour-sensitive footage. |
| `--hdr` | off | Force HDR even with no HDR sources. MP4 only. See [HDR rendering](/guides/hdr). |
| `--sdr` | off | Force SDR even when HDR sources are detected |
| `--page-motion-blur` | authored setting | Pass `off` to disable page motion blur without editing the composition. Applies to local, batch and Docker renders. Omit to preserve authored behavior. |

How it uses the machine, and when it gives up:

Expand Down
17 changes: 17 additions & 0 deletions packages/cli/src/commands/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,23 @@ describe("renderLocal browser GPU config", () => {
expect(producerState.createdJobs[0]?.variables).toBeUndefined();
});

it.each([undefined, "off"] as const)(
"preserves page motion blur %s in the producer request",
async (pageMotionBlur) => {
await renderLocal(PROJECT_DIR, "/tmp/out.mp4", {
fps: { num: 30, den: 1 },
quality: "standard",
format: "mp4",
gpu: false,
browserGpuMode: "software",
hdrMode: "auto",
quiet: true,
pageMotionBlur,
});
expect(producerState.createdJobs[0]?.pageMotionBlur).toBe(pageMotionBlur);
},
);

it("forwards entryFile to createRenderJob when --composition is set", async () => {
await renderLocal(PROJECT_DIR, "/tmp/out.mp4", {
fps: { num: 30, den: 1 },
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/src/commands/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ export default defineCommand({
"alpha-capable sources always extract as PNG.",
default: "auto",
},
"page-motion-blur": {
type: "string",
description:
"Set off to disable authored page motion blur. Omit to keep the composition's setting.",
},
workers: {
type: "string",
alias: "w",
Expand Down Expand Up @@ -503,6 +508,7 @@ export interface RenderOptions {
vp9CpuUsed?: number;
videoBitrate?: string;
videoFrameFormat?: VideoFrameFormat;
pageMotionBlur?: "off";
quiet: boolean;
debug?: boolean;
/** Segmented capture: reuse a prior run's validated segments. */
Expand Down Expand Up @@ -832,6 +838,7 @@ async function renderDocker(
vp9CpuUsed: options.vp9CpuUsed,
videoBitrate: options.videoBitrate,
videoFrameFormat: options.videoFrameFormat,
pageMotionBlur: options.pageMotionBlur,
quiet: options.quiet,
variables: options.variables,
entryFile: options.entryFile,
Expand Down Expand Up @@ -1110,6 +1117,7 @@ async function executeLocalRender(
crf: options.crf,
videoBitrate: options.videoBitrate,
videoFrameFormat: options.videoFrameFormat,
pageMotionBlur: options.pageMotionBlur,
variables: options.variables,
entryFile: options.entryFile,
outputResolution: options.outputResolution,
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/commands/render/execute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ describe("renderOptionsFromPlan", () => {
rmSync(projectDir, { recursive: true, force: true });
});

it.each([undefined, "off"] as const)(
"preserves page motion blur %s for single and batch renders",
(value) => {
const plan = createRenderPlan({ dir: projectDir, "page-motion-blur": value });
expect(renderOptionsFromPlan(plan, undefined, undefined).pageMotionBlur).toBe(value);
expect(batchRowRenderOptions(plan, undefined).pageMotionBlur).toBe(value);
},
);

it("carries the segmented resume flags from the plan into the render options", () => {
// The hop that once dropped --resume / --keep-segments silently; the plan
// -> options -> request -> config chain is otherwise only exercised by a
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands/render/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export function renderOptionsFromPlan(
vp9CpuUsed: plan.vp9CpuUsed,
videoBitrate: plan.videoBitrate,
videoFrameFormat: plan.videoFrameFormat,
pageMotionBlur: plan.pageMotionBlur,
quiet: plan.quiet,
browserPath,
debug: plan.debug,
Expand Down Expand Up @@ -357,6 +358,7 @@ export function batchRowRenderOptions(
vp9CpuUsed: plan.vp9CpuUsed,
videoBitrate: plan.videoBitrate,
videoFrameFormat: plan.videoFrameFormat,
pageMotionBlur: plan.pageMotionBlur,
quiet: batchQuiet,
browserPath,
entryFile: plan.entryFile,
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/commands/render/plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ describe("createRenderPlan", () => {
expect(plan.bestEffort).toBe(false);
});

it.each([undefined, "off"])("preserves page motion blur override %s", (value) => {
expect(createRenderPlan({ dir: projectDir, "page-motion-blur": value }).pageMotionBlur).toBe(
value,
);
});

it.each(["", "on", "false", "OFF"])("rejects unsupported page motion blur value %s", (value) => {
expect(() => createRenderPlan({ dir: projectDir, "page-motion-blur": value })).toThrow(
CliUsageError,
);
});

it("preserves an aspect-agnostic resolution alias through the execution plan", () => {
const plan = createRenderPlan({ dir: projectDir, resolution: "1080p" });
expect(plan.outputResolution).toBe("landscape");
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/src/commands/render/plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type VideoFrameFormat,
} from "@hyperframes/engine";
import { errorBox } from "../../ui/format.js";
import { parseEnumFlag } from "../../cloud/parsing.js";
import { failUsage } from "../../utils/commandResult.js";
import { resolveProject } from "../../utils/project.js";
import {
Expand Down Expand Up @@ -73,6 +74,7 @@ export interface RenderCommandArgs {
"gif-loop"?: string;
"hls-segment-seconds"?: string;
"video-frame-format"?: string;
"page-motion-blur"?: string;
workers?: string;
docker?: boolean;
hdr?: boolean;
Expand Down Expand Up @@ -126,6 +128,7 @@ export interface RenderPlan {
/** HLS target segment length in seconds; only set for `format: "hls"`. */
hlsSegmentSeconds?: number;
videoFrameFormat: VideoFrameFormat;
pageMotionBlur?: "off";
outputResolution?: CanvasResolution;
outputResolutionAspectAgnostic: boolean;
outputResolutionRaw?: string;
Expand Down Expand Up @@ -224,6 +227,9 @@ function resolveHfEnvOverrides(): readonly string[] {
/** Parse and validate command input into an immutable execution plan. */
// fallow-ignore-next-line complexity
export function createRenderPlan(args: RenderCommandArgs, now = new Date()): RenderPlan {
const pageMotionBlur = parseEnumFlag(args["page-motion-blur"], ["off"] as const, {
flag: "--page-motion-blur",
});
const hfEnvOverrides = resolveHfEnvOverrides();
const hasExplicitComposition = hasExplicitCompositionArg(args.composition);
const project = resolveProject(args.dir, { requireIndex: !hasExplicitComposition });
Expand Down Expand Up @@ -525,6 +531,7 @@ export function createRenderPlan(args: RenderCommandArgs, now = new Date()): Ren
gifFpsCapped,
hlsSegmentSeconds,
videoFrameFormat: videoFrameFormatRaw,
pageMotionBlur,
outputResolution,
outputResolutionAspectAgnostic,
outputResolutionRaw: args.resolution,
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/utils/dockerRunArgs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ const FIXED_INPUT = {
};

describe("buildDockerRunArgs", () => {
it("preserves an explicit page motion blur override inside Docker", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, pageMotionBlur: "off" },
});
expect(
args.slice(args.indexOf("--page-motion-blur"), args.indexOf("--page-motion-blur") + 2),
).toEqual(["--page-motion-blur", "off"]);
expect(buildDockerRunArgs({ ...FIXED_INPUT, options: BASE })).not.toContain(
"--page-motion-blur",
);
});
it("matches snapshot for the default render", () => {
expect(buildDockerRunArgs({ ...FIXED_INPUT, options: BASE })).toMatchInlineSnapshot(`
[
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/utils/dockerRunArgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface DockerRenderOptions {
vp9CpuUsed?: number;
videoBitrate?: string;
videoFrameFormat?: "auto" | "jpg" | "png";
pageMotionBlur?: "off";
quiet: boolean;
debug?: boolean;
bestEffort?: boolean;
Expand Down Expand Up @@ -145,6 +146,7 @@ export function buildDockerRunArgs(input: DockerRunArgsInput): string[] {
...(options.videoFrameFormat && options.videoFrameFormat !== "auto"
? ["--video-frame-format", options.videoFrameFormat]
: []),
...(options.pageMotionBlur !== undefined ? ["--page-motion-blur", options.pageMotionBlur] : []),
...(options.quiet ? ["--quiet"] : []),
...(options.debug ? ["--debug"] : []),
// The in-container CLI is best-effort by default. Only forward the
Expand Down
6 changes: 6 additions & 0 deletions packages/producer/src/services/render/segmentManifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,16 @@ const hashInput: SegmentPlanHashInput = {
outputWidth: 1920,
outputHeight: 1080,
motionBlur: "",
pageMotionBlur: undefined,
captureQuality: 80,
};

describe("computeSegmentPlanHash", () => {
it("does not reuse authored-blur segments when page blur is explicitly off", () => {
expect(computeSegmentPlanHash({ ...hashInput, pageMotionBlur: "off" })).not.toBe(
computeSegmentPlanHash(hashInput),
);
});
it("is stable and 16 hex chars", () => {
const a = computeSegmentPlanHash(hashInput);
expect(a).toMatch(/^[0-9a-f]{16}$/);
Expand Down
2 changes: 2 additions & 0 deletions packages/producer/src/services/render/segmentManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface SegmentPlanHashInput {
outputHeight: number;
/** Serialised motion-blur options, or "" when off; sample count and shutter change every pixel. */
motionBlur: string;
pageMotionBlur: "off" | undefined;
captureQuality: number | undefined;
}

Expand All @@ -71,6 +72,7 @@ export function computeSegmentPlanHash(input: SegmentPlanHashInput): string {
input.useGpu ? "gpu" : "cpu",
`${input.outputWidth}x${input.outputHeight}`,
input.motionBlur,
input.pageMotionBlur ?? "",
input.captureQuality === undefined ? "" : String(input.captureQuality),
].join(" ");
return createHash("sha256").update(ordered).digest("hex").slice(0, 16);
Expand Down
1 change: 1 addition & 0 deletions packages/producer/src/services/renderOrchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4493,6 +4493,7 @@ async function executeRenderPipeline(input: {
outputWidth: captureCompositionWidth ?? width,
outputHeight: captureCompositionHeight ?? height,
motionBlur: job.config.motionBlur ? JSON.stringify(job.config.motionBlur) : "",
pageMotionBlur: job.config.pageMotionBlur,
captureQuality: captureOptions.quality,
});
const segmentDir = segmentDirFor(join(projectDir, "renders"), segmentPlanHash);
Expand Down
Loading