Commit Graph
1416 Commits
Author SHA1 Message Date
Miguel Ángel c46adb52e2 chore: release v0.6.44 v0.6.44 2026-05-25 16:56:50 +00:00
Miguel Ángel 5fe62fc924 fix(studio): tighten media decode filter with error_name + sampled counter
Address review feedback:
- AND with error_name === "EncodingError" for tighter filtering
- Add sampled composition_asset_error_filtered tracking event (fires on
  1st occurrence, then every 100th) so filtered errors aren't completely
  invisible in telemetry
2026-05-25 12:55:41 -04:00
Miguel Ángel 87cffb6904 Merge pull request #1071 from heygen-com/fix/studio-cross-origin-iframe-guards 2026-05-25 12:53:06 -04:00
Miguel Ángel c1b26fcb32 fix(studio): guard cross-origin iframe access to prevent SecurityError crashes
Wrap all contentWindow/contentDocument access and addEventListener/removeEventListener
calls in try/catch across usePlaybackKeyboard, useAppHotkeys, and CompositionsTab.
Prevents SecurityError from propagating to the React error boundary (white screen).
Affects 1,885 crashes / 648 unique users in the last 7 days.
2026-05-25 16:49:40 +00:00
Miguel Ángel 179241e932 fix(studio): filter media decode errors from crash telemetry 2026-05-25 16:49:40 +00:00
Miguel ÁngelandClaude Sonnet 4.6 e4e2234303 chore: release v0.6.43
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
v0.6.43
2026-05-25 13:35:55 +00:00
Miguel Ángel 8b776cb3c8 Merge pull request #1069 from func25/fix/preview-reuse-and-waapi-seek-baselines 2026-05-25 09:33:54 -04:00
Miguel Ángel 375e298093 Merge pull request #1068 from func25/fix/subcomp-local-script-bundling 2026-05-25 09:32:24 -04:00
func25 a0169309ae test(cli): normalize project paths in preview reuse probe tests 2026-05-25 18:08:51 +07:00
func25 e10ec358f8 fix(core): correct WAAPI rediscovery seek baselines and preview reuse invalidation 2026-05-25 17:51:47 +07:00
func25 3bb0d1efd9 fix(core): wrap bundled composition scripts as string literals 2026-05-25 15:41:13 +07:00
func25 e101b47be4 fix(core): preserve sub-composition script order in preview bundles 2026-05-25 15:27:52 +07:00
func25 bbb7a4d965 fix(core): inline local sub-composition scripts in preview bundles 2026-05-25 15:12:11 +07:00
Miguel Ángel 68fce93a49 Merge pull request #1041 from kiyeonjeon21/docs/rendering-comparison-license
docs: update rendering comparison and license wording
2026-05-24 23:51:47 -04:00
Miguel Ángel d2b915be9e chore: release v0.6.42 v0.6.42 2026-05-24 17:36:30 -04:00
Miguel Ángel 047c1e1d33 fix: support animated media volume (#1066)
## Problem

Audio-capable media could only use a static `data-volume` value during preview/render. Issue #1064 reports this for `<audio>`: GSAP/JS attempts to fade an element from `volume: 0` to a non-zero value still rendered as silent because HyperFrames only respected the initial `data-volume` value.

The same root cause applies to `<video data-has-audio="true">`: preview/runtime can animate the media element, but the offline audio mix previously only had one static volume number for the extracted audio track.

Closes #1064.

## What this fixes

- Preserves authored `HTMLMediaElement.volume` changes made by GSAP/JS between runtime media sync ticks instead of clobbering them back to `data-volume`.
- Updates the WebAudio transport gain for active media sources when the element volume changes.
- Probes scripted media timelines in the producer browser pass and records sampled volume keyframes for both `<audio>` elements and video-derived audio tracks.
- Maps extracted video audio IDs such as `bg-video-audio` back to their source `<video id="bg-video">` so video volume automation is sampled from the actual element.
- Converts sampled keyframes into a frame-evaluated FFmpeg `volume` expression during audio mixing, so rendered output includes fades and other timeline-driven media volume changes.

## Root cause

There were two static-volume paths:

1. Runtime media sync treated `clip.volume` parsed from `data-volume` as authoritative on every tick and rewrote `el.volume = clip.volume * userVolume`, undoing GSAP/JS updates after timeline seeks.
2. The producer audio stage mixed audio from compile/probe metadata, where `volume` was a single number. The FFmpeg filter used `volume=<initial value>`, so a clip starting at `data-volume="0"` stayed silent in the muxed output even if the browser timeline had changed `audio.volume` or `video.volume`.

The fix makes the browser/runtime media volume the source of truth when authors animate it, then carries that time-varying signal into the offline mix.

## Verification

### Local checks

- `bun install` in the clean worktree
- `bun run build:hyperframes-runtime`
- `bun run --filter @hyperframes/core test -- src/runtime/media.test.ts`
- `bun run --filter @hyperframes/engine test -- src/services/audioMixer.test.ts`
- `bun run --filter @hyperframes/core typecheck && bun run --filter @hyperframes/engine typecheck && bun run --filter @hyperframes/producer typecheck`
- `bunx oxfmt --check <10 touched files>`
- `bunx oxlint <10 touched files>`
- `bun run --filter @hyperframes/producer build`

### Audio repro and render proof

Created `/tmp/hf-1064` with `<audio id="bg-audio" ... data-volume="0">` and `gsap.to(audio, { volume: 1 })`.

Before fix:
- `RMS level dB: -inf`

After fix:
- `[Probe] Runtime audio volume automation: bg-audio {"keyframeCount":12}`
- `RMS level dB: -24.133461`

### Video repro and render proof

Created `/tmp/hf-1064-video` with `<video id="bg-video" data-has-audio="true" data-volume="0">` and `gsap.to(video, { volume: 1 })`.

After widening the probe mapping:
- `[Probe] Runtime audio volume automation: bg-video-audio {"keyframeCount":12}`
- `RMS level dB: -24.203664`

### Preview verification

Used `hyperframes preview` from the PR branch and `agent-browser` against Studio:

- Audio preview direct seek through `window.__player.seek(...)` showed `#bg-audio.volume`: `0 -> 0.5 -> 1`.
- Audio preview playback advanced to ~0.92s with `#bg-audio.volume` ~0.917 and the audio element unpaused.
- Video preview direct seek showed `#bg-video.volume`: `0 -> 0.5 -> 1`.
- Video preview playback sampled during the fade at ~0.68s showed `#bg-video.volume` around `0.664`.

### Browser verification

Used `agent-browser` against the rendered MP4 from the PR branch:

- Opened `file:///tmp/hf-1064/out-pr.mp4`
- Verified the browser loaded a playable `<video>`: `{"hasVideo":true,"duration":3.020996,"readyState":4,"paused":false}`
- Screenshot artifact: `/Users/miguel07code/dev/hyperframes-oss/qa-artifacts/issue-1064/pr-rendered-video.png`
- Recording artifact: `/Users/miguel07code/dev/hyperframes-oss/qa-artifacts/issue-1064/pr-rendered-video.webm`

## Notes

- The commit was prepared in a clean worktree based on `origin/main` to avoid mixing in the existing `fix/nonlatin-media-src-resolution` checkout changes.
- Pre-commit lint, format, and typecheck passed. The hook's Fallow audit still exits non-zero on inherited duplication/complexity in the touched large runtime/probe files; the dead-code issue from the new type export was fixed before committing.
- Volume automation is sampled from the browser timeline and approximated as piecewise-linear FFmpeg volume expressions. This is intended for timeline-driven GSAP/JS fades, not audio-rate DSP.
2026-05-24 17:33:31 -04:00
Miguel Ángel 5a3b76a218 fix: parse scripted volume probes with html parser 2026-05-24 17:09:57 -04:00
Miguel Ángel 50c972fd50 fix: address audio volume review feedback 2026-05-24 17:02:35 -04:00
Miguel Ángel 947bf6cc78 Merge remote-tracking branch 'origin/main' into fix/audio-volume-automation
# Conflicts:
#	packages/engine/src/services/audioMixer.test.ts
2026-05-24 16:46:52 -04:00
Miguel Ángel 720bb9d851 Merge pull request #1065 from heygen-com/fix/nonlatin-media-src-resolution
fix(engine): resolve encoded non-Latin media paths
2026-05-24 16:41:16 -04:00
Miguel Ángel 167a222318 fix: support animated audio volume 2026-05-24 16:39:04 -04:00
Miguel Ángel a68b4b6590 fix(cli): decode linked stylesheet paths 2026-05-24 16:38:54 -04:00
Miguel Ángel 526709cad2 fix(cli): handle encoded lint asset paths 2026-05-24 16:31:45 -04:00
Miguel Ángel 7ad10a2dff fix(engine): resolve encoded media src paths 2026-05-24 15:58:42 -04:00
Miguel Ángel 7461f1df30 chore: bump version to 0.6.41 v0.6.41 2026-05-24 14:26:59 -04:00
Miguel Ángel 47789e10f9 Merge pull request #1061 from heygen-com/fix/snapshot-seek-parity
fix(core,cli): defer __renderReady until root timeline is bound
2026-05-24 14:25:05 -04:00
Miguel Ángel 1d09e6ff36 fix(core): set __renderReady unconditionally after binding attempt
The capturedTimeline guard broke CSS/WAAPI/Lottie compositions that
have no GSAP timeline — __renderReady was never set, causing the
parity harness to timeout after 30s.

renderSeek works with or without a GSAP timeline (adapter-only
seeking), so the correct invariant is "timeline binding was
attempted" not "a timeline was found." Set __renderReady
unconditionally in all three paths, after bindRootTimelineIfAvailable
has run.
2026-05-24 13:42:30 -04:00
Miguel Ángel 3347486ae9 refactor(core): remove redundant as casts using window.d.ts declarations
window.d.ts already declares __timelines, __player, __playerReady,
and __renderReady on the global Window interface. The casts in
init.ts and init.test.ts were re-asserting the same types.

- Add __hfRuntimeTeardown to window.d.ts (used 6x in init.ts)
- Remove runtimeWindow cast variable from init.ts — use window directly
- Remove all (window as Window & { __player?: ... }).__player casts
  from init.test.ts — window.__player is already typed as PlayerAPI
- Remove all (window as Window & { __timelines?: ... }).__timelines
  casts from init.test.ts — window.__timelines is already typed
- Remove (window as Window & { __playerReady/renderReady }}) casts
  from init.ts — already declared globally
