Skip to content

fix(core): stop autoplay on load + propagate seek to sibling timelines#391

Closed
jrusso1020 wants to merge 1 commit intomainfrom
fix/propagate-init-pause-and-seek-to-siblings
Closed

fix(core): stop autoplay on load + propagate seek to sibling timelines#391
jrusso1020 wants to merge 1 commit intomainfrom
fix/propagate-init-pause-and-seek-to-siblings

Conversation

@jrusso1020
Copy link
Copy Markdown
Collaborator

@jrusso1020 jrusso1020 commented Apr 21, 2026

Follow-up to #359 for the async-scene-loading path — compositions served as raw static files with data-composition-src children (e.g. the Vercel template), as opposed to the bundleToSingleHtml studio path.

The bug

On page load, <hyperframes-player> reports the master as paused at 0:00 but each scene visibly animates — the "autoplay on load" bug. Traced via a paused-setter proxy on each registered timeline: on init, each scene's _ts flipped 0→1 inside ensureChildCandidatesActive in init.ts, calling timeline.paused(false) on every child.

That helper predates #359 and was the only way a user-click Play would animate scenes — back when play didn't propagate through the registry. After #359, createRuntimePlayer.play already iterates every registry entry and un-pauses each, so the init-time un-pause is redundant for user-driven playback. It still matters for the producer's render path, though: there the master's totalTime cascade is the only driver (no GSAP ticker — virtual time), and a paused child won't re-render on cascade, so every producer regression baseline depends on children being un-paused at init time.

Fix

Scope the un-pause to render mode, signalled by window.__HF_VIRTUAL_TIME__ (set by the producer's render-mode bootstrap). Preview mode leaves children paused — no autoplay — and play/pause still propagate via the registry iteration added in #359.

Known limitation (follow-up)

In preview mode, a scrub-back after a full playthrough can still leave async scenes parked at their end-state. Root cause is deeper: addMissingChildCandidatesToRootTimeline in init.ts isn't actually reparenting async scene timelines to the master (inspection shows scene._dp === gsap.globalTimeline instead of rootTimeline after the add call). GSAP's cascade skips paused non-children. Fixing that properly needs a separate change to the async composition loader and is tracked as a follow-up; the autoplay fix here is the narrow, safe piece.

Files

  • packages/core/src/runtime/init.ts — add isRenderMode guard around the existing ensureChildCandidatesActive call
  • packages/core/src/runtime/player.ts — unchanged behavior from fix(player+core): correctly render and pause nested compositions #359; seek/renderSeek intentionally do NOT iterate the registry (clearer JSDoc explains why — iterating would overwrite GSAP's cascade with wrong absolute times)
  • packages/core/src/runtime/player.test.ts — 2 new tests documenting that seek/renderSeek don't iterate the registry

Reproducing the regression failures

This PR's previous revision removed ensureChildCandidatesActive unconditionally, which broke 25 PSNR checkpoints across overlay-montage-prod, style-4-prod, style-7-prod, style-8-prod, style-9-prod, style-17-prod, style-18-prod. Reproducing locally with:

docker build -f Dockerfile.test -t hyperframes-producer:test .
docker run --rm --security-opt seccomp=unconfined --shm-size=4g \
  -v "$PWD/packages/producer/tests:/app/packages/producer/tests" \
  hyperframes-producer:test style-17-prod

shows the same failures (would have caught it before push — lesson learned).

With the render-mode-scoped fix, all 4 tests I spot-checked run to 0/100 failed frames.

Testing

  • cd packages/core && bunx vitest run — 490/490 pass (+2 new)
  • cd packages/player && bunx vitest run — 35/35 pass (unchanged)
  • bun run typecheck in core + player — clean
  • bunx oxlint / bunx oxfmt --check clean across changed files
  • Docker regression: style-17-prod 0/100, style-7-prod 0/100, style-4-prod 0/100, overlay-montage-prod 0/100
  • Live Vercel deploy verified: scenes stay paused on load (autoplay fixed); play/pause propagates; render output unchanged

Follow-up to #359 for the async-scene-loading path (compositions served
as raw static files with `data-composition-src` children, as opposed to
the `bundleToSingleHtml` studio path).

## Problem

In the async-scene path, scenes visibly animate on page load while the
master stays paused at 0:00 — the "autoplay on load" bug.

Traced via a `paused`-setter proxy on each registered timeline: on init,
each scene's `_ts` flipped 0→1 inside `ensureChildCandidatesActive` in
`init.ts`, calling `timeline.paused(false)` on every child candidate.

That helper predates #359 and was the only way a user-click Play would
animate scenes — back when play didn't propagate through the registry.
After #359, `createRuntimePlayer.play` already iterates every registry
entry and un-pauses each, so the init-time un-pause is redundant for
user-driven playback. It still matters for the producer's render path,
though: there the master's `totalTime` cascade is the only driver (no
GSAP ticker — virtual time), and a paused child won't re-render on
cascade, so every producer regression baseline depends on children
being un-paused at init time.

## Fix

Scope the un-pause to render mode, signalled by
`window.__HF_VIRTUAL_TIME__` (set by the producer's render-mode
bootstrap script). Preview mode leaves children paused — no autoplay;
play/pause still propagate via the registry iteration added in #359.

Reproduced + verified locally with `Dockerfile.test`:

- Before this fix, `overlay-montage-prod`, `style-4-prod`, `style-7-prod`,
  `style-8-prod`, `style-9-prod`, `style-17-prod`, `style-18-prod` all
  fail PSNR with 15-30 failed frames (scenes rendering at wrong times).
- After this fix, all four run to 0/100 failed frames.

## Known limitation (follow-up)

In preview mode, a scrub-back after a full playthrough can still leave
async scenes parked at their end-state, because GSAP's cascade skips
rendering paused children and the async loader isn't fully parenting
scenes to the master. Tracked for a separate fix to the async composition
loader (see `addMissingChildCandidatesToRootTimeline` in `init.ts`).

## Files

- `packages/core/src/runtime/init.ts` — add `isRenderMode` guard around
  `ensureChildCandidatesActive`
- `packages/core/src/runtime/player.ts` — unchanged from #359's
  registry iteration on play/pause
- `packages/core/src/runtime/player.test.ts` — 2 new tests documenting
  that `seek`/`renderSeek` intentionally do NOT iterate the registry

## Testing

- [x] `cd packages/core && bunx vitest run` — 490/490 pass (+2 new)
- [x] `cd packages/player && bunx vitest run` — 35/35 pass (unchanged)
- [x] `bun run typecheck` in core + player — clean
- [x] `bunx oxlint` / `bunx oxfmt --check` clean across changed files
- [x] `docker run hyperframes-producer:test style-17-prod` — 0/100 frames fail
- [x] `docker run hyperframes-producer:test style-7-prod` — 0/100 frames fail
- [x] `docker run hyperframes-producer:test style-4-prod` — 0/100 frames fail
- [x] `docker run hyperframes-producer:test overlay-montage-prod` — 0/100 frames fail
- [x] Live Vercel deploy verified: scenes stay paused at page load
  (autoplay fixed); play/pause propagates as expected

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jrusso1020 jrusso1020 force-pushed the fix/propagate-init-pause-and-seek-to-siblings branch from 78d3e24 to b759237 Compare April 21, 2026 22:08
@jrusso1020 jrusso1020 closed this Apr 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants