Media Streaming

04. HLS Protocol: HTTP Live Streaming у Деталях

04. HLS Protocol: HTTP Live Streaming у Деталях

1. Вступ та Контекст

Проблема: Як YouTube плавно перемикає якість відео?

Уявіть ситуацію: ви дивитесь відео на YouTube у Full HD (1080p). Раптом ваш Wi-Fi погіршується (хтось почав завантажувати великий файл). Відео не зависає! Воно автоматично перемикається на 720p, потім на 480p. Коли інтернет знову стабілізується — якість повертається до 1080p. Як це працює?

Це магія HLS (HTTP Live Streaming) — протоколу, який змінив індустрію стрімінгу назавжди.

Історична еволюція: Від RTMP до HLS

2000-ті роки: Епоха RTMP (Flash)

У часи Flash Player відео стрімилось через RTMP (Real-Time Messaging Protocol). Це був бінарний протокол, який працював через порт 1935.

Проблеми RTMP:

  • Фаєрволи блокували порт 1935 (дозволений тільки HTTP 80/443)
  • Не було адаптивної якості (bitrate фіксований)
  • Потрібен спеціальний медіа-сервер (Adobe Media Server, Wowza)
  • Flash Player помер у 2020 році

2009 рік: Apple винаходить HLS

Apple хотіла стрімити відео на iPhone 3GS. Проблема: Flash не працював на iOS. Рішення: створити протокол, який:

  • Використовує звичайний HTTP (порт 80/443) — проходить крізь будь-які фаєрволи
  • Працює через CDN (Content Delivery Network) — масштабується на мільйони користувачів
  • Підтримує ABR (Adaptive Bitrate) — автоматично змінює якість

Результат: У червні 2009 року Apple представила HLS разом з iPhone 3GS.

2010-ті: HLS стає стандартом

  • 2011: YouTube переходить з Flash на HTML5 + HLS
  • 2017: RFC 8216 (офіційна специфікація HLS)
  • 2020: Flash офіційно помер. HLS переміг

Чому HLS переміг?

🌐 Працює через HTTP

Для сервера стрімінг виглядає як звичайне завантаження JPG-зображень. Не потрібні спеціальні медіа-сервери — підійде звичайний Nginx або Apache.

📊 Adaptive Bitrate

Плеєр автоматично вибирає якість залежно від швидкості інтернету. Користувач із 4G бачить 1080p, користувач із 3G — 360p.

🚀 CDN-сумісність

Стрімінг кешується на CloudFront, Cloudflare, Fastly так само, як статичні файли (CSS, JS). Це дешево і швидко.

📱 Нативний для Apple

iOS/macOS підтримують HLS на рівні ОС. Не потрібні сторонні бібліотеки — працює в Safari з тегом <video>.

Сценарії використання

Приклади: Twitch, YouTube Live, спортивні трансляції

Особливості:

  • Playlist постійно оновлюється (кожні 2-10 секунд)
  • Затримка (latency): 10-30 секунд (звичайний HLS), 2-6 секунд (Low Latency HLS)
  • Сегменти видаляються після певного часу (sliding window)

Use case:

Стрімер грає в CS:GO → OBS кодує в HLS → CDN роздає 100,000 глядачам

Передумови (Prerequisites)

Перед вивченням HLS корисно розуміти:

  • HTTP/HTTPS: Методи GET, коди відповідей (200, 404), заголовки (Content-Type, Cache-Control)
  • Відео-кодеки: H.264/AVC, H.265/HEVC (див. попередню главу)
  • Аудіо-кодеки: AAC, MP3
  • Контейнери: MP4, MPEG-TS (див. анатомію відео)
Примітка: Якщо ви розумієте, як працює завантаження зображень на веб-сторінці (<img src="photo.jpg">), ви легко зрозумієте HLS — це той самий принцип, але для відео.

2. Фундаментальні Концепції

Що таке HLS? (Formal Definition)

HLS (HTTP Live Streaming) — це протокол потокової передачі медіа-контенту через HTTP, розроблений компанією Apple. Він базується на архітектурі сегментованого стрімінгу та адаптивного вибору бітрейту.

Ключові поняття:

  • Playlist (Manifest): Текстовий файл у форматі M3U8, який містить метаінформацію про доступні потоки та сегменти
  • Segment: Короткий фрагмент відео (зазвичай 2-10 секунд), закодований у контейнер MPEG-TS (.ts) або fMP4 (.m4s)
  • Adaptive Bitrate (ABR): Механізм динамічного вибору якості потоку залежно від пропускної здатності мережі клієнта

Архітектура HLS: Огляд

Loading diagram...
graph LR
    A["🎥 Відео<br/>(MP4, AVI, MOV)"] --> B["🔄 Encoder<br/>(FFmpeg, OBS)"]
    B --> C1["📊 1080p<br/>(5 Mbps)"]
    B --> C2["📊 720p<br/>(2.5 Mbps)"]
    B --> C3["📊 480p<br/>(1 Mbps)"]
    B --> C4["📊 360p<br/>(500 Kbps)"]

    C1 --> D["☁️ Origin Server<br/>(Nginx, S3)"]
    C2 --> D
    C3 --> D
    C4 --> D

    D --> E["🌐 CDN<br/>(CloudFront, Cloudflare)"]
    E --> F["📱 Player<br/>(hls.js, Safari, VLC)"]

    F --> G{{"🔍 Bandwidth<br/>Detection"}}
    G -->|High Speed| C1
    G -->|Medium| C2
    G -->|Low| C4

    style A fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style B fill:#f59e0b,stroke:#b45309,color:#ffffff
    style D fill:#8b5cf6,stroke:#6d28d9,color:#ffffff
    style E fill:#10b981,stroke:#047857,color:#ffffff
    style F fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style G fill:#f59e0b,stroke:#b45309,color:#ffffff

Пояснення потоку:

  1. Encoder отримує вихідне відео і кодує його в кілька варіантів якості (1080p, 720p, 480p, 360p)
  2. Origin Server зберігає всі сегменти та плейлісти
  3. CDN кешує контент і роздає користувачам по всьому світу
  4. Player завантажує плейлісти, визначає швидкість інтернету і вибирає оптимальну якість