2026-05-24 13:36:57 -04:00
Miguel Ángel e7d0b392c7 fix(core,cli): address review — guard __renderReady, drop pre-quantization, add tests
- Guard __renderReady with `if (state.capturedTimeline)` in all three
  paths (setTimeout(0) and .finally() were setting it unconditionally
  even when bindRootTimelineIfAvailable returned false)
- Remove redundant fps=30 pre-quantization in snapshot — renderSeek
  already calls quantizeTimeToFrame internally with the runtime's
  canonicalFps, so pre-quantizing was double-quantizing at a
  potentially wrong grid
- Add regression tests: __renderReady is set when timeline exists,
  stays undefined when no timeline is available
2026-05-24 13:31:36 -04:00
Miguel Ángel 16e049b320 fix(cli): address review — fps comment, fileServer cross-ref, duration note
- Add comment explaining hardcoded fps=30 (runtime's canonicalFps
  default, not exposed on PlayerAPI)
- Add cross-reference comments between init.ts and fileServer.ts
  explaining their different __renderReady timing semantics
2026-05-24 13:27:54 -04:00
Miguel Ángel e8af1e4b9d refactor(cli): clean up snapshot readiness, duration, and diagnostics
- Fix broken duration getter: use getDuration() (PlayerAPI method)
  instead of .duration (property doesn't exist, always fell through
  to the DOM attribute fallback)
