Turnkey Video Upload and Transcoding System Development

Clients upload video in various formats — from MP4 from phones to MOV from professional cameras. If you convert on the fly, the server buckles under load, requests time out, and users get corrupted files. Without proper architecture, video upload becomes a bottleneck for any media site. We develop a

Development and maintenance of all types of websites:

Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1414
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1285
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    982
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1241
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    982
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Website development for FIXPER company
    995

Clients upload video in various formats — from MP4 from phones to MOV from professional cameras. If you convert on the fly, the server buckles under load, requests time out, and users get corrupted files. Without proper architecture, video upload becomes a bottleneck for any media site. We develop an async pipeline: presigned upload to the cloud immediately bypasses the server, and transcoding happens in a queue on dedicated workers or managed services.

Typical scenario: a user shoots 4K 60fps (500 MB). Direct server upload causes an nginx timeout. Presigned URL solves this — the file goes to S3 in a couple of minutes, the server only confirms. Then a worker picks up the file and transcodes in the background, not blocking the user. Progress is shown via WebSocket — the user sees status in real time.

Why move transcoding to a background queue?

Transcoding is CPU-intensive. Even a 1080p short video runs the CPU at 100% for minutes. If done in a web process, other requests are blocked. Our pipeline uses S3 Event → SQS/Lambda → workers with FFmpeg. This allows horizontal scaling without affecting site response time. CPU load drops 90% when moved to a queue.

What problems does the video upload and transcoding system solve?

  • CPU blocking: processing doesn't affect the web server.
  • Multiple resolutions: automagically generate 360p, 720p, 1080p and WebM to cover all devices.
  • Slow user upload: presigned link lets them upload directly to the cloud, bypassing the server; progress tracking via WebSocket keeps them informed.

How we build the video upload and transcoding system

We use presigned URLs from AWS S3: the client gets a temporary link and sends the file straight to a bucket. After upload completes, the server writes a DB record and queues a task (e.g., via SQS or Laravel Horizon).

Pipeline architecture

[Client] -> [Presigned S3 Upload] -> [S3: original/] | [S3 Event -> SQS/Lambda] -> [Transcoding Worker (FFmpeg)] | [S3: processed/{quality}/] -> [CDN CloudFront] | [Update DB: video.status = ready, paths = {...}] | [WebSocket/Webhook -> Client notification] 

Step 1: Presigned Upload to S3

// Generate presigned URL for direct upload from client class VideoController extends Controller { public function initiateUpload(Request $request): JsonResponse { $request->validate([ 'filename' => 'required|string|max:255', 'content_type' => 'required|in:video/mp4,video/webm,video/quicktime,video/x-msvideo', 'size' => 'required|integer|max:5368709120', // 5 GB ]); $key = sprintf( 'original/%d/%s/%s', auth()->id(), now()->format('Y/m'), Str::uuid() . '.' . pathinfo($request->filename, PATHINFO_EXTENSION) ); $s3 = app('aws')->createClient('s3'); $command = $s3->getCommand('PutObject', [ 'Bucket' => config('filesystems.disks.s3.bucket'), 'Key' => $key, 'ContentType' => $request->content_type, ]); $presigned = $s3->createPresignedRequest($command, '+2 hours'); // Create DB record with pending status $video = Video::create([ 'user_id' => auth()->id(), 'original_key' => $key, 'original_name' => $request->filename, 'status' => 'pending', 'size' => $request->size, ]); return response()->json([ 'video_id' => $video->id, 'upload_url' => (string) $presigned->getUri(), 'key' => $key, ]); } // Client calls after successful upload public function confirmUpload(Request $request, Video $video): JsonResponse { $this->authorize('update', $video); $video->update(['status' => 'uploaded']); TranscodeVideoJob::dispatch($video); return response()->json(['status' => 'processing']); } } 

Step 2: Transcoding with FFmpeg

class TranscodeVideoJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable; public int $timeout = 7200; // 2 hours public int $tries = 2; const QUALITIES = [ '360p' => ['width' => 640, 'height' => 360, 'bitrate' => '800k', 'audiorate' => '96k'], '720p' => ['width' => 1280, 'height' => 720, 'bitrate' => '2500k', 'audiorate' => '128k'], '1080p' => ['width' => 1920, 'height' => 1080, 'bitrate' => '5000k', 'audiorate' => '192k'], ]; public function __construct(private Video $video) {} public function handle(): void { $this->video->update(['status' => 'transcoding']); // Download original to temporary file $tempInput = tempnam(sys_get_temp_dir(), 'video_') . '.mp4'; Storage::disk('s3')->copy($this->video->original_key, $tempInput); // simplified // Actually: stream from S3 via signed URL $inputUrl = Storage::disk('s3')->temporaryUrl($this->video->original_key, now()->addHour()); $paths = []; foreach (self::QUALITIES as $quality => $params) { $outputKey = sprintf( 'processed/%d/%s/%s.mp4', $this->video->user_id, $this->video->id, $quality ); $outputPath = sys_get_temp_dir() . "/{$this->video->id}_{$quality}.mp4"; $scale = "scale={$params['width']}:{$params['height']}:force_original_aspect_ratio=decrease,pad={$params['width']}:{$params['height']}:(ow-iw)/2:(oh-ih)/2"; $command = [ 'ffmpeg', '-y', '-i', $inputUrl, '-vf', $scale, '-c:v', 'libx264', '-preset', 'medium', // balance speed/quality '-crf', '23', '-maxrate', $params['bitrate'], '-bufsize', (int)($params['bitrate']) * 2 . 'k', '-c:a', 'aac', '-b:a', $params['audiorate'], '-movflags', '+faststart', // for web streaming $outputPath, ]; $process = new \Symfony\Component\Process\Process($command); $process->setTimeout(3600); $process->run(); if (!$process->isSuccessful()) { throw new \RuntimeException("FFmpeg failed for {$quality}: " . $process->getErrorOutput()); } // Upload to S3 Storage::disk('s3')->putFileAs( dirname($outputKey), new \Illuminate\Http\File($outputPath), basename($outputKey), ); $paths[$quality] = $outputKey; unlink($outputPath); } // Generate thumbnail from 10% duration $thumbKey = "thumbnails/{$this->video->user_id}/{$this->video->id}.jpg"; $thumbPath = sys_get_temp_dir() . "/{$this->video->id}_thumb.jpg"; $process = new \Symfony\Component\Process\Process([ 'ffmpeg', '-y', '-i', $inputUrl, '-ss', '10%', // 10% of duration '-vframes', '1', '-q:v', '2', $thumbPath, ]); $process->run(); if (file_exists($thumbPath)) { Storage::disk('s3')->putFileAs( dirname($thumbKey), new \Illuminate\Http\File($thumbPath), basename($thumbKey), ); } // Get metadata $ffprobe = \FFMpeg\FFProbe::create(); $duration = $ffprobe->streams($inputUrl)->videos()->first()->get('duration'); $resolution = $ffprobe->streams($inputUrl)->videos()->first()->getDimensions(); $this->video->update([ 'status' => 'ready', 'paths' => $paths, 'thumbnail_key' => $thumbKey, 'duration' => (int) $duration, 'width' => $resolution->getWidth(), 'height' => $resolution->getHeight(), ]); // Notify user $this->video->user->notify(new VideoReadyNotification($this->video)); } public function failed(\Throwable $e): void { $this->video->update(['status' => 'failed', 'error' => $e->getMessage()]); Log::error('Video processing failed', ['video_id' => $this->video->id, 'error' => $e->getMessage()]); } } 

Which approach to choose: FFmpeg on your own infrastructure or AWS MediaConvert?

