---
title: Hosts
description: A host owns the clock. It picks a frame, waits until the frame is ready, then shows or captures it.
---

A composition draws whatever frame it is given. The host decides which frame that is and when
it may be shown. Every host does the same two jobs through the same two contracts, so a scene,
its renderers and its backends never know which host they are in.

| Contract | Job | Read by |
| --- | --- | --- |
| Frame source | Holds the current frame, the viewport and the preview scale | Every renderer, through `useMotionactorFrame` and the render context |
| Frame gate | Lets anything on the page hold a frame until it is ready | Fonts, images, video frames, GPU work, layout measurement |

A frame is ready when its render has committed and every hold on the gate is released. Only
then does a host show it, capture it or move on.

| Host | Package | Clock | Holds become |
| --- | --- | --- | --- |
| Native preview | `@motionactor/engine/react` | The page's clock, or the audio clock when audio is decoded | Promises that `seek` awaits |
| [Player](/docs/rendering/playing-in-the-browser) | `@motionactor/player` | The engine's playback clock, driven by Video.js controls | The player's waiting state |
| Remotion | `@motionactor/remotion` | Remotion's Player, Studio and renderer | `delayRender` handles |
| HyperFrames | `@motionactor/hyperframes` | The HyperFrames runtime, which seeks in seconds | Promises passed to `waitUntil` |

## Native preview

`mountComposition` renders a composition into an element you own and returns a controller.
It builds the scene, measures any media the build asks for, loads fonts and assets, and
resolves once frame 0 is on screen.

```ts twoslash title="preview.ts"
// @jsx: react-jsx
// @filename: title-card.tsx
import { createScene, input, LayoutActor, signal } from "@motionactor/core";
import { text } from "@motionactor/schema";
import {
  createRenderRuntime,
  defineComposition,
} from "@motionactor/renderers/react";
import { dom } from "@motionactor/dom/react";
class Title extends LayoutActor { text = signal(""); }
const titleScene = createScene(function* (ctx, { title }) {
  const heading = ctx.spawn(Title, { text: title, appearance: { opacity: 0 } });
  yield* heading.appearance.opacity.tween(1, 0.5);
  yield* ctx.wait(1.5);
  return { heading };
}, { inputs: { title: input.of(text, "Launch") } });
export const titleCard = defineComposition({
  id: "TitleCard", width: 1920, height: 1080, fps: 30, defaultProps: { title: "Launch" },
  runtime: createRenderRuntime({ backends: [dom] }),
  scene: titleScene,
});
// @filename: preview.ts
// ---cut---
import { mountComposition } from "@motionactor/engine/react";
import { titleCard } from "./title-card";

const preview = await mountComposition({
  composition: titleCard,
  element: document.getElementById("stage")!,
  props: { title: "Hello" },
});

preview.viewport.durationInFrames; // 60
await preview.seek(30); // resolves once frame 30 is on screen
preview.getSnapshot(); // { frame: 30, playing: false }

await preview.play();
preview.pause();
preview.dispose();
```

- `play` follows the clock and skips frames rather than falling behind it. It stops at the
  end, and the next `play` starts over at frame 0.
- `seek(frame)` pauses first. Seeks run one at a time in the order they were asked for, and a
  repeated frame still paints.
- `subscribe` and `getSnapshot` have the shape React's `useSyncExternalStore` takes, so a
  timeline or a play button reads the preview directly.
- `updateProps(next)` rebuilds the scene and repaints the current frame. A change that alters
  the scene's length throws, because the host's timeline has a fixed length; mount again.
- `setPreviewScale(scale)` tells canvas backends how far the stage is zoomed, so they draw at
  the pixel density the viewer sees.
- `resolver` maps the composition's asset references to URLs. Leave it out and files resolve
  against the page's base URL.

The native host adds the media renderers from `@motionactor/media` after the composition's
own, so a scene with `Video` or `Audio` actors plays without listing them.

## Remotion

