Skip to content

An Introduction to Laravel Queues and Temporary URLs

An Introduction to Laravel Queues and Temporary URLs

Laravel is a mature, robust, and powerful web framework that makes developing PHP applications a breeze. In particular, I want to demonstrate how to create a website that can be used to convert videos online using queue jobs for processing and temporary URLs for downloading the converted files.

This article is aimed at those who aren’t very familiar with Laravel yet.

Prerequisites

There are many ways to set up Laravel, and which is the best method may depend on your operating system or preference. I have found Laravel Herd to be very easy to use if you’re using Windows or macOS. Herd is a Laravel development environment that has everything you need with minimal configuration required. Command-line tools are installed and added to your path, and background services are configured automatically.

If you’re developing on Linux then Herd is not an option. However, Laravel Sail works for all major operating systems and uses a Docker based environment instead. You can find a full list of supported installation methods in the Laravel documentation. To keep things simpl,e this article assumes the use of Herd, though this won’t make a difference when it comes to implementation.

You will also need a text editor or IDE that has good PHP support. PhpStorm is a great editor that works great with Laravel, but you can also use VSCode with the Phpactor language server, and I’ve found Phpactor to work quite well.

Project Setup

With a development environment setup, you can create a new Laravel project using composer, which is the most popular package manager for PHP. Herd installs composer for you. composer installs dependencies and lets you run scripts. Let’s create a Laravel project using it:

composer create-project laravel/laravel laravel-video-converter

Once that is done you can navigate into the project directory and start the server with artisan:

php artisan serve

Awesome! You can now navigate to http://localhost:8000/ and see the Laravel starter application’s welcome page. Artisan is the command-line interface for Laravel. It comes with other utilities as well such as a database migration tool, scripts for generating classes, and other useful things.

Uploading Videos Using Livewire

Livewire is a library that allows you to add dynamic functionality to your Laravel application without having to add a frontend framework. For this guide we’ll be using Livewire to upload files to our server and update the status of the video conversion without requiring any page reloads.

Livewire can be installed with composer like so.

composer require livewire/livewire

With it installed we need to make a Livewire component now. This component will act as the controller of our video upload page.

php artisan make:livewire video-uploader

With that done you should see two new files were created according to the output of the command, one being a PHP file and the other being a Blade file. Laravel has its own HTML template syntax for views that allow you to make your pages render dynamically.

For this demo we’ll make the video conversion page render at the root of the site. You can do this by going to routes/web.php and editing the root route definition to point to our new component.

<?php

use App\Livewire\VideoUploader;
use Illuminate\Support\Facades\Route;

Route::get('/', VideoUploader::class);

However, if we visit our website now it will return an error. This is due to the app template being missing, which is the view that encapsulates all page components and contains elements such as the document head, header, footer, etc.

Create a file at resources/views/components/layouts/app.blade.php and put the following contents inside. This will give you a basic layout that we can render our page component inside of.

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
	<head>
    	<meta charset="utf-8">
    	<meta name="viewport" content="width=device-width, initial-scale=1">
    	<title>WebM Video Converter</title>

    	<style>
        	html {
            	font-family: sans-serif;
            	background-color: #eaeaea;
        	}

        	main {
            	max-width: 1000px;
            	margin: 100px auto 0 auto;
            	padding: 32px;
            	border-radius: 24px;
            	background-color: white;
        	}

        	h1 {
            	margin-top: 0;
        	}

        	a {
            	text-decoration: none;
        	}
    	</style>
	</head>
	<body>
    	<main>
        	{{ $slot }}

        	<footer>
            	Laravel v{{ Illuminate\Foundation\Application::VERSION }} (PHP v{{ PHP_VERSION }})
        	</footer>
    	</main>
	</body>
</html>

The {{ $slot }} string in the main tag is a Blade echo statement. That is where our Livewire component will be injected when loading it.