- Remove redundant sub-composition wait: __renderReady already
  guarantees all timelines are bound
- Warn on readiness timeout instead of silently capturing garbage
- Warn when shader transitions don't finish pre-rendering
- Warn when no player API is available (seeks will be no-ops)
- Remove redundant node:fs re-import (already imported at top)
- Remove stale step numbering comments
- Trim verbose comments that restate the code
2026-05-24 13:20:16 -04:00
Miguel Ángel b2828e48e5 fix(core,cli): defer __renderReady until root timeline is bound
The runtime set __renderReady at the same time as __playerReady,
before the root timeline was bound. Consumers waiting for
__renderReady (the render-safe signal) could observe a player with
no captured timeline, making renderSeek a no-op.

Root cause: init.ts set both flags together, but timeline binding
happens later — synchronously via bindRootTimelineIfAvailable(),
via a deferred setTimeout(0) for bundled compositions, or
asynchronously via loadExternalCompositions().

Fix in init.ts:
- Remove __renderReady from the __playerReady assignment
- Set it after bindRootTimelineIfAvailable() when timeline is found
- Set it in the setTimeout(0) deferred path
- Set it in the external compositions .finally() path

Fix in snapshot.ts:
- Wait for __renderReady (truthful signal) not __timelines
- Use renderSeek() with frame quantization, not seek()
- Tick the GSAP ticker after seeking
- Await document.fonts.ready before capturing

