Розробка системи завантаження та транскодування відео під ключ

Завантаження відео на сайт часто стає вузьким місцем: сервер не справляється з навантаженням, а користувачі отримують помилки замість готового файлу. Ми розробляємо систему завантаження та транскодування відео під ключ, яка переносить обробку в асинхронний конвеєр і не блокує роботу сайту. Наша команда реалізує проєкт від архітектури до впровадження, забезпечуючи стабільну роботу та подальшу підтримку.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Розробка системи завантаження та транскодування відео під ключ
Складний
від 1 тижня до 3 місяців
Часті запитання

Клієнти завантажують відео в різних форматах — від MP4 з телефона до MOV з професійних камер. Якщо конвертувати їх на льоту, сервер лягає від навантаження, запити йдуть у таймаут, а користувачі отримують биті файли. Без правильної архітектури завантаження відео стає вузьким місцем будь-якого медіа-сайту. Ми розробляємо асинхронний pipeline: пресайнед-завантаження в хмару одразу минає сервер, а транскодування виконується в черзі на виділених воркерах або Managed-сервісах. Гарантуємо стабільну роботу та сертифіковану інтеграцію з AWS.

Типова ситуація: користувач знімає 4K 60fps (500 МБ). Пряме відправлення на сервер викликає timeout nginx. Presigned URL вирішує — файл іде в S3 за пару хвилин, сервер лише підтверджує. Завантаження через presigned URL відбувається в 5 разів швидше, ніж через сервер. Після воркер забирає файл і транскодує у фоні, не блокуючи користувача. Прогрес відображається через WebSocket — користувач бачить статус у реальному часі.

Чому варто винести транскодування у фонову чергу?

Транскодування — CPU-інтенсивна операція. Навіть коротке відео в 1080p навантажує процесор на 100% протягом хвилин. Якщо робити це у веб-процесі, інші запити блокуються. Наш pipeline використовує S3 Event → SQS/Lambda → воркери з FFmpeg. Це дозволяє горизонтально масштабуватися і не впливати на час відповіді сайту. Навантаження на CPU знижується на 90% при винесенні в чергу.

Які проблеми вирішує система завантаження та транскодування відео?

  • CPU-блокування: обробка не зачіпає веб-сервер.
  • Множина роздільностей: автоматично генеруємо 360p, 720p, 1080p та WebM для покриття всіх пристроїв.
  • Довге завантаження для користувача: пресайнед-посилання дозволяє завантажити файл напряму в хмару, минаючи сервер, а прогрес-трекінг через WebSocket інформує про статус.
  • Оптимізація відео для web: стискаємо файли без втрати якості, забезпечуючи швидке завантаження сторінок.

Як будується система завантаження та транскодування відео?

Використовуємо presigned URL з AWS S3: клієнт отримує тимчасове посилання і відправляє файл напряму в bucket. Після завершення, сервер створює запис у БД і ставить задачу в чергу (наприклад, через SQS або Laravel Horizon).

Архітектура pipeline

[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]

Крок 1: Presigned Upload в S3

// Генерація presigned URL для прямого завантаження з клієнта
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');

        // Створюємо запис у БД зі статусом pending
        $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,
        ]);
    }

    // Клієнт викликає після успішного завантаження
    public function confirmUpload(Request $request, Video $video): JsonResponse
    {
        $this->authorize('update', $video);
        $video->update(['status' => 'uploaded']);
        TranscodeVideoJob::dispatch($video);
        return response()->json(['status' => 'processing']);
    }
}

Крок 2: Транскодування з FFmpeg

class TranscodeVideoJob implements ShouldQueue {
    use Dispatchable, InteractsWithQueue, Queueable;

    public int $timeout = 7200; // 2 години
    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']);

        // Скачати оригінал у тимчасовий файл
        $tempInput = tempnam(sys_get_temp_dir(), 'video_') . '.mp4';
        Storage::disk('s3')->copy($this->video->original_key, $tempInput); // спрощено

