mostafaznv/nova-video
Laravel Nova field for uploading, previewing, and encoding videos with FFmpeg. It supports queued conversions, poster generation, video previews, multiple storage disks, and seamless integration with Nova resources for managing media in admin panels.
Architecture fit. Nova-video is a deliberately thin package: a Nova form field that delegates all real behavior to two underlying libraries — mostafaznv/larupload (storage, naming, metadata, cleanup) and mostafaznv/laravel-video (FFmpeg conversions, poster extraction). That separation is the single most important architectural fact for a TPM, because it means the Nova field is not where the semantics live. Any estimate, risk assessment, or migration plan that treats this as "one small Nova field" is wrong by roughly 3x. The real footprint is three packages, a queue worker pool, an FFmpeg binary in the runtime image, and a storage disk with public/CDN access semantics.
Fit is strong if the organization is already a Laravel Nova shop on PHP 8.1+. It is a poor fit if Nova is incidental, if admin is built on something else, or if the team has no appetite for operating FFmpeg and queue workers. There is no managed-service escape hatch built in — the package commits you to in-house transcoding, and that is a strategic decision, not an implementation detail.
Integration feasibility. Mechanically straightforward and well-trodden for a Laravel team: Composer install, publish config tags, run larupload migrations, storage:link, nova:publish, attach the trait to the model, register Video::make('Video')->disk(...)->rules(...). A competent Laravel dev can run the 30-minute spike (upload a 20 MB MP4 through the Nova form, confirm inline playback, confirm a poster was generated, inspect the bucket) and get a genuine go/no-go signal the same day. The field is well-behaved with Nova conventions — onlyOnForms() / onlyOnDetail() split, readonly(), standard ->rules() — so it slots into existing resources without bespoke frontend work.
The realistic integration cost is not the field; it is the environment: FFmpeg on the PATH of the PHP-FPM user and the queue worker user, a dedicated media queue, worker timeouts sized to transcode duration, and upload limits reconciled across php.ini, nginx, and any load balancer. Budget a day for a greenfield environment, more if the platform is containerized and the base image needs rebuilding.
Technical risk. Concentrated, and mostly supply-side rather than technical:
Apache-2.0 removes license risk entirely; the residual risk is operational and lifecycle, not legal.
Key questions.
Stack fit. Requires Laravel Nova 4/5, PHP 8.1+ with fileinfo and gd/imagick, FFmpeg + ffprobe on the runtime path, a queue backend (Redis/SQS/database), a supervisor/systemd worker, and a storage disk — local with storage:link in dev, S3 or equivalent in production. If any of those are absent, the true adoption cost is building them, not installing the package. The most common fit gap is a PaaS filesystem (Heroku/Render/Fly) where storage/app is ephemeral and every uploaded asset — including the generated poster — is destroyed on deploy.
Migration path. There is no meaningful "migration" in the data sense; this is net-new capability on a resource that currently has no video field, or a replacement for ad-hoc File::move() code that should be deleted, not ported. The correct path is: spike on staging → wrap → adopt one resource → reconcile → expand.
The wrapper is the highest-value architectural decision in the whole engagement. A thin MediaService / VideoPresenter between domain code and the package (exposing src, poster, duration from larupload accessors, always null-safe) costs an afternoon and converts a future move to a managed platform into a one-file change rather than a codebase-wide grep. It also gives you a testable seam so video behavior can be exercised without a Nova form.
Compatibility. Verify three version constraints before committing: Nova major ↔ field package, PHP ↔ larupload, and laravel-video ↔ your FFmpeg build. Register the published config tags explicitly in the deploy script rather than relying on interactive vendor:publish — publish tags drift between versions, and a missed tag means defaults silently win. Keep the path convention identical between local and S3 so URLs, tests, and seeders behave identically; the parity is what makes staging meaningful.
Two compatibility traps worth naming in the plan: S3 bucket CORS must allow GET/HEAD from both admin and public origins or the inline player fails with an opaque media error, and signed URLs must not be cached in a Blade view or a Nova index column.
Sequencing. Five steps, strictly ordered:
www-data and the worker user (sudo -u www-data ffmpeg -version), upload limits are reconciled across all three layers, storage:link is idempotent in the deploy pipeline, and a media queue worker exists with timeouts longer than your worst-case transcode. Ninety percent of the documented failure modes collapse into this step.MediaResource. Nothing else consumes the package directly.media:reconcile --dry-run) and schedule it weekly before, not after, the orphaned-object bill arrives.Maintenance. This is the dominant ongoing cost and it is not the package's fault — it is the dependency chain's. Pin exact versions for the field, larupload, and laravel-video, and record the pin in an ADR. An unbounded ^1.0 constraint with a single maintainer and zero dependents is how an unrelated composer update breaks your admin panel during a sprint. Add "render the Nova video form" to the QA checklist for every Nova upgrade — the failure is a blank form, so nothing will alert you.
Support. The upside is real and is the reason to adopt: editors upload and swap videos without a deploy or a ticket, and you get a crisp acceptance criterion — "an editor can publish a video without engineering involvement." The cost is that a class of silent failures now surfaces as editor complaints rather than logs. Mitigate with visible job status in the field, alerting on queue:failed, and a short written playbook in the debugging order the docs already establish: (1) did the file land on disk? (2) did the job get picked up? (3) did it fail — queue:failed, laravel.log, FFmpeg stderr? (4) does the file exist but 404 — storage:link, visibility, CORS? That sequence resolves most incidents in minutes, but only if it is written down before the first incident.
Scaling. Two dimensions, both budgeted rather than engineered:
media queue and separate worker pool is mandatory — a 500 MB transcode must never compete with notification jobs. Set --tries=1 (retrying a deterministically broken input just burns CPU) and ensure retry_after exceeds the worker --timeout, or you get two FFmpeg processes racing on one file.Cache-Control at upload and reference a CDN host in the accessor from day one, so adding CloudFront later requires no model changes. Plan retention and deletion policy now — originals plus derived renditions is a multiplier, not a sum.Failure modes, ranked by observed likelihood:
| Failure | Signature | Mitigation |
|---|---|---|
FFmpeg not on runtime PATH |
Uploads fail, conversions error | Bake into image; boot-time health check |
| Upload size limits (php.ini / nginx / LB) | Large uploads fail silently, empty $_FILES |
Reconcile all three layers; log $_FILES['error'] during bring-up |
Queue timeout / retry_after mismatch |
Duplicate FFmpeg processes, corrupted output | --timeout per pool, retry_after > timeout, --tries=1 |
| Silent conversion failure | Playable video, missing poster/renditions | Status badge in field; alert on queue:failed; regenerate action |
| Stale poster on replace | Poster from a deleted asset | Explicitly regenerate cover on replace |
| Orphaned files on delete | Storage bill creeps | deleting hook + weekly reconcile, dry-run first |
Missing storage:link in deploy |
Works locally, 404s in prod | Idempotent deploy step |
| Ephemeral filesystem | Assets vanish on deploy | Mounted volume or object storage only |
Ramp-up. The 30-minute spike is genuinely 30 minutes for someone who knows Laravel. The deeper ramp is the larupload semantics — entity accessors, naming methods, meta keys, styles — which is where roughly 80% of the real behavior lives and where the docs point you to a second package. Budget an afternoon for the adapter plus a day of environment work, and treat the "1–3 sprints of undifferentiated engineering" the field replaces as the honest comparison. The break-even is fast; the ongoing obligation is what the plan must state explicitly, because "and then it streams to customers" is the default scope-drift path from a working admin upload field.
How can I help you explore Laravel packages today?