85453da49f
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
240 lines
12 KiB
Markdown
240 lines
12 KiB
Markdown
# @hyperframes/player
|
|
|
|
Embeddable web component for playing HyperFrames compositions. Zero dependencies, works with any framework.
|
|
|
|
## Install
|
|
|
|
```bash
|
|
npm install @hyperframes/player
|
|
```
|
|
|
|
Or load directly via CDN:
|
|
|
|
```html
|
|
<script type="module" src="https://cdn.jsdelivr.net/npm/@hyperframes/player"></script>
|
|
```
|
|
|
|
If you need a classic `<script>` tag instead of ESM, use the explicit global build:
|
|
|
|
```html
|
|
<script src="https://cdn.jsdelivr.net/npm/@hyperframes/player/dist/hyperframes-player.global.js"></script>
|
|
```
|
|
|
|
## Usage
|
|
|
|
```html
|
|
<hyperframes-player src="./my-composition/index.html" controls></hyperframes-player>
|
|
```
|
|
|
|
The player loads the composition in a sandboxed iframe, auto-detects its dimensions and duration, and scales it responsively to fit the container.
|
|
|
|
### With a framework
|
|
|
|
```typescript
|
|
import "@hyperframes/player";
|
|
|
|
// The custom element is now registered — use it in your markup
|
|
// React: <hyperframes-player src="..." controls />
|
|
// Vue: <hyperframes-player :src="url" controls />
|
|
```
|
|
|
|
### Poster image
|
|
|
|
Show a static image before playback starts:
|
|
|
|
```html
|
|
<hyperframes-player
|
|
src="./composition/index.html"
|
|
poster="./thumbnail.jpg"
|
|
controls
|
|
></hyperframes-player>
|
|
```
|
|
|
|
## Attributes
|
|
|
|
| Attribute | Type | Default | Description |
|
|
| ---------------------- | ------------------------------- | ------------- | --------------------------------------------------------------------------- |
|
|
| `src` | string | — | URL to the composition HTML file |
|
|
| `audio-src` | string | — | Audio URL for parent-frame playback (mobile) |
|
|
| `width` | number | 1920 | Composition width in pixels (aspect ratio) |
|
|
| `height` | number | 1080 | Composition height in pixels (aspect ratio) |
|
|
| `controls` | boolean | false | Show play/pause, scrubber, and time display |
|
|
| `muted` | boolean | false | Mute audio playback |
|
|
| `audio-locked` | boolean | false | Force-mute and hide the volume controls so the viewer cannot turn sound on |
|
|
| `poster` | string | — | Image URL shown before playback starts |
|
|
| `playback-rate` | number | 1 | Speed multiplier (0.5 = half, 2 = double) |
|
|
| `autoplay` | boolean | false | Start playing when ready |
|
|
| `loop` | boolean | false | Restart when the composition ends |
|
|
| `shader-capture-scale` | number | — | Shader transition snapshot scale forwarded to browser previews (`0.25`-`1`) |
|
|
| `shader-loading` | `composition \| player \| none` | `composition` | Controls shader transition prep loading UI ownership |
|
|
|
|
### Shader transition previews
|
|
|
|
When a composition uses `@hyperframes/shader-transitions`, the player can own preview-only shader capture settings:
|
|
|
|
```html
|
|
<hyperframes-player
|
|
src="./composition/index.html"
|
|
shader-capture-scale="1"
|
|
shader-loading="player"
|
|
controls
|
|
></hyperframes-player>
|
|
```
|
|
|
|
`shader-loading="player"` shows the player-owned transition-prep overlay from shader progress messages. `composition` leaves direct composition fallback behavior alone, and `none` suppresses the loader.
|
|
|
|
### Audio lock (host-mandated silent playback)
|
|
|
|
`audio-locked` forces `muted` on and hides the volume controls, with no UI path for the viewer to turn sound back on. Use it when embedding in a chat host (Claude.ai, ChatGPT, etc.) where audio must stay off regardless of viewer intent. Setting `muted` directly is _not_ enough — viewers can flip it back via the controls bar.
|
|
|
|
Removing `audio-locked` only unhides the controls; it does **not** auto-unmute. Callers manage `muted` explicitly after unlocking.
|
|
|
|
**Host-environment fallback.** Some host renderers — notably the Claude desktop Electron client — strip unknown custom-element attributes before they reach the DOM, defeating the attribute. As a safety net, the player also self-imposes the lock when it detects such an environment via `navigator.userAgent`, so audio stays muted even if the attribute never arrives. The public `audioLocked` property still reflects only the attribute, so external consumers (e.g. host widgets that mirror state) are not affected by the fallback.
|
|
|
|
### Mobile audio
|
|
|
|
Mobile browsers block `audio.play()` inside iframes when the user gesture happened in the parent frame (the [User Activation spec](https://html.spec.whatwg.org/multipage/interaction.html#tracking-user-activation) does not propagate activation across frame boundaries via `postMessage`).
|
|
|
|
The player handles this automatically for same-origin iframes (the default — `sandbox` includes `allow-same-origin`):
|
|
|
|
1. When the composition is ready, the player extracts all timed media (`audio[data-start]`, `video[data-start]`) from the iframe DOM and creates parent-frame copies.
|
|
2. The iframe originals are disabled (`src` and `data-start` removed) so the runtime doesn't try to play them.
|
|
3. When `play()` is called (from a user gesture), parent media `.play()` runs synchronously in the gesture call stack, satisfying mobile autoplay policy.
|
|
4. Both parent media and the GSAP timeline start simultaneously and free-run — no active sync needed since both are real-time systems.
|
|
|
|
No changes are required by consumers — this works out of the box.
|
|
|
|
The optional `audio-src` attribute can be used to start preloading a primary audio track before the iframe loads (useful on slow connections), but is not required for mobile playback.
|
|
|
|
## JavaScript API
|
|
|
|
```js
|
|
const player = document.querySelector("hyperframes-player");
|
|
|
|
// Playback
|
|
player.play();
|
|
player.pause();
|
|
player.seek(2.5); // jump to 2.5 seconds
|
|
|
|
// Properties
|
|
player.currentTime; // number (read/write)
|
|
player.duration; // number (read-only)
|
|
player.paused; // boolean (read-only)
|
|
player.ready; // boolean (read-only)
|
|
player.playbackRate; // number (read/write)
|
|
player.muted; // boolean (read/write)
|
|
player.audioLocked; // boolean (read/write) — force-mute + hide volume controls
|
|
player.loop; // boolean (read/write)
|
|
player.shaderCaptureScale; // number (read/write)
|
|
player.shaderLoading; // "composition" | "player" | "none" (read/write)
|
|
|
|
// Inner iframe access (for advanced consumers — see "Advanced: iframe access" below)
|
|
player.iframeElement; // HTMLIFrameElement (read-only)
|
|
```
|
|
|
|
## Advanced: iframe access
|
|
|
|
The composition runs inside a sandboxed `<iframe>` in the player's Shadow DOM. For most use cases you don't need direct access — the JavaScript API above is enough. But if you're building an editor, recorder, or custom timeline that needs to inspect the composition's DOM or read its `__player` / `__timelines` runtime objects, use the `iframeElement` getter:
|
|
|
|
```js
|
|
const player = document.querySelector("hyperframes-player");
|
|
const iframe = player.iframeElement;
|
|
|
|
// Now you can reach into the composition's DOM and runtime
|
|
iframe.contentDocument.querySelectorAll("[data-composition-id]");
|
|
iframe.contentWindow.__timelines;
|
|
```
|
|
|
|
This is the canonical way to bridge the player into tools like [`@hyperframes/studio`](../studio). The studio exports a `resolveIframe` helper that works with both iframe refs and web-component refs:
|
|
|
|
```ts
|
|
import { useTimelinePlayer, resolveIframe } from "@hyperframes/studio";
|
|
|
|
const { iframeRef } = useTimelinePlayer();
|
|
const player = document.createElement("hyperframes-player");
|
|
player.setAttribute("src", src);
|
|
container.appendChild(player);
|
|
|
|
// Forward the inner iframe so useTimelinePlayer can drive play/pause/seek.
|
|
iframeRef.current = resolveIframe(player);
|
|
```
|
|
|
|
### React: declarative ref pattern
|
|
|
|
If you prefer JSX over imperative element creation, attach a ref directly to the web component and resolve the iframe inside an effect:
|
|
|
|
```tsx
|
|
import "@hyperframes/player";
|
|
import type { HyperframesPlayer } from "@hyperframes/player";
|
|
import { useTimelinePlayer, resolveIframe } from "@hyperframes/studio";
|
|
|
|
function StudioPreview({ src }: { src: string }) {
|
|
const { iframeRef, onIframeLoad } = useTimelinePlayer();
|
|
const playerRef = useRef<HyperframesPlayer>(null);
|
|
|
|
useEffect(() => {
|
|
iframeRef.current = resolveIframe(playerRef.current);
|
|
});
|
|
|
|
return <hyperframes-player ref={playerRef} src={src} onLoad={onIframeLoad} />;
|
|
}
|
|
```
|
|
|
|
> **Heads up — common gotcha**
|
|
>
|
|
> If you pass the `<hyperframes-player>` element itself (not `iframeElement`) into a hook that expects an `<iframe>`, every `.contentWindow` / `.contentDocument` access returns `null` because the iframe lives inside the player's Shadow DOM. Always extract `iframeElement` first, or use `resolveIframe` from `@hyperframes/studio` which handles both iframe and web-component hosts transparently.
|
|
|
|
## Events
|
|
|
|
| Event | Detail | Fired when |
|
|
| ----------------------- | -------------------------- | ------------------------------------------ |
|
|
| `ready` | `{ duration }` | Composition loaded and duration determined |
|
|
| `play` | — | Playback started |
|
|
| `pause` | — | Playback paused |
|
|
| `timeupdate` | `{ currentTime }` | Playback position changed (~10 fps) |
|
|
| `ended` | — | Reached the end (when not looping) |
|
|
| `error` | `{ message }` | Composition failed to load |
|
|
| `shadertransitionstate` | `{ compositionId, state }` | Shader transition cache/capture progress |
|
|
|
|
```js
|
|
player.addEventListener("ready", (e) => {
|
|
console.log(`Duration: ${e.detail.duration}s`);
|
|
});
|
|
|
|
player.addEventListener("ended", () => {
|
|
console.log("Done!");
|
|
});
|
|
```
|
|
|
|
## Sizing
|
|
|
|
The player fills its container and scales the composition to fit while preserving aspect ratio. Set a size on the element or its parent:
|
|
|
|
```css
|
|
hyperframes-player {
|
|
width: 100%;
|
|
max-width: 800px;
|
|
aspect-ratio: 16 / 9;
|
|
}
|
|
```
|
|
|
|
The `width` and `height` attributes define the composition's native resolution for aspect ratio calculation — they don't set the player's display size.
|
|
|
|
## How it works
|
|
|
|
The player renders compositions in a sandboxed `<iframe>` inside a Shadow DOM. It communicates with the HyperFrames runtime via `postMessage`. If the composition has GSAP timelines (`window.__timelines`) but no runtime, the player auto-injects it from CDN.
|
|
|
|
## Distribution
|
|
|
|
| Format | File | Use case |
|
|
| ------ | ------------------------------ | ------------------------------ |
|
|
| ESM | `hyperframes-player.js` | Bundlers (Vite, webpack, etc.) |
|
|
| CJS | `hyperframes-player.cjs` | Node.js / require() |
|
|
| IIFE | `hyperframes-player.global.js` | `<script>` tag, CDN |
|
|
|
|
All formats are minified with source maps. TypeScript definitions included.
|
|
|
|
## License
|
|
|
|
MIT
|