Closes #1047
2026-05-24 13:13:28 -04:00
Miguel Ángel b8d521edfb Merge pull request #1045 from heygen-com/feat/vfx-liquid-glass-v2
feat(registry): Apple Liquid Glass components — iOS 26 + macOS Tahoe
2026-05-24 12:33:37 -04:00
Miguel Ángel 7eb92badb2 Merge pull request #1058 from kiyeonjeon21/fix/aws-lambda-build-zip-linux-ffmpeg
fix(aws-lambda): fail build-zip when ffmpeg-static binary isn't Linux x86-64
2026-05-24 12:30:00 -04:00
Miguel Ángel c9d5dc5ff6 Merge pull request #1056 from kiyeonjeon21/fix/aws-lambda-handle-plan-chromium-init
fix(aws-lambda): resolve Chromium for handlePlan before invoking probe
2026-05-24 12:28:36 -04:00
Miguel Ángel 7397702f3a Merge pull request #1060 from func25/f/preserve-static
fix(core): stop binding class values in composition scoped window
2026-05-24 12:27:16 -04:00
func25 d3127a7507 fix(core): preserve static methods on classes exposed through scoped window 2026-05-24 22:50:40 +07:00
Kiyeon Jeon bc20b87b9a fix(aws-lambda): resolve Chromium for handlePlan before invoking probe
The producer's probe stage launches Chromium when Plan has to resolve
browser-only data: root duration unknown, unresolved sub-compositions, or
data-hf-auto-start media. Only handleRenderChunk was setting
PRODUCER_HEADLESS_SHELL_PATH, so a cold Plan invocation launched
puppeteer-core with no executablePath and failed before writing plan.tar.gz.

Mirror the renderChunk env-var guard inside handlePlan so the bundled
Sparticuz binary gets resolved on the first Plan invocation and reused on
warm starts. The skipChromeResolution dep stays honored for SAM-local RIE
smokes.

A warm Lambda environment can mask this only if it previously served a
renderChunk from another execution and left the env var sticky. Within a
single Step Functions execution, Plan still runs before RenderChunks.

A new dispatch test exercises the guard path by pre-seeding
PRODUCER_HEADLESS_SHELL_PATH and asserting Plan does not overwrite it.
2026-05-24 16:22:31 +09:00
Kiyeon Jeon 9b34114dd1 fix(aws-lambda): fail build-zip when ffmpeg-static binary is not Linux x86-64
`ffmpeg-static` materializes a single platform-selected binary at install
time. By default that follows the install host, so a default macOS/arm64
or linux/arm64 install can stage a binary that Lambda's x86-64 runtime
cannot execute.

Verify the ELF header (ELFCLASS64, EM_X86_64) before copying the binary
into `bin/ffmpeg`. When the check fails, surface the canonical workarounds:
run the build inside a linux/amd64 container, or pre-install with
`npm_config_platform=linux npm_config_arch=x64`.

