Files
hyperframes/docs/packages/player.mdx
T
Miguel Ángel 1149602bc9 fix(studio): support web-component refs in useTimelinePlayer (#245)
* 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).
2026-04-13 17:53:01 +02:00

236 lines
7.5 KiB
Plaintext

---
title: "@hyperframes/player"
description: "Embeddable web component for playing HyperFrames compositions in any web page."
---
The player package provides a `<hyperframes-player>` custom element that embeds a HyperFrames composition anywhere — in any framework or plain HTML. Zero dependencies, 3KB gzipped.
```bash
npm install @hyperframes/player
```
## When to Use
**Use `@hyperframes/player` when you need to:**
- Embed a rendered composition in a website, dashboard, or app
- Add a video-like player to a landing page or product demo
- Show compositions in documentation or blog posts
**Use a different package if you want to:**
- Edit compositions interactively — use the [studio](/packages/studio)
- Preview during development — use the [CLI](/packages/cli) (`npx hyperframes preview`)
- Render to MP4 — use the [CLI](/packages/cli) or [producer](/packages/producer)
## Quick Start
### Via CDN
```html
<script src="https://cdn.jsdelivr.net/npm/@hyperframes/player"></script>
<hyperframes-player
src="./my-composition/index.html"
controls
autoplay
muted
style="width: 100%; max-width: 800px; aspect-ratio: 16/9"
></hyperframes-player>
```
### Via npm
```js
import '@hyperframes/player';
```
```html
<hyperframes-player src="/compositions/intro.html" controls></hyperframes-player>
```
## HTML Attributes
| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| `src` | string | required | URL or relative path to composition HTML |
| `width` | number | 1920 | Composition width in pixels |
| `height` | number | 1080 | Composition height in pixels |
| `controls` | boolean | false | Show playback controls overlay |
| `autoplay` | boolean | false | Start playing on load |
| `loop` | boolean | false | Loop playback |
| `muted` | boolean | true | Mute audio (required for autoplay in most browsers) |
| `poster` | string | — | Image URL to show before first play |
| `playback-rate` | number | 1 | Playback speed multiplier |
## JavaScript API
The player mirrors the native `<video>` element API:
```js
const player = document.querySelector('hyperframes-player');
// Playback
player.play();
player.pause();
player.seek(2.5); // seek to 2.5 seconds
// Properties
player.currentTime; // number — current position in seconds
player.currentTime = 5; // seek to 5 seconds
player.duration; // number — total duration
player.paused; // boolean
player.ready; // boolean — true after composition loads
player.playbackRate; // number — get/set speed
player.muted; // boolean — get/set mute
player.loop; // boolean — get/set loop
```
## Events
```js
const player = document.querySelector('hyperframes-player');
player.addEventListener('ready', (e) => {
console.log('Duration:', e.detail.duration);
});
player.addEventListener('timeupdate', (e) => {
console.log('Time:', e.detail.currentTime);
});
player.addEventListener('play', () => console.log('Playing'));
player.addEventListener('pause', () => console.log('Paused'));
player.addEventListener('ended', () => console.log('Ended'));
player.addEventListener('error', (e) => console.error(e.detail.message));
```
| Event | Detail | Description |
|-------|--------|-------------|
| `ready` | `{ duration }` | Composition loaded and timeline discovered |
| `timeupdate` | `{ currentTime }` | Fires during playback (~30fps) |
| `play` | — | Playback started |
| `pause` | — | Playback paused |
| `ended` | — | Playback reached end |
| `error` | `{ message }` | Load or runtime error |
## Framework Examples
### React
```jsx
import '@hyperframes/player';
function VideoPreview({ src }) {
return (
<hyperframes-player
src={src}
controls
style={{ width: '100%', maxWidth: 800 }}
/>
);
}
```
### Vue
```vue
<template>
<hyperframes-player :src="compositionUrl" controls />
</template>
<script setup>
import '@hyperframes/player';
const compositionUrl = './compositions/intro.html';
</script>
```
### Programmatic
```js
import '@hyperframes/player';
const player = document.createElement('hyperframes-player');
player.src = './my-composition/index.html';
player.controls = true;
player.addEventListener('ready', () => player.play());
document.getElementById('player-container').appendChild(player);
```
## 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 and events above are sufficient. But if you're building an editor, recorder, or custom timeline on top of the player, you'll need to inspect the composition's DOM or read its `__player` / `__timelines` runtime objects. The `iframeElement` getter exposes the inner iframe for these consumers:
```js
const player = document.querySelector('hyperframes-player');
const iframe = player.iframeElement;
// Reach into the composition's DOM
iframe.contentDocument.querySelectorAll('[data-composition-id]');
// Read the runtime (GSAP timelines, element registry, etc.)
iframe.contentWindow.__timelines;
```
This is the canonical way to bridge the player into editor tools like [`@hyperframes/studio`](/packages/studio). The studio exports a `resolveIframe` helper that handles both direct 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 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}
/>
);
}
```
<Warning>
**Common gotcha** — if you pass the `<hyperframes-player>` element itself (not `iframeElement`) into a hook or API that expects an `<iframe>`, every `.contentWindow` / `.contentDocument` access returns `null` because the iframe lives inside the player's Shadow DOM. Timeline seek, play, pause, and DOM inspection all silently no-op. **Always extract `iframeElement` first**, or use `resolveIframe` from `@hyperframes/studio` which handles both iframe and web-component hosts transparently.
</Warning>
## Architecture
The player uses an iframe inside a Shadow DOM container. This provides:
- **Isolation** — composition CSS/JS can't leak into or conflict with your page
- **Security** — iframe sandbox restricts composition capabilities
- **Scaling** — auto-scales the composition to fit the player's container via CSS transforms
The player communicates with the composition via the HyperFrames runtime bridge protocol (`postMessage`). Existing compositions work without modification.
## Controls
When the `controls` attribute is present, a minimal overlay appears at the bottom:
- **Play/Pause** button (left)
- **Scrub bar** with drag support (mouse + touch)
- **Time display** showing current / total duration (right)
- Auto-hides after 3 seconds of inactivity, reappears on hover