Skip to main content
The Bunny Player is an iframe, and player.js gives you a Player object for talking to it over postMessage. Server rendering adds two constraints. player.js reads window the moment it’s imported, so it has to be imported in the browser. And it has to be loaded before the iframe finishes loading. player.js caches the iframe’s ready message when it’s imported, so a Player created afterwards still connects. If the iframe loads first, as server-rendered HTML can, the Player never becomes ready. The component below renders a placeholder on the server, loads player.js in an effect, then mounts the iframe and creates the Player in the same commit. It uses the App Router and works unchanged with the Pages Router. The React guide covers the client-only version.

Next.js example on GitHub

An App Router app built around this component, with custom controls and an event log. It plays a demo video after bun install and bun dev.

Quickstart

1

Install 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:
player.js.d.ts
2

Create the client component

components/bunny-player.tsx
The placeholder is a black box at the same aspect ratio. Swap in the video’s thumbnail if you want a poster while the script loads; Video storage structure has the URL.
3

Render it from a Server Component

The component is a Client Component, so a server-rendered page can pass it data fetched on the server:
app/lessons/[id]/page.tsx
The library ID already appears in every embed URL, so a NEXT_PUBLIC_ variable is fine. params accepts any player parameter.

Control playback

Callback props are functions, so the component that passes them has to be a Client Component too. onReady hands you the Player:
components/lesson-player.tsx
Getters take a callback, because the answer comes back from the iframe: player.getCurrentTime((seconds) => ...). The player.js package on npm (0.1.0) has no playback speed method, so send the command directly. The build bunny.net hosts adds setPlaybackRate(), getPlaybackRate() and the playbackratechange event from version 0.1.2; see Methods.
Playback control API lists every method and event.

Save progress with a Server Action

timeupdate fires several times a second with { seconds, duration }. Throttle it before calling a Server Action:
app/actions.ts
components/lesson-player.tsx
Pass the saved position back as the t parameter to resume from there.

Sign embed URLs on the server

If the library uses embed view token authentication, the iframe URL needs a token and expires pair. Compute them in the Server Component, where the library’s security key stays, and pass them through params:

Load player.js from the CDN instead

To skip the npm dependency, load the hosted build in the root layout with beforeInteractive, so window.playerjs exists before anything hydrates:
app/layout.tsx
Replace the dynamic import with setPlayerjs(window.playerjs) and declare the global:
Keep the placeholder. beforeInteractive does not guarantee the script has run before a server-rendered iframe finishes loading, so the gate is still needed with the hosted build.

Troubleshooting

player.js is being imported on the server. Keep import("player.js") inside useEffect. A static import playerjs from "player.js" at the top of a Client Component still runs during server rendering.
The iframe finished loading before player.js was loaded, which happens when the iframe is part of the server-rendered HTML. Render it only after player.js has loaded, as the component above does. A Player created after that still connects, because player.js caches the iframe’s ready message.ready is also delayed while the tab is hidden, and fires once it becomes visible.
Every Player adds a message listener to window that is never removed. The active flag in the effect cleanup keeps stale instances quiet, so check that yours flips it.
The library’s allowed domains, direct access block, or token authentication is rejecting the embed. See Embedding restrictions.
Last modified on October 1, 2026