Реализация загрузки видео и транскодирования для сайта

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

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация загрузки видео и транскодирования для сайта
Сложная
от 1 недели до 3 месяцев
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Реализация загрузки и транскодинга видео

Загрузка видео требует отдельного pipeline: файл принимается на сервер или напрямую в облако, затем транскодируется в несколько качеств (360p, 720p, 1080p) и форматов (MP4/H.264, WebM/VP9). Транскодинг — CPU-intensive операция, выполняется в фоне.

Архитектура 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()]);
    }
}

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,
    ]);
});

Срок реализации

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