refactor(producer): extract HDR compositing helpers and rename media metadata (#373)

## Summary

Four behavior-preserving refactors that reduce complexity in `renderOrchestrator.ts` and clarify the engine ffprobe utility surface. Lands after the correctness fixes (Chunks 1–5) so the refactored code is already correct.

## Why

`Chunk 7` of `plans/hdr-followups.md`. The HDR composite block had grown a ~200 LOC inline closure with 14 captured deps, a repeated capture-options spread, a `extractVideoMetadata` name that now also handles still images, and per-frame re-creation of debug helpers.

## What changed

**7A — Hoist `compositeToBuffer` into a module-scoped helper.** Extract the inline HDR closure into a top-level `compositeHdrFrame()` that takes an `HdrCompositeContext` struct. Construct the context once at the top of the HDR render block and pass it through. Removes a deeply-nested closure from the middle of the orchestrator.

**7B — `buildHdrCaptureOptions()` helper.** Factor the repeated `{ ...captureOptions, skipReadinessVideoIds: ... }` spread into a named helper at the call site.

**7C — Rename `extractVideoMetadata` → `extractMediaMetadata`.** Reflects that the helper handles still images (PNG/JPEG/WebP) in addition to video. Update all callers in engine + producer (`videoFrameExtractor`, `htmlCompiler`, regression-harness, producer ffprobe re-export, tests). Re-export the old name as a deprecated alias from `@hyperframes/engine` for backward compatibility, plus the producer re-export shim.

**7D — Hoist debug counters to module scope.** `countNonZeroAlpha` and `countNonZeroRgb48` are now module-scoped so they aren't re-created per frame and so the closure has fewer captures.

Also touches the `hdr-regression` and `hdr-hlg-regression` README + `meta.json` files reviewed during this refactor.

## Test plan

- [x] `bunx tsc --noEmit -p packages/producer && bunx tsc --noEmit -p packages/engine` clean.
- [x] Engine tests: 313 pass, 0 fail (1218 expect calls).
- [x] `bunx oxlint` + `bunx oxfmt --check` clean on 8 changed source files.
- [x] Diff is structural only — no behavioral changes.

## Stack

Chunk 7 of `plans/hdr-followups.md`. Lands after the correctness fixes (Chunks 1–5) per the suggested merge order.
This commit is contained in:
Vance Ingalls
2026-04-22 22:41:30 -07:00
committed by GitHub
parent 2f58e9d188
commit a3d7cc1c95
10 changed files with 413 additions and 256 deletions
@@ -9,7 +9,7 @@ import { spawn } from "child_process";
import { existsSync, mkdirSync, readdirSync, rmSync } from "fs";
import { isAbsolute, join } from "path";
import { parseHTML } from "linkedom";
import { extractVideoMetadata, type VideoMetadata } from "../utils/ffprobe.js";
import { extractMediaMetadata, type VideoMetadata } from "../utils/ffprobe.js";
import {
analyzeCompositionHdr,
isHdrColorSpace as isHdrColorSpaceUtil,
@@ -158,7 +158,7 @@ export async function extractVideoFramesRange(
const videoOutputDir = join(outputDir, videoId);
if (!existsSync(videoOutputDir)) mkdirSync(videoOutputDir, { recursive: true });
const metadata = await extractVideoMetadata(videoPath);
const metadata = await extractMediaMetadata(videoPath);
const framePattern = `frame_%05d.${format}`;
const outputPattern = join(videoOutputDir, framePattern);
@@ -411,7 +411,7 @@ export async function extractAllVideoFrames(
// Phase 2: Probe color spaces and normalize if mixed HDR/SDR
const videoColorSpaces = await Promise.all(
resolvedVideos.map(async ({ videoPath }) => {
const metadata = await extractVideoMetadata(videoPath);
const metadata = await extractMediaMetadata(videoPath);
return metadata.colorSpace;
}),
);
@@ -457,7 +457,7 @@ export async function extractAllVideoFrames(
if (signal?.aborted) break;
const entry = resolvedVideos[i];
if (!entry) continue;
const metadata = await extractVideoMetadata(entry.videoPath);
const metadata = await extractMediaMetadata(entry.videoPath);
if (!metadata.isVFR) continue;
let segDuration = entry.video.end - entry.video.start;
@@ -503,7 +503,7 @@ export async function extractAllVideoFrames(
// Fallback: if no data-duration/data-end was specified (end is Infinity or 0),
// probe the actual video file to get its natural duration.
if (!Number.isFinite(videoDuration) || videoDuration <= 0) {
const metadata = await extractVideoMetadata(videoPath);
const metadata = await extractMediaMetadata(videoPath);
const sourceDuration = metadata.durationSeconds - video.mediaStart;
videoDuration = sourceDuration > 0 ? sourceDuration : metadata.durationSeconds;
video.end = video.start + videoDuration;