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
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
Set the library ID
Add your library ID to The library ID is public in every embed URL, which makes a
.env.local. It’s on the library’s API page in the dashboard..env.local
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. The
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
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, whereonReady hands you the Player.
components/lesson-player.tsx
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).
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
t parameter to resume.
Sign embed URLs on the server
With embed view token authentication on, the URL needs atoken and expires. Sign them in the Server Component, where the key stays private, and pass them through params.
Load player.js from the CDN instead
To drop the npm dependency, load our hosted build from the component withnext/script.
Replace the import("player.js") effect with a <Script> next to the placeholder.
components/bunny-player.tsx
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.The build fails with uncached or runtime data during prerendering
The build fails with uncached or runtime data during prerendering
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.onReady never fires
onReady never fires
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 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.