Now, let’s edit the Livewire component’s template so it has something meaningful in it that will allow us to verify that it renders correctly. Edit resources/views/livewire/video-uploader.blade.php and put in the following:

<div>
	<h1>Hello, Laravel!</h1>
</div>

With that done you can go to the root of the site and see this hello message rendered inside of a box. Seeing that means everything is working as it should. We may as well delete the welcome template since we’re not using it anymore. This file is located at resources/views/welcome.blade.php.

Now, let’s go ahead and add uploading functionality. For now we’ll just upload the file into storage and do nothing with it. Go ahead and edit app/Livewire/VideoUploader.php with the following:

<?php

namespace App\Livewire;

use Illuminate\Contracts\View\View;
use Livewire\Attributes\Validate;
use Livewire\Component;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Livewire\WithFileUploads;

class VideoUploader extends Component
{
	use WithFileUploads;

	/**
 	* @var TemporaryUploadedFile
 	*/
	#[Validate('mimetypes:video/avi,video/mpeg,video/quicktime')]
	public $video;

	public function save(): void
	{
    	$videoFilename = $this->video->store();
	}

	public function render(): View
	{
    	return view('livewire.video-uploader');
	}
}

This will only allow uploading files with video file MIME types. The $video class variable can be wired inside of the component’s blade template using a form.

Create a form in resources/views/livewire/video-uploader.blade.php like so:

<div>
	<h1>WebM Video Converter</h1>

	<form wire:submit="save">
    	<input type="file" wire:model="video">

    	@error('video') <span class="error">{{ $message }}</span> @enderror

    	<button type="submit">Convert Video</button>
	</form>
</div>

You will note a wire:submit attribute attached to the form. This will prevent the form submission from reloading the page and will result in Livewire calling the component’s save method using the video as a parameter. The $video property is wired with wire:model="video".

Now you can upload videos, and they will be stored into persistent storage in the storage/app/private directory. Awesome!

Increase the Filesize Limit

If you tried to upload a larger video you may have gotten an error. This is because the default upload size limit enforced by Livewire and PHP is very small. We can adjust these to accommodate our use-case.

Let’s start with adjusting the Livewire limit. To do that, we need to generate a configuration file for Livewire.

php artisan livewire:publish --config

All values in the generated file are the defaults we have been using already. Now edit config/livewire.php and make sure the temporary_file_upload looks like this:

...

'temporary_file_upload' => [
	'disk' => null,    	// Example: 'local', 's3'          	| Default: 'default'
	'rules' => null,   	// Example: ['file', 'mimes:png,jpg']  | Default: ['required', 'file', 'max:12288'] (12MB)
	'directory' => null,   // Example: 'tmp'                  	| Default: 'livewire-tmp'
	'middleware' => null,  // Example: 'throttle:5,1'         	| Default: 'throttle:60,1'
	'preview_mimes' => [   // Supported file types for temporary pre-signed file URLs...
    	'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
    	'mov', 'avi', 'wmv', 'mp3', 'm4a',
    	'jpg', 'jpeg', 'mpga', 'webp', 'wma',
	],
	'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated...
	'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs...
	'rules' => 'max:102400', // NEW: Override default so we can upload long videos.
],

...

The rules key allows us to change the maximum file size, which in this case is 100 megabytes.

This alone isn’t good enough though as the PHP runtime also has a limit of its own. We can configure this by editing the php.ini file. Since this article assumes the use of Herd, I will show how that is done with it.

Go to Herd > Settings > PHP > Max File Upload Size > and set it to 100. Once done you need to stop all Herd services in order for the changes to take effect. Also make sure to close any background PHP processes with task manager in-case any are lingering, as this happened with me. Once you’ve confirmed everything is shut off, turn on all the services again.

If you’re not using Herd, you can add the following keys to your php.ini file to get the same effect:

upload_max_filesize=100M
post_max_size=100M

Creating a Background Job

