Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Video Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Verify the runtime before you write a single line of resource code. This package is a thin Nova field over mostafaznv/larupload (storage, metadata, naming) and mostafaznv/laravel-video (FFmpeg conversions). Almost every "the package is broken" report is actually a missing binary or a too-small post_max_size. Confirm as the runtime user, not your shell:

sudo -u www-data which ffmpeg && sudo -u www-data ffmpeg -version
sudo -u www-data php -i | grep -E 'upload_max_filesize|post_max_size|memory_limit'

Check the same for the queue worker user. Both need ffmpeg/ffprobe on PATH.

Install and publish

composer require mostafaznv/nova-video
php artisan vendor:publish            # interactive first time; note the exact tags
php artisan migrate                   # larupload tables
php artisan storage:link
php artisan nova:publish

Publish interactively once, then hard-code the resulting tags into your deploy script — tag names have moved between larupload versions.

Model + migration. Add the larupload trait and the attachment column per larupload's docs. Without the trait the Nova field will render but nothing will resolve ($model->video returns null and you'll waste an afternoon).

Field registration — the smallest thing that works

use Mostafaznv\NovaVideo\Video;

public function fields(Request $request): array
{
    return [
        Video::make('Video')
            ->rules('file', 'max:512000')  // KB → 500 MB
            ->disk('videos')
            ->hideFromIndex(),
    ];
}

Where to look first, in priority order

  1. config/larupload.php (and any laravel-video config) — disk, path convention, naming strategy, conversions, queue. This is where the real behavior lives.
  2. mostafaznv/larupload docs — entity accessors (url(), meta()), styles, cleanup hooks. 80% of the semantics you'll actually use.
  3. mostafaznv/laravel-video docs — conversion presets and formats.
  4. The Nova field class itself — usually just a view + validation wrapper.

Your 30-minute spike: pick an existing resource, add the field, upload a 20 MB MP4 through the Nova form, confirm the inline player and the auto-generated cover, then inspect the disk to see exactly what was written (original, poster, any renditions, metadata row). Do this in a staging container that matches production, not on your laptop — that single loop validates FFmpeg, the queue, the disk, and your upload limits at once.

Implementation Patterns

Field dumb, model smart. Keep Video::make() to presentational concerns: label, help, rules, hideFromIndex(), onlyOnDetail(), readonly(). Storage paths, naming, conversions, and cover policy belong on the model or in config so seeders, imports, artisan commands, and API uploads produce identical output. If your video semantics only exist inside a Nova field, they're untestable.

Two field instances, one resource. The standard edit-heavy admin screen:

Video::make('Video')
    ->rules('file', 'max:512000')
    ->onlyOnForms(),

Video::make('Video')
    ->onlyOnDetail()
    ->readonly(true),

Form instance carries validation and the uploader; detail instance is playback + poster. readonly() on the detail view prevents an accidental re-upload from a display context.

Read paths go through larupload accessors, never raw columns.

$product->video?->url();          // primary file
$product->video?->url('cover');   // generated poster
$product->video?->meta('duration');

Always null-safe — the attribute is absent on rows that never had an upload. Funnel these through a presenter so a future swap to Mux/Cloudflare is a one-file change:

class ProductResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'video' => [
                'src'    => $this->video?->url(),
                'poster' => $this->video?->url('cover'),
            ],
        ];
    }
}

Define conversions once, run them on a dedicated queue. Configure resolutions/formats in the video config, not ad hoc in a controller or an observer. Give media its own queue name and a separate worker pool — a 500 MB transcode should never compete with notification jobs. Keep the original and derive the rest; never let Nova playback depend on a rendition that hasn't finished.

Covers: prefer the auto-extracted poster frame, allow a manual override upload, and explicitly regenerate on replace. Otherwise you ship a poster from a deleted asset.

Validation in both places, deliberately. Nova ->rules() gives editors instant feedback but protects nothing outside Nova. Mirror the size/MIME rules in a form request on any API or import path.

