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 a Player only connects if it exists before its iframe finishes loading, which server-rendered iframe HTML can do before your JavaScript hydrates. 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.

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 has no playback speed method, so send the command directly. The build bunny.net hosts adds setPlaybackRate() and getPlaybackRate(); 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. A server-rendered iframe can still finish loading before hydration.

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 the Player existed, 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.
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 September 21, 2026