Now, let’s get to the more interesting part that is creating a background job to run on an asynchronous queue. First off, we need a library that will allow us to convert videos. We’ll be using php-ffmpeg. It should be noted that FFmpeg needs to be installed and accessible in the system path. There are instructions on their website that tell you how to install it for all major platforms. On macOS this is automatic if you install it with homebrew. On Windows you can use winget.

On macOS and Linux you can confirm that ffmpeg is in your path like so:

which ffmpeg

If a file path to ffmpeg is returned then it’s installed correctly. Now with FFmpeg installed you can install the PHP library adapter with composer like so:

composer require php-ffmpeg/php-ffmpeg

Now that we have everything we need to convert videos, let’s make a job class that will use it:

php artisan make:job ProcessVideo

Edit app/Jobs/ProcessVideo.php and add the following:

<?php

namespace App\Jobs;

use FFMpeg\FFMpeg;
use FFMpeg\Format\Video\WebM;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\File;

class ProcessVideo implements ShouldQueue
{
	use Queueable;

	private string $videoPath;

	private string $outputPath;

	public function __construct(string $videoPath, string $outputPath)
	{
    	$this->videoPath = $videoPath;
    	$this->outputPath = $outputPath;
	}

	public function handle(): void
	{
    	$tempOutputPath = "{$this->outputPath}.tmp";

    	// Convert the video to the WebM container format (SLOW).
    	$video = FFMpeg::create()->open($this->videoPath);
    	$video->save(new WebM, $tempOutputPath);
    	File::move($tempOutputPath, $this->outputPath);
	}
}

To create a job we need to make a class that implements the ShouldQueue interface and uses the Queueable trait. The handle method is called when the job is executed. Converting videos with php-ffmpeg is done by passing in an input video path and calling the save method on the returned object. In this case we’re going to convert videos to the WebM container format. Additional options can be specified here as well, but for this example we’ll keep things simple.

One important thing to note with this implementation is that the converted video is moved to a file path known by the livewire component. Later and to keep things simple we’re going to modify the component to check this file path until the file appears, and while for demo purposes this is fine, in an app deployed at a larger scale with multiple instances it is not. In that scenario it would be better to write to a cache like Redis instead with a URL to the file (if uploaded to something like S3) that can be checked instead.

Now let’s use this job! Edit app/Livewire/VideoUploader.php and let’s add some new properties and expand on our save method.

use App\Jobs\ProcessVideo;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Storage;
use Livewire\Attributes\Validate;
use Livewire\Component;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Livewire\WithFileUploads;

class VideoUploader extends Component
{
	use WithFileUploads;

    	/**
     	* @var TemporaryUploadedFile
     	*/
    	#[Validate('mimetypes:video/avi,video/mpeg,video/quicktime')]
    	public $video;

    	public ?string $jobStatus = 'Inactive';

    	public ?string $outputVideoLink = null;

    	public ?string $outputPath = null;

    	public ?string $outputFilename = null;

    	public function save(): void
    	{
        	$this->jobStatus = 'In Progress';
        	$this->outputVideoLink = null;
        	$this->outputPath = null;
        	$this->outputFilename = null;

        	// Store the uploaded file and generate the input and output paths of
        	// the video to be converted for the job.
        	$videoFilename = $this->video->store();
        	$videoPath = Storage::disk('local')->path($videoFilename);
        	$videoPathInfo = pathinfo($videoPath);
        	$this->outputFilename = "{$videoPathInfo['filename']}.webm";
        	$this->outputPath = "{$videoPathInfo['dirname']}/{$this->outputFilename}";

        	// Add the long-running job onto the queue to be processed when possible.
        	ProcessVideo::dispatch($videoPath, $this->outputPath);
    	}

    	...
}

How this works is we tell the job where it can find the video and tell it where it should output the converted video when it’s done. We have to make the output filename be the same as the original with just the extension changed, so we use pathinfo to extract that for us.

