diff --git a/docs/docs.json b/docs/docs.json
index 314a74f98..025e5600b 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -80,6 +80,7 @@
"guides/deploy",
"guides/remove-background",
"guides/hdr",
+ "guides/4k-rendering",
"guides/performance",
"guides/timeline-editing",
"guides/video-editor-cheatsheet",
diff --git a/docs/guides/4k-rendering.mdx b/docs/guides/4k-rendering.mdx
new file mode 100644
index 000000000..c8346ccff
--- /dev/null
+++ b/docs/guides/4k-rendering.mdx
@@ -0,0 +1,138 @@
+---
+title: 4K Rendering
+description: "Render any composition to 4K (3840×2160) without rewriting it — the CLI supersamples a 1080p composition via Chrome's device scale factor."
+---
+
+Hyperframes renders to 4K (3840×2160) two ways. Both produce a true 4K MP4; pick the one that matches your project.
+
+
+
+ Scaffold the project at 4K so the composition is laid out at 4K natively. Best when you want crisp 4K-native typography and assets.
+ ```bash
+ npx hyperframes init my-video --resolution 4k
+ ```
+
+
+ Keep your existing 1080p composition. Pass `--resolution 4k` at render time and Chrome renders at 2× DPR so the screenshot lands at 4K.
+ ```bash
+ npx hyperframes render --resolution 4k --output 4k.mp4
+ ```
+
+
+
+## Quickstart
+
+
+
+ ```bash Terminal
+ npx hyperframes render --resolution 4k --output my-video-4k.mp4
+ ```
+
+ The composition's `data-width` / `data-height` are unchanged. Chrome's `deviceScaleFactor` is set to `2`, so the captured screenshot for each frame is 3840×2160. ffmpeg auto-detects the dimensions from the screenshot stream and encodes at 4K.
+
+
+ ```bash Terminal
+ npx hyperframes init my-video --resolution 4k
+ ```
+
+ Every scaffolded HTML file is patched in place: `data-width="3840"`, `data-height="2160"`, `data-resolution="landscape-4k"`, `#stage` CSS dimensions, and the `` tag.
+
+
+ ```bash Terminal
+ ffprobe -v error -select_streams v:0 -show_entries stream=width,height my-video-4k.mp4
+ ```
+
+ Expected:
+ ```
+ width=3840
+ height=2160
+ ```
+
+
+
+## Resolution presets
+
+`--resolution` accepts these values on both `init` and `render`:
+
+| Preset | Dimensions | Aliases |
+|--------|-----------|---------|
+| `landscape` | 1920×1080 | `1080p`, `hd` |
+| `portrait` | 1080×1920 | `1080p-portrait` |
+| `landscape-4k` | 3840×2160 | `4k`, `uhd` |
+| `portrait-4k` | 2160×3840 | `4k-portrait` |
+
+Examples:
+
+```bash Terminal
+npx hyperframes render --resolution 4k # landscape 4K
+npx hyperframes render --resolution portrait-4k # vertical 4K (TikTok / Reels at max quality)
+npx hyperframes render --resolution 1080p # explicit 1080p (no-op on 1080p compositions)
+```
+
+## How `--resolution` works (supersampling)
+
+The composition stays at its authored dimensions. Hyperframes computes a `deviceScaleFactor` from the ratio of output to composition dimensions and passes it to Chrome:
+
+| Composition | `--resolution` | `deviceScaleFactor` | Output |
+|-------------|---------------|--------------------|--------|
+| 1920×1080 | `4k` | 2 | 3840×2160 |
+| 1080×1920 | `portrait-4k` | 2 | 2160×3840 |
+| 3840×2160 | `4k` | 1 (no-op) | 3840×2160 |
+
+Chrome then renders the page at the higher DPR — effectively rendering each CSS pixel as 2×2 device pixels — so the captured screenshot is at the requested resolution. Text, SVG, and images served at higher intrinsic resolutions all benefit from the upsample. Pre-rasterized assets (a 1920×1080 PNG, for example) won't gain detail they don't have but won't be visibly blurred either.
+
+
+ This approach is intentionally simple — no composition edits, no second authoring pass. The tradeoff: 4K renders take roughly 4× as long per frame because there are 4× the pixels to capture and encode.
+
+
+## Constraints
+
+`--resolution` enforces three guards before any frames are captured. If any fail, the render exits before doing work.
+
+### Aspect ratio must match
+
+```bash
+# OK — both landscape
+hyperframes render --resolution 4k # composition is 1920×1080
+
+# Error — composition is landscape, target is portrait
+hyperframes render --resolution portrait-4k # composition is 1920×1080
+# → outputResolution portrait-4k (2160×3840) does not match the aspect ratio
+# of the composition (1920×1080). Pick a preset whose orientation matches.
+```
+
+### The scale must be an integer
+
+The width ratio (output ÷ composition) must be a positive integer. 1080p → 4K is exactly `2×`. 720p → 4K would be `3×` and works. Non-integer scales like 900p → 4K (`2.4×`) introduce aliasing on subpixel-positioned text — Hyperframes refuses rather than producing a blurry render.
+
+### Downsampling is not supported
+
+`--resolution` only supersamples. A 4K composition cannot be downsampled to 1080p with this flag — render at the composition's native resolution and downscale separately with ffmpeg if needed.
+
+### Not yet supported with `--hdr`
+
+The HDR layered compositor processes pixel buffers at composition dimensions; supersample + HDR would need parallel scaling for those buffers. The combination is rejected with a clear error message. Render in two passes if you need both: HDR at composition resolution, then upscale separately.
+
+## Performance
+
+A 1080p → 4K supersample is roughly 4× more pixels to capture, encode, and write. Expect:
+
+- **Per-frame capture**: 3–4× slower (Chrome paints 4× the pixels and the screenshot transfer is 4× larger)
+- **Encoding**: 2–3× slower (depends on codec; H.264 scales sublinearly with resolution)
+- **Memory**: bounded — the engine's frame data-URI cache is byte-budgeted (default 1500 MB per worker, configurable via `PRODUCER_FRAME_DATA_URI_CACHE_BYTES_MB`)
+- **Output file size**: at the default CRF, expect 3–5× the file size of the 1080p render. Pass `--video-bitrate 25M` (or higher) for predictable file sizes.
+
+For a 4K render of a 30-second composition, plan on a few minutes of wall time on a modern laptop. Add `--workers 4` (or more) on a render box for parallel capture.
+
+## Studio support
+
+A resolution selector in Studio is on the way (tracked separately). For now, switch resolution from the CLI:
+
+- **New project**: `hyperframes init my-video --resolution 4k`
+- **Existing project**: `hyperframes render --resolution 4k --output 4k.mp4`
+
+## See also
+
+- [`render` CLI reference](/packages/cli#render) — every render flag including `--video-bitrate` and `--crf`
+- [`init` CLI reference](/packages/cli#init) — the `--resolution` flag at scaffold time
+- [HDR Rendering](/guides/hdr) — color pipeline guide; HDR + 4K is not yet a supported combination
diff --git a/docs/packages/cli.mdx b/docs/packages/cli.mdx
index ca7df21e6..120bd7fc9 100644
--- a/docs/packages/cli.mdx
+++ b/docs/packages/cli.mdx
@@ -610,6 +610,7 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
| `--quality` | draft, standard, high | standard | Encoding quality preset (drives CRF/bitrate) |
| `--crf` | 0-51 | — | Override encoder CRF (lower = higher quality). Mutually exclusive with `--video-bitrate` |
| `--video-bitrate` | e.g. `10M`, `5000k` | — | Target video bitrate. Mutually exclusive with `--crf` |
+ | `--resolution` | landscape, portrait, landscape-4k, portrait-4k (aliases: `1080p`, `4k`, `uhd`) | — | Output resolution preset. Supersamples a smaller composition via Chrome `deviceScaleFactor` so the screenshot lands at the requested dimensions. Aspect ratio must match the composition; the scale must be an integer multiple. Not supported with `--hdr`. See [4K Rendering](/guides/4k-rendering) |
| `--hdr` | — | off | Force HDR output even if no HDR sources are detected. MP4 only. See [HDR Rendering](/guides/hdr) |
| `--sdr` | — | off | Force SDR output even if HDR sources are detected |
| `--workers` | 1-8 | 4 | Parallel render workers |
diff --git a/packages/cli/src/commands/render.ts b/packages/cli/src/commands/render.ts
index d0fbfb34d..574f9115a 100644
--- a/packages/cli/src/commands/render.ts
+++ b/packages/cli/src/commands/render.ts
@@ -5,6 +5,10 @@ import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, rmSync }
export const examples: Example[] = [
["Render to MP4", "hyperframes render --output output.mp4"],
["Render a specific composition", "hyperframes render -c compositions/intro.html -o intro.mp4"],
+ [
+ "Upsample any composition to 4K (supersamples via Chrome DPR)",
+ "hyperframes render --resolution 4k --output 4k.mp4",
+ ],
["Render transparent overlay (ProRes)", "hyperframes render --format mov --output overlay.mov"],
["Render transparent WebM overlay", "hyperframes render --format webm --output overlay.webm"],
[
@@ -47,8 +51,35 @@ import {
validateVariables,
formatVariableValidationIssue,
type VariableValidationIssue,
+ type CanvasResolution,
} from "@hyperframes/core";
+const VALID_RENDER_RESOLUTIONS: readonly CanvasResolution[] = [
+ "landscape",
+ "portrait",
+ "landscape-4k",
+ "portrait-4k",
+] as const;
+
+const RENDER_RESOLUTION_ALIASES: Record = {
+ "1080p": "landscape",
+ hd: "landscape",
+ "1080p-portrait": "portrait",
+ "portrait-1080p": "portrait",
+ "4k": "landscape-4k",
+ uhd: "landscape-4k",
+ "4k-portrait": "portrait-4k",
+};
+
+function normalizeRenderResolutionFlag(input: string | undefined): CanvasResolution | undefined {
+ if (!input) return undefined;
+ const lowered = input.toLowerCase();
+ if ((VALID_RENDER_RESOLUTIONS as readonly string[]).includes(lowered)) {
+ return lowered as CanvasResolution;
+ }
+ return RENDER_RESOLUTION_ALIASES[lowered];
+}
+
const VALID_FPS = new Set([24, 30, 60]);
const VALID_QUALITY = new Set(["draft", "standard", "high"]);
const VALID_FORMAT = new Set(["mp4", "webm", "mov", "png-sequence"]);
@@ -177,6 +208,11 @@ export default defineCommand({
"Fail render if any --variables key is undeclared or has a wrong type vs the composition's data-composition-variables. Without this flag, mismatches are warnings.",
default: false,
},
+ resolution: {
+ type: "string",
+ description:
+ "Output resolution preset: landscape (1920x1080), portrait (1080x1920), landscape-4k (3840x2160), portrait-4k (2160x3840). Aliases: 1080p, 4k, uhd. The composition is unchanged — Chrome renders at higher DPR (deviceScaleFactor) so the captured screenshot lands at the requested dimensions. Aspect ratio must match the composition; the scale must be an integer multiple. Not yet supported with --hdr.",
+ },
},
async run({ args }) {
// ── Resolve project ────────────────────────────────────────────────────
@@ -206,6 +242,19 @@ export default defineCommand({
}
const format = formatRaw as "mp4" | "webm" | "mov" | "png-sequence";
+ // ── Validate resolution ────────────────────────────────────────────────
+ let outputResolution: CanvasResolution | undefined;
+ if (args.resolution !== undefined) {
+ outputResolution = normalizeRenderResolutionFlag(args.resolution);
+ if (!outputResolution) {
+ errorBox(
+ "Invalid resolution",
+ `Got "${args.resolution}". Must be one of: landscape, portrait, landscape-4k, portrait-4k (or aliases 1080p, 4k, uhd).`,
+ );
+ process.exit(1);
+ }
+ }
+
// ── Validate workers ──────────────────────────────────────────────────
let workers: number | undefined;
if (args.workers != null && args.workers !== "auto") {
@@ -319,6 +368,9 @@ export default defineCommand({
c.accent("\u25C6") + " Rendering " + c.accent(nameLabel) + c.dim(" \u2192 " + outputPath),
);
console.log(c.dim(" " + fps + "fps \u00B7 " + quality + " \u00B7 " + workerLabel));
+ if (outputResolution) {
+ console.log(c.dim(" Output resolution: " + outputResolution + " (supersampled via DPR)"));
+ }
if (useGpu || browserGpuMode !== "software") {
const gpuModes = [
useGpu ? "encoder GPU" : null,
@@ -452,6 +504,7 @@ export default defineCommand({
quiet,
variables,
entryFile,
+ outputResolution,
exitAfterComplete: true,
});
} else {
@@ -469,6 +522,7 @@ export default defineCommand({
browserPath,
variables,
entryFile,
+ outputResolution,
exitAfterComplete: true,
});
}
@@ -495,6 +549,13 @@ interface RenderOptions {
variables?: Record;
entryFile?: string;
exitAfterComplete?: boolean;
+ /**
+ * Output resolution preset. When set, the orchestrator computes a Chrome
+ * deviceScaleFactor so the screenshot lands at the requested dimensions
+ * without changing the composition. See the producer's
+ * `resolveDeviceScaleFactor` for the integer-scale + aspect constraints.
+ */
+ outputResolution?: CanvasResolution;
}
export type VariablesParseError =
@@ -788,6 +849,7 @@ async function renderDocker(
quiet: options.quiet,
variables: options.variables,
entryFile: options.entryFile,
+ outputResolution: options.outputResolution,
},
});
@@ -859,6 +921,7 @@ export async function renderLocal(
videoBitrate: options.videoBitrate,
variables: options.variables,
entryFile: options.entryFile,
+ outputResolution: options.outputResolution,
});
const onProgress = options.quiet
diff --git a/packages/cli/src/utils/dockerRunArgs.test.ts b/packages/cli/src/utils/dockerRunArgs.test.ts
index b4f853ea3..981bc24c4 100644
--- a/packages/cli/src/utils/dockerRunArgs.test.ts
+++ b/packages/cli/src/utils/dockerRunArgs.test.ts
@@ -239,4 +239,19 @@ describe("buildDockerRunArgs", () => {
const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
expect(args).not.toContain("--composition");
});
+
+ it("forwards --resolution to the container when outputResolution is set", () => {
+ const args = buildDockerRunArgs({
+ ...FIXED_INPUT,
+ options: { ...BASE, outputResolution: "landscape-4k" },
+ });
+ const idx = args.indexOf("--resolution");
+ expect(idx).toBeGreaterThan(-1);
+ expect(args[idx + 1]).toBe("landscape-4k");
+ });
+
+ it("omits --resolution when outputResolution is not set", () => {
+ const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
+ expect(args).not.toContain("--resolution");
+ });
});
diff --git a/packages/cli/src/utils/dockerRunArgs.ts b/packages/cli/src/utils/dockerRunArgs.ts
index a3d275b6b..8630d3bd4 100644
--- a/packages/cli/src/utils/dockerRunArgs.ts
+++ b/packages/cli/src/utils/dockerRunArgs.ts
@@ -31,6 +31,8 @@ export interface DockerRenderOptions {
quiet: boolean;
variables?: Record;
entryFile?: string;
+ /** Output resolution preset (e.g. "landscape-4k"). Forwarded as `--resolution`. */
+ outputResolution?: string;
}
export function buildDockerRunArgs(input: DockerRunArgsInput): string[] {
@@ -69,5 +71,6 @@ export function buildDockerRunArgs(input: DockerRunArgsInput): string[] {
? ["--variables", JSON.stringify(options.variables)]
: []),
...(options.entryFile ? ["--composition", options.entryFile] : []),
+ ...(options.outputResolution ? ["--resolution", options.outputResolution] : []),
];
}
diff --git a/packages/producer/src/services/renderOrchestrator.test.ts b/packages/producer/src/services/renderOrchestrator.test.ts
index 3245b637f..587004c04 100644
--- a/packages/producer/src/services/renderOrchestrator.test.ts
+++ b/packages/producer/src/services/renderOrchestrator.test.ts
@@ -20,6 +20,7 @@ import {
isRecoverableParallelCaptureError,
materializeExtractedFramesForCompiledDir,
projectBrowserEndToCompositionTimeline,
+ resolveDeviceScaleFactor,
resolveRenderWorkerCount,
resolveCompositeTransfer,
selectCaptureCalibrationFrames,
@@ -749,3 +750,82 @@ describe("projectBrowserEndToCompositionTimeline", () => {
expect(projectBrowserEndToCompositionTimeline(21.5, 1.5, 5.5)).toBe(25.5);
});
});
+
+describe("resolveDeviceScaleFactor", () => {
+ const defaults = {
+ compositionWidth: 1920,
+ compositionHeight: 1080,
+ hdrRequested: false,
+ } as const;
+
+ it("returns 1 when no outputResolution is set (default behavior)", () => {
+ expect(resolveDeviceScaleFactor({ ...defaults, outputResolution: undefined })).toBe(1);
+ });
+
+ it("returns 2 for the canonical 1080p → 4K supersample", () => {
+ expect(resolveDeviceScaleFactor({ ...defaults, outputResolution: "landscape-4k" })).toBe(2);
+ });
+
+ it("returns 2 for portrait 1080p → portrait-4k", () => {
+ expect(
+ resolveDeviceScaleFactor({
+ ...defaults,
+ compositionWidth: 1080,
+ compositionHeight: 1920,
+ outputResolution: "portrait-4k",
+ }),
+ ).toBe(2);
+ });
+
+ it("returns 1 when the composition already matches the requested resolution", () => {
+ expect(
+ resolveDeviceScaleFactor({
+ compositionWidth: 3840,
+ compositionHeight: 2160,
+ outputResolution: "landscape-4k",
+ hdrRequested: false,
+ }),
+ ).toBe(1);
+ });
+
+ it("rejects HDR + outputResolution with a clear message", () => {
+ expect(() =>
+ resolveDeviceScaleFactor({
+ ...defaults,
+ outputResolution: "landscape-4k",
+ hdrRequested: true,
+ }),
+ ).toThrow(/hdrMode='force-hdr'/);
+ });
+
+ it("rejects orientation mismatch (landscape comp → portrait-4k)", () => {
+ expect(() =>
+ resolveDeviceScaleFactor({ ...defaults, outputResolution: "portrait-4k" }),
+ ).toThrow(/aspect ratio/);
+ });
+
+ it("rejects downsampling (4K composition → 1080p output)", () => {
+ expect(() =>
+ resolveDeviceScaleFactor({
+ compositionWidth: 3840,
+ compositionHeight: 2160,
+ outputResolution: "landscape",
+ hdrRequested: false,
+ }),
+ ).toThrow(/Downsampling/);
+ });
+
+ it("rejects non-integer scale factors", () => {
+ // 1280×720 → 3840×2160 would be 3×, but width 1280 → 3840 is also 3× — that's actually integer.
+ // Use 1280×720 → 2160×3840 (mismatched orientation triggers aspect first), so use a real
+ // non-integer: 1500×844 → 3840×2160 = 2.56×.
+ expect(() =>
+ resolveDeviceScaleFactor({
+ compositionWidth: 1500,
+ compositionHeight: 844,
+ outputResolution: "landscape-4k",
+ hdrRequested: false,
+ }),
+ ).toThrow(/aspect ratio|non-integer/);
+ });
+});
diff --git a/packages/producer/src/services/renderOrchestrator.ts b/packages/producer/src/services/renderOrchestrator.ts
index 9e700fc33..de8aa78d2 100644
--- a/packages/producer/src/services/renderOrchestrator.ts
+++ b/packages/producer/src/services/renderOrchestrator.ts
@@ -29,6 +29,7 @@ import {
symlinkSync,
} from "fs";
import { parseHTML } from "linkedom";
+import { CANVAS_DIMENSIONS, type CanvasResolution } from "@hyperframes/core";
import {
type EngineConfig,
resolveConfig,
@@ -279,6 +280,24 @@ export interface RenderConfig {
* `--variables-file `. Must be a JSON-serializable plain object.
*/
variables?: Record;
+ /**
+ * Override the output resolution. The composition's intrinsic
+ * `data-width` / `data-height` continue to drive page layout (Chrome
+ * viewport), and supersampling is achieved by setting Chrome's
+ * `deviceScaleFactor` so the captured screenshot lands at the requested
+ * dimensions. Passing a 4K preset on a 1080p composition therefore
+ * produces a 4K output without rewriting any composition HTML.
+ *
+ * Constraint: the requested dimensions must be an integer multiple of
+ * the composition's intrinsic dimensions (so DPR is a clean integer).
+ * Non-integer scales are rejected with an explanatory error before any
+ * frames are captured.
+ *
+ * Not yet supported with HDR (the layered HDR compositor processes
+ * pixel buffers at composition dimensions and would need parallel
+ * scaling); the orchestrator errors when both are set.
+ */
+ outputResolution?: CanvasResolution;
}
export interface RenderPerfSummary {
@@ -563,6 +582,68 @@ export function projectBrowserEndToCompositionTimeline(
return browserEnd + (existingStart - browserStart);
}
+/**
+ * Translate the user-facing `--resolution` flag into a Chrome
+ * `deviceScaleFactor`. The composition's intrinsic dimensions stay the
+ * page-layout viewport; the screenshot lands at output dims via DPR.
+ *
+ * The scale must be a positive integer ≥ 1 — fractional DPRs introduce
+ * visible aliasing and we'd rather fail loudly than produce a blurry
+ * 4K render. Downsampling (output < composition) is rejected because
+ * the user is unlikely to have intended it; if the use case appears
+ * we can plumb a separate flag.
+ *
+ * Throws on:
+ * - HDR + outputResolution combination (HDR layered compositor would
+ * need parallel scaling for its raw pixel buffers).
+ * - Non-integer scale (e.g. 720p composition, 4K output → 3× height
+ * but the width ratio is also 3× ✓; 1080p portrait → 4K landscape
+ * would mismatch).
+ * - Output dimensions smaller than composition dimensions.
+ */
+export function resolveDeviceScaleFactor(input: {
+ compositionWidth: number;
+ compositionHeight: number;
+ outputResolution: CanvasResolution | undefined;
+ hdrRequested: boolean;
+}): number {
+ if (!input.outputResolution) return 1;
+ if (input.hdrRequested) {
+ throw new Error(
+ "outputResolution cannot be combined with hdrMode='force-hdr'. " +
+ "HDR rendering composites at composition dimensions and does not yet " +
+ "support supersampling. Pick one or render in two passes.",
+ );
+ }
+ const target = CANVAS_DIMENSIONS[input.outputResolution];
+ const widthRatio = target.width / input.compositionWidth;
+ const heightRatio = target.height / input.compositionHeight;
+ if (widthRatio !== heightRatio) {
+ throw new Error(
+ `outputResolution ${input.outputResolution} (${target.width}×${target.height}) ` +
+ `does not match the aspect ratio of the composition ` +
+ `(${input.compositionWidth}×${input.compositionHeight}). ` +
+ `Pick a preset whose orientation matches.`,
+ );
+ }
+ if (widthRatio < 1) {
+ throw new Error(
+ `outputResolution ${input.outputResolution} (${target.width}×${target.height}) ` +
+ `is smaller than the composition (${input.compositionWidth}×${input.compositionHeight}). ` +
+ `Downsampling via --resolution is not supported.`,
+ );
+ }
+ if (!Number.isInteger(widthRatio)) {
+ throw new Error(
+ `outputResolution ${input.outputResolution} requires a non-integer ` +
+ `device scale factor (${widthRatio}×) to upsample from ` +
+ `${input.compositionWidth}×${input.compositionHeight}. ` +
+ `Pick a preset that's an integer multiple, or rescale the composition.`,
+ );
+ }
+ return widthRatio;
+}
+
function updateJobStatus(
job: RenderJob,
status: RenderStatus,
@@ -2053,6 +2134,22 @@ export async function executeRenderJob(
height: compiled.height,
};
const { width, height } = composition;
+ const deviceScaleFactor = resolveDeviceScaleFactor({
+ compositionWidth: width,
+ compositionHeight: height,
+ outputResolution: job.config.outputResolution,
+ hdrRequested: job.config.hdrMode === "force-hdr",
+ });
+ if (deviceScaleFactor > 1) {
+ log.info("Supersampling composition via deviceScaleFactor", {
+ compositionWidth: width,
+ compositionHeight: height,
+ outputResolution: job.config.outputResolution,
+ outputWidth: width * deviceScaleFactor,
+ outputHeight: height * deviceScaleFactor,
+ deviceScaleFactor,
+ });
+ }
const probeStart = Date.now();
const needsBrowser = composition.duration <= 0 || compiled.unresolvedCompositions.length > 0;
@@ -2077,6 +2174,7 @@ export async function executeRenderJob(
fps: job.config.fps,
format: needsAlpha ? "png" : "jpeg",
quality: needsAlpha ? undefined : 80,
+ deviceScaleFactor,
};
probeSession = await createCaptureSession(
fileServer.url,
@@ -2543,6 +2641,7 @@ export async function executeRenderJob(
format: needsAlpha ? "png" : "jpeg",
quality: needsAlpha ? undefined : job.config.quality === "draft" ? 80 : 95,
variables: job.config.variables,
+ deviceScaleFactor,
};
// Capture sessions do not need native browser metadata for videos whose
@@ -3915,7 +4014,7 @@ export async function executeRenderJob(
chunkSizeFrames: enableChunkedEncode ? chunkedEncodeSize : null,
compositionDurationSeconds: composition.duration,
totalFrames: totalFrames,
- resolution: { width, height },
+ resolution: { width: width * deviceScaleFactor, height: height * deviceScaleFactor },
videoCount: composition.videos.length,
audioCount: composition.audios.length,
stages: perfStages,