fix(producer): keep auto renders SDR when a graded canvas is present

This commit is contained in:
Miguel Ángel
2026-10-28 12:59:47 -04:00
parent 5085f495b5
commit 919df04fc0
10 changed files with 30 additions and 6 deletions
+1
View File
@@ -9,6 +9,7 @@ import { loadProducer } from "./producer.js";
export interface ProjectMedia {
videos: readonly Pick<VideoElement, "src" | "hasAudio" | "graded">[];
images: readonly { src: string; graded?: boolean }[];
gradedCanvas?: boolean;
audios: readonly AudioElement[];
/** The bundled document the lists were read from. */
html: string;
+1
View File
@@ -219,6 +219,7 @@ export {
// ── Media processing ───────────────────────────────────────────────────────────
export {
parseVideoElements,
hasGradedCanvas,
parseImageElements,
extractVideoFramesRange,
extractAllVideoFrames,
@@ -16,6 +16,7 @@ import { tmpdir } from "node:os";
import { spawnSync } from "node:child_process";
import {
parseVideoElements,
hasGradedCanvas,
parseImageElements,
extractAllVideoFrames,
extractVideoFramesRange,
@@ -751,6 +752,10 @@ describe("parseVideoElements", () => {
["gi", true],
["pi", undefined],
]);
expect(hasGradedCanvas(html)).toBe(false);
expect(
hasGradedCanvas(`<canvas data-color-grading='{"preset":"clean-studio"}'></canvas>`),
).toBe(true);
});
it.each([
@@ -105,7 +105,6 @@ export interface VideoElement {
loop: boolean;
hasAudio: boolean;
hidden?: boolean;
/** Carries a `data-color-grading` treatment, which grades in SDR. */
graded?: boolean;
}
@@ -858,10 +857,16 @@ export interface ImageElement {
src: string;
start: number;
end: number;
/** Carries a `data-color-grading` treatment, which grades in SDR. */
graded?: boolean;
}
export function hasGradedCanvas(html: string): boolean {
return (
parseHTML(unwrapTemplate(html)).document.querySelector(`canvas[${HF_COLOR_GRADING_ATTR}]`) !==
null
);
}
export function parseImageElements(html: string): ImageElement[] {
const images: ImageElement[] = [];
const { document } = parseHTML(unwrapTemplate(html));
+1
View File
@@ -233,5 +233,6 @@ describe("gradingKeepsSdr", () => {
expect(gradingKeepsSdr("auto", graded)).toBe(true);
expect(gradingKeepsSdr("force-hdr", graded)).toBe(false);
expect(gradingKeepsSdr("auto", { videos: [{}], images: [] })).toBe(false);
expect(gradingKeepsSdr("auto", { videos: [], images: [], gradedCanvas: true })).toBe(true);
});
});
+7 -2
View File
@@ -145,11 +145,16 @@ export function findHdrAutoPromotion(
/** Grading is SDR-only, so in auto mode a composition with graded media renders SDR, as `--sdr` does. */
export function gradingKeepsSdr(
hdrMode: "auto" | "force-hdr" | "force-sdr" | undefined,
media: { videos: readonly { graded?: boolean }[]; images: readonly { graded?: boolean }[] },
media: {
videos: readonly { graded?: boolean }[];
images: readonly { graded?: boolean }[];
gradedCanvas?: boolean;
},
): boolean {
return (
(hdrMode ?? "auto") === "auto" &&
[...media.videos, ...media.images].some(({ graded }) => graded === true)
(media.gradedCanvas === true ||
[...media.videos, ...media.images].some(({ graded }) => graded === true))
);
}
@@ -118,6 +118,7 @@ export interface CompiledComposition {
audios: AudioElement[];
images: ImageElement[];
droppedMediaIds: Set<string>;
gradedCanvas: boolean;
masterFxChain?: string;
unresolvedCompositions: UnresolvedElement[];
/** Assets that resolve outside projectDir. Keys are the path used in HTML, values are absolute filesystem paths. */
@@ -2236,7 +2237,7 @@ export async function compileForRender(
// within one composition file: two scenes declaring `<video id="clip">` — or
// two bare `<video>`s, both auto-numbered `hf-video-0` — collapsed into one
// entry and injected frames onto whichever element came first. See #3340.
const { videos, audios, images, droppedMediaIds } = collectRenderMedia(html);
const { videos, audios, images, droppedMediaIds, gradedCanvas } = collectRenderMedia(html);
const masterFxChain = parseMasterFxChain(html);
// Advisory video checks (sparse keyframes, VFR). Fire-and-forget — these spawn
@@ -2294,6 +2295,7 @@ export async function compileForRender(
audios,
images,
droppedMediaIds,
gradedCanvas,
...(masterFxChain ? { masterFxChain } : {}),
unresolvedCompositions,
externalAssets,
@@ -47,6 +47,7 @@ export interface CompositionMetadata {
videos: VideoElement[];
audios: AudioElement[];
images: ImageElement[];
gradedCanvas?: boolean;
masterFxChain?: string;
width: number;
height: number;
@@ -298,6 +298,7 @@ export async function runCompileStage(input: CompileStageInput): Promise<Compile
videos: compiled.videos,
audios: compiled.audios,
images: compiled.images,
gradedCanvas: compiled.gradedCanvas,
masterFxChain: compiled.masterFxChain,
width: compiled.width,
height: compiled.height,
@@ -26,6 +26,7 @@ import {
} from "@hyperframes/core/media-timing";
import {
parseVideoElements,
hasGradedCanvas,
parseImageElements,
parseAudioElements,
isSelfOrAncestorHidden,
@@ -211,6 +212,7 @@ export interface RenderMedia {
images: ImageElement[];
/** Ids of clips in the document that never play; the browser probe must not add them back. */
droppedMediaIds: Set<string>;
gradedCanvas: boolean;
}
/**
@@ -261,7 +263,7 @@ export function collectRenderMedia(html: string): RenderMedia {
else droppedMediaIds.add(elementId);
}
return { videos, audios, images, droppedMediaIds };
return { videos, audios, images, droppedMediaIds, gradedCanvas: hasGradedCanvas(html) };
}
function placeAudio(audio: AudioElement, window: HostWindow): AudioElement | null {