@hyperframes/engine 0.6.119 → 0.6.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/package.json +24 -7
  2. package/scripts/generate-lut-reference.py +0 -168
  3. package/scripts/test-fitTextFontSize-browser.ts +0 -135
  4. package/src/cdp-headless-experimental.d.ts +0 -54
  5. package/src/config.test.ts +0 -213
  6. package/src/config.ts +0 -417
  7. package/src/index.ts +0 -273
  8. package/src/services/audioMixer.test.ts +0 -326
  9. package/src/services/audioMixer.ts +0 -604
  10. package/src/services/audioMixer.types.ts +0 -35
  11. package/src/services/audioVolumeEnvelope.test.ts +0 -176
  12. package/src/services/audioVolumeEnvelope.ts +0 -138
  13. package/src/services/browserManager.test.ts +0 -330
  14. package/src/services/browserManager.ts +0 -670
  15. package/src/services/chunkEncoder.test.ts +0 -1415
  16. package/src/services/chunkEncoder.ts +0 -831
  17. package/src/services/chunkEncoder.types.ts +0 -60
  18. package/src/services/extractionCache.test.ts +0 -199
  19. package/src/services/extractionCache.ts +0 -216
  20. package/src/services/fileServer.ts +0 -110
  21. package/src/services/frameCapture-discardWarmup.test.ts +0 -183
  22. package/src/services/frameCapture-namePolyfill.test.ts +0 -78
  23. package/src/services/frameCapture-pollImagesReady.test.ts +0 -153
  24. package/src/services/frameCapture-staticDedupIndex.test.ts +0 -76
  25. package/src/services/frameCapture-warmupTicks.test.ts +0 -174
  26. package/src/services/frameCapture.test.ts +0 -192
  27. package/src/services/frameCapture.ts +0 -1934
  28. package/src/services/hdrCapture.test.ts +0 -159
  29. package/src/services/hdrCapture.ts +0 -315
  30. package/src/services/parallelCoordinator.test.ts +0 -139
  31. package/src/services/parallelCoordinator.ts +0 -437
  32. package/src/services/screenshotService.test.ts +0 -510
  33. package/src/services/screenshotService.ts +0 -615
  34. package/src/services/streamingEncoder.test.ts +0 -832
  35. package/src/services/streamingEncoder.ts +0 -594
  36. package/src/services/systemMemory.test.ts +0 -324
  37. package/src/services/systemMemory.ts +0 -180
  38. package/src/services/videoFrameExtractor.test.ts +0 -1062
  39. package/src/services/videoFrameExtractor.ts +0 -1139
  40. package/src/services/videoFrameInjector.test.ts +0 -300
  41. package/src/services/videoFrameInjector.ts +0 -687
  42. package/src/services/vp9Options.ts +0 -13
  43. package/src/types.ts +0 -191
  44. package/src/utils/alphaBlit.test.ts +0 -1349
  45. package/src/utils/alphaBlit.ts +0 -1015
  46. package/src/utils/assertSwiftShader.test.ts +0 -130
  47. package/src/utils/assertSwiftShader.ts +0 -126
  48. package/src/utils/ffmpegBinaries.test.ts +0 -43
  49. package/src/utils/ffmpegBinaries.ts +0 -63
  50. package/src/utils/ffprobe.test.ts +0 -342
  51. package/src/utils/ffprobe.ts +0 -457
  52. package/src/utils/gpuEncoder.test.ts +0 -140
  53. package/src/utils/gpuEncoder.ts +0 -268
  54. package/src/utils/hdr.test.ts +0 -191
  55. package/src/utils/hdr.ts +0 -137
  56. package/src/utils/hdrCompositing.test.ts +0 -130
  57. package/src/utils/htmlTemplate.test.ts +0 -42
  58. package/src/utils/htmlTemplate.ts +0 -42
  59. package/src/utils/layerCompositor.test.ts +0 -150
  60. package/src/utils/layerCompositor.ts +0 -58
  61. package/src/utils/parityContract.ts +0 -1
  62. package/src/utils/processTracker.test.ts +0 -74
  63. package/src/utils/processTracker.ts +0 -41
  64. package/src/utils/readWebGlVendorInfoFromCanvas.ts +0 -52
  65. package/src/utils/runFfmpeg.test.ts +0 -102
  66. package/src/utils/runFfmpeg.ts +0 -136
  67. package/src/utils/shaderTransitions.test.ts +0 -738
  68. package/src/utils/shaderTransitions.ts +0 -1130
  69. package/src/utils/uint16-alignment-audit.test.ts +0 -125
  70. package/src/utils/urlDownloader.test.ts +0 -65
  71. package/src/utils/urlDownloader.ts +0 -143
  72. package/tsconfig.json +0 -19
  73. package/vitest.config.ts +0 -7
