Реалізація 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.shINPUT="$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.
Процес роботи
- Аналіз — вивчаємо вихідні відео, вимоги до DRM, цільову аудиторію.
- Проектування — обираємо стек (self-hosted FFmpeg або MediaConvert), визначаємо бітрейти, CDN.
- Реалізація — пишемо транскодер, job, плеєр.
- Тестування — перевіряємо LCP, CLS, INP, Core Web Vitals.
- Деплой — розгортаємо на 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 стрімінгу та отримайте консультацію. Ми гарантуємо, що ваше відео буде працювати без лагів на будь-яких пристроях.







