From 5a0179b88eb3da0e10797288b0c3ce2435e33d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 28 Oct 2026 09:44:32 +0000 Subject: [PATCH] fix(studio-server): the render route passes a MOV ProRes profile to the render (#6644) * fix(studio-server): the render route passes a MOV ProRes profile to the render * fix(studio-server): drop the added comment line so the comment share does not rise --- .../src/helpers/encoderOptions.test.ts | 9 +++++++++ .../studio-server/src/helpers/encoderOptions.ts | 15 ++++++++++++++- packages/studio-server/src/routes/render.test.ts | 7 +++++++ packages/studio-server/src/types.ts | 1 + 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/studio-server/src/helpers/encoderOptions.test.ts b/packages/studio-server/src/helpers/encoderOptions.test.ts index d00cd46f91..a055ef8d5a 100644 --- a/packages/studio-server/src/helpers/encoderOptions.test.ts +++ b/packages/studio-server/src/helpers/encoderOptions.test.ts @@ -41,6 +41,15 @@ describe("parseEncoderOptions", () => { expect(parseEncoderOptions({ hdrMode: "on" }, "mp4").ok).toBe(false); }); + it("takes a ProRes profile for mov only, and rejects an unknown one", () => { + expect(parseEncoderOptions({ proresProfile: "hq" }, "mov")).toEqual({ + ok: true, + options: { proresProfile: "hq" }, + }); + expect(parseEncoderOptions({ proresProfile: "hq" }, "mp4")).toEqual({ ok: true, options: {} }); + expect(parseEncoderOptions({ proresProfile: "422hq" }, "mov").ok).toBe(false); + }); + it("accepts a keyframe interval and a rate cap for mp4", () => { expect( parseEncoderOptions( diff --git a/packages/studio-server/src/helpers/encoderOptions.ts b/packages/studio-server/src/helpers/encoderOptions.ts index 17384fbbd0..a3274768a6 100644 --- a/packages/studio-server/src/helpers/encoderOptions.ts +++ b/packages/studio-server/src/helpers/encoderOptions.ts @@ -2,6 +2,7 @@ const CRF_MAX_BY_FORMAT: Record = { mp4: 51, webm: 63 }; const VIDEO_BITRATE = /^\d+(\.\d+)?[kKmMgG]?$/; const VIDEO_FRAME_FORMATS = ["auto", "jpg", "png"] as const; const HDR_MODES = ["auto", "force-hdr", "force-sdr"] as const; +const PRORES_PROFILES = ["4444", "hq", "422", "lt", "proxy"] as const; interface EncoderOptions { gifLoop?: number; @@ -9,6 +10,7 @@ interface EncoderOptions { videoBitrate?: string; videoFrameFormat?: (typeof VIDEO_FRAME_FORMATS)[number]; hdrMode?: (typeof HDR_MODES)[number]; + proresProfile?: (typeof PRORES_PROFILES)[number]; keyframeIntervalSeconds?: number; maxBitrate?: string; bufferSize?: string; @@ -20,6 +22,7 @@ export interface EncoderOptionsBody { videoBitrate?: unknown; videoFrameFormat?: unknown; hdrMode?: unknown; + proresProfile?: unknown; keyframeIntervalSeconds?: unknown; maxBitrate?: unknown; bufferSize?: unknown; @@ -96,13 +99,23 @@ function parseDelivery(body: EncoderOptionsBody, format: string): Parsed { return { ok: true, options }; } +function parseProres(value: unknown, format: string): Parsed { + if (!isSet(value)) return { ok: true, options: {} }; + if (!oneOf(value, PRORES_PROFILES)) { + return { ok: false, error: `proresProfile must be one of ${PRORES_PROFILES.join(", ")}` }; + } + return { ok: true, options: format === "mov" ? { proresProfile: value } : {} }; +} + /** Explicit encoder overrides from a render request. MOV (ProRes) takes no rate control. */ export function parseEncoderOptions(body: EncoderOptionsBody, format: string): Parsed { const rate = parseRateControl(body, format); if (!rate.ok) return rate; const delivery = parseDelivery(body, format); if (!delivery.ok) return delivery; - const options: EncoderOptions = { ...rate.options, ...delivery.options }; + const prores = parseProres(body.proresProfile, format); + if (!prores.ok) return prores; + const options: EncoderOptions = { ...rate.options, ...delivery.options, ...prores.options }; if (isSet(body.gifLoop)) { if ( typeof body.gifLoop !== "number" || diff --git a/packages/studio-server/src/routes/render.test.ts b/packages/studio-server/src/routes/render.test.ts index 20ef482818..1cee39f31d 100644 --- a/packages/studio-server/src/routes/render.test.ts +++ b/packages/studio-server/src/routes/render.test.ts @@ -374,6 +374,13 @@ describe("POST /projects/:id/render — crf", () => { expect(both.opts).toBeUndefined(); }); + it("forwards a ProRes profile for mov to the render adapter and refuses an unknown one", async () => { + expect((await post({ format: "mov", proresProfile: "hq" })).opts.proresProfile).toBe("hq"); + const unknown = await post({ format: "mov", proresProfile: "422hq" }); + expect(unknown.res.status).toBe(400); + expect(unknown.opts).toBeUndefined(); + }); + it("forwards the keyframe interval and the rate cap to the render adapter", async () => { const { res, opts } = await post({ format: "mp4", diff --git a/packages/studio-server/src/types.ts b/packages/studio-server/src/types.ts index 360d3d4a97..85efaf974d 100644 --- a/packages/studio-server/src/types.ts +++ b/packages/studio-server/src/types.ts @@ -183,6 +183,7 @@ export interface StudioApiAdapter { bufferSize?: string; videoFrameFormat?: "auto" | "jpg" | "png"; hdrMode?: "auto" | "force-hdr" | "force-sdr"; + proresProfile?: "4444" | "hq" | "422" | "lt" | "proxy"; jobId: string; /** * The triggering browser profile has telemetry disabled (localStorage