mirror of
https://github.com/hanzoai/frames.git
synced 2026-09-24 00:08:03 +00:00
@hanzo/frame is taken by the hero video primitive — one full-bleed video element — which has held that name on npm since it shipped. This repo is the video toolchain: a CLI, a renderer, a studio, a player. Plural is what it is, so the CLI publishes as @hanzo/frames and the workspace packages follow it (@hanzo/frames-core, @hanzo/frames-engine, and the rest). The binary stays `frames`, and now actually ships. The manifest mapped it to bin/frames.mjs while the file on disk was bin/hyperframes.mjs, so a global install produced no working command. The paths that name a package to fetch were pointing elsewhere. The update check read registry.npmjs.org/frames, a different author's package, and the npm, bun and pnpm installers would have pulled it over the CLI; the render image installed the upstream hyperframes; scaffolded projects and the docs pinned the unscoped name. All of them now name @hanzo/frames.
242 lines
7.6 KiB
Plaintext
242 lines
7.6 KiB
Plaintext
---
|
|
title: "@frames/player"
|
|
description: "Embeddable web component for playing Frames compositions in any web page."
|
|
---
|
|
|
|
The player package provides a `<frames-player>` custom element that embeds a Frames composition anywhere — in any framework or plain HTML. Zero dependencies, 3KB gzipped.
|
|
|
|
```bash
|
|
npm install @frames/player
|
|
```
|
|
|
|
## When to Use
|
|
|
|
**Use `@frames/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 @hanzo/frames preview`)
|
|
- Render to MP4 — use the [CLI](/packages/cli) or [producer](/packages/producer)
|
|
|
|
## Quick Start
|
|
|
|
### Via CDN
|
|
|
|
```html
|
|
<script type="module" src="https://cdn.jsdelivr.net/npm/@frames/player"></script>
|
|
|
|
<frames-player
|
|
src="./my-composition/index.html"
|
|
controls
|
|
autoplay
|
|
muted
|
|
style="width: 100%; max-width: 800px; aspect-ratio: 16/9"
|
|
></frames-player>
|
|
```
|
|
|
|
If you need a classic `<script>` tag instead of ESM, use the explicit global build:
|
|
|
|
```html
|
|
<script src="https://cdn.jsdelivr.net/npm/@frames/player/dist/frames-player.global.js"></script>
|
|
```
|
|
|
|
### Via npm
|
|
|
|
```js
|
|
import '@frames/player';
|
|
```
|
|
|
|
```html
|
|
<frames-player src="/compositions/intro.html" controls></frames-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 | false | Mute audio (set to `true` 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('frames-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('frames-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 '@frames/player';
|
|
|
|
function VideoPreview({ src }) {
|
|
return (
|
|
<frames-player
|
|
src={src}
|
|
controls
|
|
style={{ width: '100%', maxWidth: 800 }}
|
|
/>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Vue
|
|
|
|
```vue
|
|
<template>
|
|
<frames-player :src="compositionUrl" controls />
|
|
</template>
|
|
|
|
<script setup>
|
|
import '@frames/player';
|
|
const compositionUrl = './compositions/intro.html';
|
|
</script>
|
|
```
|
|
|
|
### Programmatic
|
|
|
|
```js
|
|
import '@frames/player';
|
|
|
|
const player = document.createElement('frames-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('frames-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 [`@frames/studio`](/packages/studio). The studio exports a `resolveIframe` helper that handles both direct iframe refs and web-component refs:
|
|
|
|
```ts
|
|
import { useTimelinePlayer, resolveIframe } from '@frames/studio';
|
|
|
|
const { iframeRef } = useTimelinePlayer();
|
|
const player = document.createElement('frames-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 '@frames/player';
|
|
import type { HyperframesPlayer } from '@frames/player';
|
|
import { useTimelinePlayer, resolveIframe } from '@frames/studio';
|
|
|
|
function StudioPreview({ src }: { src: string }) {
|
|
const { iframeRef, onIframeLoad } = useTimelinePlayer();
|
|
const playerRef = useRef<HyperframesPlayer>(null);
|
|
|
|
useEffect(() => {
|
|
iframeRef.current = resolveIframe(playerRef.current);
|
|
});
|
|
|
|
return (
|
|
<frames-player
|
|
ref={playerRef}
|
|
src={src}
|
|
onLoad={onIframeLoad}
|
|
/>
|
|
);
|
|
}
|
|
```
|
|
|
|
<Warning>
|
|
**Common gotcha** — if you pass the `<frames-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 `@frames/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 Frames 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
|