Deletion and cleanup. Nova's delete removes the row; derived artifacts may not follow. Handle it in the model's deleting event (or larupload's cleanup hooks), and add a reconciliation command:

php artisan media:reconcile --dry-run
# reports: DB records with missing files, files with no DB record

Schedule it weekly. Cheapest possible insurance against an orphaned-object bill.

Local/S3 parity. local disk in dev, S3 in prod — but keep the path convention identical so URLs, tests, and seeders behave the same. For S3 set visibility and CORS at the bucket level, and be explicit whether the browser gets a public URL, a CDN URL, or a signed URL. That decision shows up on your egress bill and in how cacheable your admin is.

Testing

Storage::fake('videos');
Queue::fake();
// flip a config flag so tests never shell out to ffmpeg

Assert the record exists and the job dispatched. Then keep one small integration test that actually runs FFmpeg against a generated 2-second clip (ffmpeg -f lavfi -i testsrc...) so a broken binary fails CI before staging does.

Wrap it. A VideoPresenter / MediaService between your domain and the package is the highest-value architecture decision here. An afternoon of work that turns "we're moving to a managed video platform" into a contained adapter swap instead of a grep-and-pray.

Gotchas and Tips

"FFmpeg not found" is the #1 report and it's almost never the package. The binary is in your shell's PATH but not in PHP-FPM's or the worker's. Bake it into the image, verify as www-data, and add a boot-time health check rather than discovering it on a customer upload.

Upload size is a three-layer problem. php.ini (upload_max_filesize, post_max_size, memory_limit, max_execution_time); web server (client_max_body_size in nginx, LimitRequestBody in Apache); and any upstream proxy/LB body limit. Symptom of a blown post_max_size is a silent failure — PHP hands you an empty $_FILES. Log $_FILES['error'] during bring-up.

Queue timeouts will bite. A 4-minute transcode dies against a 60-second worker timeout. Set --timeout per pool, keep retry_after in config/queue.php above it (otherwise two FFmpeg processes chew the same file), and use --tries=1 for transcode jobs — retrying a deterministically bad input just burns CPU.

Conversion failures are quiet. A failed job leaves a playable video with no poster or no renditions. Surface status next to the field (a Badge/Status column), alert on queue:failed, and give editors an explicit "regenerate" action rather than an FAQ entry.

storage:link missing from the deploy pipeline is the classic: perfect locally, every asset 404s in production. Make it idempotent and put it in the deploy script.

S3 specifics. Bucket CORS must allow GET/HEAD from both admin and public origins or the inline player fails with an opaque media error. Signed URLs expire — never cache one in a Blade view or a Nova index column. Set Cache-Control on upload and return the CDN host from your accessor so adding CloudFront later doesn't touch models.

Replacing a video doesn't replace its cover by default. Regenerate on replace or delete the stale poster explicitly.

Ephemeral filesystems. Heroku/Render/Fly-style containers are wiped on deploy. Anything written to storage/app outside a mounted volume is gone — including the poster frame.

Naming collisions. Larupload exposes naming strategies; set one deliberately. Two files called video.mp4 in the same folder will surprise you during a bulk import.

Pin the exact version and record it in your ADR. Single maintainer, near-zero downstream adoption, and a Nova-major-coupled frontend mean an unbounded ^1.0 constraint is how a routine composer update breaks your admin panel. Re-test that the field renders after every Nova upgrade — a broken field usually looks like a blank form, not an exception.

Debugging order that resolves most issues in minutes: (1) did the file land on disk/bucket? (2) did the job hit the queue and get picked up? (3) did it fail — queue:failed, laravel.log, FFmpeg stderr in the exception? (4) does the derived file exist but the URL 404 — storage:link, visibility, CORS?

Extension point worth knowing: because the field sits on larupload, any custom UI or non-Nova upload path should go through larupload's storage API rather than reimplementing file moves. That keeps metadata, naming, and cleanup consistent — and keeps a future package swap to a single adapter replacement.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums