Реализация потокового видео (HLS/DASH) на сайте

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

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация потокового видео (HLS/DASH) на сайте
Сложная
~1-2 недели
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • 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

Реализация HLS и DASH стриминга видео

HLS (HTTP Live Streaming) и DASH (Dynamic Adaptive Streaming over HTTP) — протоколы адаптивного стриминга. Видео нарезается на сегменты по 2–10 секунд; плеер автоматически переключается между качествами в зависимости от скорости соединения пользователя.

HLS vs DASH

Характеристика HLS DASH
Поддержка Все браузеры (hls.js для non-Safari) Все браузеры (dash.js)
Контейнер MPEG-TS или fMP4 fMP4
DRM FairPlay (Apple), Widevine Widevine, PlayReady
Латентность 6–30 сек (LL-HLS: <2 сек) 2–10 сек
CDN совместимость Отличная Отличная

HLS — стандартный выбор. DASH используется для сервисов с несколькими DRM.

FFmpeg: создание HLS плейлиста

#!/bin/bash
# transcode-hls.sh

INPUT="$1"
OUTPUT_DIR="$2"

mkdir -p "$OUTPUT_DIR"

ffmpeg -i "$INPUT" \
  -filter_complex \
    "[0:v]split=3[v1][v2][v3]; \
     [v1]scale=1920:1080:force_original_aspect_ratio=decrease[v1080]; \
     [v2]scale=1280:720:force_original_aspect_ratio=decrease[v720]; \
     [v3]scale=640:360:force_original_aspect_ratio=decrease[v360]" \
  \
  -map "[v1080]" -map 0:a -c:v:0 libx264 -crf 22 -preset fast \
    -b:v:0 5000k -maxrate:v:0 5500k -bufsize:v:0 10000k \
    -c:a:0 aac -b:a:0 192k \
  \
  -map "[v720]" -map 0:a -c:v:1 libx264 -crf 23 -preset fast \
    -b:v:1 2500k -maxrate:v:1 2750k -bufsize:v:1 5000k \
    -c:a:1 aac -b:a:1 128k \
  \
  -map "[v360]" -map 0:a -c:v:2 libx264 -crf 24 -preset fast \
    -b:v:2 800k -maxrate:v:2 880k -bufsize:v:2 1600k \
    -c:a:2 aac -b:a:2 96k \
  \
  -f hls \
  -hls_time 6 \
  -hls_playlist_type vod \
  -hls_flags independent_segments \
  -hls_segment_type fmp4 \
  -hls_segment_filename "$OUTPUT_DIR/v%v/seg%06d.m4s" \
  -master_pl_name master.m3u8 \
  -var_stream_map "v:0,a:0,name:1080p v:1,a:1,name:720p v:2,a:2,name:360p" \
  "$OUTPUT_DIR/v%v/index.m3u8"

Результат:

output/
├── master.m3u8
├── 1080p/
│   ├── index.m3u8
│   ├── seg000001.m4s
│   └── ...
├── 720p/
│   └── ...
└── 360p/
    └── ...

PHP: интеграция в очередь

class TranscodeToHlsJob implements ShouldQueue
{
    public int $timeout = 7200;
    public int $tries = 2;

    public function __construct(private Video $video) {}

    public function handle(): void
    {
        $this->video->update(['status' => 'transcoding']);

        $inputUrl   = Storage::disk('s3')->temporaryUrl($this->video->original_key, now()->addHours(3));
        $outputDir  = sys_get_temp_dir() . '/hls_' . $this->video->id;
        $s3Prefix   = "hls/{$this->video->user_id}/{$this->video->id}";

        mkdir($outputDir, 0777, true);

        $process = new \Symfony\Component\Process\Process([
            '/usr/local/bin/transcode-hls.sh', $inputUrl, $outputDir
        ]);
        $process->setTimeout(7200);
        $process->run();

        if (!$process->isSuccessful()) {
            throw new \RuntimeException('HLS transcoding failed: ' . $process->getErrorOutput());
        }

        // Загрузить все файлы в S3
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($outputDir)
        );

        foreach ($iterator as $file) {
            if (!$file->isFile()) continue;

            $relativePath = str_replace($outputDir . '/', '', $file->getPathname());
            $s3Key = "{$s3Prefix}/{$relativePath}";

            $contentType = str_ends_with($file->getFilename(), '.m3u8')
                ? 'application/x-mpegURL'
                : 'video/mp4';

            Storage::disk('s3')->put($s3Key, file_get_contents($file->getPathname()), [
                'ContentType'  => $contentType,
                'CacheControl' => str_ends_with($file->getFilename(), '.m3u8')
                    ? 'no-cache'          // плейлист не кэшируем
                    : 'public, max-age=31536000',  // сегменты кэшируем навсегда
            ]);
        }

        $this->video->update([
            'status'       => 'ready',
            'hls_manifest' => "{$s3Prefix}/master.m3u8",
        ]);