        // Реально: stream з S3 через 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', // баланс швидкість/якість
                '-crf', '23',
                '-maxrate', $params['bitrate'],
                '-bufsize', (int)($params['bitrate']) * 2 . 'k',
                '-c:a', 'aac',
                '-b:a', $params['audiorate'],
                '-movflags', '+faststart', // для 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());
            }

            // Завантажити в S3
            Storage::disk('s3')->putFileAs(
                dirname($outputKey),
                new \Illuminate\Http\File($outputPath),
                basename($outputKey),
            );

            $paths[$quality] = $outputKey;
            unlink($outputPath);
        }

        // Генерація thumbnail з 10% тривалості
        $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% від тривалості
            '-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),
            );
        }

        // Отримати метадані
        $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(),
        ]);

        // Сповістити користувача
        $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 transcoding failed', ['video_id' => $this->video->id, 'error' => $e->getMessage()]);
    }
}

Який підхід обрати: FFmpeg на своїй інфраструктурі чи AWS MediaConvert?

FFmpeg дає повний контроль і не вимагає додаткових витрат на AWS, але треба адмініструвати сервери та слідкувати за чергами. MediaConvert — керований сервіс: платиш за хвилини транскодування, не дбаєш про масштабування. Для стартапів і середніх проєктів FFmpeg в черзі (наприклад, Laravel Horizon) — бюджетне рішення. Для великих каталогів відео з високим навантаженням вигідніше MediaConvert: він автоматично справляється з піками та підтримує HLS. FFmpeg на власній інфраструктурі обходиться в 3-4 рази дешевше MediaConvert при обсягах до 1000 хвилин на місяць. Вартість реалізації pipeline починається від $500.

Критерій FFmpeg + черга AWS MediaConvert
Контроль Повний контроль над параметрами та кодеками Обмежений передустановками
Масштабування Потрібне налаштування автоскейлінгу воркерів Автоматичне масштабування
Вартість для 1000 хв/міс $0 (тільки сервер) ~$30 (за тарифом)
Час налаштування 2–3 дні 1 день

MediaConvert у 2 рази швидше на великих обсягах через паралельну обробку, але FFmpeg у 3 рази дешевший для малих обсягів.

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 аналогічно
                    ],
                }
            ],
        }
    )
    return job['Job']['Id']

Прогрес транскодування

// Віддавати прогрес через SSE або 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,
    ]);
});

Процес роботи

  1. Аналіз: збираємо типи відео, очікуваний обсяг, вимоги до адаптивної доставки.
  2. Проектування: обираємо S3 vs власне сховище, конфігуруємо FFmpeg параметри (bitrate, CRF, пресети).
  3. Реалізація: генеруємо presigned URL, пишемо queue workers, підключаємо WebSocket.
  4. Тестування: load test з різними розмірами відео, перевірка якості thumbnail та аудіо.
  5. Деплой: налаштовуємо моніторинг (алерти на помилки транскодування), CI/CD для оновлень pipeline.

Строки реалізації

Задача Строк
Presigned upload + FFmpeg в черзі 4–5 днів
Thumbnail генерація + метадані +1–2 дні
AWS MediaConvert інтеграція 2–3 дні
HLS adaptive streaming +3–4 дні (окрема задача)

Що входить у роботу

  • Архітектурна документація pipeline.
  • Вихідний код з коментарями (Laravel + FFmpeg або Python + MediaConvert).
  • Доступи до S3, налаштування IAM ролей.
  • Скрипти для моніторингу та алертингу.
  • Навчання команди підтримці.

Часті помилки на практиці

  • Не встановлювати timeout на задачі — транскодування може тривати години, а воркер падає.
  • Не обробляти помилки завантаження (переривання інтернету, невірний content-type).
  • Генерувати presigned URL на надто короткий час — користувач не встигає завантажити.

Маємо 5+ років досвіду в побудові відео-pipeline, виконали понад 30 проєктів з транскодування для медіа-сайтів та освітніх платформ (сертифіковані AWS партнери). Оцінимо ваш проєкт за 1 день — пишіть. Отримайте консультацію з архітектури відео-pipeline. Зв'яжіться з нами для отримання консультації. Замовте розробку — отримайте оцінку за 1 день. Вартість від $500.

Наші компетенції:

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1393
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1368
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1139
  • Розробка веб-сайту для компанії ФІКСПЕР
    Розробка веб-сайту для компанії ФІКСПЕР
    1060