Основні компоненти HLS

Master Playlist (.m3u8)
File
Головний маніфест, який містить посилання на всі доступні варіанти якості (bitrate варіанти). Це перший файл, який завантажує плеєр.
  • Приклад імені: master.m3u8, index.m3u8, playlist.m3u8
Media Playlist (.m3u8)
File
Список конкретних медіа-сегментів для певної якості. Кожний варіант bitrate має свій Media Playlist.
  • Приклад імені: 1080p/playlist.m3u8, 720p/playlist.m3u8
Segment (.ts, .m4s)
File
Фрагмент відео тривалістю 2-10 секунд. Це те, що реально завантажується і програється.
  • Формати:
  • .ts — MPEG-2 Transport Stream (класичний)
  • .m4s — Fragmented MP4 (сучасний, CMAF-сумісний)
Init Segment (init.mp4)
File required
Обов'язковий для fMP4. Містить метадані (кодек, роздільну здатність, sample rate), без яких сегменти не можуть бути декодовані.

Аналогія 1: Книга з перекладами

Уявіть дуже товсту книгу (фільм на 2 години):

Крок 1: Обираєте мову (Master Playlist)

Ви беріть книгу у руки і бачите на обкладинці:

"Ця книга доступна мовами:
- Українська (сторінка 1)
- English (сторінка 500)
- Deutsch (сторінка 1000)"

Це Master Playlist — він каже, які варіанти доступні.

Крок 2: Знаходите зміст (Media Playlist)

Ви відкриваєте сторінку 1 (українська версія) і бачите зміст:

Розділ 1: сторінки 10-25
Розділ 2: сторінки 26-40
Розділ 3: сторінки 41-55

Це Media Playlist — список конкретних фрагментів.

Крок 3: Читаєте по частинах (Segments)

Ви не читаєте всю книгу за раз. Ви читаєте Розділ 1 (15 сторінок), потім Розділ 2 (14 сторінок), і так далі.

Кожний розділ — це Segment (фрагмент відео).

Ключова ідея: Ви не завантажуєте весь фільм (2 GB) одразу. Ви завантажуєте маленькі шматки по 2-5 MB (10 секунд відео) і починаєте дивитись, поки завантажуються наступні.

Аналогія 2: Ресторан з меню

📋 Головне меню

Master Playlist

Ви заходите в ресторан і отримуєте головне меню:

Меню:
- Англійською (сторінка 2)
- Французькою (сторінка 10)
- Китайською (сторінка 20)

::card{title="🍕 Розділ "Піца"" icon="i-lucide-pizza"} Media Playlist

Ви обираєте англійську версію і відкриваєте розділ "Піца":

Піца:
1. Маргарита
2. Пепероні
3. Чотири сири

::

🍽️ Конкретна страва

Segment

Ви обираєте "Маргарита" і отримуєте конкретну порцію їжі. Це і є сегмент — те, що ви реально "споживаєте".


3. Архітектура та Механіка

3.1. Анатомія Master Playlist

Master Playlist — це текстовий файл UTF-8 з розширенням .m3u8. Формат базується на M3U (MP3 URL), але з додатковими тегами для HLS.

Приклад мінімального Master Playlist:

#EXTM3U
#EXT-X-VERSION:3

#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360
360p/playlist.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=1400000,RESOLUTION=1280x720
720p/playlist.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
1080p/playlist.m3u8

Розбір кожного рядка:

