Add video upload and playback to Next.js
Use FastPix SDKs to add resumable uploads and adaptive-bitrate playback to your Next.js application.
Uploading video is different from uploading other files. Large uploads need to resume after interruptions, and media must be processed into streaming formats before they can be played reliably across devices.
FastPix handles uploads, processing, and adaptive streaming. Your application only needs to create a signed upload URL on the server and render a player. Uploads go directly from the browser to FastPix, so video data never passes through your Next.js server.
This guide uses the Next.js App Router with React because the uploader is a React component. The FastPix Player is a web component and does not require React. If you’d rather not use React components, the quickstart covers the same workflow using the API directly, and resumable uploads for web covers the underlying @fastpix/resumable-uploads engine.
How it works
The upload workflow is straightforward:
- Your Next.js server creates a signed upload URL using the FastPix Node SDK.
- The React Uploader uploads the selected file directly to FastPix using the signed URL.
- FastPix processes the uploaded media asynchronously.
- FastPix sends a
video.media.readywebhook when the media is ready for playback. - Your application retrieves the playback ID and plays the video using the FastPix Player.
What you’ll build
By the end of this guide, you’ll have:
- A Next.js application that uploads videos directly to FastPix.
- Resumable uploads with upload progress and retry support.
- A FastPix Player that streams the uploaded video.
- A workflow that waits for media processing before playback.
Before you begin
- A FastPix account with an Access Token ID and Secret Key. See get your Access Token ID and Secret Key.
- A Next.js application using the App Router.
- React 18 or later.
- Node.js.
Install
From your project directory:
Each package covers one part of the flow: the Node SDK talks to the FastPix API from your server, the React Uploader is the browser-side upload UI, and the Web Player plays the result.
Add your credentials to .env.local:
WARNING:
Never prefix these withNEXT_PUBLIC_. That inlines them into the browser bundle and publishes your Secret Key to every visitor.
Then import the uploader stylesheet once, in app/layout.tsx:
Without it the components render as an unstyled skeleton and you supply the entire look yourself. With it you get the default drop zone, progress track, status text, and buttons, which you can then adjust through the appearance prop or the --fastpix-* CSS variables, both documented in the uploader README.
Create an upload URL
Signed upload URLs are created with your Secret Key, so this belongs on the server. Put it in a route handler:
app/api/upload-url/route.ts
app/api/upload-url/route.js
NOTE:
X-Client-Type: web-browsertells FastPix that a browser will perform the upload, so the signed URL is issued for browser use. Send it whenever the file is uploaded from a browser, including this setup, where your server requests the URL and the browser uploads to it.Leave it out when your own server, a CLI, or a native Android or iOS app uploads the bytes. See upload videos from device.
Keep the mediaId. You’ll use the media ID to retrieve the playback ID after FastPix finishes processing the video. Full options are in upload videos from device, or use create media from a URL to import video you already host.
Add the uploader
Give <FastPixUploader> a function instead of a URL. It runs when a file is picked, so a URL is minted per upload rather than per page view:
app/upload/page.tsx
app/upload/page.jsx
That’s a working uploader: drag and drop, a file picker, progress, and pause, resume, and cancel controls.
You can drag a video into the uploader and the upload begins automatically.
NOTE:
The uploader ships with its own"use client"banner, so the component itself needs no directive. The directive at the top of this page exists for thecreateUploadfunction: functions can’t cross the server-to-client boundary, so any file that passes one as a prop must be a client module.
Build your own layout instead
Pass the sub-components as children and arrange them yourself. Each reads upload state from the parent through context, so placement and order are yours:
autoStart={false} holds the file until someone presses start, which is why a start button appears here and not in the default layout.
Tracking progress yourself, driving the uploader from a ref, reading live state with useUploaderContext(), and building a fully headless UI are covered in the uploader README.
Respond to upload events
The uploader reports its lifecycle through callback props: onProgress, onSuccess, onError, and more. All are optional:
onSuccess fires when the bytes finish uploading. The media still has to be encoded before it can play, which is what the next section covers. The full callback list is in the uploader README.
At this point files upload successfully, but they aren’t playable yet. Next, learn how to know when a video has finished encoding and how to capture its playback ID.
Wait for the media to be ready
A finished upload isn’t a playable video yet. It still has to be encoded. FastPix sends a video.media.ready webhook once playback is available.
Register your endpoint under Org Settings > Webhooks (see Set up webhooks), then handle the event:
app/api/fastpix/webhook/route.ts
app/api/fastpix/webhook/route.js
WARNING:
Verify the signature before trusting a payload. Your endpoint is a public URL, so anyone who finds it could claim a video is ready. Check theFastPix-Signatureheader, an HMAC-SHA256 of the raw body, as described in Set up webhooks. Read the body withrequest.text()and verify that exact string. A parsed and re-serialized body produces different bytes and never matches.
Other events, including video.media.failed, are in the webhook event reference.
Webhooks can’t reach localhost, so while developing either expose your dev server through a tunnel or poll instead:
Play the video
<fastpix-player> is a web component, so it registers itself against customElements, a browser-only API. Next.js server-renders client components for the initial HTML, so import the player after mount rather than at module scope. The element upgrades itself as soon as the definition arrives:
app/components/Player.tsx
app/components/Player.jsx
Give it a size, since it fills whatever container it sits in:
Then fetch the media and pass its playback ID in:
app/play/[mediaId]/page.tsx
app/play/[mediaId]/page.jsx
A playback ID isn’t the same as a media ID. One media asset can carry several playback IDs with different access policies. This guide uses accessPolicy: "public", so the ID alone is enough to play the video. Private and DRM playback need a signed token, covered in play uploaded videos along with autoplay, captions, and the player’s full attribute and event surface.
Troubleshooting
Uploads fail with “error code 0” after 5 attempts, but the video still processes.
The signed URL was created without the X-Client-Type: web-browser header, so it isn’t valid for uploads started from a browser. Add it to your upload call as shown above, or send it on your POST /v1/on-demand/upload request if you call the API directly. The rule is about who uploads the bytes, not who requests the URL. A browser doing the upload needs the header even though your server is the one asking for the URL.
Every visitor gets the same upload URL.
The upload URL is being created while the page renders, so Next.js prerenders it into the HTML at build time. Create it in a route handler called from endpoint, as above, so each upload gets a fresh URL. Passing a URL string from a Server Component instead requires export const dynamic = "force-dynamic" on that page.
ReferenceError: window is not defined.
The player is being imported at module scope. Load it inside useEffect, as above. Adding "use client" alone doesn’t help, because client components are still server-rendered for the initial HTML.
Error: Functions cannot be passed directly to Client Components.
Your endpoint function or an on* callback is defined in a Server Component. Define them in a file marked "use client". Functions can’t cross the server-to-client boundary. Avoid passing a Server Action as endpoint. It receives the selected File and uploads it to your own server first.
The file picker does nothing over a tunnel.
Next.js rejects dev requests from unrecognized origins. Add the host to next.config.ts:
Chunk size is rejected.
chunkSize is in KB, between 5120 and 512000, in multiples of 256.