The ProcessVideo::dispatch method is fire and forget. We aren’t given a handle of any kind to be able to check the status of a job out of the box. For this example we’ll be waiting for the video to appear at the output location.

To process jobs on the queue you need to start a queue worker as jobs are not processed in the same process as the server that we are currently running. You can start the queue with artisan:

php artisan queue:work

Now the queue is running and ready to process jobs! Technically you can upload videos for conversion right now and have them be processed by the job, but you won’t be able to download the file in the browser yet.

Generating a Temporary URL and Sending it with Livewire

To download the file we need to generate a temporary URL. Traditionally this feature has only been available for S3, but as of Laravel v11.24.0 this is also usable with the local filesystem, which is really useful for development.

Let’s add a place to render the download link and the status of the job. Edit resources/views/livewire/video-uploader.blade.php and add a new section under the form:

<div>
	<h1>WebM Video Converter</h1>

	<form wire:submit="save">
    	<input type="file" wire:model="video">

    	@error('video') <span class="error">{{ $message }}</span> @enderror

    	<button type="submit">Convert Video</button>
	</form>

	<div wire:poll>
    	<p>Job Status: {{ $jobStatus }}</p>
    	@if ($outputVideoLink != null)
        	<a href="{{ $outputVideoLink }}" download>Download Converted File</a>
    	@endif
	</div>
</div>

Note the wire:poll attribute. This will cause the Blade echo statements inside of the div to refresh occasionally and will re-render if any of them changed. By default, it will re-render every 2.5 seconds. Let’s edit app/Livewire/VideoUploader.php to check the status of the conversion, and generate a download URL.

use App\Jobs\ProcessVideo;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\File;
use Livewire\Attributes\Validate;
use Livewire\Component;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Livewire\WithFileUploads;

class VideoUploader extends Component
{
	use WithFileUploads;

    	...

    	public function render(): View
    	{
        	if ($this->outputPath != null && File::exists($this->outputPath)) {
            	// Create a temporary URL that lasts for 10 minutes and allows the
            	// user to download the processed video file.
            	$this->outputVideoLink = Storage::temporaryUrl(
                	$this->outputFilename, now()->addMinutes(10)
            	);

            	$this->jobStatus = 'Done';
            	$this->outputPath = null;
            	$this->outputFilename = null;
        	}

        	return view('livewire.video-uploader', [
            	'jobStatus' => $this->jobStatus,
            	'outputVideoLink' => $this->outputVideoLink,
        	]);
    	}
}

Every time the page polls we check if the video has appeared at the output path. Once it’s there we generate the link, store it to state, and pass it to the view. Temporary URLs are customizable as well. You can change the expiration time to any duration you want, and if you’re using S3, you can also pass S3 request parameters using the optional 3rd argument.

Now you should be able to upload videos and download them with a link when they’re done processing!

Limitations

Although this setup works fine in a development environment with a small application, there are some changes you might need to make if you plan on scaling beyond that.

If your application is being served by multiple nodes then you will need to use a remote storage driver such as the S3 driver, which works with any S3 compatible file storage service. The same Laravel API calls are used regardless of the driver you use. You would only have to update the driver passed into the Storage facade methods from local to s3, or whichever driver you choose.

You also wouldn’t be able to rely on the same local filesystem being shared between your job workers and your app server either and would have to use a storage driver or database to pass files between them. This demo uses the database driver for simplicity’s sake, but it's also worth noting that by default, queues and jobs use the database driver, but SQS, Redis, and Beanstalkd can also be used. Consider using these other drives instead of depending on how much traffic you need to process.

Conclusion

In this article, we explored how to utilize queues and temporary URLs to implement a video conversion site. Laravel queues allow for efficient processing of long-running tasks like video conversion in a way that won’t bog down your backend servers that are processing web requests.

While this setup works fine for development, some changes would need to be made for scaling this such as using remote storage drivers for passing data between the web server and queue workers. By effectively leveraging Laravel’s features, developers can create robust and scalable applications with relative ease.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co