mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
feat(cli): preserve page motion blur off in local renders (#6081)
This commit is contained in:
@@ -1448,6 +1448,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:
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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",
|
||||
@@ -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. */
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 });
|
||||
@@ -525,6 +531,7 @@ export function createRenderPlan(args: RenderCommandArgs, now = new Date()): Ren
|
||||
gifFpsCapped,
|
||||
hlsSegmentSeconds,
|
||||
videoFrameFormat: videoFrameFormatRaw,
|
||||
pageMotionBlur,
|
||||
outputResolution,
|
||||
outputResolutionAspectAgnostic,
|
||||
outputResolutionRaw: args.resolution,
|
||||
|
||||
@@ -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(`
|
||||
[
|
||||
|
||||
@@ -51,6 +51,7 @@ export interface DockerRenderOptions {
|
||||
vp9CpuUsed?: number;
|
||||
videoBitrate?: string;
|
||||
videoFrameFormat?: "auto" | "jpg" | "png";
|
||||
pageMotionBlur?: "off";
|
||||
quiet: boolean;
|
||||
debug?: boolean;
|
||||
bestEffort?: boolean;
|
||||
@@ -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
|
||||
|
||||
@@ -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}$/);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user