> ## Documentation Index
> Fetch the complete documentation index at: https://bunny.net/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Use Bunny Player with React

> Embed the Bunny Stream player in a React app and control playback with player.js events and methods.

React renders the Bunny Player iframe like any other element. The `BunnyPlayer` component below adds [player.js](https://github.com/embedly/player.js), forwards the player's events as props, and hands you a `Player` for controlling playback.

It assumes a client-rendered app, such as one built with Vite. For server rendering, including Remix and React Router framework mode, follow the [Next.js guide](/docs/stream/player/nextjs).

> **[React example on GitHub](https://github.com/BunnyWay/examples/tree/main/stream/player-react)**
>
> A Vite app with custom controls and an event log.

## Quickstart

**1. Install player.js**

```bash npm theme={null}
npm install player.js
```

```bash pnpm theme={null}
pnpm add player.js
```

```bash yarn theme={null}
yarn add player.js
```

```bash bun theme={null}
bun add player.js
```

player.js ships without types. Add a declaration file anywhere your `tsconfig.json` includes, for example `player.js.d.ts`. It covers the methods and events the Bunny Player supports:

```ts player.js.d.ts theme={null}
declare module "player.js" {
  export type PlayerEvent =
    | "ready"
    | "play"
    | "pause"
    | "ended"
    | "timeupdate"
    | "progress"
    | "seeked"
    | "error"
    | "playbackratechange";

  export type TimeUpdate = { seconds: number; duration: number };
  export type Progress = { percent: number; seconds: number; duration: number };
  /** Present when a command fails. Empty when the media itself errors. */
  export type PlayerError = { code: number; msg: string };

  export class Player {
    constructor(iframe: HTMLIFrameElement | string);

    on(event: "ready", callback: () => void): void;
    on(event: "timeupdate", callback: (data: TimeUpdate) => void): void;
    on(event: "progress", callback: (data: Progress) => void): void;
    on(event: "playbackratechange", callback: (rate: number) => void): void;
    on(event: "error", callback: (error?: PlayerError) => void): void;
    on(event: PlayerEvent, callback: (data?: unknown) => void): void;
    off(event: PlayerEvent, callback?: (...args: never[]) => void): void;
    supports(kind: "method" | "event", name: string | string[]): boolean;
    /** Send a raw command, for methods player.js does not expose such as setPlaybackRate. Getters answer through the callback. */
    send(message: { method: string; value?: unknown }, callback?: (value: unknown) => void): void;

    play(): void;
    pause(): void;
    mute(): void;
    unmute(): void;
    setVolume(percent: number): void;
    setCurrentTime(seconds: number): void;
    setLoop(loop: boolean): void;

    getPaused(callback: (paused: boolean) => void): void;
    getMuted(callback: (muted: boolean) => void): void;
    getVolume(callback: (percent: number) => void): void;
    getDuration(callback: (seconds: number) => void): void;
    getCurrentTime(callback: (seconds: number) => void): void;
    getLoop(callback: (loop: boolean) => void): void;
  }

  const playerjs: {
    Player: typeof Player;
    addEvent(elem: EventTarget, type: string, handler: EventListener): void;
  };
  export default playerjs;
}
```

**2. Create the component**

The `Player` is created once the iframe is in the DOM. Callbacks live in a ref, which stops inline props from re-creating the player on every render.

```tsx components/bunny-player.tsx theme={null}
import { useEffect, useRef } from "react";
import playerjs, { type Player, type TimeUpdate } from "player.js";

export type BunnyPlayerProps = {
  libraryId: string;
  videoId: string;
  params?: Record<string, string | number | boolean>;
  title?: string;
  onReady?: (player: Player) => void;
  onPlay?: () => void;
  onPause?: () => void;
  onEnded?: () => void;
  onTimeUpdate?: (time: TimeUpdate) => void;
};

export function BunnyPlayer({
  libraryId,
  videoId,
  params,
  title = "Video player",
  onReady,
  onPlay,
  onPause,
  onEnded,
  onTimeUpdate,
}: BunnyPlayerProps) {
  const iframeRef = useRef<HTMLIFrameElement>(null);

  // Keep the latest callbacks without re-creating the player.
  const handlers = useRef({ onReady, onPlay, onPause, onEnded, onTimeUpdate });
  useEffect(() => {
    handlers.current = { onReady, onPlay, onPause, onEnded, onTimeUpdate };
  });

  const query = new URLSearchParams(
    Object.entries(params ?? {}).map(([key, value]) => [key, String(value)]),
  ).toString();
  const src = `https://player.mediadelivery.net/embed/${libraryId}/${videoId}${query ? `?${query}` : ""}`;

  useEffect(() => {
    const iframe = iframeRef.current;
    if (!iframe) return;

    // player.js never removes the window listener each Player adds,
    // so grab it while constructing and remove it on cleanup.
    let onMessage: EventListener = () => {};
    const addEvent = playerjs.addEvent;
    playerjs.addEvent = (elem, type, handler) => addEvent(elem, type, (onMessage = handler));
    const player = new playerjs.Player(iframe);
    playerjs.addEvent = addEvent;

    player.on("ready", () => handlers.current.onReady?.(player));
    player.on("play", () => handlers.current.onPlay?.());
    player.on("pause", () => handlers.current.onPause?.());
    player.on("ended", () => handlers.current.onEnded?.());
    player.on("timeupdate", (time) => handlers.current.onTimeUpdate?.(time));

    return () => window.removeEventListener("message", onMessage);
  }, [src]);

  return (
    <iframe
      ref={iframeRef}
      src={src}
      title={title}
      loading="lazy"
      style={{
        display: "block",
        width: "100%",
        height: "auto",
        aspectRatio: "16 / 9",
        border: 0,
      }}
      allow="autoplay; encrypted-media; picture-in-picture; fullscreen"
      allowFullScreen
    />
  );
}
```

**3. Render a video**

The library ID and video GUID are on the video's page in the dashboard.

```tsx theme={null}
import { BunnyPlayer } from "./components/bunny-player";

export function Lesson() {
  return (
    <BunnyPlayer
      libraryId="12345"
      videoId="your-video-guid"
      params={{ autoplay: false, preload: true }}
      onEnded={() => console.log("Video finished")}
    />
  );
}
```

`params` takes any [player parameter](/docs/stream/embedding#supported-parameters), such as `captions`, `t`, or `muted`.

## Control playback

`onReady` hands you the `Player`. Keep it in state for your own controls.

```tsx theme={null}
import { useState } from "react";
import type { Player } from "player.js";
import { BunnyPlayer } from "./components/bunny-player";

export function Lesson() {
  const [player, setPlayer] = useState<Player | null>(null);
  const [playing, setPlaying] = useState(false);

  return (
    <>
      <BunnyPlayer
        libraryId="12345"
        videoId="your-video-guid"
        onReady={setPlayer}
        onPlay={() => setPlaying(true)}
        onPause={() => setPlaying(false)}
      />

      <button onClick={() => (playing ? player?.pause() : player?.play())}>
        {playing ? "Pause" : "Play"}
      </button>
      <button onClick={() => player?.setCurrentTime(0)}>Restart</button>
      <button onClick={() => player?.mute()}>Mute</button>
    </>
  );
}
```

Getters answer through a callback, since the value comes back from the iframe.

```tsx theme={null}
player.getCurrentTime((seconds) => console.log(seconds));
player.getDuration((seconds) => console.log(seconds));
```

Playback speed is missing from the npm build (0.1.0), and `send()` covers the gap. The build we host adds `setPlaybackRate()`, `getPlaybackRate()` and `playbackratechange` ([Methods](/docs/stream/playback-api#methods)).

```tsx theme={null}
player.send({ method: "setPlaybackRate", value: 1.5 });
player.on("playbackratechange", (rate) => console.log(rate));
```

Browsers block unmuted `play()` before the viewer has clicked anything. Call `player.mute()` first if playback has to start on its own. The [Playback control API](/docs/stream/playback-api) lists every method and event.

## Track progress

`timeupdate` fires several times a second. Throttle it before it reaches your backend.

```tsx theme={null}
import { useRef } from "react";
import { BunnyPlayer } from "./components/bunny-player";

export function Lesson({ videoId }: { videoId: string }) {
  const lastSaved = useRef(0);

  return (
    <BunnyPlayer
      libraryId="12345"
      videoId={videoId}
      onTimeUpdate={({ seconds, duration }) => {
        if (Math.abs(seconds - lastSaved.current) < 5) return;
        lastSaved.current = seconds;
        saveProgress(videoId, seconds, duration);
      }}
      onEnded={() => markComplete(videoId)}
    />
  );
}
```

Pass the saved position back as `params={{ t: savedSeconds }}` to resume.

## Load player.js from the CDN instead

To drop the npm dependency, load our hosted build from the component.

```ts lib/load-playerjs.ts theme={null}
type PlayerJs = (typeof import("player.js"))["default"];

declare global {
  interface Window {
    playerjs: PlayerJs;
  }
}

let loading: Promise<PlayerJs> | undefined;

export function loadPlayerjs() {
  loading ??= new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = "https://assets.mediadelivery.net/playerjs/playerjs-latest.min.js";
    script.onload = () => resolve(window.playerjs);
    script.onerror = (error) => {
      loading = undefined;
      reject(error);
    };
    document.head.append(script);
  });
  return loading;
}
```

Hold `playerjs` in state, set it from `loadPlayerjs()` in an effect, and render a placeholder until it loads, as the [Next.js component](/docs/stream/player/nextjs#quickstart) does.

## Troubleshooting

> **onReady never fires**
>
> player.js has to be loaded before the iframe finishes loading. It caches the iframe's `ready` message on import, and a `Player` created later still connects. In a client-rendered app, importing it with the component is early enough. Server-rendered HTML can load the iframe before your JavaScript runs, which the [Next.js guide](/docs/stream/player/nextjs) handles.
>
> A hidden tab also holds `ready` back until the viewer switches to it.

> **The iframe shows a 403**
>
> The library's allowed domains, direct access block, or token authentication is rejecting the embed. See [Embedding restrictions](/docs/stream/embedding#embedding-restrictions).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.