* fix(player): parent-frame media playback for mobile
Mobile browsers block media.play() inside iframes when the user
gesture happened in the parent frame — postMessage doesn't transfer
user activation (per the User Activation v2 spec).
## Problem
The player renders compositions in a sandboxed iframe. When a user
taps play in the parent frame, the player sends a postMessage to the
iframe's runtime, which calls audio.play(). On mobile, this fails
silently because the iframe has no user activation context.
## Solution
The player now extracts ALL timed media elements (audio/video with
data-start) from the iframe's DOM (same-origin access), creates
parent-frame copies, and disables the iframe originals. On play(),
parentMedia.play() runs synchronously in the gesture call stack,
satisfying mobile autoplay policy.
### Generic media handling
- Finds all `audio[data-start], video[data-start]` in the iframe
- Creates a parent-frame copy for each (Audio or Video element)
- Preserves data-start offsets for correct seek positioning
- Strips data-start from iframe elements so the runtime ignores them
- Falls back to iframe media for cross-origin iframes
### `audio-src` attribute
Convenience for the common single-narration case. When set, the
player starts preloading audio immediately — before the iframe loads.
This eliminates the loading delay that caused jittery playback.
### No active sync
Both parent media and the GSAP timeline are real-time systems. When
started simultaneously, they naturally stay within ~10ms — no drift
correction needed. Active sync with coarse granularity (50ms polling)
caused MORE jitter than it prevented via repeated audio seeks.
## CI
- Added unified `test` job replacing separate per-package test jobs
- Added root `test` script: `bun run --filter '*' test`
- New packages with test scripts are automatically included
- Added happy-dom for player DOM tests
## Tests
- 10 new tests for parent-frame media: preloading, play, pause,
seek, muted/rate sync, cleanup, attribute changes
- All 21 player tests pass
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(shader-transitions): pass CI when no test files exist
Add --passWithNoTests to vitest run so the unified test job
doesn't fail on packages that have a test script but no test
files yet.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): update tests for new id field and GSAP lint rule
- normalize.test.ts: loadTranscript now assigns id fields (w0, w1, etc.)
to SRT/VTT results and empty string for words-json passthrough
- lintProject.test.ts: add GSAP CDN script to validHtml() fixture to
satisfy the missing_gsap_script lint rule added in core
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): add missing data-start/data-duration to validHtml fixture
The validHtml() test fixture was missing data-start and data-duration
attributes, triggering the root_composition_missing_data_start and
root_composition_missing_data_duration lint warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(ci): fetch LFS objects for producer test job
Producer regression tests compare rendered output against reference MP4
files stored in git LFS. Without lfs: true, checkout fetches pointer
files instead of actual videos, causing "moov atom not found" errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ci: remove redundant test-producer job
The regression workflow already runs the same 28 producer fixtures
in a Docker container with prod-matching Chrome/fonts/ffmpeg, sharded
across 8 parallel matrix jobs with 40-min timeouts. The CI test-producer
job was a duplicate that ran on bare runners with worse determinism
and a 15-min timeout too short for all fixtures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(studio): support web-component refs in useTimelinePlayer
The studio's `useTimelinePlayer` hook returns an `iframeRef` that
consumers attach to an `<iframe>` element. When consumers wrap the
iframe in a custom element (e.g. `<hyperframes-player>`) that puts
the iframe inside its shadow DOM, every `iframeRef.current.contentWindow`
access returned `null` and `getAdapter()` silently failed — meaning
timeline seek, play, pause, and `refreshPlayer` all became no-ops.
Changes:
- Add `resolveIframe(el)` helper that returns the underlying iframe
whether the host is the iframe itself, a custom element with a
shadow-DOM iframe, or a wrapper with a descendant iframe.
- Export `resolveIframe` from the studio so consumers can pre-resolve
the iframe before assigning it to `iframeRef`.
- Internal `useTimelinePlayer` keeps the strict `HTMLIFrameElement`
ref type, so existing consumers attaching directly to an `<iframe>`
are unaffected.
Also adds:
- JSDoc on the player's `iframeElement` getter.
- "Advanced: iframe access" docs section in `packages/player/README.md`
and `docs/packages/player.mdx`.
- Type-safety lint rules in `.oxlintrc.json` and a "Type-safety
conventions" section in `CONTRIBUTING.md`.
Backward compatible — App.tsx and NLELayout.tsx continue to work
unchanged.
* chore(lint): defer no-explicit-any rule; it broke existing codebase
The new rules added 37 errors across 32 existing files — mostly
legitimate `window as any` casts at browser-global and test-mock
boundaries. Enabling them without fixing all violations breaks CI.
Revert the `.oxlintrc.json` additions and soften the CONTRIBUTING.md
wording to describe the convention without claiming lint enforcement
(that enforcement will come in a follow-up PR that fixes all sites).
## Summary
- Add comprehensive README for `@hyperframes/player` covering installation, usage, full API reference (attributes, properties, methods, events), sizing, and distribution formats
- Bump version from 0.2.2 to 0.2.4 to align with monorepo release
## Test plan
- [x] Verify README renders correctly on GitHub
- [x] Confirm package.json version matches monorepo (0.2.4)