mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 10:06:21 +00:00
136 lines
4.7 KiB
TypeScript
136 lines
4.7 KiB
TypeScript
/**
|
||
* HDR Color Space Utilities
|
||
*
|
||
* Centralized HDR detection, transfer type handling, and FFmpeg color
|
||
* parameter generation for the HDR rendering pipeline.
|
||
*/
|
||
|
||
import type { VideoColorSpace } from "./ffprobe.js";
|
||
|
||
export type HdrTransfer = "hlg" | "pq";
|
||
|
||
/**
|
||
* Check if a video's color space indicates HDR content.
|
||
* Re-exported from videoFrameExtractor for backward compatibility.
|
||
*/
|
||
export function isHdrColorSpace(cs: VideoColorSpace | null): boolean {
|
||
if (!cs) return false;
|
||
// BT.2020 describes a color gamut/matrix, not a dynamic range. SDR media
|
||
// can legitimately carry BT.2020 primaries with a BT.709 transfer. Only
|
||
// the transfer function distinguishes HDR here: PQ (SMPTE 2084) or HLG.
|
||
return cs.colorTransfer === "smpte2084" || cs.colorTransfer === "arib-std-b67";
|
||
}
|
||
|
||
/**
|
||
* Determine the HDR transfer function from a video's color space metadata.
|
||
*
|
||
* IMPORTANT: Callers must gate on `isHdrColorSpace(cs)` first. This function
|
||
* assumes the input has already been classified as HDR and defaults ambiguous
|
||
* inputs to "hlg" — calling it with an SDR color space silently returns "hlg",
|
||
* which is wrong for SDR.
|
||
*
|
||
* Returns "pq" for SMPTE 2084, "hlg" for ARIB STD-B67, defaults to "hlg".
|
||
*/
|
||
export function detectTransfer(cs: VideoColorSpace | null): HdrTransfer {
|
||
if (cs?.colorTransfer === "smpte2084") return "pq";
|
||
return "hlg";
|
||
}
|
||
|
||
/**
|
||
* HDR static metadata for the encoded stream.
|
||
*
|
||
* `masterDisplay` is the SMPTE ST 2086 mastering-display color volume string
|
||
* accepted by x265 (`G(Gx,Gy)B(Bx,By)R(Rx,Ry)WP(WPx,WPy)L(Lmax,Lmin)`).
|
||
* Chromaticity values are scaled by 50000 (0.00002 cd/m² per unit) and
|
||
* luminance values by 10000 (0.0001 cd/m² per unit).
|
||
*
|
||
* `maxCll` is the CTA-861.3 Content Light Level pair `MaxCLL,MaxFALL` in
|
||
* cd/m². Without these SEI messages, downstream players (Apple QuickTime,
|
||
* YouTube, HDR TVs) treat the stream as SDR BT.2020 and tone-map incorrectly
|
||
* — see packages/producer/scripts/hdr-smoke.ts for the regression assertion.
|
||
*/
|
||
export interface HdrMasteringMetadata {
|
||
masterDisplay: string;
|
||
maxCll: string;
|
||
}
|
||
|
||
/**
|
||
* Default HDR10 mastering metadata: P3-D65 primaries inside a BT.2020
|
||
* container, mastered for 0.0001–1000 cd/m² with MaxCLL=1000, MaxFALL=400.
|
||
*
|
||
* These are conservative defaults that match how most HDR10 grading suites
|
||
* (Premiere, DaVinci Resolve) tag content when per-frame measured values
|
||
* aren't available. A future PR can plumb measured MaxCLL through `--hdr-opt`.
|
||
*/
|
||
export const DEFAULT_HDR10_MASTERING: HdrMasteringMetadata = {
|
||
masterDisplay: "G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1)",
|
||
maxCll: "1000,400",
|
||
};
|
||
|
||
export interface HdrEncoderColorParams {
|
||
colorPrimaries: string;
|
||
colorTrc: string;
|
||
colorspace: string;
|
||
pixelFormat: string;
|
||
/**
|
||
* Full x265-params string including color tagging and HDR static metadata.
|
||
* Pass directly to `-x265-params` (concatenate with other options via `:`).
|
||
*/
|
||
x265ColorParams: string;
|
||
/** The mastering metadata that was baked into `x265ColorParams`. */
|
||
mastering: HdrMasteringMetadata;
|
||
}
|
||
|
||
/**
|
||
* Get FFmpeg encoder color parameters for a given HDR transfer function.
|
||
*
|
||
* The returned `x265ColorParams` includes both color tagging
|
||
* (`colorprim`/`transfer`/`colormatrix`) and HDR static metadata
|
||
* (`master-display`/`max-cll`). Without the static metadata the encoded
|
||
* stream is rejected as SDR by most HDR-aware players and CDNs.
|
||
*/
|
||
export function getHdrEncoderColorParams(
|
||
transfer: HdrTransfer,
|
||
mastering: HdrMasteringMetadata = DEFAULT_HDR10_MASTERING,
|
||
): HdrEncoderColorParams {
|
||
const colorTrc = transfer === "pq" ? "smpte2084" : "arib-std-b67";
|
||
const tagging = `colorprim=bt2020:transfer=${colorTrc}:colormatrix=bt2020nc`;
|
||
const metadata = `master-display=${mastering.masterDisplay}:max-cll=${mastering.maxCll}`;
|
||
return {
|
||
colorPrimaries: "bt2020",
|
||
colorTrc,
|
||
colorspace: "bt2020nc",
|
||
pixelFormat: "yuv420p10le",
|
||
x265ColorParams: `${tagging}:${metadata}`,
|
||
mastering,
|
||
};
|
||
}
|
||
|
||
export interface CompositionHdrInfo {
|
||
hasHdr: boolean;
|
||
dominantTransfer: HdrTransfer | null;
|
||
}
|
||
|
||
/**
|
||
* Analyze a set of video color spaces to determine if the composition
|
||
* contains HDR content and what the dominant transfer function is.
|
||
*/
|
||
export function analyzeCompositionHdr(
|
||
colorSpaces: Array<VideoColorSpace | null>,
|
||
): CompositionHdrInfo {
|
||
let hasPq = false;
|
||
let hasHdr = false;
|
||
|
||
for (const cs of colorSpaces) {
|
||
if (!isHdrColorSpace(cs)) continue;
|
||
hasHdr = true;
|
||
if (cs?.colorTransfer === "smpte2084") hasPq = true;
|
||
}
|
||
|
||
if (!hasHdr) return { hasHdr: false, dominantTransfer: null };
|
||
|
||
// PQ takes priority — it's the more common HDR10 format
|
||
const dominantTransfer: HdrTransfer = hasPq ? "pq" : "hlg";
|
||
return { hasHdr: true, dominantTransfer };
|
||
}
|