`toRemotionComposition` is the whole integration for a composition. It returns the props of a
Remotion `<Composition>`, with the duration measured by a build in `calculateMetadata`. See
[Compositions](/docs/rendering/compositions#in-remotion).

Underneath, `RemotionHostBinding` adapts Remotion to the two contracts: `useCurrentFrame` and
`useVideoConfig` become the frame source, `delayRender` becomes the gate, and `staticFile`
resolves assets. To draw a compiled scene inside a component you already have, wrap it
yourself:

```tsx twoslash title="intro.tsx"
// @jsx: react-jsx
// @filename: title.scene.ts
import { createScene, LayoutActor, signal } from "@motionactor/core";
export class Title extends LayoutActor { text = signal(""); }
export const compiled = createScene(function* (ctx) {
  const heading = ctx.spawn(Title, { text: "Hello" });
  yield* ctx.wait(2);
  return { heading };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });
// @filename: intro.tsx
// ---cut---
import { useCurrentFrame } from "remotion";
import { createRenderRuntime, SceneRenderer } from "@motionactor/renderers/react";
import { RemotionHostBinding } from "@motionactor/remotion";
import { dom } from "@motionactor/dom/react";
import { compiled } from "./title.scene";

const runtime = createRenderRuntime({ backends: [dom] });

export function Intro() {
  const frame = useCurrentFrame();
  return (
    <RemotionHostBinding>
      <SceneRenderer
        compiled={compiled}
        frame={frame}
        width={1920}
        height={1080}
        runtime={runtime}
      />
    </RemotionHostBinding>
  );
}
```

`toRemotionComposition` adds the media renderers after the composition's own, as every host
does. A `SceneRenderer` you mount yourself draws with the runtime you pass it, so a scene with
media actors lists `mediaRendererRegistry` from `@motionactor/media/react` in that runtime's
`renderers`.

## HyperFrames

The HyperFrames runtime owns time and dispatches an `hf-seek` event with a time in seconds.
`mountHyperframesComposition` listens for it, rounds the time to a frame, and passes the
frame's readiness to the event's `waitUntil`, so HyperFrames captures only finished frames.

```ts twoslash title="page.ts"
// @jsx: react-jsx
// @filename: title-card.tsx
import { createScene, input, LayoutActor, signal } from "@motionactor/core";
import { text } from "@motionactor/schema";
import {
  createRenderRuntime,
  defineComposition,
} from "@motionactor/renderers/react";
import { dom } from "@motionactor/dom/react";
class Title extends LayoutActor { text = signal(""); }
const titleScene = createScene(function* (ctx, { title }) {
  const heading = ctx.spawn(Title, { text: title, appearance: { opacity: 0 } });
  yield* heading.appearance.opacity.tween(1, 0.5);
  yield* ctx.wait(1.5);
  return { heading };
}, { inputs: { title: input.of(text, "Launch") } });
export const titleCard = defineComposition({
  id: "TitleCard", width: 1920, height: 1080, fps: 30, defaultProps: { title: "Launch" },
  runtime: createRenderRuntime({ backends: [dom] }),
  scene: titleScene,
});
// @filename: page.ts
// ---cut---
import { mountHyperframesComposition } from "@motionactor/hyperframes/react";
import { titleCard } from "./title-card";

const mounted = await mountHyperframesComposition({
  composition: titleCard,
  element: document.getElementById("composition")!,
  props: { title: "Hello" },
});

window.addEventListener("pagehide", () => mounted.dispose());
```

The element gets the `data-composition-id`, `data-duration`, `data-width` and `data-height`
attributes HyperFrames reads. A seek that arrives while another is running waits its turn.

HyperFrames Studio discovers compositions from generated pages. `buildHyperframesProject`
writes them from your entries, taking the id, size and duration from each composition:

```ts twoslash title="hyperframes.config.ts"
import {
  buildHyperframesProject,
  defineHyperframesProject,
} from "@motionactor/hyperframes/node";

const project = defineHyperframesProject({
  compositions: [
    { entry: "./title-card.tsx", export: "titleCard" },
    {
      entry: "./title-card.tsx",
      export: "titleCard",
      path: "hello",
      props: { title: "Hello" },
    },
  ],
  publicDir: "./public",
  outDir: "./.hyperframes",
});

await buildHyperframesProject(project);
```

The first composition goes at the root of `outDir`, and each other one in a directory named
by `path` or by its id. Props have to be JSON. Files in `publicDir` are copied into each
composition's `assets` directory, and media is measured with FFprobe at build time.
`esbuild`, `@hyperframes/core` and `@hyperframes/engine` are optional peers needed only for
this build.

## Writing a host

A host is a clock wrapped around three calls. `prepareCompositionHost` from
`@motionactor/engine/react` mounts a composition into an element and resolves to `commitFrame`,
which renders a frame, and `settle`, which resolves when the gate is idle and the browser has
painted. `createSceneController` from `@motionactor/engine` queues those calls so that seeks
never overlap:

```ts twoslash title="stepper.ts"
// @jsx: react-jsx
// @filename: title-card.tsx
import { createScene, input, LayoutActor, signal } from "@motionactor/core";
import { text } from "@motionactor/schema";
import {
  createRenderRuntime,
  defineComposition,
} from "@motionactor/renderers/react";
import { dom } from "@motionactor/dom/react";
class Title extends LayoutActor { text = signal(""); }
const titleScene = createScene(function* (ctx, { title }) {
  const heading = ctx.spawn(Title, { text: title, appearance: { opacity: 0 } });
  yield* heading.appearance.opacity.tween(1, 0.5);
  yield* ctx.wait(1.5);
  return { heading };
}, { inputs: { title: input.of(text, "Launch") } });
export const titleCard = defineComposition({
  id: "TitleCard", width: 1920, height: 1080, fps: 30, defaultProps: { title: "Launch" },
  runtime: createRenderRuntime({ backends: [dom] }),
  scene: titleScene,
});
// @filename: stepper.ts
// ---cut---
import { createSceneController } from "@motionactor/engine";
import {
  createDOMMediaAdapter,
  prepareCompositionHost,
} from "@motionactor/engine/react";
import { titleCard } from "./title-card";

const host = await prepareCompositionHost({
  composition: titleCard,
  element: document.getElementById("stage")!,
  props: titleCard.defaultProps,
  mediaAdapter: createDOMMediaAdapter(),
});

const controller = createSceneController({
  host,
  fps: host.viewport.fps,
  totalFrames: host.viewport.durationInFrames,
});

// One frame per key press.
let frame = 0;
window.addEventListener("keydown", () => {
  frame = (frame + 1) % host.viewport.durationInFrames;
  void controller.seek(frame);
});
```

`prepareCompositionHost` measures media durations before it mounts. The media adapter decides
how video and audio play; `createDOMMediaAdapter` uses `<video>` and `<audio>` elements.

A failed frame fails the controller: every later seek rejects with the same error and reports
it to `onError`. `controller.withFrame({ frame, signal, read })` holds a frame on screen while
`read` runs, which is how [capture](/docs/rendering/capture-and-export) reads pixels without a
later seek painting over them.