In the distributed pipeline this mismatch can fail as early as Plan's
`ffmpeg -version` probe after browser probing and before chunks are
scheduled. Later encode/assemble spawns would fail for the same reason.

ffprobe already goes through the platform-segmented
`ffprobe-static/bin/linux/x64` path, so that side doesn't need the same
check.
2026-05-24 16:21:01 +09:00
Miguel Ángel d97935b336 fix(engine): scope WebGPU flag to hardware mode 2026-05-24 01:53:24 -04:00
Miguel Ángel b37236ed97 fix(registry): match Tahoe notification motion 2026-05-24 01:19:24 -04:00
Miguel Ángel 4819d84786 feat(registry): polish macOS Tahoe liquid glass 2026-05-24 00:44:21 -04:00
James 90bf485db8 fix(distributed): reject cfr:true with h265 codec (per review)
The cfr re-encode pass hardcodes `-c:v libx264`. Pairing it with
`codec: "h265"` would silently transcode the h265 chunks to h264.
Detect the encoder discriminant in `meta/encoder.json` and throw a
typed error parallel to the existing non-mp4 format guard, so callers
surface the conflict instead of producing a wrong-codec deliverable.

— Rames Jusso
2026-05-24 00:24:30 -04:00
James 71d1889da6 feat(distributed): add optional cfr flag for exact constant frame rate
Distributed-render output today uses -c:v copy through concat → mux →
faststart, which means PTS timestamps from each chunk pass through
unchanged. Container r_frame_rate is exact (#1040 + this PR's parent),
but stream-level avg_frame_rate stays PTS-derived and can land on
fractional rationals like 27648000/921677 over a 60s render. Same for
sub-ms duration drift.

This is the achievable bar within -c copy stream-copy concat. For most
consumers (browser playback, YouTube, etc.) the difference is invisible.
For downstream tools that strict-check avg_frame_rate or
ms-precision duration (broadcast workflows, frame-accurate compositors,
some third-party transcoders), it matters.

Adds an opt-in cfr config flag (default false). When true, the
assemble step's final pass re-encodes with -fps_mode cfr -r <fps>
instead of -c copy, producing exact CFR output. Trade-off: ~2-5x the
stitch time for a 60s 1080p clip; second-generation H.264 quality loss
is negligible at -crf 18 but is non-zero.
2026-05-24 00:24:30 -04:00
James Russo 4729254b7e Merge pull request #1053 from heygen-com/fix/distributed-single-chunk-fps-flag
fix(distributed): apply -r <fps> to single-chunk pass-through path
2026-05-23 23:50:08 -04:00
Miguel Ángel 83dc57ad77 feat(registry): frost liquid glass widgets 2026-05-23 23:25:44 -04:00
James 49281a5c17 fix(distributed): apply -r <fps> to single-chunk pass-through path
The v0.6.39 fix added -r <fps> to the multi-chunk concat ffmpeg
invocation but didn't reach the single-chunk pass-through path,
which is taken when totalFrames * fpsDen / fpsNum fits in one
chunk. Result: 1-chunk renders shipped with fractional
r_frame_rate (e.g. 359/12) while multi-chunk renders shipped
with exact 30/1.

Single-chunk path now goes through the same -r <fps> + -c copy
ffmpeg invocation as the concat path, ensuring uniform exact
r_frame_rate metadata across all chunk-count configurations.

Adds a regression test exercising the 1-chunk path and asserting
r_frame_rate === "<fpsNum>/<fpsDen>" exact.
2026-05-24 03:15:56 +00:00
Miguel Ángel f9fcf33c08 feat(registry): polish liquid glass previews 2026-05-23 22:37:37 -04:00
Miguel Ángel 4913bf3782 feat(registry): refine iOS Liquid Glass home screen 2026-05-23 20:52:30 -04:00
Miguel Ángel f63c2d40fb docs: remove stale liquid glass catalog entry 2026-05-23 19:49:56 -04:00