Media viewer · Open source
Laravel Media Viewer
Show private PDFs, video and audio from any Laravel disk without making the files public.
What it solves
Showing an uploaded file in Laravel usually means a compromise.
storage:link makes the file public to anyone with the URL, a plain download route breaks video seeking and makes large PDFs load in full, and the browser's own PDF viewer looks and behaves differently in every browser.
Files stay on a private disk. Blade and Livewire components render a pdf.js viewer and a Video.js player, and each file is served through a signed URL that expires, answers Range requests and, when someone is signed in, only works for that user.
- pdf.js viewer with page navigation, zoom, text search, full screen and an optional download button
- Lazy PDF thumbnails: through Range requests the browser fetches only what each page needs
- Video.js player for video and audio, with seeking over Range requests
- HLS from a private disk: playlists are rewritten so every segment and key is a signed URL
- Signed, expiring URLs, bound to the user when someone is signed in, and an optional Gate checked on every request
- Disk and extension allow-lists, path-traversal checks, and Subresource Integrity for pdf.js and Video.js
Install
Requires PHP 8.3+ and Laravel 12 and 13.
composer require amjadiqbal/laravel-media-viewer
Usage
<x-media-viewer::pdf disk="s3" path="contracts/{{ $contract->file }}" />
<x-media-viewer::video disk="local" path="courses/intro/index.m3u8" />
Setup
There is nothing to build and nothing to publish. The first viewer on a page prints the small script and stylesheet it needs, and pdf.js and Video.js load from jsDelivr with integrity hashes the first time they are used. The local and public disks are allowed by default; publish the config to allow others.
- 1 composer require amjadiqbal/laravel-media-viewer
- 2 php artisan vendor:publish --tag=media-viewer-config
- 3 Add your disks to 'disks' in config/media-viewer.php.
// config/media-viewer.php
'disks' => ['local', 'public', 's3'],
PDF viewer and thumbnails
scale is auto, page-width (the default), page-fit, or a number such as 1.5. Thumbnails render lazily as they scroll into view.
<x-media-viewer::pdf
disk="s3"
:path="$invoice->pdf_path"
height="70vh"
:page="3"
:download="true"
/>
<x-media-viewer::pdf-thumbnail disk="s3" :path="$document->path" :width="160" :page="1" />
Video, HLS and audio
A .m3u8 path switches to HLS automatically, and segments next to it on the same disk are signed for you. Options include poster or poster-path, aspect-ratio, autoplay, muted, loop and preload. <x-media-viewer::file> picks the viewer from the file extension.
<x-media-viewer::video disk="local" path="videos/intro.mp4" poster-path="videos/intro.jpg" />
<x-media-viewer::video disk="s3" path="courses/101/index.m3u8" aspect-ratio="21 / 9" muted autoplay />
<x-media-viewer::audio disk="local" path="podcast/episode-12.mp3" />
<x-media-viewer::file :disk="$attachment->disk" :path="$attachment->path" />
Livewire gallery
The components work inside Livewire components and with wire:navigate. The gallery shows a file list with thumbnails next to a viewer. Its disk and paths are locked, so the browser can choose among the files you passed but cannot add another. If a viewer only appears after a Livewire action, put @mediaViewerAssets in your layout's <head> and set MEDIA_VIEWER_INJECT_ASSETS=false.
<livewire:media-viewer-gallery disk="s3" :paths="$ticket->attachments->pluck('path')->all()" />
Security model
- Only disks in the disks allow-list are served.
- Only extensions in the extensions list are served, each with a fixed Content-Type. The disk's MIME guess is never used.
- URLs are signed with Laravel URL signing and expire after ttl minutes (120 by default). Keep ttl longer than your longest video.
- With bind_to_user on (the default), a URL created for a signed-in user only works for that user.
- An optional Gate ability is checked when the viewer renders and again on every request, including each HLS segment.
- Paths that try to leave the disk root with ../ are refused, and files are sent with Cache-Control: private, no-cache, nosniff and a sandbox Content-Security-Policy.
- Delivery is stream by default. Local disks seek straight to the requested range; a remote disk whose stream cannot seek (S3 through Flysystem, for example) is read up to the offset first. Set delivery to redirect for any disk with temporary URLs and the bytes come straight from the bucket, after the same signature, user and Gate checks.
- A viewer is not DRM: a browser that may view a file can always save it.
Hire me
Need this built into your app, or something custom?
I wrote Laravel Media Viewer. I can wire it into your Laravel app, extend it for your case, or build the feature around it. Tell me what you need and you'll get a plan and a quote, free.
Support
Found a bug or want a feature? Open an issue on GitHub. For help adding it to your own project, use the quote form above.
More Laravel packages
All packages
Laravel Driver.js
Build Driver.js product tours in PHP and render them with one Blade component.
Laravel Sonner
Sonner toast notifications for Laravel, sent from PHP and shown without React.
Laravel Ninja Keys
A Cmd+K / Ctrl+K command palette for Laravel, with the actions defined in PHP.
Laravel SweetAlert2
SweetAlert2 popups, toasts and confirm dialogs from Laravel controllers, Livewire and Alpine.
Laravel Grid.js
Turn any Eloquent query into a searchable, sortable, paginated Grid.js data table with one fluent PHP call.
Laralink
Work on a Composer package inside the Laravel app that uses it, then switch back to the Packagist release.
Refresher
A drop-in replacement for Laravel's RefreshDatabase that restores a cached snapshot of your test database instead of running migrate:fresh.
Kiln
Artisan commands and an optional signed HTTP route to clear, warm and inspect OPcache when you deploy.
Laravel Swiper
Swiper sliders, carousels and image galleries for Laravel as Blade components.
Laravel MySQL Advisor
Read-only health checks for the MySQL or MariaDB database behind your Laravel app, with the SQL and the Laravel migration to fix a problem wherever a fix applies.
Laravel Webhook Console
A dashboard and replay tool for the webhooks spatie/laravel-webhook-client stores in your Laravel app.