mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-04 21:17:30 +00:00
fix(producer): auto renders with color-graded media stay SDR instead of promoting to HDR (#6671)
* fix(producer): auto renders with color-graded media stay SDR instead of promoting to HDR * fix(producer): keep auto renders SDR when a graded canvas is present
This commit is contained in:
@@ -38,6 +38,20 @@ function bundledMedia(videos: string[], images: string[]) {
|
||||
}
|
||||
|
||||
describe("inspectHdrAutoPromotion", () => {
|
||||
it("reports no promotion when graded media keeps the render SDR, without probing", async () => {
|
||||
const extractMetadata = vi.fn(async () => metadata(HDR));
|
||||
const media = bundledMedia(["assets/source-hdr.mp4"], []);
|
||||
|
||||
await expect(
|
||||
inspectHdrAutoPromotion(
|
||||
PROJECT,
|
||||
{ ...media, images: [{ src: "assets/look.png", graded: true }] },
|
||||
extractMetadata,
|
||||
),
|
||||
).resolves.toBeNull();
|
||||
expect(extractMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports the local HDR asset that promotes automatic output", async () => {
|
||||
const assetPath = resolveProjectRelativeSrc("assets/source-hdr.mp4", PROJECT.dir, PROJECT.dir);
|
||||
const extractMetadata = vi.fn(async () => metadata(HDR));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
extractMediaMetadata,
|
||||
findHdrAutoPromotion,
|
||||
gradingKeepsSdr,
|
||||
type HdrAutoPromotion,
|
||||
} from "@hyperframes/engine";
|
||||
import type { ProjectDir } from "./project.js";
|
||||
@@ -13,6 +14,7 @@ export async function inspectHdrAutoPromotion(
|
||||
media: Pick<ProjectMedia, "videos" | "images">,
|
||||
extractMetadata: ExtractMetadata = extractMediaMetadata,
|
||||
): Promise<HdrAutoPromotion | null> {
|
||||
if (gradingKeepsSdr("auto", media)) return null;
|
||||
const assets: Array<{
|
||||
asset: string;
|
||||
colorSpace: Awaited<ReturnType<typeof extractMediaMetadata>>["colorSpace"];
|
||||
|
||||
@@ -7,8 +7,9 @@ import { loadProducer } from "./producer.js";
|
||||
|
||||
/** The render's own media list for a project: what it extracts, probes and mixes. */
|
||||
export interface ProjectMedia {
|
||||
videos: readonly Pick<VideoElement, "src" | "hasAudio">[];
|
||||
images: readonly { src: string }[];
|
||||
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;
|
||||
|
||||
@@ -221,6 +221,7 @@ export {
|
||||
// ── Media processing ───────────────────────────────────────────────────────────
|
||||
export {
|
||||
parseVideoElements,
|
||||
hasGradedCanvas,
|
||||
parseImageElements,
|
||||
extractVideoFramesRange,
|
||||
extractAllVideoFrames,
|
||||
@@ -487,6 +488,7 @@ export {
|
||||
getHdrEncoderColorParams,
|
||||
analyzeCompositionHdr,
|
||||
findHdrAutoPromotion,
|
||||
gradingKeepsSdr,
|
||||
formatHdrAutoPromotionWarning,
|
||||
sanitizeHdrAutoPromotionAsset,
|
||||
DEFAULT_HDR10_MASTERING,
|
||||
|
||||
@@ -16,6 +16,7 @@ import { tmpdir } from "node:os";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import {
|
||||
parseVideoElements,
|
||||
hasGradedCanvas,
|
||||
parseImageElements,
|
||||
extractAllVideoFrames,
|
||||
extractVideoFramesRange,
|
||||
@@ -738,6 +739,25 @@ describe("resolveFrameFormat", () => {
|
||||
});
|
||||
|
||||
describe("parseVideoElements", () => {
|
||||
it("marks a graded video and image, and only those", () => {
|
||||
const html = `<video id="g" src="g.mp4" data-color-grading='{"preset":"clean-studio"}'></video>
|
||||
<video id="p" src="p.mp4"></video>
|
||||
<img id="gi" src="gi.png" data-color-grading='{"adjust":{"exposure":0.2}}'>
|
||||
<img id="pi" src="pi.png">`;
|
||||
expect(parseVideoElements(html).map(({ id, graded }) => [id, graded])).toEqual([
|
||||
["g", true],
|
||||
["p", undefined],
|
||||
]);
|
||||
expect(parseImageElements(html).map(({ id, graded }) => [id, graded])).toEqual([
|
||||
["gi", true],
|
||||
["pi", undefined],
|
||||
]);
|
||||
expect(hasGradedCanvas(html)).toBe(false);
|
||||
expect(
|
||||
hasGradedCanvas(`<canvas data-color-grading='{"preset":"clean-studio"}'></canvas>`),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "valueless playback-start",
|
||||
|
||||
@@ -12,6 +12,7 @@ import { basename, join } from "path";
|
||||
import { cpus } from "os";
|
||||
import { createConcurrencyLimit } from "../utils/concurrencyLimit.js";
|
||||
import { parseHTML } from "linkedom";
|
||||
import { HF_COLOR_GRADING_ATTR } from "@hyperframes/core";
|
||||
import { isPathInside } from "@hyperframes/parsers/asset-paths";
|
||||
import { resolveProjectRelativeSrc } from "@hyperframes/parsers/asset-resolution";
|
||||
import { alphaDecoderFor } from "@hyperframes/parsers/media-alpha";
|
||||
@@ -104,6 +105,7 @@ export interface VideoElement {
|
||||
loop: boolean;
|
||||
hasAudio: boolean;
|
||||
hidden?: boolean;
|
||||
graded?: boolean;
|
||||
}
|
||||
|
||||
export interface ExtractedFrames {
|
||||
@@ -844,6 +846,7 @@ export function parseVideoElements(html: string): VideoElement[] {
|
||||
(isAudibleVideoElement(el) && isMemberGroupHidden(groupsById, el))
|
||||
? { hidden: true }
|
||||
: {}),
|
||||
...(el.hasAttribute(HF_COLOR_GRADING_ATTR) ? { graded: true } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -855,6 +858,14 @@ export interface ImageElement {
|
||||
src: string;
|
||||
start: number;
|
||||
end: number;
|
||||
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[] {
|
||||
@@ -894,7 +905,13 @@ export function parseImageElements(html: string): ImageElement[] {
|
||||
end = Infinity;
|
||||
}
|
||||
|
||||
images.push({ id, src, start, end });
|
||||
images.push({
|
||||
id,
|
||||
src,
|
||||
start,
|
||||
end,
|
||||
...(el.hasAttribute(HF_COLOR_GRADING_ATTR) ? { graded: true } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
return images;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
getHdrEncoderColorParams,
|
||||
analyzeCompositionHdr,
|
||||
findHdrAutoPromotion,
|
||||
gradingKeepsSdr,
|
||||
formatHdrAutoPromotionWarning,
|
||||
sanitizeHdrAutoPromotionAsset,
|
||||
DEFAULT_HDR10_MASTERING,
|
||||
@@ -224,3 +225,14 @@ describe("HDR auto-promotion reporting", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("gradingKeepsSdr", () => {
|
||||
const graded = { videos: [{}], images: [{ graded: true }] };
|
||||
it("keeps auto renders with graded media SDR, never a forced mode or ungraded media", () => {
|
||||
expect(gradingKeepsSdr(undefined, graded)).toBe(true);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -142,6 +142,22 @@ export function findHdrAutoPromotion(
|
||||
: null;
|
||||
}
|
||||
|
||||
/** 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 }[];
|
||||
gradedCanvas?: boolean;
|
||||
},
|
||||
): boolean {
|
||||
return (
|
||||
(hdrMode ?? "auto") === "auto" &&
|
||||
(media.gradedCanvas === true ||
|
||||
[...media.videos, ...media.images].some(({ graded }) => graded === true))
|
||||
);
|
||||
}
|
||||
|
||||
export function formatHdrAutoPromotionWarning(promotion: HdrAutoPromotion): string {
|
||||
return `[Render] HDR auto-promotion triggered by "${promotion.triggeringAsset}", output: ${promotion.output.colorSpace} / ${promotion.output.codec}`;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -7,7 +7,14 @@
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { ExtractionResult, VideoColorSpace } from "@hyperframes/engine";
|
||||
import { findRenderHdrAutoPromotionTrigger, resolveEffectiveHdrMode } from "./hdrMode.js";
|
||||
import {
|
||||
findRenderHdrAutoPromotionTrigger,
|
||||
rendersSdrOnly,
|
||||
resolveEffectiveHdrMode,
|
||||
} from "./hdrMode.js";
|
||||
|
||||
const NO_MEDIA = { videos: [], images: [] };
|
||||
const GRADED_VIDEO = { videos: [{ graded: true }, {}], images: [] };
|
||||
|
||||
function makeLog() {
|
||||
return { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() };
|
||||
@@ -37,9 +44,41 @@ const HDR_PQ: VideoColorSpace = {
|
||||
};
|
||||
|
||||
describe("resolveEffectiveHdrMode", () => {
|
||||
it("keeps an auto render with graded media SDR when an HDR source would promote it, and says why", () => {
|
||||
const log = makeLog();
|
||||
const result = resolveEffectiveHdrMode({
|
||||
media: GRADED_VIDEO,
|
||||
hdrMode: "auto",
|
||||
outputFormat: "mp4",
|
||||
extractionResult: extractionWith([HDR_PQ]),
|
||||
imageColorSpaces: [],
|
||||
log,
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining("color grading is SDR-only"));
|
||||
expect(log.warn).not.toHaveBeenCalledWith(expect.stringContaining("auto-promotion"));
|
||||
});
|
||||
|
||||
it("keeps --hdr with graded media HDR and warns that the grades are SDR", () => {
|
||||
const log = makeLog();
|
||||
const result = resolveEffectiveHdrMode({
|
||||
media: { videos: [], images: [{ graded: true }] },
|
||||
hdrMode: "force-hdr",
|
||||
outputFormat: "mp4",
|
||||
extractionResult: extractionWith([HDR_PQ]),
|
||||
imageColorSpaces: [],
|
||||
log,
|
||||
});
|
||||
expect(result).toEqual({ transfer: "pq" });
|
||||
expect(log.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("graded elements are graded in SDR"),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns undefined when force-sdr is set, regardless of HDR sources", () => {
|
||||
const log = makeLog();
|
||||
const result = resolveEffectiveHdrMode({
|
||||
media: NO_MEDIA,
|
||||
hdrMode: "force-sdr",
|
||||
outputFormat: "mp4",
|
||||
extractionResult: extractionWith([HDR_PQ]),
|
||||
@@ -53,6 +92,7 @@ describe("resolveEffectiveHdrMode", () => {
|
||||
it("auto-detects HDR from video sources when format=mp4", () => {
|
||||
const log = makeLog();
|
||||
const result = resolveEffectiveHdrMode({
|
||||
media: NO_MEDIA,
|
||||
hdrMode: undefined,
|
||||
outputFormat: "mp4",
|
||||
extractionResult: extractionWith([HDR_PQ]),
|
||||
@@ -71,6 +111,7 @@ describe("resolveEffectiveHdrMode", () => {
|
||||
it("auto-detects SDR with no HDR sources", () => {
|
||||
const log = makeLog();
|
||||
const result = resolveEffectiveHdrMode({
|
||||
media: NO_MEDIA,
|
||||
hdrMode: "auto",
|
||||
outputFormat: "mp4",
|
||||
extractionResult: extractionWith([]),
|
||||
@@ -84,6 +125,7 @@ describe("resolveEffectiveHdrMode", () => {
|
||||
it("force-hdr without sources falls back to HLG and warns", () => {
|
||||
const log = makeLog();
|
||||
const result = resolveEffectiveHdrMode({
|
||||
media: NO_MEDIA,
|
||||
hdrMode: "force-hdr",
|
||||
outputFormat: "mp4",
|
||||
extractionResult: extractionWith([]),
|
||||
@@ -99,6 +141,7 @@ describe("resolveEffectiveHdrMode", () => {
|
||||
it("force-hdr uses the dominant probed transfer when sources are HDR", () => {
|
||||
const log = makeLog();
|
||||
const result = resolveEffectiveHdrMode({
|
||||
media: NO_MEDIA,
|
||||
hdrMode: "force-hdr",
|
||||
outputFormat: "mp4",
|
||||
extractionResult: extractionWith([HDR_PQ]),
|
||||
@@ -113,6 +156,7 @@ describe("resolveEffectiveHdrMode", () => {
|
||||
for (const fmt of ["webm", "mov", "png-sequence"] as const) {
|
||||
const log = makeLog();
|
||||
const result = resolveEffectiveHdrMode({
|
||||
media: NO_MEDIA,
|
||||
hdrMode: "auto",
|
||||
outputFormat: fmt,
|
||||
extractionResult: extractionWith([HDR_PQ]),
|
||||
@@ -135,6 +179,7 @@ describe("resolveEffectiveHdrMode", () => {
|
||||
it("downgrades auto-detected HDR on hls with an SDR-only reason, not the alpha one", () => {
|
||||
const log = makeLog();
|
||||
const result = resolveEffectiveHdrMode({
|
||||
media: NO_MEDIA,
|
||||
hdrMode: "auto",
|
||||
outputFormat: "hls",
|
||||
extractionResult: extractionWith([HDR_PQ]),
|
||||
@@ -154,6 +199,7 @@ describe("resolveEffectiveHdrMode", () => {
|
||||
it("force-hdr without sources + non-mp4 format: still downgrades, two warns fire", () => {
|
||||
const log = makeLog();
|
||||
const result = resolveEffectiveHdrMode({
|
||||
media: NO_MEDIA,
|
||||
hdrMode: "force-hdr",
|
||||
outputFormat: "webm",
|
||||
extractionResult: extractionWith([]),
|
||||
@@ -202,3 +248,11 @@ describe("findRenderHdrAutoPromotionTrigger", () => {
|
||||
).toBe("assets/hero.png");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rendersSdrOnly", () => {
|
||||
it("is true for an auto render with graded media, so HDR sources are tone-mapped before the grade", () => {
|
||||
expect(rendersSdrOnly({ hdrMode: undefined, format: "mp4" }, GRADED_VIDEO)).toBe(true);
|
||||
expect(rendersSdrOnly({ hdrMode: "auto", format: "mp4" }, NO_MEDIA)).toBe(false);
|
||||
expect(rendersSdrOnly({ hdrMode: "force-hdr", format: "mp4" }, GRADED_VIDEO)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import {
|
||||
analyzeCompositionHdr,
|
||||
formatHdrAutoPromotionWarning,
|
||||
gradingKeepsSdr,
|
||||
HDR_AUTO_PROMOTION_PIPELINE,
|
||||
isHdrColorSpace,
|
||||
sanitizeHdrAutoPromotionAsset,
|
||||
@@ -21,8 +22,10 @@ import type { ProducerLogger } from "../../logger.js";
|
||||
import type { RenderConfig } from "../renderOrchestrator.js";
|
||||
|
||||
type EffectiveHdr = { transfer: HdrTransfer } | undefined;
|
||||
type RenderMedia = Parameters<typeof gradingKeepsSdr>[1];
|
||||
type HdrModeInput = {
|
||||
hdrMode: RenderConfig["hdrMode"];
|
||||
media: RenderMedia;
|
||||
outputFormat: NonNullable<RenderConfig["format"]>;
|
||||
extractionResult: ExtractionResult | null | undefined;
|
||||
imageColorSpaces: (VideoColorSpace | null)[];
|
||||
@@ -69,18 +72,41 @@ export function findRenderHdrAutoPromotionTrigger(input: {
|
||||
return input.images.find(({ id }) => input.nativeHdrImageIds.has(id))?.src;
|
||||
}
|
||||
|
||||
/** Warns when grading decided the output or will be wrong in it; true when it kept an HDR source's render SDR. */
|
||||
function logGradingResolution(
|
||||
input: HdrModeInput,
|
||||
hdrMode: NonNullable<RenderConfig["hdrMode"]>,
|
||||
effectiveHdr: EffectiveHdr,
|
||||
hasHdrSource: boolean,
|
||||
): boolean {
|
||||
const keptSdr = hasHdrSource && gradingKeepsSdr(hdrMode, input.media);
|
||||
if (keptSdr) {
|
||||
input.log.warn(
|
||||
"[Render] HDR source detected, but color grading is SDR-only, so rendering SDR: HDR sources are tone-mapped before the grade. Use --hdr to force HDR output (grades stay SDR).",
|
||||
);
|
||||
} else if (effectiveHdr && [...input.media.videos, ...input.media.images].some((m) => m.graded)) {
|
||||
input.log.warn(
|
||||
"[Render] HDR output with graded media: color grading is SDR-only, so graded elements are graded in SDR and placed in the HDR output.",
|
||||
);
|
||||
}
|
||||
return keptSdr;
|
||||
}
|
||||
|
||||
function logHdrResolution(
|
||||
input: HdrModeInput,
|
||||
hdrMode: NonNullable<RenderConfig["hdrMode"]>,
|
||||
effectiveHdr: EffectiveHdr,
|
||||
forcedHdrWithoutSources: boolean,
|
||||
hasHdrSource: boolean,
|
||||
): void {
|
||||
const keptSdrForGrading = logGradingResolution(input, hdrMode, effectiveHdr, hasHdrSource);
|
||||
if (forcedHdrWithoutSources) {
|
||||
input.log.warn(
|
||||
"[Render] HDR forced by --hdr flag, but no HDR sources were detected, defaulting to HLG. SDR-only compositions may look perceptually wrong on HDR displays.",
|
||||
);
|
||||
}
|
||||
if (!effectiveHdr) {
|
||||
if (keptSdrForGrading) return;
|
||||
input.log.info(
|
||||
hdrMode === "force-sdr"
|
||||
? "[Render] SDR forced by --sdr flag"
|
||||
@@ -111,8 +137,15 @@ function formatCarriesHdr(format: NonNullable<RenderConfig["format"]>): boolean
|
||||
return format === "mp4";
|
||||
}
|
||||
|
||||
export function rendersSdrOnly(config: Pick<RenderConfig, "hdrMode" | "format">): boolean {
|
||||
return config.hdrMode === "force-sdr" || !formatCarriesHdr(config.format ?? "mp4");
|
||||
export function rendersSdrOnly(
|
||||
config: Pick<RenderConfig, "hdrMode" | "format">,
|
||||
media: RenderMedia,
|
||||
): boolean {
|
||||
return (
|
||||
config.hdrMode === "force-sdr" ||
|
||||
gradingKeepsSdr(config.hdrMode, media) ||
|
||||
!formatCarriesHdr(config.format ?? "mp4")
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveEffectiveHdrMode(input: HdrModeInput): EffectiveHdr {
|
||||
@@ -126,7 +159,7 @@ export function resolveEffectiveHdrMode(input: HdrModeInput): EffectiveHdr {
|
||||
let effectiveHdr: EffectiveHdr;
|
||||
let forcedHdrWithoutSources = false;
|
||||
|
||||
if (hdrMode === "force-sdr") {
|
||||
if (hdrMode === "force-sdr" || gradingKeepsSdr(hdrMode, input.media)) {
|
||||
effectiveHdr = undefined;
|
||||
} else if (hdrMode === "force-hdr") {
|
||||
if (info?.hasHdr && info.dominantTransfer) {
|
||||
@@ -144,7 +177,7 @@ export function resolveEffectiveHdrMode(input: HdrModeInput): EffectiveHdr {
|
||||
effectiveHdr = undefined;
|
||||
}
|
||||
|
||||
logHdrResolution(input, hdrMode, effectiveHdr, forcedHdrWithoutSources);
|
||||
logHdrResolution(input, hdrMode, effectiveHdr, forcedHdrWithoutSources, info?.hasHdr === true);
|
||||
|
||||
return effectiveHdr;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* - `perfStages.videoExtractMs` is set at the same end-of-stage point.
|
||||
* - `materializeExtractedFramesForCompiledDir` is still called once
|
||||
* when `extractionResult.extracted` is non-empty.
|
||||
* - `force-sdr` mode still skips ALL ffprobe overhead.
|
||||
* - `force-sdr` mode, and auto mode with graded media, still skip ALL ffprobe overhead.
|
||||
*
|
||||
* New for distributed mode:
|
||||
* - `materializeSymlinks` (default `false`) — when `true`, the stage
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
detectTransfer,
|
||||
extractAllVideoFrames,
|
||||
extractMediaMetadata,
|
||||
gradingKeepsSdr,
|
||||
isVideoPastTimelineEnd,
|
||||
isHdrColorSpace,
|
||||
rebaseVideoToWindow,
|
||||
@@ -418,12 +419,14 @@ export async function runExtractVideosStage(
|
||||
|
||||
// Probe ORIGINAL color spaces before extraction (which may convert SDR→HDR).
|
||||
// This is needed to identify which videos are natively HDR vs converted-SDR
|
||||
// for the two-pass compositing path. Skipped only in force-sdr mode to
|
||||
// avoid ffprobe overhead when the user has explicitly opted out.
|
||||
// for the two-pass compositing path. Skipped when the render is SDR anyway:
|
||||
// force-sdr, or auto mode with graded media (grading is SDR-only).
|
||||
const probesHdr =
|
||||
job.config.hdrMode !== "force-sdr" && !gradingKeepsSdr(job.config.hdrMode, composition);
|
||||
const nativeHdrVideoIds = new Set<string>();
|
||||
const videoTransfers = new Map<string, HdrTransfer>();
|
||||
let hdrProbeTransientRetries = 0;
|
||||
if (job.config.hdrMode !== "force-sdr" && composition.videos.length > 0) {
|
||||
if (probesHdr && composition.videos.length > 0) {
|
||||
log?.info("Probing video color spaces...", { videoCount: composition.videos.length });
|
||||
const probeFailures = await Promise.all(
|
||||
composition.videos.map(async (v) => {
|
||||
@@ -473,14 +476,13 @@ export async function runExtractVideosStage(
|
||||
|
||||
// Probe images for HDR color spaces (16-bit PNGs tagged BT.2020 PQ/HLG).
|
||||
// Mirrors the video probe loop above so image-only compositions can
|
||||
// trigger HDR output without any video sources present. Skipped only in
|
||||
// force-sdr mode to avoid ffprobe overhead when the user has explicitly
|
||||
// opted out.
|
||||
// trigger HDR output without any video sources present. Skipped when the
|
||||
// video probe is.
|
||||
const nativeHdrImageIds = new Set<string>();
|
||||
const imageTransfers = new Map<string, HdrTransfer>();
|
||||
const hdrImageSrcPaths = new Map<string, string>();
|
||||
const imageColorSpaces: (VideoColorSpace | null)[] = [];
|
||||
if (job.config.hdrMode !== "force-sdr" && composition.images.length > 0) {
|
||||
if (probesHdr && composition.images.length > 0) {
|
||||
const probed = await Promise.all(
|
||||
composition.images.map(async (img) => {
|
||||
// Same shared resolver as the video probe above — a percent-encoded
|
||||
@@ -522,7 +524,7 @@ export async function runExtractVideosStage(
|
||||
fps: job.config.fps,
|
||||
outputDir: join(compiledDir, "__hyperframes_video_frames"),
|
||||
format: job.config.videoFrameFormat ?? "auto",
|
||||
toneMapHdrToSdr: rendersSdrOnly(job.config),
|
||||
toneMapHdrToSdr: rendersSdrOnly(job.config, composition),
|
||||
timelineEnd: composition.duration,
|
||||
maxTransientRetries: extractionPolicy.maxTransientRetries,
|
||||
collectProbeFailures: extractionPolicy.failureMode === "enforce",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -3557,6 +3557,7 @@ async function executeRenderPipeline(input: {
|
||||
});
|
||||
const effectiveHdr = resolveEffectiveHdrMode({
|
||||
hdrMode: job.config.hdrMode,
|
||||
media: composition,
|
||||
outputFormat,
|
||||
extractionResult,
|
||||
imageColorSpaces,
|
||||
|
||||
Reference in New Issue
Block a user