Commit Graph
5 Commits
Author SHA1 Message Date
Vance Ingalls 2b25023e10 test(engine): cover spawnStreamingEncoder lifecycle and cleanup paths (#380)
## Summary

Add unit tests that mock `child_process.spawn` to drive an in-memory "ffmpeg" through the success/failure paths used by the producer's HDR encoder and by Chunk 5A's defensive `close()` in `renderOrchestrator`.

## Why

`Chunk 9D` of `plans/hdr-followups.md`. The contracts the orchestrator's try/finally cleanup (Chunk 5A) and the abort path rely on were entirely uncovered. A regression in `spawnStreamingEncoder`'s lifecycle handling would surface as a leaked ffmpeg process or a hung render, both of which are hard to diagnose after the fact.

## What changed

`packages/engine/src/services/streamingEncoder.test.ts`: 7 new specs covering:

- Successful exit after explicit `close()`.
- Non-zero exit before `close()` returns a failure result (no throw).
- `ENOENT` on spawn returns a failure result (no throw).
- Abort signal triggers `SIGTERM` and a `"cancelled"` result.
- `close()` is idempotent and never throws on a second call.
- `writeFrame` returns `false` after the encoder has exited.
- `close()` detaches the abort listener so post-close aborts don't re-kill ffmpeg.

These contracts are what the `renderOrchestrator` try/finally cleanup added in Chunk 5A relies on, and what the ffprobe-unavailable test (Chunk 9B) hinted at for the encoder side.

## Test plan

- [x] All new specs pass.
- [x] No production code changes — pure regression coverage of existing lifecycle behavior.

## Stack

Chunk 9D of `plans/hdr-followups.md`. Test-only change, complements Chunk 5A.
2026-04-23 13:10:41 -07:00
roiizchakandroi32 c11a332ef8 test(engine): cover h265 NVENC in preset-mapping regression tests (#443)
hevc_nvenc uses the same p1..p7 preset vocabulary as h264_nvenc, so the
mapping in `mapPresetForGpuEncoder` applies to both codecs. The initial
regression suite only covered `codec: "h264"`, which left a gap: a
future refactor that split the H.264 and H.265 NVENC paths could
silently regress one codec without any test catching it.

Add three-case loops (ultrafast → p1, medium → p4, veryslow → p7) under
`codec: "h265"` to both `buildEncoderArgs` and `buildStreamingArgs`
test blocks. Each case also asserts that `-c:v hevc_nvenc` is selected
so the test fails loudly if the codec plumbing is broken, not just the
preset translation.

Follow-up to #442 per review comment from @jrusso1020.

Co-authored-by: roi32 <75878108+roi32@users.noreply.github.com>
2026-04-23 09:20:44 -07:00
roiizchakandroi32 3b8de7a5eb fix(engine): accept libx264 preset names with NVENC and QSV (#442)
NVENC rejects the libx264 preset vocabulary (ultrafast / medium / slow /
...) with AVERROR(EINVAL) ("Error applying encoder options: Invalid
argument"), which surfaces as a bare `FFmpeg exited with code -22` from
spawn(). Because ENCODER_PRESETS passes these names straight through to
h264_nvenc / hevc_nvenc, every `--gpu` render using the `draft` tier
failed; `standard` (medium) and `high` (slow) only worked coincidentally
on ffmpeg builds that happened to accept those aliases. QSV has the same
problem on a narrower set (ultrafast / superfast / placebo).

Add `mapPresetForGpuEncoder` in utils/gpuEncoder.ts that translates the
libx264 vocabulary to each encoder's native names:

- nvenc: libx264 -> p1..p7 (already-native pN values pass through);
  unknown values fall back to p4 (medium)
- qsv:   ultrafast / superfast -> veryfast; placebo -> veryslow;
  everything else passes through
- videotoolbox / vaapi / null: unchanged

Both buildEncoderArgs (chunkEncoder.ts) and buildStreamingArgs
(streamingEncoder.ts) now route through the helper before pushing
`-preset` to the ffmpeg arg vector.

To make the next encoder-options failure diagnosable without re-running
ffmpeg by hand, \`formatFfmpegError\` in utils/runFfmpeg.ts now appends
the last 15 non-empty stderr lines to the error string. The four call
sites that previously swallowed stderr (encodeFramesFromDir,
muxVideoWithAudio, applyFaststart, and the streaming encoder exit
handler) have been updated.

Tested end-to-end on an RTX 4080 with ffmpeg 8.1 NVENC across
\`--quality draft|standard|high\` plus \`--video-bitrate\` and \`--crf\`
overrides; the 6 renders were visually equivalent to the CPU baseline.

Co-authored-by: roi32 <75878108+roi32@users.noreply.github.com>
2026-04-23 08:38:12 -07:00
James Russo 7b0c7e73b2 refactor: frame reorder buffer + port probe cleanup; add CREDITS.md and missing skill (#341)
* refactor(engine): restructure frame reorder buffer with Map-keyed storage

Rewrites createFrameReorderBuffer to use a Map<number, Array<() => void>>
keyed by frame index instead of a flat Array<{frame, resolve}> scanned on
every advance. O(1) lookups in enqueue/flush, fast-paths for the matching-
cursor and overshoot cases, and a small fix: waitForAllDone now coexists
with the writer still waiting on the final frame instead of colliding on
the same waiter slot.

Also adds 5 unit tests (there were none before) covering the fast-path,
out-of-order gating, multi-waiter-per-frame semantics, waitForAllDone
normal path, and the overshoot case.

Comment tweaks on buildChromeArgs — the flag profile is the standard
headless-for-capture set (Puppeteer / Playwright / Chrome headless-shell
all converge on similar flags); rephrased for clarity.

* refactor(cli): simplify port availability probe with async/await

Rewrites isPortAvailableOnHost from a single new-Promise callback into an
async/await form with an intermediate `bindError: ErrnoException | null`
variable. Makes the bind-then-release flow explicit as two sequential
awaits, and broadens the non-EADDRINUSE errno commentary (EADDRNOTAVAIL
for disabled IPv6, EACCES for privileged ports, EAFNOSUPPORT for missing
address families — all treated as "this host doesn't apply", not "port
occupied").

No behavior change to existing callers; all four portUtils tests still
pass.

* docs: add CREDITS.md and surface website-to-hyperframes skill

- New CREDITS.md acknowledging prior art in the browser-based video
  rendering space (Remotion) and the ecosystem HyperFrames builds on
  (Puppeteer, FFmpeg, GSAP, Hono). Standard OSS practice.

- Adds the `website-to-hyperframes` skill to the skills tables in
  README.md, docs/guides/prompting.mdx, and the project template at
  packages/cli/src/templates/_shared/CLAUDE.md. The skill ships in
  skills/ but was missing from every table.

- Adds `/hyperframes-registry` to the prose mention in the repo
  CLAUDE.md.
2026-04-19 16:33:42 -07:00
Vance Ingalls 5a3fde19d4 feat(engine): add HDR video output pipeline (#265)
## Summary

Adds the ability to render HDR video output (H.265 10-bit, BT.2020) from HyperFrames compositions. When the renderer detects HDR source video, it automatically switches to the HDR output pipeline — no flags needed.

## What it does

- **Auto-detection** — Probes each video source with `ffprobe`. If any has bt2020/PQ/HLG color metadata, the output switches to H.265 10-bit with correct color tags. SDR-only compositions are unaffected (H.264, bt709).
- **HLG pass-through** — Native HLG pixels from FFmpeg extraction are piped directly to the encoder without conversion. This avoids brightness loss from HLG→linear→PQ conversion (which requires an OOTF system gamma we can't reliably apply).
- **Encoder HDR support** — Both chunk and streaming encoders accept HDR presets: `libx265`, `yuv420p10le`, BT.2020 color primaries, `hvc1` codec tag (required for Apple playback).
- **WebGPU HDR capture (gated)** — A complete WebGPU float16 readback pipeline is implemented and tested but gated behind headed Chrome (headless doesn't expose WebGPU). Ready for future use with WebGPU canvas content.
- **HDR utilities** — `detectTransfer()` (PQ vs HLG), `getHdrEncoderColorParams()`, `analyzeCompositionHdr()`. 15 unit tests.

## Key design decisions

| Decision | Why |
|----------|-----|
| No `--hdr` flag | SDR content encoded as HDR causes orange shift in browsers. Auto-detect eliminates this. |
| HLG pass-through (not HLG→PQ) | Conversion loses brightness without OOTF. Pass-through matches source exactly. |
| `hvc1` codec tag | Apple QuickTime requires `hvc1` (not `hev1`) for HEVC playback. |
| 1-hour streaming timeout | HDR capture at ~6fps needs more time than the default 10-minute FFmpeg timeout. |

## Files changed

| File | What changed |
|------|-------------|
| `packages/engine/src/utils/hdr.ts` | **NEW** — HDR detection, transfer types, encoder params (15 tests) |
| `packages/engine/src/services/hdrCapture.ts` | **NEW** — WebGPU readback, HLG conversion, PQ encode |
| `packages/engine/src/services/streamingEncoder.ts` | HDR presets, raw rgb48le input, color tags |
| `packages/engine/src/services/chunkEncoder.ts` | HDR presets, conditional color tags |
| `packages/producer/src/services/renderOrchestrator.ts` | Auto-detection loop, HDR pass-through capture path |

## How to test

Render a composition with an HDR video source. The output should be H.265 10-bit with HDR metadata visible in `ffprobe` (bt2020, arib-std-b67 or smpte2084). Plays correctly in QuickTime and on HDR displays.

## Stack position

**2 of 6** — Stacked on #258 (SDR/HDR normalization). Provides the encoder infrastructure that phases 1-5 build on.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-19 15:10:59 -07:00