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

Ваш відеосервіс гальмує, а користувачі на мобільних бачать лише нескінченне завантаження? Ми реалізуємо адаптивний HLS/DASH-стрімінг, який забезпечує плавне відтворення на будь-яких пристроях. Наша команда бере на себе весь цикл — від транскодингу до інтеграції плеєра, створюючи надійне рішення під ключ із подальшою підтримкою.

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

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

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

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Реалізація HLS та DASH стрімінгу відео на сайті
Складний
~1-2 тижні
Часті запитання

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

Ви запускаєте відео-сервіс, і після завантаження перших роликів розумієте — плеєр гальмує, буферизація нескінченна, а користувачі на мобільних не бачать нічого, крім «спінера». Це знайома ситуація — ми стикалися з цим не раз. Наш досвід — 7+ років у відео-стрімінгу. Рішення — адаптивний стрімінг з правильною сегментацією, CDN-інтеграцією та підтримкою різних роздільностей. У цій статті розповімо, як ми реалізуємо HLS/DASH pipeline «під ключ», і які граблі нас чекали.

HLS vs DASH: що вибрати?

HLS (HTTP Live Streaming) — стандарт Apple, підтримується всіма браузерами через hls.js (крім Safari, де нативно). DASH (Dynamic Adaptive Streaming over HTTP) — відкритий стандарт, використовується для DRM-сумісності. На практиці HLS — вибір за замовчуванням: менше latency, ширша CDN-підтримка. DASH потрібен, якщо потрібні кілька DRM-систем (Widevine, PlayReady).

Ми в проєктах використовуємо HLS як базовий, а 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 показав у 2 рази меншу латентність, ніж DASH.

Чому FFmpeg — не єдиний варіант?

Для транскодування відео ми використовуємо FFmpeg у зв'язці з чергами (Laravel Horizon), але при великих обсягах підключаємо AWS MediaConvert. Розберемо обидва підходи.

FFmpeg + черга (self-hosted)

Скрипт transcode-hls.sh нарізає відео на три якості: 1080p, 720p, 360p. Сегменти тривалістю 6 секунд (fMP4). На виході — master.m3u8 та sub-playlistи.

Подивитися повний скрипт транскодування ```bash #!/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"

</details> Результат: 

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

#### PHP-джоба для черги Клас TranscodeToHlsJob — типова job для Laravel. Таймаут 2 години, дві спроби. Обробляємо тільки успішний кейс.
```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 (managed)

Для проєктів з високим навантаженням використовуємо MediaConvert. Конфігурація на Python:

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

MediaConvert сам створює HLS-плейлисти. Ми платимо тільки за хвилини транскодування.

CloudFront CDN

Щоб відео завантажувалося швидко по всьому світу, налаштовуємо CloudFront з двома кеш-політиками: для плейлистів (TTL 5 секунд) і сегментів (TTL 1 рік).

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
  }
}

React-плеєр з hls.js

Інтеграція на клієнті: hls.js для сучасних браузерів, fallback на нативний HLS у Safari.

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' }}
    />
  );
}

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

Ми виконуємо повний цикл з впровадження HLS/DASH стрімінгу на вашому сайті:

  • Транскодування відео в три якості (1080p, 720p, 360p) з використанням FFmpeg або AWS MediaConvert.
  • Інтеграція з чергами (Laravel Horizon) для асинхронної обробки.
  • Розробка кастомного React-плеєра на hls.js з підтримкою кастомізації UI.
  • Налаштування CDN (CloudFront або Cloudflare) для швидкої роздачі контенту.
  • Розгорнута документація з архітектури pipeline.
  • Навчання вашої команди роботі з системою.
  • 2 тижні підтримки після запуску.

Типові помилки

  • Неправильні бітрейти: занадто низькі для 1080p, занадто високі для 360p. Використовуємо рекомендації Apple.
  • Кеш плейлистів: якщо кешувати master.m3u8, плеєр не побачить нові якості після перетранскодування. Налаштовуємо no-cache.
  • Missing CORS: браузер блокує сегменти. Налаштовуємо CORS на S3/CloudFront.
  • Hydration mismatch: якщо плеєр рендериться на сервері, то може не співпасти з клієнтом. Використовуємо dynamic import.

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

  1. Аналіз — вивчаємо вихідні відео, вимоги до DRM, цільову аудиторію.
  2. Проектування — обираємо стек (self-hosted FFmpeg або MediaConvert), визначаємо бітрейти, CDN.
  3. Реалізація — пишемо транскодер, job, плеєр.
  4. Тестування — перевіряємо LCP, CLS, INP, Core Web Vitals.
  5. Деплой — розгортаємо на production, моніторимо логи.

Строки та вартість

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

Вартість розраховується індивідуально в залежності від обсягу відео, бітрейтів та необхідності DRM. Зв'яжіться з нами для оцінки вашого проєкту — ми розрахуємо строки та вартість. Замовте реалізацію HLS/DASH стрімінгу та отримайте консультацію. Ми гарантуємо, що ваше відео буде працювати без лагів на будь-яких пристроях.

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

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

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