Skip to main content
player.js controls the Bunny Player iframe, and server rendering gives it two rules. It reads window on import, which means importing it in the browser. It also has to load before the iframe does. player.js caches the iframe’s ready message on import, and a Player created any time after that still connects. An iframe that finishes loading first, straight from the server HTML, 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 works with the App Router and the Pages Router. For a client-only app, see the React guide.

Next.js example on GitHub

An App Router app with custom controls and an event log.

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 16:9 box. To show a poster until the script loads, use the video’s thumbnail from Video storage structure.
3

Set the library ID

Add your library ID to .env.local. It’s on the library’s API page in the dashboard.
.env.local
The library ID is public in every embed URL, which makes a NEXT_PUBLIC_ variable safe. Restart the dev server after changing it.
4

Render it from a Server Component

A Server Component can fetch the video and pass it straight in. params is request data, so read it in a component inside <Suspense>. With Cache Components on, as in a new create-next-app project, reading it outside <Suspense> fails the build.
app/lessons/[id]/page.tsx
The params prop on BunnyPlayer takes any player parameter.

Control playback

Callback props are functions, and functions can’t cross from a Server Component. Pass them from a Client Component, where onReady hands you the Player.
components/lesson-player.tsx
Getters answer through a callback, as in player.getCurrentTime((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).
The Playback control API lists every method and event.

Save progress with a Server Action

timeupdate fires several times a second. Throttle it before calling a Server Action. A Server Action is a public POST endpoint, and anyone can call it with any arguments. Read the user from the session inside the action, never from its arguments, and validate the values before you write them.
app/actions.ts
components/lesson-player.tsx
Pass the saved position back as the t parameter to resume.

Sign embed URLs on the server

With embed view token authentication on, the URL needs a token and expires. Sign them in the Server Component, where the key stays private, and pass them through params.
Sign embed URLs on the server has the signing function.

Load player.js from the CDN instead

To drop the npm dependency, load our hosted build from the component with next/script. Replace the import("player.js") effect with a <Script> next to the placeholder.
components/bunny-player.tsx
Then declare the global.

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 page reads params or other request data outside <Suspense>, and Cache Components is on. Keep the page synchronous and read the data in an async child inside <Suspense>, as the lesson page above does.
The iframe loaded before player.js, which happens when it’s part of the server-rendered HTML. Render it only once player.js has loaded, as the component above does.A hidden tab also holds ready back until the viewer switches to it.
The library’s allowed domains, direct access block, or token authentication is rejecting the embed. See Embedding restrictions.
Last modified on October 7, 2026