@@ -1,268 +0,0 @@
1
- // fallow-ignore-file complexity
2
- /**
3
- * GPU Encoder Detection
4
- *
5
- * Shared GPU encoder detection and naming utilities used by both
6
- * chunkEncoder and streamingEncoder services.
7
- */
8
-
9
- import { spawn } from "child_process";
10
- import { getFfmpegBinary } from "./ffmpegBinaries.js";
11
-
12
- export type ConcreteGpuEncoder = "nvenc" | "videotoolbox" | "vaapi" | "qsv" | "amf";
13
- export type GpuEncoder = ConcreteGpuEncoder | null;
14
-
15
- const GPU_ENCODER_CANDIDATES: ConcreteGpuEncoder[] = [
16
- "nvenc",
17
- "videotoolbox",
18
- "vaapi",
19
- "qsv",
20
- "amf",
21
- ];
22
-
23
- const H264_ENCODER_BY_GPU: Record<ConcreteGpuEncoder, string> = {
24
- nvenc: "h264_nvenc",
25
- videotoolbox: "h264_videotoolbox",
26
- vaapi: "h264_vaapi",
27
- qsv: "h264_qsv",
28
- amf: "h264_amf",
29
- };
30
-
31
- const GPU_PROBE_TIMEOUT_MS = 2000;
32
- const GPU_PROBE_KILL_GRACE_MS = 1000;
33
-
34
- export function getCompiledGpuEncoders(ffmpegEncodersStdout: string): ConcreteGpuEncoder[] {
35
- return GPU_ENCODER_CANDIDATES.filter((encoder) =>
36
- ffmpegEncodersStdout.includes(H264_ENCODER_BY_GPU[encoder]),
37
- );
38
- }
39
-
40
- export async function selectUsableGpuEncoder(
41
- candidates: readonly ConcreteGpuEncoder[],
42
- isUsable: (encoder: ConcreteGpuEncoder) => Promise<boolean>,
43
- ): Promise<GpuEncoder> {
44
- const results = await Promise.all(
45
- candidates.map(async (encoder) => {
46
- try {
47
- return { encoder, usable: await isUsable(encoder) };
48
- } catch {
49
- return { encoder, usable: false };
50
- }
51
- }),
52
- );
53
-
54
- for (const result of results) {
55
- if (result.usable) {
56
- return result.encoder;
57
- }
58
- }
59
- return null;
60
- }
61
-
62
- export async function detectGpuEncoder(): Promise<GpuEncoder> {
63
- return new Promise((resolve) => {
64
- const ffmpeg = spawn(getFfmpegBinary(), ["-encoders"], {
65
- stdio: ["pipe", "pipe", "pipe"],
66
- });
67
- let stdout = "";
68
-
69
- ffmpeg.stdout.on("data", (data) => {
70
- stdout += data.toString();
71
- });
72
-
73
- ffmpeg.on("close", () => {
74
- const candidates = getCompiledGpuEncoders(stdout);
75
- void selectUsableGpuEncoder(candidates, canUseGpuEncoder)
76
- .then(resolve)
77
- .catch(() => resolve(null));
78
- });
79
-
80
- ffmpeg.on("error", () => resolve(null));
81
- });
82
- }
83
-
84
- let cachedGpuEncoder: GpuEncoder | undefined = undefined;
85
-
86
- export async function getCachedGpuEncoder(): Promise<GpuEncoder> {
87
- if (cachedGpuEncoder === undefined) {
88
- cachedGpuEncoder = await detectGpuEncoder();
89
- }
90
- return cachedGpuEncoder;
91
- }
92
-
93
- export function getGpuEncoderName(encoder: GpuEncoder, codec: "h264" | "h265"): string {
94
- if (!encoder) return codec === "h264" ? "libx264" : "libx265";
95
- switch (encoder) {
96
- case "nvenc":
97
- return codec === "h264" ? "h264_nvenc" : "hevc_nvenc";
98
- case "videotoolbox":
99
- return codec === "h264" ? "h264_videotoolbox" : "hevc_videotoolbox";
100
- case "vaapi":
101
- return codec === "h264" ? "h264_vaapi" : "hevc_vaapi";
102
- case "qsv":
103
- return codec === "h264" ? "h264_qsv" : "hevc_qsv";
104
- case "amf":
105
- return codec === "h264" ? "h264_amf" : "hevc_amf";
106
- default:
107
- return codec === "h264" ? "libx264" : "libx265";
108
- }
109
- }
110
-
111
- // Minimum probe dimensions must clear every GPU encoder's hardware minimum.
112
- // NVIDIA data-center SKUs (L4/T4/A10/A100) reject frames below ~257px on
113
- // either dimension with "Frame Dimension less than the minimum supported
114
- // value" (observed on driver 595.58.03, CUDA 13.2). The documented SDK
115
- // minimums (145×49 H.264, 129×33 HEVC) are lower, but the driver enforces
116
- // a stricter per-SKU alignment. 320×240 clears all known GPU encoder
117
- // minimums (NVENC, VideoToolbox, VAAPI, QSV, AMF) while staying cheap.
118
- const GPU_PROBE_WIDTH = 320;
119
- const GPU_PROBE_HEIGHT = 240;
120
-
121
- export function getProbeArgs(encoder: ConcreteGpuEncoder): string[] {
122
- const args = [
123
- "-hide_banner",
124
- "-loglevel",
125
- "error",
126
- "-f",
127
- "lavfi",
128
- "-i",
129
- `color=size=${GPU_PROBE_WIDTH}x${GPU_PROBE_HEIGHT}:rate=1:duration=1`,
130
- "-frames:v",
131
- "1",
132
- "-an",
133
- ];
134
-
135
- if (encoder === "vaapi") {
136
- args.push("-vaapi_device", "/dev/dri/renderD128", "-vf", "format=nv12,hwupload");
137
- }
138
-
139
- args.push("-c:v", getGpuEncoderName(encoder, "h264"));
140
-
141
- if (encoder === "amf") {
142
- args.push("-rc", "cqp", "-qp_i", "28", "-qp_p", "28");
143
- }
144
-
145
- args.push("-f", "null", "-");
146
- return args;
147
- }
148
-
149
- async function canUseGpuEncoder(encoder: ConcreteGpuEncoder): Promise<boolean> {
150
- return new Promise((resolve) => {
151
- let settled = false;
152
- let timedOut = false;
153
- let killTimer: ReturnType<typeof setTimeout> | undefined;
154
- let stderr = "";
155
- const finish = (usable: boolean) => {
156
- if (settled) return;
157
- settled = true;
158
- clearTimeout(timer);
159
- if (killTimer) clearTimeout(killTimer);
160
- resolve(usable);
161
- };
162
- const ffmpeg = spawn(getFfmpegBinary(), getProbeArgs(encoder), {
163
- stdio: ["ignore", "ignore", "pipe"],
164
- });
165
-
166
- ffmpeg.stderr?.on("data", (data) => {
167
- stderr += data.toString();
168
- });
169
-
170
- const timer = setTimeout(() => {
171
- timedOut = true;
172
- ffmpeg.kill("SIGTERM");
173
- killTimer = setTimeout(() => {
174
- ffmpeg.kill("SIGKILL");
175
- finish(false);
176
- }, GPU_PROBE_KILL_GRACE_MS);
177
- }, GPU_PROBE_TIMEOUT_MS);
178
-
179
- ffmpeg.on("close", (code, signal) => {
180
- const usable = code === 0;
181
- logGpuProbeFailure(encoder, { code, signal, stderr, timedOut });
182
- finish(usable);
183
- });
184
-
185
- ffmpeg.on("error", (error) => {
186
- logGpuProbeFailure(encoder, { error, timedOut });
187
- finish(false);
188
- });
189
- });
190
- }
191
-
192
- function logGpuProbeFailure(
193
- encoder: ConcreteGpuEncoder,
194
- result: {
195
- code?: number | null;
196
- error?: Error;
197
- signal?: NodeJS.Signals | null;
198
- stderr?: string;
199
- timedOut?: boolean;
200
- },
201
- ): void {
202
- if (!isGpuProbeDebugEnabled()) return;
203
- if (result.code === 0 && !result.error && !result.timedOut) return;
204
-
205
- const reason = result.error
206
- ? result.error.message
207
- : result.timedOut
208
- ? `timed out after ${GPU_PROBE_TIMEOUT_MS}ms`
209
- : `exit=${String(result.code)} signal=${String(result.signal ?? "")}`;
210
- const stderr = result.stderr?.trim();
211
- console.warn(`[gpuEncoder] ${encoder} probe failed: ${reason}${stderr ? `\n${stderr}` : ""}`);
212
- }
213
-
214
- function isGpuProbeDebugEnabled(): boolean {
215
- const value = process.env.HYPERFRAMES_DEBUG_GPU_PROBE;
216
- return value === "1" || value === "true";
217
- }
218
-
219
- // libx264 preset names (ultrafast/superfast/.../placebo) mapped to the
220
- // equivalent NVENC p1..p7 preset. NVENC rejects libx264 names with
221
- // AVERROR(EINVAL) ("Error applying encoder options: Invalid argument"),
222
- // which surfaces as a generic "FFmpeg exited with code -22" — so callers
223
- // that share a single `preset` field across CPU and GPU paths (e.g. the
224
- // `draft`/`standard`/`high` quality tiers) must translate before passing
225
- // the value to h264_nvenc / hevc_nvenc.
226
- const NVENC_PRESET_MAP: Record<string, string> = {
227
- ultrafast: "p1",
228
- superfast: "p1",
229
- veryfast: "p2",
230
- faster: "p3",
231
- fast: "p4",
232
- medium: "p4",
233
- slow: "p5",
234
- slower: "p6",
235
- veryslow: "p7",
236
- placebo: "p7",
237
- };
238
-
239
- // QSV accepts most libx264 preset names but rejects `ultrafast`,
240
- // `superfast`, and `placebo`. Map those to the nearest supported values.
241
- const QSV_PRESET_MAP: Record<string, string> = {
242
- ultrafast: "veryfast",
243
- superfast: "veryfast",
244
- placebo: "veryslow",
245
- };
246
-
247
- /**
248
- * Translate a libx264-style `-preset` value to one accepted by the given
249
- * GPU encoder.
250
- *
251
- * - `nvenc`: libx264 names → `p1`..`p7`. Already-native `pN` values pass
252
- * through unchanged. Unknown values fall back to `p4` (medium).
253
- * - `qsv`: `ultrafast`/`superfast`/`placebo` → nearest supported name;
254
- * everything else passes through.
255
- * - `videotoolbox`, `vaapi`, `amf`, `null`: no remap (they either ignore
256
- * `-preset` entirely or accept the libx264 vocabulary).
257
- */
258
- export function mapPresetForGpuEncoder(encoder: GpuEncoder, preset: string): string {
259
- switch (encoder) {
260
- case "nvenc":
261
- if (/^p[1-7]$/.test(preset)) return preset;
262
- return NVENC_PRESET_MAP[preset] ?? "p4";
263
- case "qsv":
264
- return QSV_PRESET_MAP[preset] ?? preset;
265
- default:
266
- return preset;
267
- }
268
- }
@@ -1,191 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- isHdrColorSpace,
4
- detectTransfer,
5
- getHdrEncoderColorParams,
6
- analyzeCompositionHdr,
7
- DEFAULT_HDR10_MASTERING,
8
- } from "./hdr.js";
9
- import type { VideoColorSpace } from "./ffprobe.js";
10
-
11
- describe("isHdrColorSpace", () => {
12
- it("returns false for null", () => {
13
- expect(isHdrColorSpace(null)).toBe(false);
14
- });
15
-
16
- it("returns false for bt709 SDR", () => {
17
- expect(
18
- isHdrColorSpace({ colorTransfer: "bt709", colorPrimaries: "bt709", colorSpace: "bt709" }),
19
- ).toBe(false);
20
- });
21
-
22
- it("detects bt2020 primaries", () => {
23
- expect(
24
- isHdrColorSpace({ colorTransfer: "bt709", colorPrimaries: "bt2020", colorSpace: "bt709" }),
25
- ).toBe(true);
26
- });
27
-
28
- it("detects smpte2084 (PQ)", () => {
29
- expect(
30
- isHdrColorSpace({
31
- colorTransfer: "smpte2084",
32
- colorPrimaries: "bt2020",
33
- colorSpace: "bt2020nc",
34
- }),
35
- ).toBe(true);
36
- });
37
-
38
- it("detects arib-std-b67 (HLG)", () => {
39
- expect(
40
- isHdrColorSpace({
41
- colorTransfer: "arib-std-b67",
42
- colorPrimaries: "bt2020",
43
- colorSpace: "bt2020nc",
44
- }),
45
- ).toBe(true);
46
- });
47
- });
48
-
49
- describe("detectTransfer", () => {
50
- it("returns hlg for null", () => {
51
- expect(detectTransfer(null)).toBe("hlg");
52
- });
53
-
54
- it("returns pq for smpte2084", () => {
55
- expect(
56
- detectTransfer({
57
- colorTransfer: "smpte2084",
58
- colorPrimaries: "bt2020",
59
- colorSpace: "bt2020nc",
60
- }),
61
- ).toBe("pq");
62
- });
63
-
64
- it("returns hlg for arib-std-b67", () => {
65
- expect(
66
- detectTransfer({
67
- colorTransfer: "arib-std-b67",
68
- colorPrimaries: "bt2020",
69
- colorSpace: "bt2020nc",
70
- }),
71
- ).toBe("hlg");
72
- });
73
-
74
- it("returns hlg for bt709 (fallback)", () => {
75
- expect(
76
- detectTransfer({ colorTransfer: "bt709", colorPrimaries: "bt709", colorSpace: "bt709" }),
77
- ).toBe("hlg");
78
- });
79
- });
80
-
81
- describe("getHdrEncoderColorParams", () => {
82
- it("returns PQ params with mastering metadata", () => {
83
- const params = getHdrEncoderColorParams("pq");
84
- expect(params.colorTrc).toBe("smpte2084");
85
- expect(params.colorPrimaries).toBe("bt2020");
86
- expect(params.colorspace).toBe("bt2020nc");
87
- expect(params.pixelFormat).toBe("yuv420p10le");
88
- expect(params.x265ColorParams).toContain("colorprim=bt2020");
89
- expect(params.x265ColorParams).toContain("transfer=smpte2084");
90
- expect(params.x265ColorParams).toContain("colormatrix=bt2020nc");
91
- expect(params.mastering).toEqual(DEFAULT_HDR10_MASTERING);
92
- });
93
-
94
- it("returns HLG params with mastering metadata", () => {
95
- const params = getHdrEncoderColorParams("hlg");
96
- expect(params.colorTrc).toBe("arib-std-b67");
97
- expect(params.colorPrimaries).toBe("bt2020");
98
- expect(params.pixelFormat).toBe("yuv420p10le");
99
- expect(params.x265ColorParams).toContain("transfer=arib-std-b67");
100
- expect(params.mastering).toEqual(DEFAULT_HDR10_MASTERING);
101
- });
102
-
103
- // Regression guard for the side_data=[none] bug. See
104
- // packages/producer/scripts/hdr-smoke.ts and the bug-1 entry in
105
- // hdr-deferred-followups.md. Without master-display + max-cll in the
106
- // x265-params, downstream players (Apple QuickTime, YouTube, HDR TVs) treat
107
- // the file as SDR BT.2020 and tone-map incorrectly.
108
- it("emits master-display and max-cll for PQ", () => {
109
- const params = getHdrEncoderColorParams("pq");
110
- expect(params.x265ColorParams).toContain(
111
- `master-display=${DEFAULT_HDR10_MASTERING.masterDisplay}`,
112
- );
113
- expect(params.x265ColorParams).toContain(`max-cll=${DEFAULT_HDR10_MASTERING.maxCll}`);
114
- });
115
-
116
- it("emits master-display and max-cll for HLG", () => {
117
- const params = getHdrEncoderColorParams("hlg");
118
- expect(params.x265ColorParams).toContain(
119
- `master-display=${DEFAULT_HDR10_MASTERING.masterDisplay}`,
120
- );
121
- expect(params.x265ColorParams).toContain(`max-cll=${DEFAULT_HDR10_MASTERING.maxCll}`);
122
- });
123
-
124
- it("respects an explicit mastering override", () => {
125
- const custom = {
126
- masterDisplay: "G(1,2)B(3,4)R(5,6)WP(7,8)L(9,10)",
127
- maxCll: "500,200",
128
- };
129
- const params = getHdrEncoderColorParams("pq", custom);
130
- expect(params.mastering).toBe(custom);
131
- expect(params.x265ColorParams).toContain("master-display=G(1,2)B(3,4)R(5,6)WP(7,8)L(9,10)");
132
- expect(params.x265ColorParams).toContain("max-cll=500,200");
133
- });
134
-
135
- // The DEFAULT_HDR10_MASTERING values are tagged as "P3-D65 inside BT.2020,
136
- // 0.0001-1000 nits, MaxCLL 1000 / MaxFALL 400". If anyone tweaks these
137
- // numbers without updating the docstring or the deferred-followups doc,
138
- // this test will fail and force a deliberate review.
139
- it("DEFAULT_HDR10_MASTERING matches the documented HDR10 reference", () => {
140
- expect(DEFAULT_HDR10_MASTERING.masterDisplay).toBe(
141
- "G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1)",
142
- );
143
- expect(DEFAULT_HDR10_MASTERING.maxCll).toBe("1000,400");
144
- });
145
- });
146
-
147
- describe("analyzeCompositionHdr", () => {
148
- const sdr: VideoColorSpace = {
149
- colorTransfer: "bt709",
150
- colorPrimaries: "bt709",
151
- colorSpace: "bt709",
152
- };
153
- const hlg: VideoColorSpace = {
154
- colorTransfer: "arib-std-b67",
155
- colorPrimaries: "bt2020",
156
- colorSpace: "bt2020nc",
157
- };
158
- const pq: VideoColorSpace = {
159
- colorTransfer: "smpte2084",
160
- colorPrimaries: "bt2020",
161
- colorSpace: "bt2020nc",
162
- };
163
-
164
- it("returns no HDR for all SDR", () => {
165
- expect(analyzeCompositionHdr([sdr, sdr, null])).toEqual({
166
- hasHdr: false,
167
- dominantTransfer: null,
168
- });
169
- });
170
-
171
- it("detects HLG", () => {
172
- expect(analyzeCompositionHdr([sdr, hlg])).toEqual({
173
- hasHdr: true,
174
- dominantTransfer: "hlg",
175
- });
176
- });
177
-
178
- it("detects PQ", () => {
179
- expect(analyzeCompositionHdr([sdr, pq])).toEqual({
180
- hasHdr: true,
181
- dominantTransfer: "pq",
182
- });
183
- });
184
-
185
- it("PQ takes priority over HLG in mixed HDR", () => {
186
- expect(analyzeCompositionHdr([hlg, pq])).toEqual({
187
- hasHdr: true,
188
- dominantTransfer: "pq",
189
- });
190
- });
191
- });
package/src/utils/hdr.ts DELETED
@@ -1,137 +0,0 @@
1
- /**
2
- * HDR Color Space Utilities
3
- *
4
- * Centralized HDR detection, transfer type handling, and FFmpeg color
5
- * parameter generation for the HDR rendering pipeline.
6
- */
7
-
8
- import type { VideoColorSpace } from "./ffprobe.js";
9
-
10
- export type HdrTransfer = "hlg" | "pq";
11
-
12
- /**
13
- * Check if a video's color space indicates HDR content.
14
- * Re-exported from videoFrameExtractor for backward compatibility.
15
- */
16
- export function isHdrColorSpace(cs: VideoColorSpace | null): boolean {
17
- if (!cs) return false;
18
- return (
19
- cs.colorPrimaries.includes("bt2020") ||
20
- cs.colorSpace.includes("bt2020") ||
21
- cs.colorTransfer === "smpte2084" ||
22
- cs.colorTransfer === "arib-std-b67"
23
- );
24
- }
25
-
26
- /**
27
- * Determine the HDR transfer function from a video's color space metadata.
28
- *
29
- * IMPORTANT: Callers must gate on `isHdrColorSpace(cs)` first. This function
30
- * assumes the input has already been classified as HDR and defaults ambiguous
31
- * inputs to "hlg" — calling it with an SDR color space silently returns "hlg",
32
- * which is wrong for SDR.
33
- *
34
- * Returns "pq" for SMPTE 2084, "hlg" for ARIB STD-B67, defaults to "hlg".
35
- */
36
- export function detectTransfer(cs: VideoColorSpace | null): HdrTransfer {
37
- if (cs?.colorTransfer === "smpte2084") return "pq";
38
- return "hlg";
39
- }
40
-
41
- /**
42
- * HDR static metadata for the encoded stream.
43
- *
44
- * `masterDisplay` is the SMPTE ST 2086 mastering-display color volume string
45
- * accepted by x265 (`G(Gx,Gy)B(Bx,By)R(Rx,Ry)WP(WPx,WPy)L(Lmax,Lmin)`).
46
- * Chromaticity values are scaled by 50000 (0.00002 cd/m² per unit) and
47
- * luminance values by 10000 (0.0001 cd/m² per unit).
48
- *
49
- * `maxCll` is the CTA-861.3 Content Light Level pair `MaxCLL,MaxFALL` in
50
- * cd/m². Without these SEI messages, downstream players (Apple QuickTime,
51
- * YouTube, HDR TVs) treat the stream as SDR BT.2020 and tone-map incorrectly
52
- * — see packages/producer/scripts/hdr-smoke.ts for the regression assertion.
53
- */
54
- export interface HdrMasteringMetadata {
55
- masterDisplay: string;
56
- maxCll: string;
57
- }
58
-
59
- /**
60
- * Default HDR10 mastering metadata: P3-D65 primaries inside a BT.2020
61
- * container, mastered for 0.0001–1000 cd/m² with MaxCLL=1000, MaxFALL=400.
62
- *
63
- * These are conservative defaults that match how most HDR10 grading suites
64
- * (Premiere, DaVinci Resolve) tag content when per-frame measured values
65
- * aren't available. A future PR can plumb measured MaxCLL through `--hdr-opt`.
66
- */
67
- export const DEFAULT_HDR10_MASTERING: HdrMasteringMetadata = {
68
- masterDisplay: "G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1)",
69
- maxCll: "1000,400",
70
- };
71
-
72
- export interface HdrEncoderColorParams {
73
- colorPrimaries: string;
74
- colorTrc: string;
75
- colorspace: string;
76
- pixelFormat: string;
77
- /**
78
- * Full x265-params string including color tagging and HDR static metadata.
79
- * Pass directly to `-x265-params` (concatenate with other options via `:`).
80
- */
81
- x265ColorParams: string;
82
- /** The mastering metadata that was baked into `x265ColorParams`. */
83
- mastering: HdrMasteringMetadata;
84
- }
85
-
86
- /**
87
- * Get FFmpeg encoder color parameters for a given HDR transfer function.
88
- *
89
- * The returned `x265ColorParams` includes both color tagging
90
- * (`colorprim`/`transfer`/`colormatrix`) and HDR static metadata
91
- * (`master-display`/`max-cll`). Without the static metadata the encoded
92
- * stream is rejected as SDR by most HDR-aware players and CDNs.
93
- */
94
- export function getHdrEncoderColorParams(
95
- transfer: HdrTransfer,
96
- mastering: HdrMasteringMetadata = DEFAULT_HDR10_MASTERING,
97
- ): HdrEncoderColorParams {
98
- const colorTrc = transfer === "pq" ? "smpte2084" : "arib-std-b67";
99
- const tagging = `colorprim=bt2020:transfer=${colorTrc}:colormatrix=bt2020nc`;
100
- const metadata = `master-display=${mastering.masterDisplay}:max-cll=${mastering.maxCll}`;
101
- return {
102
- colorPrimaries: "bt2020",
103
- colorTrc,
104
- colorspace: "bt2020nc",
105
- pixelFormat: "yuv420p10le",
106
- x265ColorParams: `${tagging}:${metadata}`,
107
- mastering,
108
- };
109
- }
110
-
111
- export interface CompositionHdrInfo {
112
- hasHdr: boolean;
113
- dominantTransfer: HdrTransfer | null;
114
- }
115
-
116
- /**
117
- * Analyze a set of video color spaces to determine if the composition
118
- * contains HDR content and what the dominant transfer function is.
119
- */
120
- export function analyzeCompositionHdr(
121
- colorSpaces: Array<VideoColorSpace | null>,
122
- ): CompositionHdrInfo {
123
- let hasPq = false;
124
- let hasHdr = false;
125
-
126
- for (const cs of colorSpaces) {
127
- if (!isHdrColorSpace(cs)) continue;
128
- hasHdr = true;
129
- if (cs?.colorTransfer === "smpte2084") hasPq = true;
130
- }
131
-
132
- if (!hasHdr) return { hasHdr: false, dominantTransfer: null };
133
-
134
- // PQ takes priority — it's the more common HDR10 format
135
- const dominantTransfer: HdrTransfer = hasPq ? "pq" : "hlg";
136
- return { hasHdr: true, dominantTransfer };
137
- }