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
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
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:The library ID already appears in every embed URL, so a
app/lessons/[id]/page.tsx
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
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.
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
t parameter to resume from there.
Sign embed URLs on the server
If the library uses embed view token authentication, the iframe URL needs atoken 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 withbeforeInteractive, so window.playerjs exists before anything hydrates:
app/layout.tsx
setPlayerjs(window.playerjs) and declare the global:
Troubleshooting
ReferenceError: window is not defined
ReferenceError: window is not defined
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.onReady never fires
onReady never fires
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.Events fire twice after changing the video
Events fire twice after changing the video
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 iframe shows a 403
The iframe shows a 403
The library’s allowed domains, direct access block, or token authentication is rejecting the embed. See Embedding restrictions.