FFmpeg gives full control and no extra AWS costs, but you need to manage servers and queues. MediaConvert is a managed service: you pay per minute of transcoding, no scaling worries. For startups and mid-size projects, FFmpeg in a queue (e.g., Laravel Horizon) is budget-friendly. For large video catalogs with high load, MediaConvert is more cost-effective: it handles spikes automatically and supports HLS. For volumes up to 1000 minutes per month, FFmpeg on your own infrastructure is three times cheaper than MediaConvert, saving you up to $30 per month.

Criterion FFmpeg + queue AWS MediaConvert
Control Full control over parameters and codecs Limited to presets
Scaling Requires autoscaling setup for workers Automatic scaling
Cost for 1000 min/month $0 (only server) ~$30 (per tariff)
Setup time 2–3 days 1 day

MediaConvert is twice as fast on large volumes due to parallel processing, but FFmpeg is three times cheaper for small volumes.

AWS Elastic Transcoder / MediaConvert

import boto3 def transcode_with_mediaconvert(input_key: str, output_prefix: str) -> str: client = boto3.client('mediaconvert', region_name='eu-west-1', endpoint_url='https://abc123.mediaconvert.eu-west-1.amazonaws.com') job = client.create_job( Role='arn:aws:iam::123456789:role/MediaConvertRole', Settings={ 'Inputs': [{ 'FileInput': f's3://my-bucket/{input_key}', 'AudioSelectors': {'Audio Selector 1': {'DefaultSelection': 'DEFAULT'}}, 'VideoSelector': {}, }], 'OutputGroups': [{ 'Name': 'File Group', 'OutputGroupSettings': { 'Type': 'FILE_GROUP_SETTINGS', 'FileGroupSettings': { 'Destination': f's3://my-bucket/{output_prefix}/', }, }, 'Outputs': [ { 'NameModifier': '_720p', 'VideoDescription': { 'Width': 1280, 'Height': 720, 'CodecSettings': { 'Codec': 'H_264', 'H264Settings': {'Bitrate': 2500000, 'RateControlMode': 'CBR'}, }, }, 'AudioDescriptions': [{'CodecSettings': {'Codec': 'AAC', 'AacSettings': {'Bitrate': 128000}}}], 'ContainerSettings': {'Container': 'MP4'}, }, # ... 360p, 1080p similarly ], }], } ) return job['Job']['Id'] 

Transcoding progress

// Serve progress via SSE or WebSocket Route::get('/videos/{video}/status', function (Video $video) { return response()->json([ 'status' => $video->status, 'progress' => $video->transcoding_progress, 'paths' => $video->status === 'ready' ? $video->paths : null, ]); }); 

Process of work

  1. Analysis: collect video types, expected volume, requirements for adaptive delivery.
  2. Design: choose S3 vs own storage, configure FFmpeg parameters (bitrate, CRF, presets).
  3. Implementation: generate presigned URLs, write queue workers, set up WebSocket.
  4. Testing: load test with different video sizes, verify thumbnail and audio quality.
  5. Deploy: configure monitoring (alerts on transcoding errors), CI/CD for pipeline updates.

Timeline

Task Time
Presigned upload + FFmpeg in queue 4–5 days
Thumbnail generation + metadata +1–2 days
AWS MediaConvert integration 2–3 days
HLS adaptive streaming +3–4 days (separate task)

What’s included

  • Architectural pipeline documentation.
  • Source code with comments (Laravel + FFmpeg or Python + MediaConvert).
  • S3 access setup, IAM role configuration.
  • Monitoring and alerting scripts.
  • Team training for support.
  • Guaranteed response time for support tickets (SLA).
  • Certified integration with major cloud providers (AWS, GCP, Azure).
  • 5+ years of experience building video pipelines, with over 30 successful projects for media sites and educational platforms.

Common mistakes in practice: not setting a timeout on jobs — transcoding can take hours and workers crash; not handling upload errors (internet interruption, wrong content-type); presigned URL too short — user doesn’t finish uploading.

We assess your project in one day — just get in touch. Get a consultation on video pipeline architecture. Leave a request to get your estimate within one day.