elegantly / laravel-media
A flexible media library for Laravel
Fund package maintenance!
Requires
- php: ^8.2
- illuminate/contracts: ^11.0||^12.0||^13.0
- maennchen/zipstream-php: ^3.1
- spatie/image: ^3.0.0
- spatie/laravel-package-tools: ^1.14.0
- spatie/pdf-to-image: ^3.1
- spatie/temporary-directory: ^2.4
- symfony/process: ^6.4||^7.0||^8.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.1.1
- orchestra/testbench: ^9.0.0||^10.0.0||^11.0
- pestphp/pest: ^4.0||^5.0
- pestphp/pest-plugin-arch: ^4.0||^5.0
- pestphp/pest-plugin-laravel: ^4.0||^5.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^1.1||^2.0
- phpstan/phpstan-phpunit: ^1.3||^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v5.13.1
- v5.13.0
- v5.12.0
- v5.11.1
- v5.11.0
- v5.10.2
- v5.10.1
- v5.10.0
- v5.9.10
- v5.9.9
- v5.9.8
- v5.9.7
- v5.9.6
- v5.9.5
- v5.9.4
- v5.9.3
- v5.9.2
- v5.9.1
- v5.9.0
- v5.8.0
- v5.7.3
- v5.7.2
- v5.7.1
- v5.7.0
- v5.6.0
- v5.5.1
- v5.5.0
- v5.4.5
- v5.4.4
- v5.4.3
- v5.4.2
- v5.4.1
- v5.4.0
- v5.3.10
- v5.3.9
- v5.3.8
- v5.3.7
- v5.3.6
- v5.3.5
- v5.3.4
- v5.3.3
- v5.3.2
- v5.3.1
- v5.3.0
- v5.2.0
- v5.1.0
- v5.0.0
- v4.x-dev
- v4.9.0
- v4.8.1
- v4.8.0
- v4.7.1
- v4.7.0
- v4.6.1
- v4.6.0
- v4.5.10
- v4.5.9
- v4.5.8
- v4.5.7
- v4.5.6
- v4.5.5
- v4.5.4
- v4.5.3
- v4.5.2
- v4.5.1
- v4.5.0
- v4.4.2
- v4.4.1
- v4.4.0
- v4.3.1
- v4.3.0
- v4.2.0
- v4.1.1
- v4.1.0
- v4.0.10
- v4.0.9
- v4.0.8
- v4.0.7
- v4.0.6
- v4.0.5
- v4.0.4
- v4.0.3
- v4.0.2
- v4.0.1
- v4.0.0
- v3.7.2
- v3.7.1
- v3.7.0
- v3.6.13
- v3.6.12
- v3.6.11
- v3.6.10
- v3.6.9
- v3.6.8
- v3.6.7
- v3.6.6
- v3.6.5
- v3.6.4
- v3.6.3
- v3.6.2
- v3.6.1
- v3.6.0
- v3.5.2
- v3.5.1
- v3.5.0
- v3.4.11
- v3.4.10
- v3.4.9
- 3.4.8
- v3.4.7
- v3.4.6
- v3.4.5
- v3.4.4
- v3.4.3
- v3.3.3
- v3.3.2
- v3.3.1
- v3.3.0
- v3.2.2
- v3.2.1
- v3.2.0
- v3.1.15
- v3.1.14
- v3.1.13
- v3.1.12
- v3.1.11
- v3.1.10
- v3.1.9
- v3.1.8
- v3.1.7
- v3.1.6
- v3.1.5
- v3.1.4
- v3.1.3
- v3.1.2
- v3.1.1
- v3.1.0
- v3.0.10
- v3.0.9
- v3.0.8
- v3.0.7
- v3.0.6
- v3.0.5
- v3.0.4
- v3.0.3
- v3.0.2
- v3.0.1
- v3.0.0
- v2.2.8
- v2.2.7
- v2.2.6
- v2.2.5
- v2.2.4
- v2.2.3
- v2.2.2
- v2.2.1
- v2.2.0
- v2.1.7
- v2.1.6
- v2.1.5
- v2.1.4
- v2.1.3
- v2.1.2
- v2.1.1
- v2.1.0
- v2.0.1
- v2.0.0
- v1.2.2
- v1.2.1
- v1.2.0
- v1.1.1
- v1.1.0
- v1.0.14
- v1.0.13
- v1.0.12
- v1.0.11
- v1.0.10
- v1.0.9
- v1.0.8
- v1.0.7
- v1.0.6
- v1.0.5
- v1.0.4
- v1.0.3
- v1.0.2
- v1.0.1
- v1.0.0
- v0.0.3
- v0.0.2
- v0.0.1
This package is auto-updated.
Last update: 2026-11-01 23:23:36 UTC
README
This package offers an extremely flexible media library, enabling you to store any type of file along with their conversions.
It provides advanced features such as:
- 🌐 Supports any filesystem solutions (local or cloud), such as S3, R2, Bunny.net, DO...
- ⚡ Supports any file conversion solutions (local or cloud), such as ffmpeg, Transloadit, Cloudflare, Coconut, and others.
- 🔄 Advanced nested media conversions
- 🚀 Rich metadata automatically extracted
- 🛠️ Highly flexible and customizable
I developed this package with the highest degree of flexibility possible and I have been using it in production for nearly two years, handling terabytes of files monthly.
Table of Contents
Requirements
- PHP 8.1+
- Laravel 11.0+
spatie/imagefor image conversionsspatie/pdf-to-imagefor PDF to image conversionsffmpegfor video/audio processing
Installation
You can install the package via composer:
composer require elegantly/laravel-media
You have to publish and run the migrations with:
php artisan vendor:publish --tag="media-migrations"
php artisan migrate
You can publish the config file with:
php artisan vendor:publish --tag="media-config"
This is the contents of the published config file:
use Elegantly\Media\Jobs\DeleteMediaFileJob; use Elegantly\Media\Models\Media; use Elegantly\Media\Models\MediaConversion; use Elegantly\Media\PathGenerators\UuidPathGenerator; use Elegantly\Media\UrlFormatters\DefaultUrlFormatter; return [ /** * The media model. * Define your own model here by extending \Elegantly\Media\Models\Media::class. */ 'model' => Media::class, /** * The MediaConversion model. * Define your own model here by extending \Elegantly\Media\Models\MediaConversion::class. */ 'media_conversion_model' => MediaConversion::class, /** * The path used to store temporary file copies for conversions. * This will be used with the storage_path() function. */ 'temporary_storage_path' => 'app/tmp/media', /** * The default disk used for storing files. */ 'disk' => env('MEDIA_DISK', env('FILESYSTEM_DISK', 'local')), /** * Determine if media should be deleted with the model * when using the HasMedia Trait. */ 'delete_media_with_model' => true, /** * Determine if media should be deleted with the model * when it is soft deleted. */ 'delete_media_with_trashed_model' => false, /** * Job class responsible for deleting each original, conversion, or additional file. * File cleanup is queued after the database transaction commits. */ 'delete_media_file_job' => DeleteMediaFileJob::class, /** * The default collection name assigned media. */ 'default_collection_name' => 'default', /** * The default URL formatter class. * Used when calling `$media->getUrl()`. */ 'default_url_formatter' => DefaultUrlFormatter::class, /** * The default path generator class. * Used when storing new files. */ 'default_path_generator' => UuidPathGenerator::class, /** * Prefix for the generated file path. * Set to null to disable the prefix. * Override the generateBasePath method in the Media model for full customization. */ 'generated_path_prefix' => null, /** * Queue connection name to use for media conversion and file deletion jobs. */ 'queue_connection' => env('QUEUE_CONNECTION', 'sync'), /** * Queue name to use for media conversion and file deletion jobs. * Set to null to use the default Laravel queue. */ 'queue' => null, /** * Configuration for FFmpeg processing. */ 'ffmpeg' => [ /** * The binary path to the FFmpeg executable. */ 'ffmpeg_binaries' => env('FFMPEG_BINARIES', 'ffmpeg'), /** * The binary path to the FFprobe executable. */ 'ffprobe_binaries' => env('FFPROBE_BINARIES', 'ffprobe'), /** * Optional log channel for FFmpeg operations. * Set to null to disable logging. */ 'log_channel' => null, /** * The timeout in seconds of FFmpeg and FFprobe processes. * Set to null to disable the timeout. */ 'timeout' => null, ], ];
Media and conversion records are deleted immediately; their files are deleted by DeleteMediaFileJob after the surrounding database transaction commits. With an asynchronous queue connection, run a worker for the configured queue to process cleanup. Failed file deletions throw an exception so the queue can retry them according to your worker settings.
When upgrading from delete_media_with_model_job, replace that config key with delete_media_file_job. Custom deletion jobs now receive string $disk and string $path instead of a Media model; extend DeleteMediaFileJob to retain after-commit cleanup and failure handling.
Optionally, you can publish the views using
php artisan vendor:publish --tag="media-views"
Basic Usage
Defining Media Collections
Media Collections define how media are stored, transformed, and processed for a specific model. They provide granular control over file handling, accepted types, and transformations.
To associate a media collection with a Model, start by adding the InteractWithMedia interface and the HasMedia trait.
Next, define your collections in the registerMediaCollections method, as shown below:
namespace App\Models; use Illuminate\Database\Eloquent\Model; use Elegantly\Media\Concerns\HasMedia; use Elegantly\Media\Contracts\InteractWithMedia; use Elegantly\Media\MediaCollection; class Channel extends Model implements InteractWithMedia { use HasMedia; public function registerMediaCollections(): array; { return [ new MediaCollection( name: 'avatar', single: true, // If true, only the latest file will be kept disk: 's3', // (optional) Specify where the file will be stored acceptedMimeTypes: [ // (optional) Specify accepted file types 'image/jpeg', 'image/png', 'image/webp' ] ) ]; } }
Defining Media Conversions
Media conversions create different variants of your media files. For example, a 720p version of a 1440p video or a WebP or PNG version of an image are common types of media conversions. Interestingly, a media conversion can also have its own additional conversions.
This package provides common converter to simplify your work:
MediaImageConverter: This converter optimizes, resizes, or converts any image usingspatie/image.MediaMp4Converter: This conversion optimizes, resizes, or converts any video or gif to mp4 video usingffmpeg.MediaWebmConverter: This conversion optimizes, resizes, or converts any video or gif to webm video usingffmpeg.MediaAv1Converter: This conversion optimizes, resizes, or converts any video or gif to AV1 video (in an mp4 or webm container) usingffmpeg.MediaHlsConverter: This conversion encodes any video into an adaptive HLS stream with several resolutions usingffmpeg. See Video Streaming (HLS).MediaWavConverter: This conversion optimizes, resizes, converts or extract any audio in wav format usingffmpeg.MediaMp3Converter: This conversion optimizes, resizes, converts or extract any audio in mp3 format usingffmpeg.MediaOpusConverter: This conversion optimizes, resizes, converts or extract any audio in opus format usingffmpeg.MediaFrameConverter: This conversion extracts a frame from a video usingffmpeg.MediaThumbnailConverter: This conversion smartly extracts the best thumbnail from a video usingffmpeg.MediaSpritesheetConverter: This conversion tiles frames of a video or gif into sprite sheets, with a WebVTT file for timeline previews, usingffmpeg. See Video Sprite Sheets.MediaPdfToImageConverter: This conversion extracts an image from the PDF usingspatie/pdf-to-image.
namespace App\Models; use Illuminate\Database\Eloquent\Model; use Elegantly\Media\Concerns\HasMedia; use Elegantly\Media\Contracts\InteractWithMedia; use Elegantly\Media\MediaCollection; use Elegantly\Media\MediaConversionDefinition; use Elegantly\Media\Converters\MediaMp4Converter; use Elegantly\Media\Converters\MediaFrameConverter; use Elegantly\Media\Converters\MediaImageConverter; class Channel extends Model implements InteractWithMedia { use HasMedia; public function registerMediaCollections(): array; { return [ new MediaCollection( name: 'videos', conversions: [ new MediaConversionDefinition( name: 'poster', converter: fn ($media) => new MediaFrameConverter( media: $media, filename: "{$media->name}-thumbnail.jpg", timecode: 0, ), conversions: [ new MediaConversionDefinition( name: '360p', converter: fn ($media) => new MediaImageConverter( media: $media, filename: "{$media->name}.jpg" width: 360 ) ), ], ), new MediaConversionDefinition( name: '720p', converter: fn ($media) => new MediaMp4Converter( media: $media, filename: "{$media->name}.mp4" width: 720 ) ), ] ) ]; } }
Adding Media
Add media to your model, using the addMedia method, from various sources:
- an url
- a resource or stream
- a
\Illuminate\Http\UploadedFileinstance - a
\Illuminate\Http\Fileinstance
From a Controller
use Elegantly\Media\Exceptions\InvalidMimeTypeException; public function store(Request $request, Channel $channel) { try { $channel->addMedia( file: $request->file('avatar'), collectionName: 'avatar', name: "{$channel->name}-avatar" ); } catch (InvalidMimeTypeException $exception){ // Will throw an error if the mime type is not included in the collection's `acceptedMimeTypes` parameter. } }
From a Livewire Component
use Livewire\WithFileUploads; use Elegantly\Media\Exceptions\InvalidMimeTypeException; use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; class ImageUploader extends Component { use WithFileUploads; /** @var ?TemporaryUploadedFile */ public $avatar = null; public function save() { try { $this->channel->addMedia( file: $this->avatar->getRealPath(), collectionName: 'avatar', name: "{$this->channel->name}-avatar" ); } catch (InvalidMimeTypeException $exception){ // Will throw an error if the mime type is not included in the collection's `acceptedMimeTypes` parameter. } } }
From an Url
use Elegantly\Media\Exceptions\InvalidMimeTypeException; try { $channel->addMedia( file: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", collectionName: 'videos', name: "BigBuckBunny" ); } catch (InvalidMimeTypeException $exception){ // Will throw an error if the mime type is not included in the collection's `acceptedMimeTypes` parameter. }
Retreiving Media
Retrieve media from your model:
// Get all media from a specific collection $avatars = $channel->getMedia('avatar'); // Get the first media from a collection $avatar = $channel->getFirstMedia('avatar'); // Check if media exists $hasAvatar = $channel->hasMedia('avatar');
Media properties
Each media item provides rich metadata automatically:
$media = $channel->getFirstMedia('avatar'); // File properties $media->name; // file_name without the extension $media->file_name; $media->extension; $media->mime_type; $media->size; // in bytes $media->humanReadableSize(); // Image/Video specific properties $media->width; // in pixels $media->height; // in pixels $media->aspect_ratio; $media->duration; // for video/audio
You can use dot notation to access either the root properties or a specific conversion:
// Get the original media URL $originalUrl = $media->getUrl(); // Get a specific conversion URL $thumbnailUrl = $media->getUrl( conversion: '360p', fallback: true // Falls back to original if conversion doesn't exist ); $posterUrl = $media->getUrl( conversion: 'poster.360p', fallback: 'poster' // Falls back to an other conversion if conversion doesn't exist ); // Use the same logic with other properties such as $media->getPath(); $media->getWidth(); // ...
Access Media Conversions
To directly access conversions, use:
// Check if a conversion exists $hasThumbnail = $media->hasConversion('100p'); // Get a specific conversion $thumbnailConversion = $media->getConversion('100p'); // Get the 'poster' conversion $media->getParentConversion('poster.360p'); // Only get children conversions of poster $media->getChildrenConversions('poster');
Blade components
The package also provides blade components.
<!-- fallback to the root media url if the conversion doesn't exist --> <!-- allows you to specify query parameters --> <x-media::img :media="$user->getFirstMedia('poster')" conversion="360p" fallback parameters="['foo'=>'bar']" alt="Video poster" />
<!-- fallback to the root media url if the conversion doesn't exist --> <!-- allows you to specify query parameters --> <x-media::video :media="$user->getFirstMedia('videos')" conversion="720p" fallback muted playsinline autoplay loop />
Advanced Usage
Transforming a file before storing it
You can define a transformation step that runs before a file is stored by using the transform method on a media collection.
This is useful if you want to resize, optimize, or process an uploaded file before it gets saved to disk.
For example, the snippet below shows how you can resize and optimize an uploaded image before storing it:
use Illuminate\Http\File; use Elegantly\Media\TemporaryDirectory; use Spatie\Image\Image; use Spatie\Image\Enums\Fit; new MediaCollection( name: 'avatar', acceptedMimeTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], transform: function (File $file, TemporaryDirectory $temporaryDirectory): File { $input = $file->getRealPath(); $output = $temporaryDirectory->path('avatar.jpg'); Image::load($input) ->fit(Fit::Contain, 500) ->optimize() ->save($output); return new File($output); } );
Async vs. Sync Conversions
When adding new media, its conversions can be either dispatched asynchronously or generated synchronously.
You can configure the strategy in the conversion definition using the queued and queue parameters:
new MediaCollection( name: 'avatar', conversions: [ new MediaConversionDefinition( name: '360p', queued: true, // (default) Dispatch as a background job queue: 'slow' // (optional) Specify a custom queue converter: fn ($media) => new MediaImageConverter( media: $media, filename: "{$media->name}.jpg" width: 360 ) ), new MediaConversionDefinition( name: '180p', queued: false, // Generate the conversion synchronously converter: fn ($media) => new MediaImageConverter( media: $media, filename: "{$media->name}.jpg" width: 180 ) ), ] )
Synchronous conversions can be particularly useful in specific use cases, such as generating a poster immediately upon upload.
Delayed Conversions
There are scenarios where you might want to define conversions that should not be generated immediately. For instance, if a conversion is resource-intensive or not always required, you can defer its generation to a later time.
To achieve this, configure the conversion with the immediate parameter set to false. This allows you to generate the conversion manually when needed:
new MediaCollection( name: 'avatar', conversions: [ new MediaConversionDefinition( name: '360p', immediate: false, // Conversion will not be generated at upload time converter: fn ($media) => new MediaImageConverter( media: $media, filename: "{$media->name}.jpg" width: 360 ) ), ] )
To generate the conversion later, you can use the following methods:
// Generate the conversion synchronously $media->executeConversion( conversion: '360', force: false, // Skips execution if the conversion already exists withChildren: true, // Generate children conversion withForceChildren: true, // Force children conversion to be re-generated ); // Dispatch the conversion as a background job $media->dispatchConversion( conversion: '360', force: false, // Skips execution if the conversion already exists withChildren: true, // Generate children conversion withForceChildren: true, // Force children conversion to be re-generated );
onAdded MediaCollection Callback
The onAdded callback allows you to define custom logic that will be executed whenever new media is added to your collection.
To use it, simply set the onAdded parameter when defining a MediaCollection. For example:
new MediaCollection( name: 'avatar', onAdded: function ($media) { // Example: Notify the model when new media is added // $media->model->notify(new MediaAddedNotification($media)); } );
With this, you can easily hook into the media addition process and trigger actions like sending notifications, logging, or other custom behavior.
Tip
The same behavior can be achieved by listening to Elegantly\Media\Events\MediaAddedEvent.
onCompleted MediaConversionDefinition Callback
The onCompleted callback allows you to define custom logic that will be executed whenever a new conversion is generated.
To use it, simply set the onCompleted parameter when defining a MediaConversionDefinition. For example:
new MediaConversionDefinition( name: '360', onCompleted: function ($conversion, $media, $parent) { // Example: Refresh your UI // broadcast(new MyEvent($media)); } );
This allows you to hook into the conversion process and execute additional logic, such as updating your UI or triggering other actions.
Tip
The same behavior can be achieved by listening to Elegantly\Media\Events\MediaConversionAddedEvent.
Regenerating Child Conversions When a Parent Runs
In some cases, you may want all child conversions of a given conversion to be regenerated whenever the parent conversion is re-executed.
There are two ways to achieve this, depending on where you handle conversions:
From the conversion definition
You can hook into the onCompleted callback of a conversion definition and instruct Laravel to delete all child conversions when the parent finishes.
new MediaConversionDefinition( name: '360', onCompleted: function (?MediaConversion $conversion, Media $media, ?MediaConversion $parent) { if ($conversion) { // Delete any children so they’ll be regenerated automatically $media->deleteChildrenConversions($conversion->conversion_name); } } );
From addConversion
When adding a new conversion programmatically, you can explicitly tell it to remove children and regenerate them automatically:
$mediaConversion = $media->addConversion( file: $file, deleteChildren: true, // ensures child conversions are refreshed ); // regenerate children conversion $this->media->generateConversions( parent: $mediaConversion, );
Custom Conversions
Conversions can be anything: a variant of a file, a transcription of a video, a completely new file, or even just a string.
You can use built-in presets or define your own custom converter. To create a custom converter, start by creating a new class extending the MediaConverter class:
namespace App\Media\Converters\Image; use Elegantly\Media\Converters\MediaConverter; use Elegantly\Media\Enums\MediaType; use Elegantly\Media\Models\Media; use Elegantly\Media\Models\MediaConversion; use Illuminate\Contracts\Filesystem\Filesystem; use Spatie\Image\Enums\Fit; use Spatie\Image\Image; use Spatie\ImageOptimizer\OptimizerChain; use Elegantly\Media\TemporaryDirectory; class MediaImageConverter extends MediaConverter { public function __construct( Media $media, public string $filename, public ?int $width = null, public ?int $height = null, public Fit $fit = Fit::Max, public ?OptimizerChain $optimizerChain = null, ) { parent::__construct($media); } public function canExecute(Media $media, ?MediaConversion $parent): bool { $source = $parent ?? $media; return $source->type === MediaType::Image; } public function convert( Media $media, ?MediaConversion $parent, ?string $file, Filesystem $filesystem, TemporaryDirectory $temporaryDirectory ): ?MediaConversion { if (! $file) { return $this->unsupportedConversion(); } $input = $filesystem->path($file); $output = $filesystem->path($this->filename); Image::load($input) ->fit($this->fit, $this->width, $this->height) ->optimize($this->optimizerChain) ->save($output); return $media->addConversion( file: $output, conversionName: $this->conversion, parent: $parent, ); } }
The convert method is where the logic for the conversion is implemented. It provides the following parameters:
$media: The Media model.$parent: The MediaConversion model, if the conversion is nested.$file: A local copy of the file associated with either$mediaor$parent.$filesystem: An instance of the local filesystem where the file copy is stored.$temporaryDirectory: An instance ofTemporaryDirectorywhere the file copy is temporarily stored.
You don’t need to worry about cleaning up the files, as the $temporaryDirectory will be deleted automatically when the process completes or fails.
To finalize the conversion, ensure you save it by calling $media->addConversion or $media->replaceConversion at the end of the handle method.
Manually Generate Conversions
You can manage your media conversions programmatically using the following methods:
// Store a new file as a conversion $media->addConversion( file: $file, // Can be an HTTP File, URL, or file path conversionName: 'transcript', parent: $mediaConversion // (Optional) Specify a parent conversion // Additional parameters... ); // Replace an existing conversion safely // If the same conversion already exists, it ensures the new file is stored before deleting the previous one. $media->replaceConversion( conversion: $mediaConversion ); // Safely delete a specific conversion and all its children $media->deleteConversion('360'); // Safely delete only the child conversions of a parent conversion $media->deleteChildrenConversions('poster'); // Dispatch or execute a conversion $media->dispatchConversion('360'); // Runs asynchronously as a job $media->executeConversion('poster.360'); // Executes synchronously $media->getOrExecuteConversion('poster.360'); // Retrieves or generates the conversion // Retrieve conversion information $media->getConversion('360'); // Fetch a specific conversion $media->hasConversion('360'); // Check if a conversion exists $media->getParentConversion('poster.360'); // Retrieve the parent (poster) of a conversion $media->getChildrenConversions('poster'); // Retrieve child conversions
Additionally, you can use an Artisan command to generate conversions with various options:
php artisan media:generate-conversions
You can also use the following command to retry failed conversions:
php artisan media-conversions:retry
To regenerate existing conversions, filtered by name, state and creation date (--with-force-children also re-generates their existing children):
php artisan media-conversions:regen --conversions=poster --states=succeeded --after=2026-01-01 --before=2026-06-01 --with-force-children
This provides a convenient way to process conversions in bulk or automate them within your workflows.
Format Media URLs
Some cloud providers like Cloudflare, Bunny, or ImageKit allow you to create instant transformations of your images and videos using specially formatted URLs.
This package gives you a simple way to format your URLs so you can take advantage of these services.
When using the $media->getUrl() method, you can specify two parameters:
parameters: An array of valuesformatter: The class name of the formatter you want to use
By combining these parameters, you can retrieve formatted URLs like this:
use \Elegantly\Media\UrlFormatters\CloudflareImageUrlFormatter; // Default formatter (query parameters) $default = $media->getUrl( parameters: ['width' => 360], ); // https://your-url.com?width=360 // Cloudflare formatter (path-based format) $cloudflare = $media->getUrl( parameters: ['width' => 360], formatter: CloudflareImageUrlFormatter::class ); // /cdn-cgi/media/width=360/https://your-url.com
This package comes with 3 formatters out of the box:
\Elegantly\Media\UrlFormatters\DefaultUrlFormatter\Elegantly\Media\UrlFormatters\CloudflareImageUrlFormatter\Elegantly\Media\UrlFormatters\CloudflareVideoUrlFormatter
Feel free to implement your own formatter by extending \Elegantly\Media\UrlFormatters\AbstractUrlFormatter.
Conversions Presets
Image Placeholder
When loading images on the web, it’s common to show a low-resolution blurred placeholder first, then swap it with the full-resolution image once it’s ready. This improves perceived performance, reduces layout shifts, and gives a polished feel to your UI.
This package includes a ready-to-use preset for generating these placeholders in Base64-encoded format. The placeholder is tiny in size, quick to load, and designed to be displayed while the actual image is being fetched.
Defining the Placeholder Conversion
To create a placeholder, simply define a media conversion using the MediaImagePlaceholderConverter:
namespace App\Models; use Illuminate\Database\Eloquent\Model; use Elegantly\Media\Concerns\HasMedia; use Elegantly\Media\Contracts\InteractWithMedia; use Elegantly\Media\MediaCollection; use Elegantly\Media\MediaConversionDefinition; use Elegantly\Media\Converters\MediaImagePlaceholderConverter; class User extends Model implements InteractWithMedia { use HasMedia; public function registerMediaCollections(): array; { return [ new MediaCollection( name: 'avatar', single: true, conversions: [ new MediaConversionDefinition( name: 'placeholder', converter: fn ($media) => new MediaImagePlaceholderConverter($media), ), ] ) ]; } }
Using the Placeholder in Your Views
If you’re using the built-in x-media::img Blade component, you can directly specify the placeholder conversion name:
<x-media::img :media="$user->getFirstMedia('avatar')" placeholder /> <x-media::img :media="$user->getFirstMedia('avatar')" placeholder="custom-placeholder-name" />
This will automatically load the blurred placeholder as a background-image.
Retrieving the Base64 Value Directly
If you need to manually work with the placeholder (e.g., for API responses or inline styles), you can retrieve it like this:
$placeholder = $media->getConversion('placeholder'); $base64Image = $placeholder?->contents;
Video Streaming (HLS)
HLS (HTTP Live Streaming) splits a video into short segments encoded at several resolutions. Players start quickly, then switch to the resolution the connection allows.
The MediaHlsConverter encodes a variant for each rendition the source fills, without upscaling, and writes the master playlist listing them with the attributes Apple devices require: the peak and average bandwidths, the codecs, the resolution and the frame rate.
Defining the HLS Conversion
use Elegantly\Media\Converters\Video\MediaHlsConverter; new MediaCollection( name: 'videos', conversions: [ new MediaConversionDefinition( name: 'hls', converter: fn ($media) => new MediaHlsConverter( media: $media, renditions: [360, 720, 1080], ), ), ] );
| Parameter | Default | Description |
|---|---|---|
renditions |
null |
The sizes of the renditions to generate, among media.videos.hls.renditions. null generates all of them. |
startingRendition |
720 |
The size of the rendition players start with, listed first in the master playlist. |
segmentLength |
6 |
The duration of a segment, in seconds. |
fps |
null |
The maximum frame rate, never raising the frame rate of the source. |
crf, preset |
23, slow |
The quality targeted by x264, and its compression speed. |
playlist |
master.m3u8 |
The file name of the master playlist. |
Stored Files and Variants
The conversion file is the master playlist. The media playlists and segments of each variant are stored as additional files next to it, as the playlists reference them by their file name. The variants are saved in the conversion metadata, in the order of the master playlist:
$hls = $media->getConversion('hls'); $hls->getUrl(); // The master playlist $hls->metadata; // [ // 'variants' => [ // [ // 'name' => '720p', // 'playlist' => '720p_playlist.m3u8', // The media playlist // 'width' => 1280, // 'height' => 720, // 'frameRate' => 30.0, // 'bandwidth' => 2287548, // The peak bit rate, in bits per second // 'averageBandwidth' => 1946342, // The average bit rate, in bits per second // 'codecs' => 'avc1.64001f,mp4a.40.2', // ], // // ... // ], // ]
Playing the Stream
Safari plays HLS natively. Other browsers play it through hls.js, which most video players are built on:
<video id="video" controls></video> <script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script> <script> const video = document.getElementById('video'); const url = @js($media->getUrl(conversion: 'hls')); if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = url; } else if (Hls.isSupported()) { const hls = new Hls(); hls.loadSource(url); hls.attachMedia(video); } </script>
Video Sprite Sheets
A sprite sheet is a single image tiling many frames of a video in a grid. It powers the previews shown when hovering the timeline of a video player, or when scrubbing a video card with the mouse, with a single request.
The MediaSpritesheetConverter extracts frames at a regular interval, tiles them into one or several sheets, and writes the WebVTT file video players read timeline thumbnails from. Like an HLS playlist, the WebVTT file is the conversion file and references the sheets stored next to it.
Defining the Sprite Sheet Conversion
use Elegantly\Media\Converters\Video\MediaSpritesheetConverter; new MediaCollection( name: 'videos', conversions: [ // 100 frames spread evenly over the video, in a single 10x10 sheet new MediaConversionDefinition( name: 'sprites', converter: fn ($media) => new MediaSpritesheetConverter( media: $media, filename: 'sprites.vtt', ), ), // A frame every 2 seconds, in as many 10x10 sheets as needed new MediaConversionDefinition( name: 'timeline', converter: fn ($media) => new MediaSpritesheetConverter( media: $media, filename: 'timeline.vtt', format: 'webp', interval: 2, width: 240, ), ), ] );
| Parameter | Default | Description |
|---|---|---|
filename |
The WebVTT file, also naming the sheets (sprites_0.jpg, sprites_1.jpg... for sprites.vtt). Use a .vtt extension. |
|
format |
jpg |
The image format of the sheets: jpg, webp or png. |
interval |
null |
The time between two frames in seconds. null spreads the frames evenly over the whole video. |
frames |
null |
The maximum number of frames, the interval being widened on long videos. Defaults to a single sheet (columns × rows) without interval, and to no limit otherwise. |
width, height |
160, null |
The maximum size of a frame, keeping the aspect ratio. Frames are never upscaled. |
columns, rows |
10, 10 |
The grid of a sheet, the following frames going to a new sheet. The grid shrinks when the frames don't fill a single sheet. |
Stored Files and Layout
Like an HLS playlist, the conversion file is the WebVTT file. The sheets are stored as additional files next to it, and the layout is saved in the conversion metadata:
$sprites = $media->getConversion('sprites'); $sprites->getUrl(); // The WebVTT file $sprites->metadata; // [ // 'frames' => 100, // The number of frames // 'interval' => 1.2, // The time between two frames, in seconds // 'columns' => 10, // The number of frames per row // 'rows' => 10, // The number of rows per sheet // 'width' => 160, // The width of a frame // 'height' => 90, // The height of a frame // 'sheets' => ['sprites_0.jpg'], // ]
Frame n shows the video at n × interval seconds. It is in the sheet floor(n / (columns × rows)), at x = (n % columns) × width and y = floor(n % (columns × rows) / columns) × height.
Timeline Previews in Video Players
Most video players (Vidstack, Video.js, Plyr, Media Chrome...) display timeline thumbnails from a WebVTT file. Each cue references its frame with a media fragment (e.g. sprites_0.jpg#xywh=160,0,160,90), relative to the WebVTT file, so give the player its URL:
$url = $media->getUrl(conversion: 'timeline');
Hover Previews
To scrub a video card with the mouse, the metadata is enough to display the right frame of a single sheet:
@php $sprites = $media->getConversion('sprites'); $sheet = $sprites->additional_files->first(); ['frames' => $frames, 'columns' => $columns, 'width' => $width, 'height' => $height] = $sprites->metadata; @endphp <div x-data="{ frame: 0 }" x-on:mousemove="frame = Math.min({{ $frames - 1 }}, Math.floor($event.offsetX / $el.clientWidth * {{ $frames }}))" x-bind:style="{ backgroundPosition: `-${frame % {{ $columns }} * {{ $width }}}px -${Math.floor(frame / {{ $columns }}) * {{ $height }}}px` }" style="width: {{ $width }}px; height: {{ $height }}px; background-image: url('{{ $sheet->getDisk()->url($sheet->path) }}')" ></div>
In JavaScript, the sheets can also be resolved from the URL of the WebVTT file: new URL(metadata.sheets[0], url).
Customization
Custom Media Model
You can define your own Media model to use with the library.
First, create your own model class:
namespace App\Models; use Elegantly\Media\Models\Media as ElegantlyMedia; class Media extends ElegantlyMedia { // ... }
Then, update the config file:
use App\Models\Media; return [ 'model' => Media::class, // ... ];
The library is typed with generics, so you can use your own Media model seamlessly:
namespace App\Models; use App\Models\Media; use Elegantly\Media\Concerns\HasMedia; use Elegantly\Media\Contracts\InteractWithMedia; /** * @implements InteractWithMedia<Media> */ class Post extends Model implements InteractWithMedia { /** @use HasMedia<Media> **/ use HasMedia; // ... }
Troubleshooting
Ghostscript and Imagick Issues
This package relies on the spatie/pdf-to-image library, which uses Ghostscript via Imagick to convert PDFs into images.
If you encounter errors while generating images from PDFs, such as:
attempt to perform an operation not allowed by the security policy 'PDF'Uncaught ImagickException: FailedToExecuteCommand 'gs'
these issues are likely related to the configuration of Ghostscript or Imagick on your system.
For detailed guidance on resolving these errors, refer to the spatie/pdf-to-image documentation on Ghostscript issues.
Testing
composer test
Changelog
Please see the CHANGELOG for more information on recent changes.
Contributing
Feel free to open an issue or a discussion.
Security Vulnerabilities
Please contact me to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see the License File for more information.