---
title: Playing in the browser
description: SceneMedia plays a compiled scene behind Video.js controls.
---

`@motionactor/player` plays a compiled scene on a page. `SceneMedia` is a
[Video.js](https://videojs.org) media component: it goes where `<Video src>` would, and the
Video.js player around it supplies the controls, skin, keyboard shortcuts and fullscreen. Every
figure on this site plays through it.

```tsx twoslash title="intro-player.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", appearance: { opacity: 0 } });
  yield* heading.appearance.opacity.tween(1, 0.5);
  yield* ctx.wait(1.5);
  return { heading };
}).build({ viewport: { width: 1920, height: 1080 }, fps: 30 });
// @filename: intro-player.tsx
// ---cut---
import { createPlayer, Hotkey } from "@videojs/react";
import { VideoSkin, videoFeatures } from "@videojs/react/video";
import { SceneMedia } from "@motionactor/player";
import { createRenderRuntime, SceneRenderer } from "@motionactor/renderers/react";
import { dom } from "@motionactor/dom/react";
import { compiled } from "./title.scene";

const { Player } = createPlayer({ features: videoFeatures });
const runtime = createRenderRuntime({ backends: [dom] });

export function IntroPlayer() {
  return (
    <Player>
      <VideoSkin>
        <SceneMedia compiled={compiled} width={1920} height={1080} loop>
          {({ frame }) => (
            <SceneRenderer compiled={compiled} frame={frame} width={1920} height={1080} runtime={runtime} />
          )}
        </SceneMedia>
        <Hotkey keys="." action="seekStep" value={1 / compiled.fps} />
        <Hotkey keys="," action="seekStep" value={-1 / compiled.fps} />
      </VideoSkin>
    </Player>
  );
}
```

- Import the skin's styles once with `import "@videojs/react/video/skin.css"`. Video.js themes
  it through CSS custom properties such as `--media-accent-color` and `--media-border-radius`.
- The stage is `width` by `height` scene pixels, scaled to the player's width. Canvas backends
  receive that scale as the preview scale, so they draw at the density the viewer sees.
- `SceneMedia` provides the [frame source and gate](/docs/rendering/hosts). A hold on the gate, from
  a font, an image or a `useHold`, keeps the frame on screen and puts the player in its waiting
  state until the hold is released.
- A seek lands on a whole frame. The player's time is rounded to the nearest frame, so a
  `seekStep` of `1 / fps` moves exactly one frame.
- The scene is silent. `SceneMedia` reports no volume, so the skin leaves out its mute button.
  A scene with `Video` or `Audio` actors plays on the [native preview](/docs/rendering/hosts#native-preview).

## Reading the frame beside the player

`useSceneFrame` returns the frame on screen and a seek by frame. It works anywhere under the
Video.js `Player`, including beside the skin, so a readout or a timeline can sit under the video.

```tsx twoslash title="frame-readout.tsx"
// @jsx: react-jsx
// ---cut---
import { useSceneFrame } from "@motionactor/player";

export function FrameReadout() {
  const { frame, seek } = useSceneFrame();
  return <button onClick={() => seek(0)}>frame {frame}, back to the start</button>;
}
```
