mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
feat(producer): render at 720p and 480p from a larger composition
This commit is contained in:
@@ -1871,10 +1871,10 @@ describe("checkRenderResolutionPreflight", () => {
|
||||
expect(result?.kind).toBe("hdr-incompatible");
|
||||
});
|
||||
|
||||
it("classifies a preset smaller than the composition as downsampling", async () => {
|
||||
// 3840×2160 comp + landscape (1920×1080): same 16:9 aspect, target smaller.
|
||||
it("passes a preset smaller than the composition", async () => {
|
||||
// 3840×2160 comp + landscape (1920×1080): same 16:9 aspect, rendered at 0.5× DPR.
|
||||
const result = await checkRenderResolutionPreflight(comp(3840, 2160), "landscape", noModes);
|
||||
expect(result?.kind).toBe("downsampling");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("classifies a non-integer upscale as non-integer-scale", async () => {
|
||||
@@ -1944,12 +1944,9 @@ describe("checkRenderResolutionPreflight", () => {
|
||||
expect(result?.kind).toBe("hdr-incompatible");
|
||||
});
|
||||
|
||||
it("still flags downsampling + aspect-agnostic (same-orientation, smaller preset)", async () => {
|
||||
// 3840×2160 comp with `--resolution 1080p` → `landscape` (1920×1080).
|
||||
// Same orientation, but tier smaller than comp — user asked for a
|
||||
// downsample. That's a real incompatibility, not an orientation swap.
|
||||
it("passes `--resolution 1080p` on a 4K composition (a smaller, same-orientation preset)", async () => {
|
||||
const result = await checkRenderResolutionPreflight(comp(3840, 2160), "landscape", agnostic);
|
||||
expect(result?.kind).toBe("downsampling");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does NOT auto-clear when the flag was explicit (orientation-locked preset stays strict)", async () => {
|
||||
@@ -1983,14 +1980,10 @@ describe("checkRenderResolutionPreflight", () => {
|
||||
expect(result?.message).toMatch(/preset whose orientation matches|omit --resolution/i);
|
||||
});
|
||||
|
||||
it("blocks a portrait-4K comp + `1080p` downsample early (orientation-flip masks the tier gap)", async () => {
|
||||
// 2160×3840 (portrait 4K) + `--resolution 1080p` — the compile stage
|
||||
// remaps `landscape` → `portrait` (1080×1920), and *then* the preset
|
||||
// is smaller than the composition. The un-remapped preflight let this
|
||||
// slip through as an aspect-mismatch downgrade; the remap-then-check
|
||||
// catches the real failure — downsampling — early.
|
||||
it("passes a portrait-4K comp + `1080p` once remapped to portrait (a smaller preset)", async () => {
|
||||
// 2160×3840 + `--resolution 1080p` remaps `landscape` → `portrait` (1080×1920), rendered at 0.5× DPR.
|
||||
const result = await checkRenderResolutionPreflight(comp(2160, 3840), "landscape", agnostic);
|
||||
expect(result?.kind).toBe("downsampling");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("blocks a portrait 720p comp + `1080p` non-integer upscale early (orientation-flip masks the fractional DPR)", async () => {
|
||||
|
||||
@@ -22,6 +22,12 @@ describe("@hyperframes/core public API exports", () => {
|
||||
"portrait-4k",
|
||||
"square",
|
||||
"square-4k",
|
||||
"landscape-720p",
|
||||
"portrait-720p",
|
||||
"square-720p",
|
||||
"landscape-480p",
|
||||
"portrait-480p",
|
||||
"square-480p",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -907,18 +907,18 @@ async function waitForCloseWithTimeout(promise: Promise<unknown>): Promise<boole
|
||||
* screenshots carry a real alpha channel (Chrome resets the override on every
|
||||
* navigation, so this must run after page load).
|
||||
*
|
||||
* drawElement is also skipped when supersampling (deviceScaleFactor > 1):
|
||||
* drawElement is also skipped when scaling (deviceScaleFactor other than 1):
|
||||
* `drawElementImage` reads the canvas at CSS pixels and has no equivalent of
|
||||
* `Page.captureScreenshot`'s clip+scale, so it would silently capture at 1x and
|
||||
* drop the requested supersample. Such renders fall through to the screenshot
|
||||
* path (preMode already forces "screenshot" for DPR > 1).
|
||||
* path (preMode already forces "screenshot" for DPR other than 1).
|
||||
*/
|
||||
async function initDrawElementOrTransparentBackground(
|
||||
session: CaptureSession,
|
||||
page: Page,
|
||||
logInitPhase: (phase: string) => void,
|
||||
): Promise<void> {
|
||||
const supersampling = (session.options.deviceScaleFactor ?? 1) > 1;
|
||||
const supersampling = (session.options.deviceScaleFactor ?? 1) !== 1;
|
||||
// forceScreenshot is an explicit routing decision made upstream (render-mode
|
||||
// compat hints like raw requestAnimationFrame, alpha formats, low-memory) —
|
||||
// drawElement must not override it. Concretely: an rAF-compat comp on
|
||||
@@ -948,7 +948,7 @@ async function initDrawElementOrTransparentBackground(
|
||||
session.deFallbackTrigger = "supersampling";
|
||||
console.log(
|
||||
"[engine] --experimental-fast-capture disabled for this render: drawElementImage " +
|
||||
"ignores deviceScaleFactor, so supersampled (DPR > 1) output uses screenshot capture.",
|
||||
"ignores deviceScaleFactor, so scaled (DPR other than 1) output uses screenshot capture.",
|
||||
);
|
||||
}
|
||||
if ((session.config?.useDrawElement ?? false) && !supersampling && forceScreenshot) {
|
||||
@@ -1286,8 +1286,8 @@ export async function createCaptureSession(
|
||||
// (the captured surface is sized by the OS window in CSS pixels regardless
|
||||
// of `Emulation.setDeviceMetricsOverride`'s DPR). When supersampling we
|
||||
// need explicit clip+scale on `Page.captureScreenshot`, so fall back to
|
||||
// the screenshot path for any DPR > 1.
|
||||
const supersampling = (options.deviceScaleFactor ?? 1) > 1;
|
||||
// the screenshot path for any DPR other than 1.
|
||||
const supersampling = (options.deviceScaleFactor ?? 1) !== 1;
|
||||
const requestedGpuMode = config?.browserGpuMode ?? DEFAULT_CONFIG.browserGpuMode;
|
||||
const resolvedGpuMode = await resolveBrowserGpuMode(requestedGpuMode, {
|
||||
chromePath: headlessShell ?? undefined,
|
||||
|
||||
@@ -116,15 +116,52 @@ describe("checkOutputResolutionCompatibility", () => {
|
||||
});
|
||||
|
||||
describe("scale constraints", () => {
|
||||
it("flags downsampling (preset smaller than composition)", () => {
|
||||
// 3840×2160 composition, landscape (1920×1080) preset → 0.5× DPR.
|
||||
it("allows a preset smaller than the composition (0.5× DPR)", () => {
|
||||
const result = checkOutputResolutionCompatibility({
|
||||
compositionWidth: 3840,
|
||||
compositionHeight: 2160,
|
||||
outputResolution: "landscape",
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.kind).toBe("downsampling");
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[1920, 1080, "landscape-720p"],
|
||||
[1920, 1080, "landscape-480p"],
|
||||
[1080, 1920, "portrait-720p"],
|
||||
[1080, 1920, "portrait-480p"],
|
||||
[1080, 1080, "square-720p"],
|
||||
[1080, 1080, "square-480p"],
|
||||
] as const)(
|
||||
"allows %i×%i down to %s",
|
||||
(compositionWidth, compositionHeight, outputResolution) => {
|
||||
expect(
|
||||
checkOutputResolutionCompatibility({
|
||||
compositionWidth,
|
||||
compositionHeight,
|
||||
outputResolution,
|
||||
}),
|
||||
).toEqual({ ok: true });
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps the 480p tier when suggesting a swap for the wrong orientation", () => {
|
||||
const result = checkOutputResolutionCompatibility({
|
||||
compositionWidth: 1920,
|
||||
compositionHeight: 1080,
|
||||
outputResolution: "portrait-480p",
|
||||
});
|
||||
expect(result.kind).toBe("aspect-mismatch");
|
||||
expect(result.suggestedResolution).toBe("landscape-480p");
|
||||
});
|
||||
|
||||
it("still refuses a near-16:9 composition a pixel too wide for an exact preset", () => {
|
||||
const result = checkOutputResolutionCompatibility({
|
||||
compositionWidth: 1921,
|
||||
compositionHeight: 1080,
|
||||
outputResolution: "landscape",
|
||||
});
|
||||
expect(result.kind).toBe("aspect-mismatch");
|
||||
});
|
||||
|
||||
it("flags a non-integer scale factor for a same-aspect custom composition", () => {
|
||||
|
||||
@@ -28,7 +28,6 @@ import { CANVAS_DIMENSIONS, VALID_CANVAS_RESOLUTIONS, type CanvasResolution } fr
|
||||
export type OutputResolutionIssueKind =
|
||||
| "hdr-incompatible"
|
||||
| "aspect-mismatch"
|
||||
| "downsampling"
|
||||
| "non-integer-scale";
|
||||
|
||||
export interface OutputResolutionCompatibility {
|
||||
@@ -69,24 +68,35 @@ export function suggestMatchingPreset(
|
||||
compositionHeight: number,
|
||||
chosen: CanvasResolution,
|
||||
): CanvasResolution | undefined {
|
||||
const aspectMatches: CanvasResolution[] = VALID_CANVAS_RESOLUTIONS.filter((preset) => {
|
||||
const { width, height } = CANVAS_DIMENSIONS[preset];
|
||||
// Integer-safe aspect compare (cross-multiplication), matching the
|
||||
// producer's mismatch check exactly.
|
||||
return width * compositionHeight === height * compositionWidth;
|
||||
});
|
||||
const aspectMatches: CanvasResolution[] = VALID_CANVAS_RESOLUTIONS.filter((preset) =>
|
||||
fitsAspect(CANVAS_DIMENSIONS[preset], compositionWidth, compositionHeight),
|
||||
);
|
||||
if (aspectMatches.length === 0) return undefined;
|
||||
|
||||
// Prefer the aspect-matching preset in the same resolution tier as the chosen
|
||||
// one so we don't silently downgrade 4K → HD (or vice versa). Tier is keyed
|
||||
// off the `-4k` suffix rather than long-side pixels: a 4K *square* (2160×2160)
|
||||
// one so we don't silently change 4K → HD (or vice versa). Tier is keyed
|
||||
// off the suffix (`-4k`, `-720p`, …) rather than long-side pixels: a 4K *square* (2160×2160)
|
||||
// has a shorter long side than 4K landscape (3840), so a pixel-based compare
|
||||
// would fail to recognise `square-4k` as the 4K peer and downgrade to `square`.
|
||||
const chosenIs4k = chosen.endsWith("-4k");
|
||||
const sameTier = aspectMatches.find((preset) => preset.endsWith("-4k") === chosenIs4k);
|
||||
const sameTier = aspectMatches.find((preset) => tierOf(preset) === tierOf(chosen));
|
||||
return sameTier ?? aspectMatches[0];
|
||||
}
|
||||
|
||||
/** `landscape-4k` → `-4k`, `portrait` → `` (1080p). */
|
||||
const tierOf = (preset: CanvasResolution) =>
|
||||
preset.includes("-") ? preset.slice(preset.indexOf("-")) : "";
|
||||
|
||||
/** Integer-safe aspect compare. A smaller preset may be under a pixel off on its long side (854×480 at 16:9). */
|
||||
function fitsAspect(
|
||||
target: { width: number; height: number },
|
||||
compositionWidth: number,
|
||||
compositionHeight: number,
|
||||
): boolean {
|
||||
const drift = Math.abs(target.width * compositionHeight - target.height * compositionWidth);
|
||||
if (drift === 0) return true;
|
||||
return target.width < compositionWidth && drift < Math.min(compositionWidth, compositionHeight);
|
||||
}
|
||||
|
||||
function describeOrientation(width: number, height: number): string {
|
||||
if (width > height) return "landscape";
|
||||
if (width < height) return "portrait";
|
||||
@@ -154,26 +164,14 @@ export function checkOutputResolutionCompatibility(input: {
|
||||
}
|
||||
|
||||
const target = CANVAS_DIMENSIONS[outputResolution];
|
||||
// Aspect-ratio compare via cross-multiplication so the equality is integer-
|
||||
// safe. Float division (`target.width / compositionWidth`) loses precision
|
||||
// for non-power-of-2 ratios (e.g. cinema 4K 4096×2160 = 1.8963…) and a
|
||||
// future preset could trip a false-mismatch on otherwise valid input.
|
||||
if (target.width * compositionHeight !== target.height * compositionWidth) {
|
||||
if (!fitsAspect(target, compositionWidth, compositionHeight)) {
|
||||
return buildAspectMismatch(compositionWidth, compositionHeight, outputResolution, target);
|
||||
}
|
||||
|
||||
// Aspect ratios match → widthRatio === heightRatio. Compute once.
|
||||
// A smaller preset renders at a device scale factor under 1: Chrome lays the
|
||||
// page out at composition size and rasterizes it straight at the preset's.
|
||||
const widthRatio = target.width / compositionWidth;
|
||||
if (widthRatio < 1) {
|
||||
return {
|
||||
ok: false,
|
||||
kind: "downsampling",
|
||||
message:
|
||||
`outputResolution ${outputResolution} (${target.width}×${target.height}) ` +
|
||||
`is smaller than the composition (${compositionWidth}×${compositionHeight}). ` +
|
||||
`Downsampling via --resolution is not supported.`,
|
||||
};
|
||||
}
|
||||
if (widthRatio < 1) return OK;
|
||||
|
||||
if (!Number.isInteger(widthRatio)) {
|
||||
return {
|
||||
|
||||
@@ -23,6 +23,13 @@ export const CANVAS_DIMENSIONS = {
|
||||
"portrait-4k": { width: 2160, height: 3840 },
|
||||
square: { width: 1080, height: 1080 },
|
||||
"square-4k": { width: 2160, height: 2160 },
|
||||
"landscape-720p": { width: 1280, height: 720 },
|
||||
"portrait-720p": { width: 720, height: 1280 },
|
||||
"square-720p": { width: 720, height: 720 },
|
||||
// 480 tall at 16:9 is 853.33 wide: 854 keeps both sides even for 4:2:0 encoders.
|
||||
"landscape-480p": { width: 854, height: 480 },
|
||||
"portrait-480p": { width: 480, height: 854 },
|
||||
"square-480p": { width: 480, height: 480 },
|
||||
} as const;
|
||||
|
||||
// Single source of truth: derive the type from the table so adding a preset
|
||||
@@ -49,6 +56,12 @@ const RESOLUTION_ALIASES: Record<string, CanvasResolution> = {
|
||||
"1080p-square": "square",
|
||||
"square-1080p": "square",
|
||||
"4k-square": "square-4k",
|
||||
"720p": "landscape-720p",
|
||||
"720p-portrait": "portrait-720p",
|
||||
"720p-square": "square-720p",
|
||||
"480p": "landscape-480p",
|
||||
"480p-portrait": "portrait-480p",
|
||||
"480p-square": "square-480p",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -70,6 +83,8 @@ const ASPECT_AGNOSTIC_RESOLUTION_ALIASES: ReadonlySet<string> = new Set([
|
||||
"hd",
|
||||
"4k",
|
||||
"uhd",
|
||||
"720p",
|
||||
"480p",
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -1072,8 +1072,8 @@ export async function renderChunk(
|
||||
outputPath: outputChunkPath,
|
||||
framesDir,
|
||||
videoOnlyPath,
|
||||
width: plan.dimensions.width * encoder.deviceScaleFactor,
|
||||
height: plan.dimensions.height * encoder.deviceScaleFactor,
|
||||
width: Math.round(plan.dimensions.width * encoder.deviceScaleFactor),
|
||||
height: Math.round(plan.dimensions.height * encoder.deviceScaleFactor),
|
||||
needsAlpha: plan.dimensions.format !== "mp4",
|
||||
captureImageFormat: captureOptions.format ?? "jpeg",
|
||||
// Each chunk produces video only — audio is muxed once at assemble
|
||||
|
||||
@@ -105,18 +105,16 @@ export function projectBrowserEndToCompositionTimeline(
|
||||
* `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
|
||||
* Upsampling must be by a whole number — fractional DPRs above 1 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.
|
||||
* 4K render. A smaller preset (720p, 480p) takes a DPR under 1, so Chrome
|
||||
* rasterizes straight at the smaller size.
|
||||
*
|
||||
* Throws on:
|
||||
* - HDR + outputResolution (HDR compositor processes raw pixel buffers
|
||||
* at composition dimensions and would need parallel scaling).
|
||||
* - Aspect-ratio mismatch (e.g. landscape composition → portrait-4k).
|
||||
* - Non-integer scale ratio.
|
||||
* - Downsampling (output dimensions smaller than composition).
|
||||
* - Non-integer upsampling ratio.
|
||||
*/
|
||||
export function resolveDeviceScaleFactor(input: {
|
||||
compositionWidth: number;
|
||||
@@ -140,8 +138,9 @@ export function resolveDeviceScaleFactor(input: {
|
||||
if (!compat.ok) throw new Error(compat.message);
|
||||
|
||||
const target = CANVAS_DIMENSIONS[input.outputResolution];
|
||||
// Aspect ratios match → widthRatio === heightRatio. Compute once.
|
||||
return target.width / input.compositionWidth;
|
||||
// The two ratios match, except a smaller preset's long side rounded to an even pixel (854×480): the long side's, so
|
||||
// Chrome draws all 854 and the short side's 480.375 rounds to 480.
|
||||
return Math.max(target.width / input.compositionWidth, target.height / input.compositionHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -336,10 +336,11 @@ export async function runCompileStage(input: CompileStageInput): Promise<Compile
|
||||
hdrRequested: job.config.hdrMode === "force-hdr",
|
||||
alphaRequested: needsAlpha,
|
||||
});
|
||||
const outputWidth = width * deviceScaleFactor;
|
||||
const outputHeight = height * deviceScaleFactor;
|
||||
if (deviceScaleFactor > 1) {
|
||||
log.info("Supersampling composition via deviceScaleFactor", {
|
||||
// Rounded as Chrome sizes the screenshot: 854×480.375 → 854×480.
|
||||
const outputWidth = Math.round(width * deviceScaleFactor);
|
||||
const outputHeight = Math.round(height * deviceScaleFactor);
|
||||
if (deviceScaleFactor !== 1) {
|
||||
log.info("Scaling composition via deviceScaleFactor", {
|
||||
compositionWidth: width,
|
||||
compositionHeight: height,
|
||||
outputResolution: effectiveResolution,
|
||||
|
||||
@@ -1761,15 +1761,26 @@ describe("resolveDeviceScaleFactor", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects downsampling (4K composition → 1080p output)", () => {
|
||||
expect(() =>
|
||||
it("returns 0.5 for a 4K composition → 1080p output", () => {
|
||||
expect(
|
||||
resolveDeviceScaleFactor({
|
||||
...defaults,
|
||||
compositionWidth: 3840,
|
||||
compositionHeight: 2160,
|
||||
outputResolution: "landscape",
|
||||
}),
|
||||
).toThrow(/Downsampling/);
|
||||
).toBe(0.5);
|
||||
});
|
||||
|
||||
it("scales 1080p down for 720p and 480p, so 480p frames are 854×480", () => {
|
||||
const scale = (outputResolution: "landscape-720p" | "landscape-480p") =>
|
||||
resolveDeviceScaleFactor({ ...defaults, outputResolution });
|
||||
expect(scale("landscape-720p")).toBeCloseTo(2 / 3);
|
||||
expect(scale("landscape-480p")).toBe(854 / 1920);
|
||||
expect([
|
||||
Math.round(1920 * scale("landscape-480p")),
|
||||
Math.round(1080 * scale("landscape-480p")),
|
||||
]).toEqual([854, 480]);
|
||||
});
|
||||
|
||||
it("rejects non-integer scale factors", () => {
|
||||
|
||||
@@ -1669,7 +1669,7 @@ export function scanElementTags(html: string): ElementTagScan {
|
||||
// the count this gate reads. Converges: every iteration strictly shortens
|
||||
// the string or changes nothing and exits.
|
||||
let markup = html;
|
||||
for (let previous = ""; markup !== previous; ) {
|
||||
for (let previous = ""; markup !== previous;) {
|
||||
previous = markup;
|
||||
markup = markup.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, "");
|
||||
}
|
||||
@@ -2122,7 +2122,7 @@ export function shouldPreferSingleWorkerDrawElement(args: {
|
||||
* telemetry and keep the probe session alive through the heaviest stage.
|
||||
*/
|
||||
layeredOrEffectRoute: boolean;
|
||||
/** deviceScaleFactor > 1 — the engine's supersampling gate blocks DE. */
|
||||
/** deviceScaleFactor other than 1 — the engine's supersampling gate blocks DE. */
|
||||
supersampling: boolean;
|
||||
/**
|
||||
* The probe session already ran the engine's init-time DE gates and DE did
|
||||
@@ -2559,7 +2559,7 @@ export function resolveParallelCaptureMode(args: {
|
||||
forceScreenshot: boolean;
|
||||
deviceScaleFactor?: number;
|
||||
}): "screenshot" | "beginframe" {
|
||||
const supersampling = (args.deviceScaleFactor ?? 1) > 1;
|
||||
const supersampling = (args.deviceScaleFactor ?? 1) !== 1;
|
||||
return args.headlessShell && args.platform === "linux" && !args.forceScreenshot && !supersampling
|
||||
? "beginframe"
|
||||
: "screenshot";
|
||||
@@ -3643,7 +3643,7 @@ async function executeRenderPipeline(input: {
|
||||
minFrames: deSingleMinFrames,
|
||||
singleWorkerStreamingOk: shouldUseStreamingEncode(cfg, outputFormat, 1, job.duration),
|
||||
layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
|
||||
supersampling: deviceScaleFactor > 1,
|
||||
supersampling: deviceScaleFactor !== 1,
|
||||
probeDeGated:
|
||||
probeSession !== null &&
|
||||
probeSession.captureMode !== "drawelement" &&
|
||||
@@ -3736,7 +3736,7 @@ async function executeRenderPipeline(input: {
|
||||
totalFrames,
|
||||
minFrames: deParallelMinFrames,
|
||||
layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
|
||||
supersampling: deviceScaleFactor > 1,
|
||||
supersampling: deviceScaleFactor !== 1,
|
||||
probeDeGated:
|
||||
probeSession !== null &&
|
||||
probeSession.captureMode !== "drawelement" &&
|
||||
@@ -4492,8 +4492,8 @@ async function executeRenderPipeline(input: {
|
||||
outputFormat,
|
||||
streamingEncoderOptions: {
|
||||
fps: job.config.fps,
|
||||
width,
|
||||
height,
|
||||
width: captureCompositionWidth ?? width,
|
||||
height: captureCompositionHeight ?? height,
|
||||
codec: preset.codec,
|
||||
preset: preset.preset,
|
||||
quality: effectiveQuality,
|
||||
@@ -4595,8 +4595,8 @@ async function executeRenderPipeline(input: {
|
||||
outputFormat,
|
||||
streamingEncoderOptions: {
|
||||
fps: job.config.fps,
|
||||
width,
|
||||
height,
|
||||
width: captureCompositionWidth ?? width,
|
||||
height: captureCompositionHeight ?? height,
|
||||
codec: preset.codec,
|
||||
preset: preset.preset,
|
||||
quality: effectiveQuality,
|
||||
|
||||
Reference in New Issue
Block a user