#EXTM3U
Обов'язковий тег required
Перший рядок будь-якого HLS playlist. Каже парсеру: "Це M3U8 файл, а не звичайний текст".
#EXT-X-VERSION
Integer
Версія протоколу HLS. Можливі значення: 1-7.Рекомендовано: версія 3 (підтримка floating-point тривалості сегментів) або вище.
#EXT-X-STREAM-INF
Обов'язковий для варіантів
Описує один варіант потоку. Атрибути:
  • BANDWIDTH (обов'язково): Пікова швидкість у бітах/сек (800000 = 800 Kbps)
  • RESOLUTION: Роздільна здатність відео (ширина×висота)
  • CODECS: Кодеки аудіо і відео (опціонально, але рекомендовано)
  • FRAME-RATE: Частота кадрів (опціонально)
Наступний рядок після цього тегу — URL Media Playlist.

Приклад розширеного Master Playlist:

#EXTM3U
#EXT-X-VERSION:6

# Варіант 1: 1080p 60fps (High Quality)
#EXT-X-STREAM-INF:BANDWIDTH=7800000,AVERAGE-BANDWIDTH=7200000,RESOLUTION=1920x1080,FRAME-RATE=60.000,CODECS="avc1.640028,mp4a.40.2",AUDIO="audio-high"
1080p60/playlist.m3u8

# Варіант 2: 1080p 30fps (Standard)
#EXT-X-STREAM-INF:BANDWIDTH=5000000,AVERAGE-BANDWIDTH=4500000,RESOLUTION=1920x1080,FRAME-RATE=30.000,CODECS="avc1.64001f,mp4a.40.2",AUDIO="audio-high"
1080p30/playlist.m3u8

# Варіант 3: 720p (Medium Quality)
#EXT-X-STREAM-INF:BANDWIDTH=2800000,AVERAGE-BANDWIDTH=2500000,RESOLUTION=1280x720,FRAME-RATE=30.000,CODECS="avc1.64001f,mp4a.40.2",AUDIO="audio-medium"
720p/playlist.m3u8

# Варіант 4: 480p (Low Quality)
#EXT-X-STREAM-INF:BANDWIDTH=1400000,AVERAGE-BANDWIDTH=1200000,RESOLUTION=854x480,FRAME-RATE=30.000,CODECS="avc1.64001e,mp4a.40.2",AUDIO="audio-low"
480p/playlist.m3u8

# Варіант 5: 360p (Mobile)
#EXT-X-STREAM-INF:BANDWIDTH=800000,AVERAGE-BANDWIDTH=700000,RESOLUTION=640x360,FRAME-RATE=30.000,CODECS="avc1.64001e,mp4a.40.2",AUDIO="audio-low"
360p/playlist.m3u8

# Альтернативні аудіо-треки
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-high",NAME="English",LANGUAGE="en",AUTOSELECT=YES,DEFAULT=YES,URI="audio/en/playlist.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-high",NAME="Ukrainian",LANGUAGE="uk",AUTOSELECT=NO,DEFAULT=NO,URI="audio/uk/playlist.m3u8"
Важливо: BANDWIDTH — це пікова швидкість (максимум). AVERAGE-BANDWIDTH — це середня швидкість. Плеєр використовує ці значення для вибору якості.Практичний приклад:
  • У користувача швидкість 6 Mbps
  • Плеєр НЕ обере варіант 1080p60 (7.8 Mbps), бо недостатньо запасу
  • Плеєр обере 1080p30 (5 Mbps) — є запас 1 Mbps для стабільності

3.2. Анатомія Media Playlist

Media Playlist містить список конкретних сегментів для відтворення.

Приклад Media Playlist (VOD):

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD

#EXTINF:9.009,
segment_00001.ts
#EXTINF:9.009,
segment_00002.ts
#EXTINF:9.009,
segment_00003.ts
#EXTINF:9.009,
segment_00004.ts
#EXTINF:9.009,
segment_00005.ts

#EXT-X-ENDLIST

Розбір тегів:

#EXT-X-TARGETDURATION
Integer required
Максимальна тривалість сегмента (у секундах). Жоден сегмент не може бути довшим за це значення.Практичне значення: Плеєр знає, скільки буферу тримати. Якщо TARGETDURATION=10, плеєр буферує мінімум 30 секунд (3 сегменти).
#EXT-X-MEDIA-SEQUENCE
Integer
Порядковий номер першого сегмента в плейлісті. Використовується для Live стрімів.Приклад (Live):
#EXT-X-MEDIA-SEQUENCE:1523  ← перший сегмент має номер 1523
segment_01523.ts
segment_01524.ts
segment_01525.ts
#EXT-X-PLAYLIST-TYPE
Enum
Тип плейліста. Можливі значення:
  • VOD — статичний контент (кінець файлу позначений #EXT-X-ENDLIST)
  • EVENT — live з можливістю перемотування (DVR)
Якщо тег відсутній — це live стрім без DVR.
#EXTINF
Float,String
Тривалість наступного сегмента (у секундах). Опціонально: опис після коми.
  • Формат: #EXTINF:<duration>,[description]
Приклад:
#EXTINF:10.500,This is chapter 1
segment_001.ts
#EXT-X-ENDLIST
Flag
Позначає кінець VOD-відео. Після цього тега сегментів немає. Плеєр зупиняє оновлення плейліста.
  • Якщо тег відсутній → це live стрім, плеєр продовжує оновлювати плейліст кожні TARGETDURATION/2 секунд.

Приклад Media Playlist (Live):

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:4521

#EXTINF:6.000,
segment_04521.ts
#EXTINF:6.000,
segment_04522.ts
#EXTINF:6.000,
segment_04523.ts
#EXTINF:6.000,
segment_04524.ts
#EXTINF:6.000,
segment_04525.ts
Примітка: Live плейліст НЕ має тега #EXT-X-ENDLIST. Плеєр періодично оновлює цей файл (кожні 3 секунди, якщо TARGETDURATION=6), щоб отримати нові сегменти.

3.3. Формати сегментів: MPEG-TS vs fMP4

MPEG-2 Transport Stream — класичний контейнер для HLS.

Структура:

┌─────────────┬─────────────┬─────────────┬─────────────┐
│ TS Packet   │ TS Packet   │ TS Packet   │ TS Packet   │
│ (188 bytes) │ (188 bytes) │ (188 bytes) │ (188 bytes) │
└─────────────┴─────────────┴─────────────┴─────────────┘

Кожен пакет:

  • Header (4 bytes): Sync byte (0x47), PID (ідентифікатор потоку), counters
  • Payload (184 bytes): Аудіо, відео або метадані

Переваги:

  • ✅ Дуже простий формат
  • ✅ Можна склеїти файли командою cat seg1.ts seg2.ts > full.ts
  • ✅ Працює навіть якщо частина файлу пошкоджена (error resilience)

Недоліки:

  • ❌ Багато overhead (заголовки займають ~10-15% розміру)
  • ❌ Не можна використовувати для DASH (потрібен окремий набір сегментів)
Рекомендація 2024+: Використовуйте fMP4 (CMAF). Це дозволяє мати один набір сегментів для HLS (Apple) і DASH (Android). Google рекомендує CMAF для YouTube.
  • Як створити:
ffmpeg -i input.mp4 \
  -c:v libx264 -c:a aac \
  -f hls -hls_segment_type fmp4 \
  -hls_playlist_type vod \
  output.m3u8

3.4. Як працює HLS під капотом

Loading diagram...
@startuml
skinparam style plain
skinparam defaultFontName Arial

actor User
participant "📱 Player" as Player
participant "🌐 CDN" as CDN
database "💾 Origin" as Origin

User -> Player: Натискає Play
activate Player

Player -> CDN: GET /master.m3u8
activate CDN
CDN -> Origin: Cache miss?
activate Origin
Origin --> CDN: master.m3u8
deactivate Origin
CDN --> Player: master.m3u8
deactivate CDN

Player -> Player: Парсинг master.m3u8\nОбирає 720p варіант

Player -> CDN: GET /720p/playlist.m3u8
activate CDN
CDN --> Player: playlist.m3u8\n(список сегментів)
deactivate CDN

Player -> Player: Парсинг playlist\nБуферизація 3 сегментів

loop Кожні 6 секунд
    Player -> CDN: GET /720p/segment_001.ts
    activate CDN
    CDN --> Player: segment_001.ts (2 MB)
    deactivate CDN

    Player -> Player: Декодування H.264\nВідтворення відео

    Player -> Player: Bandwidth Detection:\nШвидкість = 3 Mbps
    Player -> Player: Якість 720p OK (2.5 Mbps)
end

note over Player
  Після кожних 3-5 сегментів:
  Перевірка швидкості →
  Можливе переключення якості
end note

User -> Player: Мережа погіршилась
Player -> Player: Bandwidth: 1 Mbps 📉
Player -> Player: Перемикається на 480p

Player -> CDN: GET /480p/playlist.m3u8
activate CDN
CDN --> Player: playlist.m3u8
deactivate CDN

Player -> CDN: GET /480p/segment_015.ts
activate CDN
CDN --> Player: segment (меньший розмір)
deactivate CDN

Player -> Player: Безшовне відтворення\nЯкість знизилась, але без зависання

@enduml

Пояснення кроків:

1. Завантаження Master Playlist

Плеєр завжди починає з завантаження master.m3u8. Якщо файл закешований на CDN — відповідь приходить за 50-100 мс.

2. Вибір початкової якості

Плеєр не знає швидкості інтернету на старті. Тому обирає середню якість (зазвичай 720p або 480p). Агресивні плеєри (YouTube) починають з найвищої якості.

3. Завантаження Media Playlist

Обравши якість (наприклад, 720p), плеєр завантажує /720p/playlist.m3u8 — список конкретних сегментів.

4. Початкова буферизація

Плеєр завантажує 3-5 сегментів (30-50 секунд відео) в буфер перед початком відтворення. Це захист від мікро-зависань.

5. Паралельне завантаження та відтворення

Поки програє перший сегмент, плеєр вже завантажує 4-й, 5-й сегменти. Це конвеєр (pipelining).

6. Вимірювання швидкості (Bandwidth Detection)

При завантаженні кожного сегмента плеєр вимірює:

Швидкість = (Розмір сегмента в бітах) / (Час завантаження)

Приклад:

  • Сегмент: 2 MB = 16 Мбіт
  • Час завантаження: 4 секунди
  • Швидкість: 16 / 4 = 4 Mbps

7. Рішення про переключення

Кожні 3-5 сегментів плеєр аналізує:

Якщо (Середня швидкість > Поточний bitrate × 1.2):
    Спробувати вищу якість
Якщо (Середня швидкість < Поточний bitrate × 0.8):
    ТЕРМІНОВО знизити якість

8. Seamless Switch (безшовне переключення)

Плеєр НЕ перезавантажується. Він просто:

  1. Завантажує playlist нової якості (/480p/playlist.m3u8)
  2. Знаходить сегмент з такою самою часовою міткою (timestamp)
  3. Продовжує відтворення з нового потоку

Користувач бачить зміну якості, але без паузи.


3.5. Adaptive Bitrate (ABR): Серце HLS

ABR — це алгоритм автоматичного вибору якості. Без нього HLS був би звичайним завантаженням відео.

Як працює ABR

Loading diagram...
graph TD
    A["🎬 Початок відтворення"] --> B["📊 Обрати початкову якість<br/>(Medium, 720p)"]
    B --> C["📥 Завантажити сегмент"]
    C --> D["⏱️ Виміряти швидкість<br/>Speed = Size / Time"]
    D --> E{"🤔 Швидкість<br/>достатня?"}

    E -->|"Швидкість > 120% bitrate"| F["⬆️ Спробувати вищу якість"]
    E -->|"Швидкість OK"| G["✅ Залишити поточну якість"]
    E -->|"Швидкість < 80% bitrate"| H["⬇️ ТЕРМІНОВО знизити якість"]

    F --> C
    G --> C
    H --> C

    E -->|"Буфер порожній!"| I["🚨 PANIC: Найнижча якість"]
    I --> C

    style A fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style E fill:#f59e0b,stroke:#b45309,color:#ffffff
    style F fill:#10b981,stroke:#047857,color:#ffffff
    style H fill:#ef4444,stroke:#dc2626,color:#ffffff
    style I fill:#dc2626,stroke:#991b1b,color:#ffffff

Приклад реального ABR-алгоритму (спрощений)

// Це псевдокод, що показує логіку hls.js
class ABRController {
    constructor() {
        this.bandwidthHistory = [] // Історія вимірювань швидкості
        this.currentLevel = 2 // Поточна якість (0=lowest, 4=highest)
    }

    // Викликається після завантаження кожного сегмента
    onSegmentLoaded(segmentSize, downloadTime) {
        // 1. Виміряти швидкість
        const bandwidth = (segmentSize * 8) / downloadTime // біти/сек
        this.bandwidthHistory.push(bandwidth)

        // Тримаємо тільки останні 5 вимірювань
        if (this.bandwidthHistory.length > 5) {
            this.bandwidthHistory.shift()
        }

        // 2. Розрахувати середню швидкість (Exponential Moving Average)
        const avgBandwidth = this.calculateEMA()

        // 3. Доступні якості (bitrate'и в бітах/сек)
        const levels = [
            { bitrate: 500000, name: '360p' }, // 0
            { bitrate: 1000000, name: '480p' }, // 1
            { bitrate: 2500000, name: '720p' }, // 2
            { bitrate: 5000000, name: '1080p' }, // 3
            { bitrate: 8000000, name: '1080p60' }, // 4
        ]

        // 4. Знайти оптимальну якість з запасом 20%
        const targetBandwidth = avgBandwidth * 0.8 // Запас 20%
        let newLevel = 0

        for (let i = levels.length - 1; i >= 0; i--) {
            if (levels[i].bitrate <= targetBandwidth) {
                newLevel = i
                break
            }
        }

        // 5. Антифлікер: не змінювати якість занадто часто
        if (Math.abs(newLevel - this.currentLevel) === 1) {
            // Якщо різниця тільки 1 рівень — чекаємо ще 2 сегменти
            return
        }

        // 6. Негайне зниження якості при проблемах
        const bufferLength = this.getBufferLength()
        if (bufferLength < 10) {
            // Менше 10 секунд у буфері
            console.warn('⚠️ Low buffer! Switching to lower quality')
            newLevel = Math.max(0, this.currentLevel - 2)
        }

        // 7. Переключення
        if (newLevel !== this.currentLevel) {
            console.log(`📊 Quality switch: ${levels[this.currentLevel].name}${levels[newLevel].name}`)
            this.currentLevel = newLevel
            this.switchToLevel(newLevel)
        }
    }

    calculateEMA() {
        // Exponential Moving Average: новіші значення важать більше
        let ema = this.bandwidthHistory[0]
        const alpha = 0.3 // Sensitivity factor

        for (let i = 1; i < this.bandwidthHistory.length; i++) {
            ema = alpha * this.bandwidthHistory[i] + (1 - alpha) * ema
        }

        return ema
    }
}

6.2. Performance Optimization (продовження)

🚀 Preloading

Браузер може завантажити перші сегменти ДО того, як користувач натисне Play.

<link rel="preload" as="fetch" href="https://cdn.example.com/master.m3u8" crossorigin />
<link rel="preload" as="fetch" href="https://cdn.example.com/720p/segment_000.ts" crossorigin />

Економія: 500-800ms швидше старт відтворення.

📦 Segment Size Tuning

Правило: Розмір сегмента = Bitrate × Duration

Приклад:

  • Bitrate: 2.5 Mbps = 2,500,000 bits/sec
  • Duration: 6 секунд
  • Розмір: 2,500,000 × 6 ÷ 8 = 1.875 MB

Оптимальні значення:

  • Mobile (3G/4G): 1-2 MB (4-6 сек по 2 Mbps)
  • Desktop (Wi-Fi): 3-5 MB (6-10 сек по 5 Mbps)

🌐 CDN Caching Headers

Правильні Cache-Control headers економлять bandwidth:

# Master Playlist: короткий TTL (для Live)
location ~ master\.m3u8$ {
    add_header Cache-Control "public, max-age=5, stale-while-revalidate=10";
}

# Media Playlist: середній TTL
location ~ \.m3u8$ {
    add_header Cache-Control "public, max-age=10, stale-while-revalidate=30";
}

# Segments: довгий TTL (immutable)
location ~ \.(ts|m4s)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

⚡ HTTP/2 Server Push

Сервер може "проштовхнути" сегменти разом з плейлістом:

http2_push /720p/segment_000.ts;
http2_push /720p/segment_001.ts;

Економія: 1 RTT (Round Trip Time) на кожен сегмент.

6.3. Low Latency HLS (LL-HLS)

Traditional HLS latency: 20-40 секунд

LL-HLS latency: 2-6 секунд (як у WebRTC!)

Ключові зміни в LL-HLS:

Partial Segments
Feature
Сегмент ділиться на часткові фрагменти (0.5-1 секунда). Плеєр не чекає повного сегмента (6 сек), а завантажує по 0.5 сек.
#EXT-X-PART:DURATION=0.500,URI="segment_001_part_0.m4s"
#EXT-X-PART:DURATION=0.500,URI="segment_001_part_1.m4s"
#EXT-X-PART:DURATION=0.500,URI="segment_001_part_2.m4s"
Playlist Delta Updates
Feature
Плеєр отримує тільки нові рядки плейліста, а не весь файл. Економія bandwidth.
Blocking Playlist Reload
Feature
HTTP запит до плейліста не повертається доти, поки не з'явиться новий сегмент. Це уникає зайвих polling-запитів.HTTP/2 Server Push використовується для instant delivery.
Увага: LL-HLS вимагає:
  • HTTP/2 (або HTTP/3)
  • Спеціальний origin server (Apple Unified Streaming, Wowza, AWS MediaLive)
  • iOS 14+ / macOS 11+ для нативної підтримки
  • hls.js v1.1.0+ для інших браузерів
Use case: Спортивні ставки в реальному часі, інтерактивні live шоу, аукціони.

6.4. Demos та Експерименти

Demo 1: Ручний HLS Плеєр (Shell Script)

Напишемо плеєр на Bash, який завантажує та програє HLS вручну:

#!/bin/bash

HLS_URL="https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8"
OUTPUT_DIR="hls_download"

mkdir -p $OUTPUT_DIR
cd $OUTPUT_DIR

echo "📥 Step 1: Download Master Playlist"
curl -s "$HLS_URL" > master.m3u8
cat master.m3u8

echo -e "\n📊 Step 2: Choose quality (picking first variant)"
VARIANT_URL=$(grep -v '^#' master.m3u8 | head -1)
BASE_HLS_URL="https://test-streams.mux.dev/x36xhzz"
FULL_VARIANT_URL="$BASE_HLS_URL/$VARIANT_URL"

echo "Selected: $FULL_VARIANT_URL"

echo -e "\n📥 Step 3: Download Media Playlist"
curl -s "$FULL_VARIANT_URL" > playlist.m3u8
cat playlist.m3u8

echo -e "\n🎬 Step 4: Download first 5 segments"
BASE_URL=$(dirname "$FULL_VARIANT_URL")
SEGMENTS=$(grep -v '^#' playlist.m3u8 | head -5)

i=1
for segment in $SEGMENTS; do
    echo "Downloading segment $i: $segment"
    curl -s "$BASE_URL/$segment" > "segment_$i.m4s"
    i=$((i+1))
done

echo -e "\n✅ Done! Downloaded 5 segments"
ls -lh *.m4s

echo -e "\n▶️  Step 5: Play (requires ffplay)"
# Concatenate segments and play
cat segment_*.m4s > combined.m4s
ffplay combined.m4s

# Or open in default video player
# open combined.m4s  # macOS
# xdg-open combined.m4s  # Linux

Запуск:

chmod +x hls_manual_player.sh
./hls_manual_player.sh

Що ви побачите:

📥 Step 1: Download Master Playlist
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-STREAM-INF:BANDWIDTH=2227464,AVERAGE-BANDWIDTH=2218327,RESOLUTION=960x540...

📊 Step 2: Choose quality (picking first variant)
Selected: https://.../v2/prog_index.m3u8

📥 Step 3: Download Media Playlist
#EXTM3U
#EXT-X-TARGETDURATION:6
#EXTINF:6.00600,...

🎬 Step 4: Download first 5 segments
Downloading segment 1: main1.m4s
Downloading segment 2: main2.m4s
...

✅ Done! Downloaded 5 segments
-rw-r--r--  1 user  staff   1.2M main1.m4s
...

Demo 2: Симуляція Live Streaming

Створимо fake live stream з існуючого відео:

#!/bin/bash

INPUT_VIDEO="movie.mp4"
OUTPUT_DIR="live_stream"
SEGMENT_DURATION=4

mkdir -p $OUTPUT_DIR

# Крок 1: Розбити відео на сегменти
echo "🔪 Segmenting video..."
ffmpeg -i "$INPUT_VIDEO" \
  -c copy \
  -f segment \
  -segment_time $SEGMENT_DURATION \
  -reset_timestamps 1 \
  "$OUTPUT_DIR/segment_%05d.ts"

# Крок 2: Симулювати live додавання сегментів до playlist
echo "🔴 Starting live simulation..."

PLAYLIST="$OUTPUT_DIR/live.m3u8"
MEDIA_SEQUENCE=0
MAX_SEGMENTS=5  # Sliding window (тримаємо тільки 5 останніх сегментів)

# Ініціалізація playlist
cat > "$PLAYLIST" <<EOF
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:$SEGMENT_DURATION
#EXT-X-MEDIA-SEQUENCE:$MEDIA_SEQUENCE
EOF

# Додаємо сегменти по одному (кожні 4 секунди)
for segment in $OUTPUT_DIR/segment_*.ts; do
    SEGMENT_NAME=$(basename "$segment")

    echo "➕ Adding segment: $SEGMENT_NAME (sequence: $MEDIA_SEQUENCE)"

    # Додаємо новий сегмент
    echo "#EXTINF:$SEGMENT_DURATION.0," >> "$PLAYLIST"
    echo "$SEGMENT_NAME" >> "$PLAYLIST"

    # Зберігаємо тільки останні N сегментів (sliding window)
    TOTAL_SEGMENTS=$(grep -c "^segment_" "$PLAYLIST")
    if [ $TOTAL_SEGMENTS -gt $MAX_SEGMENTS ]; then
        # Видаляємо перший сегмент з плейліста
        sed -i.bak '/#EXTINF/,+1d' "$PLAYLIST"  # Видаляє перші 2 рядки (#EXTINF + filename)
        MEDIA_SEQUENCE=$((MEDIA_SEQUENCE + 1))

        # Оновлюємо MEDIA-SEQUENCE
        sed -i.bak "s/#EXT-X-MEDIA-SEQUENCE:.*/#EXT-X-MEDIA-SEQUENCE:$MEDIA_SEQUENCE/" "$PLAYLIST"
    fi

    echo "📄 Current playlist:"
    cat "$PLAYLIST"
    echo ""

    # Чекаємо 4 секунди (симуляція real-time)
    sleep 4
done

# Додаємо ENDLIST (трансляція закінчилась)
echo "#EXT-X-ENDLIST" >> "$PLAYLIST"
echo "✅ Live simulation ended"

Запуск:

./simulate_live.sh

Що відбувається:

  1. Відео розбивається на сегменти по 4 секунди
  2. Кожні 4 секунди новий сегмент додається до live.m3u8
  3. Старі сегменти видаляються (sliding window)
  4. MEDIA-SEQUENCE збільшується

Результат: Ви бачите, як плейліст live stream оновлюється в real-time!

Demo 3: Візуалізація якості ABR

Створимо HTML сторінку, яка показує зміни якості в real-time:

<!DOCTYPE html>
<html>
    <head>
        <title>HLS ABR Visualizer</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                max-width: 1200px;
                margin: 0 auto;
                padding: 20px;
                background: #0f172a;
                color: #e2e8f0;
            }

            video {
                width: 100%;
                max-width: 800px;
                border-radius: 8px;
            }

            .stats {
                display: grid;
                grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
                gap: 15px;
                margin-top: 20px;
            }

            .stat-card {
                background: #1e293b;
                padding: 15px;
                border-radius: 8px;
                border-left: 4px solid #3b82f6;
            }

            .stat-label {
                font-size: 12px;
                color: #94a3b8;
                text-transform: uppercase;
            }

            .stat-value {
                font-size: 24px;
                font-weight: bold;
                margin-top: 5px;
            }

            #quality-chart {
                margin-top: 30px;
                height: 200px;
                background: #1e293b;
                border-radius: 8px;
                padding: 20px;
                position: relative;
            }

            .quality-bar {
                height: 100%;
                display: flex;
                align-items: flex-end;
                gap: 2px;
            }

            .bar {
                flex: 1;
                background: #3b82f6;
                border-radius: 2px 2px 0 0;
                transition: height 0.3s;
            }
        </style>
    </head>
    <body>
        <h1>🎬 HLS Adaptive Bitrate Visualizer</h1>

        <video id="video" controls></video>

        <div class="stats">
            <div class="stat-card">
                <div class="stat-label">Current Quality</div>
                <div class="stat-value" id="current-quality">-</div>
            </div>

            <div class="stat-card">
                <div class="stat-label">Bandwidth</div>
                <div class="stat-value" id="bandwidth">- Mbps</div>
            </div>

            <div class="stat-card">
                <div class="stat-label">Buffer Length</div>
                <div class="stat-value" id="buffer">- sec</div>
            </div>

            <div class="stat-card">
                <div class="stat-label">Dropped Frames</div>
                <div class="stat-value" id="dropped-frames">0</div>
            </div>
        </div>

        <div id="quality-chart">
            <h3>Quality History (Last 60 seconds)</h3>
            <div class="quality-bar" id="quality-bars"></div>
        </div>

        <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
        <script>
            const video = document.getElementById('video')
            const HLS_URL = 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8'

            const qualityHistory = []
            const MAX_HISTORY = 60

            if (Hls.isSupported()) {
                const hls = new Hls({
                    debug: false,
                    enableWorker: true,
                })

                hls.loadSource(HLS_URL)
                hls.attachMedia(video)

                // Оновлення статистики кожну секунду
                setInterval(() => {
                    updateStats(hls)
                }, 1000)

                // Відслідковування змін якості
                hls.on(Hls.Events.LEVEL_SWITCHED, (event, data) => {
                    const level = hls.levels[data.level]
                    console.log(`Quality switched to: ${level.height}p`)

                    qualityHistory.push(level.height)
                    if (qualityHistory.length > MAX_HISTORY) {
                        qualityHistory.shift()
                    }

                    updateQualityChart()
                })

                hls.on(Hls.Events.MANIFEST_PARSED, () => {
                    video.play()
                })
            }

            function updateStats(hls) {
                const level = hls.levels[hls.currentLevel]
                if (!level) return

                // Current Quality
                document.getElementById('current-quality').textContent = `${level.height}p`

                // Bandwidth (Mbps)
                const bandwidth = (hls.bandwidthEstimate / 1000000).toFixed(2)
                document.getElementById('bandwidth').textContent = `${bandwidth} Mbps`

                // Buffer Length
                const buffer = video.buffered.length > 0 ? (video.buffered.end(0) - video.currentTime).toFixed(1) : 0
                document.getElementById('buffer').textContent = `${buffer} sec`

                // Dropped Frames
                const dropped = video.getVideoPlaybackQuality?.().droppedVideoFrames || 0
                document.getElementById('dropped-frames').textContent = dropped
            }

            function updateQualityChart() {
                const container = document.getElementById('quality-bars')
                container.innerHTML = ''

                const maxQuality = Math.max(...qualityHistory)

                qualityHistory.forEach((quality) => {
                    const bar = document.createElement('div')
                    bar.className = 'bar'
                    const heightPercent = (quality / maxQuality) * 100
                    bar.style.height = `${heightPercent}%`

                    // Колір залежно від якості
                    if (quality >= 1080) bar.style.background = '#10b981'
                    else if (quality >= 720) bar.style.background = '#3b82f6'
                    else if (quality >= 480) bar.style.background = '#f59e0b'
                    else bar.style.background = '#ef4444'

                    container.appendChild(bar)
                })
            }
        </script>
    </body>
