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

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

Разработка и обслуживание любых видов сайтов:

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

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

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

Клиенты загружают видео в разных форматах — от MP4 с телефона до MOV с профессиональных камер. Если конвертировать их на лету, сервер ложится от нагрузки, запросы уходят в таймаут, а пользователи получают битые файлы. Без правильной архитектуры загрузка видео становится узким местом любого медиа-сайта. Мы разрабатываем асинхронный pipeline: пресайнед-загрузка в облако сразу минует сервер, а транскодинг выполняется в очереди на выделенных воркерах или Managed-сервисах.

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

Почему стоит вынести транскодинг в фоновую очередь?

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

Какие проблемы решает система загрузки и транскодирования видео?

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

Как строится система загрузки и транскодирования видео?

Используем 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 минут в месяц.

Критерий 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 с разными размерами видео, проверка качества thumbnails и аудио.
  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 проектов по транскодингу для медиа-сайтов и образовательных платформ. Оценим ваш проект за 1 день — пишите. Получите консультацию по архитектуре видео-pipeline. Свяжитесь с нами для получения консультации. Закажите разработку — получите оценку за 1 день.

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

Последние работы

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