martin6363 / filament-video-engine
Enterprise HLS video transcoding, adaptive streaming, and Filament UI for Laravel — FFmpeg, multi-resolution, Plyr/hls.js player, polymorphic HasVideoEngine.
Package info
github.com/Martin6363/filament-video-engine
pkg:composer/martin6363/filament-video-engine
Requires
- php: ^8.3
- filament/filament: ^4.0|^5.0
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/queue: ^11.0|^12.0|^13.0
- illuminate/routing: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- livewire/livewire: ^3.0|^4.0
- symfony/process: ^7.0|^8.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
Suggests
- laravel/sanctum: Required for token-based private video API access.
- league/flysystem-aws-s3-v3: Required for AWS S3 / Cloudflare R2 / DigitalOcean Spaces disks.
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-06 13:54:46 UTC
README
Self-hosted HLS transcoding and adaptive playback for Laravel + Filament.
Upload a source video once. A queue worker runs FFmpeg, builds a multi-bitrate HLS ladder, extracts (or accepts) a poster, and optionally burns in a watermark. Filament shows live progress. A Plyr + hls.js player plays the result. Everything stays on your disks and your servers.
Table of contents
- Demo
- What you get
- How it works
- Requirements
- Installation
- License key
- Quick start
- VideoEnginePicker
- Posters
- Watermarks
- Videos admin
- Front-end player
- Queue worker
- Programmatic usage
- HTTP API
- Configuration
- Smart resolution
- Storage and cleanup
- Security
- Software license
- Support
Demo
See Filament Video Engine in action — upload, live progress, watermark, and HLS playback:
▶ Watch the demo on YouTube
Opens in a new browser tab
What you get
| Area | What it does |
|---|---|
| Transcoding | Multi-bitrate HLS (240p–4K) via FFmpeg |
| Filament | VideoEnginePicker — upload, poster, watermark, live progress |
| Admin | Videos resource — preview, retry, regenerate, watermark re-apply, trash |
| Posters | Custom image upload, or auto frame extract; restore via queue when custom is cleared |
| Watermarks | Per-video image upload + layout controls (config supplies defaults only) |
| License | Lemon Squeezy key — required to unlock picker + queue processing |
| Player | Blade <x-video-engine-player> with ABR, quality, speed |
| API | Manifest, progress, quality endpoints (optional signed URLs) |
| Storage | Separate input / output disks (local, S3, R2, Spaces, …) |
No third-party transcoding SaaS. You run FFmpeg on a worker.
Important: Watermark and HLS segments are baked in at encode time. Changing the watermark image later updates the database only — use Apply watermark to streams (or regenerate qualities) to refresh existing playback.
Requirements
| Dependency | Version |
|---|---|
| PHP | 8.3+ |
| Laravel | 11 / 12 / 13 |
| Filament | v4 / v5 |
| FFmpeg + FFprobe | On the queue worker PATH, or set in config |
| Queue | Redis / database / SQS recommended in production |
Optional: Sanctum or Passport for API tokens; Flysystem S3 for cloud disks.
Installation
1. Require the package
composer require martin6363/filament-video-engine
2. Publish config and assets
php artisan vendor:publish --tag=filament-video-engine-config php artisan vendor:publish --tag=filament-video-engine-assets
Optional:
php artisan vendor:publish --tag=filament-video-engine-translations php artisan vendor:publish --tag=filament-video-engine-views
3. Migrate
php artisan migrate
Creates video_media and video_conversions.
4. Register the Filament plugin
In your panel provider (e.g. AdminPanelProvider):
use Martin6363\FilamentVideoEngine\FilamentVideoEnginePlugin; public function panel(Panel $panel): Panel { return $panel ->plugins([ FilamentVideoEnginePlugin::make(), ]); }
Hide the built-in Videos resource if you manage media yourself:
FilamentVideoEnginePlugin::make() ->registerResource(false);
5. Add your license key
Purchase a license, then set the key in .env:
FILAMENT_VIDEO_ENGINE_LICENSE_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
(see License key).
6. Link storage and check FFmpeg
php artisan storage:link ffmpeg -version ffprobe -version
Useful .env keys:
FFMPEG_BINARY=/usr/bin/ffmpeg FFPROBE_BINARY=/usr/bin/ffprobe FFMPEG_TIMEOUT=3600 FILAMENT_VIDEO_ENGINE_LICENSE_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx FILAMENT_VIDEO_ENGINE_QUEUE=video-engine FILAMENT_VIDEO_ENGINE_INPUT_DISK=local FILAMENT_VIDEO_ENGINE_OUTPUT_DISK=public
7. Start the worker
php artisan queue:work --queue=video-engine --timeout=3600
Keep this process running wherever videos are encoded.
License key
Commercial use is gated with a Lemon Squeezy license key.
Setup
- Buy a license for Filament Video Engine.
- Copy the key into
.env:
FILAMENT_VIDEO_ENGINE_LICENSE_KEY=your-lemonsqueezy-license-key
Banner copy:
Filament Video Engine license key is missing or invalid. Please set FILAMENT_VIDEO_ENGINE_LICENSE_KEY in your .env file.
After rotating a key
Clear the local license cache (or wait for TTL), then set the new env value and reload config:
php artisan config:clear php artisan cache:clear
Or call app(\Martin6363\FilamentVideoEngine\Services\LicenseManager::class)->forget();
Quick start
1. Trait on your model
use Martin6363\FilamentVideoEngine\Concerns\HasVideoEngine; class Movie extends Model { use HasVideoEngine; }
2. Picker on the Filament form
use Martin6363\FilamentVideoEngine\Filament\Forms\Components\VideoEnginePicker; VideoEnginePicker::make('video') ->label('Video') ->qualities(['1080p', '720p', '480p', '360p']) ->posterControls() ->watermarkControls() ->columnSpanFull() ->titleFromRecord(fn ($record) => $record?->title);
3. Save in admin
- Create or edit the record.
- Choose a source video (optional poster / watermark).
- Save — upload + queue start automatically.
- Watch progress in the picker until
completed.
4. Play
<x-video-engine-player :video="$movie" />
if ($movie->hasHlsStream()) { $manifest = $movie->getVideoManifestUrl(); $poster = $movie->getPosterUrl(); }
VideoEnginePicker
One field for source upload, poster, watermark, progress, and polymorphic attach.
- Form state is the video UUID (
string|null). - The field is not dehydrated; work runs on parent form save.
- Disks, mime types, and size limits come from package config — you do not wire separate
FileUploaddisks.
Fluent API
| Method | Default | Purpose |
|---|---|---|
qualities([...]) |
default_qualities in config |
Ladder to encode |
posterControls() |
true |
Poster upload + extract time |
posterControls(false) |
— | Hide UI; auto-extract still runs |
watermarkControls() |
false |
Per-video watermark upload + layout |
watermarkControls(true, ['1:1', '16:9']) |
— | Crop ratios in the image editor |
pollingInterval('3s') |
Config | Poll while processing |
titleFromRecord(fn) |
null |
Title on VideoMedia |
columnSpanFull() |
— | Full-width layout in sections |
On save
- Source file → input disk
VideoMediacreated/updated and linked- Poster / watermark settings stored
- Job(s) dispatched to
video-engine - Picker polls until done or failed
Posters
| Mode | Behaviour |
|---|---|
| Auto | FFmpeg extracts a frame at thumbnail.default_timestamp (or the time chosen in the picker) |
| Custom | Admin uploads an image; poster_is_custom = true |
| Clear custom | Restore is queued on video-engine — save stays fast; a new frame is extracted from the source |
| Replace custom | New file is stored; the previous poster file is deleted from the output disk |
Videos admin View shows the poster image and an HLS preview player (lazy segment load). Edit allows updating the poster image only.
Watermarks
Watermarks are per video, driven by an image upload in the picker — not a single fixed file path in day-to-day use.
Recommended flow (Filament)
- Enable controls:
->watermarkControls()onVideoEnginePicker. - Toggle watermark on.
- Upload a PNG/WebP/JPEG (image editor + optional crop ratios).
- Set position, opacity, margin, and size (% of frame width).
- Save the form — settings are stored on that
VideoMedia. - On first encode, FFmpeg burns the uploaded image into every HLS quality.
Config defaults (fallback only)
Config / .env supply defaults when a video has no override yet (or when you encode without picker UI):
FILAMENT_VIDEO_ENGINE_WATERMARK=false FILAMENT_VIDEO_ENGINE_WATERMARK_PATH= # optional global fallback image path on the output disk FILAMENT_VIDEO_ENGINE_WATERMARK_POSITION=bottom-right FILAMENT_VIDEO_ENGINE_WATERMARK_OPACITY=0.6 FILAMENT_VIDEO_ENGINE_WATERMARK_MARGIN=20 FILAMENT_VIDEO_ENGINE_WATERMARK_MAX_WIDTH_PERCENT=12
Prefer uploading in the picker for flexibility. A global WATERMARK_PATH is only a fallback, not the primary workflow.
After you change an existing watermark
Saving the new image/settings does not rewrite old HLS files by itself.
- Use Apply watermark to streams in the picker footer or Videos admin row menu, or
- Regenerate quality for selected renditions.
Both re-encode from the original source — no re-upload of the video file.
Videos admin
Enabled when filament.register_resource is true (default).
FILAMENT_VIDEO_ENGINE_REGISTER_RESOURCE=true
'filament' => [ 'navigation_group' => 'Media', 'navigation_sort' => 20, 'polling_interval' => '3s', ],
| Action | Use when |
|---|---|
| View | Preview player + poster + metadata |
| Edit | Title / replace poster image |
| Apply watermark to streams | Watermark image or layout changed |
| Retry transcoding | Failed / partial / cancelled |
| Regenerate quality | Re-encode selected qualities (above-source heights disabled) |
| Trash / Force delete | Soft delete keeps files; force delete can purge storage |
Row actions are grouped in Filament’s ActionGroup (⋮ menu).
Front-end player
{{-- Model with HasVideoEngine, VideoMedia, or UUID --}} <x-video-engine-player :video="$movie" /> {{-- Admin-style lazy preview (segments after play) --}} <x-video-engine-player :video="$movie" mode="preview" />
- Adaptive bitrate (hls.js; native HLS on Safari)
- Quality + speed in Plyr settings
- Poster, keyboard, PiP, fullscreen
- Assets under
public/vendor/filament-video-engine/
Model helpers (HasVideoEngine)
| Method | Returns |
|---|---|
getPrimaryVideoMedia() |
Latest VideoMedia |
hasHlsStream() |
Master playlist ready |
getVideoManifestUrl() |
Manifest API URL |
getVideoQuality('720p') |
Single quality URL |
getPosterUrl() |
Poster URL |
videoMedia() |
MorphMany |
Queue worker
All heavy work runs asynchronously on the configured queue (default name video-engine):
- Full transcoding
- Per-quality regenerate / watermark re-apply
- Poster restore after clearing a custom image
php artisan queue:work --queue=video-engine --timeout=3600
FILAMENT_VIDEO_ENGINE_QUEUE=video-engine FILAMENT_VIDEO_ENGINE_QUEUE_CONNECTION=redis
Use a dedicated worker in production. Set --timeout ≥ ffmpeg.timeout.
Programmatic usage
use Martin6363\FilamentVideoEngine\Actions\UploadVideoAction; use Martin6363\FilamentVideoEngine\Actions\DispatchTranscodingAction; use Martin6363\FilamentVideoEngine\Actions\RegenerateQualityAction; use Martin6363\FilamentVideoEngine\Actions\ReapplyWatermarkAction; use Martin6363\FilamentVideoEngine\Actions\RestoreExtractedVideoPosterAction; use Martin6363\FilamentVideoEngine\Services\VideoEngineManager; $media = app(UploadVideoAction::class)->execute( file: $request->file('video'), videoable: $movie, ); app(DispatchTranscodingAction::class)->execute($media); app(RegenerateQualityAction::class)->executeMany($media, ['720p', '1080p']); app(ReapplyWatermarkAction::class)->execute($media); // Clear custom poster → queue FFmpeg extract (same as admin) app(RestoreExtractedVideoPosterAction::class)->queue($media); $manifest = app(VideoEngineManager::class)->manifest($media); $progress = app(VideoEngineManager::class)->progress($media);
HTTP API
Prefix: /api/v1 (configurable).
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/videos/{uuid}/progress |
Status, %, step, errors |
| GET | /api/v1/videos/{uuid}/manifest |
Master URL, poster, qualities |
| GET | /api/v1/videos/{uuid}/qualities/{quality} |
One rendition |
When api.security.enabled is true, use signed URLs or Sanctum/Passport.
use Martin6363\FilamentVideoEngine\Services\Security\SignedUrlGenerator; $url = app(SignedUrlGenerator::class)->manifestUrl($videoMedia);
Local / public demo:
FILAMENT_VIDEO_ENGINE_API_SECURITY=false
Configuration
Published file: config/filament-video-engine.php.
| Key | Purpose |
|---|---|
enabled |
Package on/off |
license_key |
Lemon Squeezy license key (FILAMENT_VIDEO_ENGINE_LICENSE_KEY) |
disks.* |
input, output, temp |
paths.* |
Uploads, HLS, posters, watermarks, chunks |
queue.* |
Connection + queue name |
ffmpeg.* |
Binaries, codec, CRF, preset, timeout |
qualities / default_qualities |
Ladder |
hls.* |
Segment length, optional AES-128 |
thumbnail.* |
Default extract time, format |
watermark.* |
Defaults + accepted mime types (upload is primary) |
uploads.* |
Max size, chunk size, video mime types |
api.* |
Routes + security |
player.* |
Speeds, theme, PiP |
filament.* |
Resource, nav, polling |
cleanup.* |
Delete files on force delete / failure |
FILAMENT_VIDEO_ENGINE_ENABLED=true FILAMENT_VIDEO_ENGINE_LICENSE_KEY=your-lemonsqueezy-license-key FILAMENT_VIDEO_ENGINE_INPUT_DISK=local FILAMENT_VIDEO_ENGINE_OUTPUT_DISK=public FILAMENT_VIDEO_ENGINE_QUEUE=video-engine FILAMENT_VIDEO_ENGINE_API_SECURITY=true FILAMENT_VIDEO_ENGINE_API_AUTH=signed FILAMENT_VIDEO_ENGINE_SIGNED_TTL=60 FILAMENT_VIDEO_ENGINE_DELETE_FILES_ON_FORCE_DELETE=true FFMPEG_PRESET=medium FFMPEG_CRF=23
Without a valid FILAMENT_VIDEO_ENGINE_LICENSE_KEY, the picker stays locked and encoding jobs will not run. Details: License key.
Smart resolution
Source height is probed before encode.
Example: Upload is 480p; defaults include 720p and 1080p.
- Dimensions stored on
VideoMedia - Higher targets skipped (
source_resolution_lower) — no upscaling - Allowed qualities encode → status
completed - Manifest / admin expose source and skipped list
partial means real encode failures, not skipped upscales. The same rule disables invalid options in Regenerate quality.
Storage and cleanup
| Action | Database | Files |
|---|---|---|
| Soft delete | Trashed | Kept (restorable) |
| Force delete | Removed | Purged if cleanup.delete_files_on_force_delete is true |
Force delete removes originals, posters, HLS tree, per-video watermark uploads, and encryption keys for that video.
Tip: keep originals on a private input disk; serve HLS + posters from public or a CDN output disk.
Security
Please report vulnerabilities privately — see SECURITY.md.
Do not open a public GitHub issue for security reports. Email martin.khachatryan.2024@gmail.com.
Software license
This plugin is a paid commercial product. A Lemon Squeezy license key is required to activate and use it.
See License key.
Support
| Author | Martin Khachatryan — martin.khachatryan.2024@gmail.com |
Built with Laravel, Filament, FFmpeg, Plyr, and hls.js.