</html>

Що показує демо:

  • Поточну якість відео (360p / 720p / 1080p)
  • Виміряну швидкість інтернету
  • Довжину буфера
  • Графік змін якості за останню хвилину

7. Troubleshooting та Діагностика

7.1. Типові Проблеми

7.2. Інструменти Діагностики

🔍 Chrome DevTools Network Tab

Як використовувати:

  1. Відкрийте DevTools (F12)
  2. Вкладка Network
  3. Фільтр: Залишіть тільки .m3u8 та .ts файли
  4. Натисніть Play на відео

Що дивитись:

  • Time: Час завантаження (має бути < тривалості сегмента)
  • Size: Розмір файлу (має відповідати bitrate × duration)
  • Status: Код відповіді (200 = OK, 404 = не знайдено, 403 = заборонено)

📊 VLC Media Info

VLC показує детальну інформацію про стрім:

  1. Відкрийте HLS в VLC: Media → Open Network Stream
  2. Вставте https://cdn.example.com/master.m3u8
  3. Tools → Codec Information (Ctrl+J)

Що дивитись:

  • Кодеки (має бути H.264 + AAC)
  • Bitrate (реальний vs очікуваний)
  • Dropped frames (якщо > 0 — проблема з decode)

⚙️ FFprobe для аналізу сегментів