        // Очистить временные файлы
        exec("rm -rf {$outputDir}");
    }
}

AWS MediaConvert: HLS

def create_hls_job(input_key: str, output_prefix: str) -> str:
    client = boto3.client('mediaconvert', endpoint_url=MEDIACONVERT_ENDPOINT)

    job = client.create_job(
        Role=MEDIACONVERT_ROLE,
        Settings={
            'Inputs': [{'FileInput': f's3://bucket/{input_key}', 'VideoSelector': {}, 'AudioSelectors': {'Audio 1': {'DefaultSelection': 'DEFAULT'}}}],
            'OutputGroups': [{
                'Name': 'Apple HLS',
                'OutputGroupSettings': {
                    'Type': 'HLS_GROUP_SETTINGS',
                    'HlsGroupSettings': {
                        'Destination': f's3://bucket/{output_prefix}/',
                        'SegmentLength': 6,
                        'MinSegmentLength': 0,
                        'DirectoryStructure': 'SUBDIRECTORY_PER_STREAM',
                    },
                },
                'Outputs': [
                    {'NameModifier': '_1080p', 'VideoDescription': {'Width': 1920, 'Height': 1080, 'CodecSettings': {'Codec': 'H_264', 'H264Settings': {'Bitrate': 5000000, 'RateControlMode': 'CBR', 'GopSize': 90}}}, 'AudioDescriptions': [{'CodecSettings': {'Codec': 'AAC', 'AacSettings': {'Bitrate': 192000}}}]},
                    {'NameModifier': '_720p', 'VideoDescription': {'Width': 1280, 'Height': 720, 'CodecSettings': {'Codec': 'H_264', 'H264Settings': {'Bitrate': 2500000, 'RateControlMode': 'CBR', 'GopSize': 90}}}, 'AudioDescriptions': [{'CodecSettings': {'Codec': 'AAC', 'AacSettings': {'Bitrate': 128000}}}]},
                    {'NameModifier': '_360p', 'VideoDescription': {'Width': 640, 'Height': 360, 'CodecSettings': {'Codec': 'H_264', 'H264Settings': {'Bitrate': 800000, 'RateControlMode': 'CBR', 'GopSize': 90}}}, 'AudioDescriptions': [{'CodecSettings': {'Codec': 'AAC', 'AacSettings': {'Bitrate': 96000}}}]},
                ],
            }],
        }
    )
    return job['Job']['Id']

React плеер: hls.js

import Hls from 'hls.js';
import { useEffect, useRef } from 'react';

interface VideoPlayerProps {
  manifestUrl: string;
  poster?: string;
}

export function VideoPlayer({ manifestUrl, poster }: VideoPlayerProps) {
  const videoRef = useRef<HTMLVideoElement>(null);

  useEffect(() => {
    const video = videoRef.current;
    if (!video) return;

    if (Hls.isSupported()) {
      const hls = new Hls({
        maxBufferLength: 30,
        maxMaxBufferLength: 60,
        lowLatencyMode: false,
      });

      hls.loadSource(manifestUrl);
      hls.attachMedia(video);

      hls.on(Hls.Events.ERROR, (_, data) => {
        if (data.fatal) {
          if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
            hls.startLoad();
          } else {
            hls.destroy();
          }
        }
      });

      return () => hls.destroy();

    } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
      // Safari: нативная поддержка HLS
      video.src = manifestUrl;
    }
  }, [manifestUrl]);

  return (
    <video
      ref={videoRef}
      poster={poster}
      controls
      playsInline
      style={{ width: '100%', maxHeight: '80vh' }}
    />
  );
}

CloudFront для CDN раздачи

resource "aws_cloudfront_distribution" "video" {
  origin {
    domain_name            = aws_s3_bucket.videos.bucket_regional_domain_name
    origin_id              = "s3-videos"
    origin_access_control_id = aws_cloudfront_origin_access_control.default.id
  }

  enabled         = true
  is_ipv6_enabled = true

  default_cache_behavior {
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = "s3-videos"
    viewer_protocol_policy = "redirect-to-https"

    forwarded_values {
      query_string = false
      cookies { forward = "none" }
      headers = ["Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"]
    }

    min_ttl     = 0
    default_ttl = 86400
    max_ttl     = 31536000
  }

  # Плейлисты .m3u8 — короткий кэш
  ordered_cache_behavior {
    path_pattern           = "*.m3u8"
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = "s3-videos"
    viewer_protocol_policy = "redirect-to-https"

    forwarded_values {
      query_string = false
      cookies { forward = "none" }
    }

    min_ttl     = 0
    default_ttl = 5
    max_ttl     = 30
  }
}

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

Задача Срок
FFmpeg HLS транскодинг в очереди 4–5 дней
AWS MediaConvert интеграция 2–3 дня
hls.js плеер с React 1–2 дня
CloudFront CDN раздача 1–2 дня
Полный pipeline (upload → transcode → player) 7–10 дней