Add video upload and playback to Laravel
Use FastPix SDKs to add resumable uploads and adaptive-bitrate playback to a Laravel application with Inertia and React.
Uploading video is different from uploading other files. Large uploads need to resume after interruptions, and media must be processed into streaming formats before it 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 Laravel application.
This guide uses Inertia with React, as provided by Laravel’s React starter kit, because the uploader is a React component. The FastPix Player is a web component and does not require React. If your application uses Blade instead of React, see the note at the end of the playback section.
How it works
The upload workflow is straightforward:
- Your Laravel application creates a signed upload URL using the FastPix API.
- The uploader uploads the selected file directly to FastPix using the signed URL.
- FastPix processes the uploaded media asynchronously.
- FastPix sends a webhook event when the media is ready for playback.
- Your application retrieves the playback ID and plays the video.
What you’ll build
By the end of this guide, you’ll have:
- A Laravel 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 Laravel application using Inertia with React.
- PHP 8.x
- Composer
- Node.js
Install
From your project directory:
The PHP 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.
Laravel’s React starter kit already ships with Inertia on both sides. If your application was not created from the starter kit, add the Inertia server and client adapters as well:
Follow the Inertia server-side setup once (root template and middleware) if the app has never used Inertia before.
Add your credentials to .env:
Read them through config rather than env() directly, so they keep working once you run php artisan config:cache:
Then wrap the client in a small service:
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. Setting it on the Guzzle client applies it to every SDK call.Leave it out when PHP itself, a CLI command, or a native Android or iOS app uploads the bytes.
Create an upload URL
Signed upload URLs are created with your Secret Key, so this belongs on the server:
Register the routes:
Keep the mediaId. It identifies the media everywhere else in the API, and you’ll use it later to play the video. Full options are in Upload media 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 load.
Routes in routes/web.php are CSRF-protected, so the request needs the token Laravel set in the XSRF-TOKEN cookie:
resources/js/pages/upload.tsx
resources/js/pages/upload.jsx
That’s a working uploader: drag and drop, a file picker, progress, and pause, resume, and cancel controls.
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.
Know when the video is 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:
FastPix can’t send a CSRF token, so exclude the webhook route. Without this every delivery fails with a 419:
WARNING:
Verify the signature before trusting a payload. Excluding the route from CSRF means anything can post to it. Check theFastPix-Signatureheader, an HMAC-SHA256 of the raw body, as described in Set up webhooks. Use$request->getContent()and verify those exact bytes. A decoded and re-encoded payload 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 app through a tunnel or check the status directly:
Play the video
Fetch the media in the controller. That runs on the server, so your credentials stay there and only the playback ID reaches the browser:
<fastpix-player> is a web component, so it registers itself against customElements, a browser-only API. Inertia server-renders pages when SSR is enabled, so import the player after mount. The element upgrades itself as soon as the definition arrives:
resources/js/components/player.tsx
resources/js/components/player.jsx
resources/js/pages/play.tsx
resources/js/pages/play.jsx
Give it a size, since it fills whatever container it sits in:
Playing video in a Blade view, without React
The player needs no React. Load it from your bundle and use the tag directly:
with import '@fastpix/fp-player'; in resources/js/app.js. Only the upload UI requires React.
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
composer require fastpix/sdk fails with a dependency version conflict.
The SDK’s dependencies (such as Guzzle) can conflict with versions your project has locked. Let Composer update the locked transitive dependencies alongside it:
419 on /api/upload-url.
The request is missing Laravel’s CSRF token. Send it as the X-XSRF-TOKEN header, read from the XSRF-TOKEN cookie, as above.
419 on the webhook route.
FastPix can’t send a CSRF token. Add the route to validateCsrfTokens(except: [...]) in bootstrap/app.php, and verify the signature instead.
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. Set it on the Guzzle client as shown. 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.
Credentials work until you cache config.
php artisan config:cache stops env() from reading .env outside config files. Read credentials through config('fastpix.username') and keep env() calls inside config/fastpix.php.
ReferenceError: window is not defined with Inertia SSR.
The player is being imported at module scope. Import it inside useEffect, as above.
Chunk size is rejected.
chunkSize is in KB, between 5120 and 512000, in multiples of 256.