# Перевірити кодеки
ffprobe -v error -show_streams segment_001.ts

# Перевірити тривалість
ffprobe -v error -show_entries format=duration segment_001.ts

# Перевірити bitrate
ffprobe -v error -show_entries format=bit_rate segment_001.ts

Очікуваний output:

duration=6.006000
bit_rate=2534567

🌐 Online HLS Validators

Сервіси для перевірки:

Вставте URL вашого master.m3u8 і отримайте звіт про помилки.


8. HLS vs Інші Протоколи

8.1. HLS vs DASH

HTTP Live Streaming від Apple

Переваги:

  • ✅ Натівна підтримка iOS/macOS/Safari
  • ✅ Простіший формат (M3U8 текстові плейлісти)
  • ✅ Менша затримка setup (швидше стартує)
  • ✅ Краща екосистема (більше CDN provider'ів підтримують)

Недоліки:

  • ❌ Формат плейліста не XML (складніше парсити програмно)
  • ❌ Менше опцій для DRM (тільки FairPlay для Apple)

Порівняльна таблиця:

ХарактеристикаHLSDASH
РозробникAppleMPEG
Формат ManifestM3U8 (текст)MPD (XML)
Нативна підтримка iOS✅ Так❌ Потрібен dash.js
Нативна підтримка Android⚠️ Частково✅ Так (ExoPlayer)
Латентність (традиційна)20-40 сек10-30 сек
Латентність (Low Latency)2-6 сек (LL-HLS)3-5 сек (LL-DASH)
DRMFairPlayWidevine, PlayReady
CDN підтримкаВідміннаДуже добра
ЕкосистемаБільшаМенша
Рекомендація 2024+:
  • Якщо ваша аудиторія primarily iOS/Safari → HLS
  • Якщо потрібен DRM (Netflix-style protection) → DASH + Widevine
  • Оптимальне рішення: CMAF (Common Media Application Format) — один набір сегментів і для HLS, і для DASH!
ffmpeg -i input.mp4 \
  -c:v libx264 -c:a aac \
  -f hls -hls_segment_type fmp4 \  fMP4 (CMAF-сумісний)
  output_hls.m3u8

# Той самий набір .m4s файлів можна використати для DASH

8.2. HLS vs RTMP

HLSRTMP
ПротоколHTTP/HTTPSTCP (порт 1935)
Firewall friendly✅ Так (порт 80/443)❌ Блокується фаєрволами
CDN✅ Працює❌ Потрібен спеціальний сервер
Латентність20-40 сек (traditional)
2-6 сек (LL-HLS)
2-5 сек
Підтримка браузерів✅ Всі (нативно або hls.js)❌ Помер з Flash
Use caseVOD, Live, DVRТільки Live (більше не використовується)
RTMP помер у 2020 році разом із Flash Player. Використовуйте тільки для:
  • Ingest (OBS → сервер кодування)
  • Legacy systems (старі камери відеоспостереження)
Для delivery до глядачів завжди використовуйте HLS або DASH.

9. Практичні Завдання

Завдання 1: Створити HLS стрім з MP4

Мета: Перетворити movie.mp4 на HLS з 3 варіантами якості.

Кроки:

  1. Встановіть FFmpeg
  2. Створіть bash-скрипт, який генерує три варіанти: 360p (800 Kbps), 720p (2.5 Mbps), 1080p (5 Mbps)
  3. Створіть Master Playlist вручну
  4. Запустіть локальний HTTP сервер (Python або Nginx)
  5. Відкрийте в браузері

Критерії успіху:

  • Плеєр перемикає якість автоматично (можна побачити в DevTools)
  • Відео програє без зависань

Завдання 2: Налаштувати Nginx для HLS

Мета: Створити production-ready Nginx конфігурацію.

Вимоги:

  • CORS headers (щоб працював з будь-якого домену)
  • Правильні Cache-Control headers (10 сек для .m3u8, 1 рік для .ts)
  • Gzip compression для плейлістів
  • Логування (окремий access.log для HLS)

Перевірка:

curl -I https://your-domain.com/master.m3u8 | grep Cache-Control
# Має бути: Cache-Control: public, max-age=10

Завдання 3: Діагностика проблемного стріма

Даний buggy HLS:

https://test-broken-hls.example.com/master.m3u8

Проблеми:

  • Відео зависає кожні 20 секунд
  • Якість не перемикається автоматично
  • У Safari працює, у Chrome — ні

Завдання:

  1. Використайте DevTools Network tab
  2. Завантажте master.m3u8 та проаналізуйте
  3. Завантажте перший сегмент та перевірте FFprobe
  4. Знайдіть та опишіть мінімум 3 проблеми

Підказка: Перевірте CORS, bitrate mismatch, codec compatibility.


10. FAQ (Часті Питання)

::

A: Так! Потрібен:

  • Кодек: H.265/HEVC або AV1 (H.264 занадто великий bitrate для 4K)
  • Bitrate: 15-25 Mbps для 4K 30fps, 35-50 Mbps для 4K 60fps
  • Safari підтримує HEVC нативно, Chrome потрібен апаратний декодер

Приклад:

ffmpeg -i input_4k.mp4 \
  -c:v libx265 -preset medium -crf 23 \
  -c:a aac -b:a 192k \
  -f hls -hls_time 6 \
  output_4k.m3u8

A: Використайте Service Worker або IndexedDB:

// Завантажити всі сегменти наперед
async function downloadForOffline(masterUrl) {
    const response = await fetch(masterUrl)
    const playlist = await response.text()

    // Parse playlist, get segments URLs
    const segments = extractSegmentUrls(playlist)

    // Download and store in IndexedDB
    for (const segmentUrl of segments) {
        const segmentBlob = await fetch(segmentUrl).then((r) => r.blob())
        await saveToIndexedDB(segmentUrl, segmentBlob)
    }
}

iOS/Android apps: Використайте нативні API (AVAssetDownloadTask для iOS).

A: Стандартна затримка HLS складається з:

Encoder (2-4 сек) +
Segmentation (6-10 сек) +
CDN propagation (1-3 сек) +
Player buffer (15-30 сек)
─────────────────────────
= 24-47 секунд

Рішення:

  1. Зменшіть segment duration (4 сек замість 10)
  2. Зменшіть player buffer (hls.js config: maxBufferLength: 15)
  3. Використайте LL-HLS (Low Latency HLS) — затримка 2-6 сек

::


11. Резюме та Ключові Висновки

🎯 Що таке HLS

  • Протокол стрімінгу через звичайний HTTP
  • Відео нарізається на маленькі сегменти (2-10 сек)
  • Текстовий плейліст (.m3u8) каже плеєру, що завантажувати
  • ABR автоматично обирає якість

📐 Архітектура

  1. Encoder створює кілька варіантів якості
  2. Origin зберігає сегменти
  3. CDN кешує та роздає
  4. Player вимірює швидкість та перемикає якість

⚙️ Як створити HLS

ffmpeg -i input.mp4 \
  -c:v libx264 -c:a aac \
  -f hls -hls_time 6 \
  output.m3u8

Для багатоякісного: кодуйте кілька варіантів + створіть master.m3u8

🔐 Безпека

  • AES-128 encryption (базовий захист)
  • Signed URLs (token authentication)
  • DRM (FairPlay / Widevine) для premium контенту

Ключові метрики HLS:

Segment Duration
4-10 seconds
Баланс між латентністю та HTTP overhead.
Bitrate Ladder
5-7 variants
Наприклад: 360p, 480p, 720p, 1080p, 1080p60.
Target Buffer
30-60 seconds
Для стабільного відтворення без зависань.
ABR Safety Margin
20%
Обирати якість на 20% нижче виміряної швидкості.

12. Подальше Навчання

📚 Офіційна документація

Apple HLS Authoring Specification (RFC 8216)

🎥 Практичні туторіали

YouTube туторіали по FFmpeg та HLS

🧪 Тестові стріми

Безкоштовні HLS стріми для тестування

💬 Спільнота

Stack Exchange: Video Production

Вітаємо! Ви завершили глибоке занурення в HLS Protocol. Тепер ви розумієте:
  • Як YouTube перемикає якість без зависань
  • Чому Netflix використовує DASH, а Twitch — HLS
  • Як створити власний стрімінг-сервіс
Наступний крок: Вивчіть DASH Protocol для порівняння або перейдіть до CDN Architecture для розуміння масштабування.
Copyright © 2026