Уявіть ситуацію: ви дивитесь відео на YouTube у Full HD (1080p). Раптом ваш Wi-Fi погіршується (хтось почав завантажувати великий файл). Відео не зависає! Воно автоматично перемикається на 720p, потім на 480p. Коли інтернет знову стабілізується — якість повертається до 1080p. Як це працює?
Це магія HLS (HTTP Live Streaming) — протоколу, який змінив індустрію стрімінгу назавжди.
У часи Flash Player відео стрімилось через RTMP (Real-Time Messaging Protocol). Це був бінарний протокол, який працював через порт 1935.
Проблеми RTMP:
Apple хотіла стрімити відео на iPhone 3GS. Проблема: Flash не працював на iOS. Рішення: створити протокол, який:
Результат: У червні 2009 року Apple представила HLS разом з iPhone 3GS.
🌐 Працює через HTTP
📊 Adaptive Bitrate
🚀 CDN-сумісність
📱 Нативний для Apple
<video>.Приклади: Twitch, YouTube Live, спортивні трансляції
Особливості:
Use case:
Стрімер грає в CS:GO → OBS кодує в HLS → CDN роздає 100,000 глядачам
Приклади: Netflix, Amazon Prime, YouTube (записані відео)
Особливості:
Use case:
Користувач вибирає серіал → CDN віддає готовий HLS → перегляд без буферизації
Приклади: IPTV-сервіси, спортивні трансляції з перемоткою
Особливості:
EXT-X-PLAYLIST-TYPE:EVENTUse case:
Футбольний матч live → глядач запізнився → перемотує на початок → дивиться з затримкою
Перед вивченням HLS корисно розуміти:
<img src="photo.jpg">), ви легко зрозумієте HLS — це той самий принцип, але для відео.HLS (HTTP Live Streaming) — це протокол потокової передачі медіа-контенту через HTTP, розроблений компанією Apple. Він базується на архітектурі сегментованого стрімінгу та адаптивного вибору бітрейту.
Ключові поняття:
Пояснення потоку:
master.m3u8, index.m3u8, playlist.m3u81080p/playlist.m3u8, 720p/playlist.m3u8.ts — MPEG-2 Transport Stream (класичний).m4s — Fragmented MP4 (сучасний, CMAF-сумісний)Уявіть дуже товсту книгу (фільм на 2 години):
Ви беріть книгу у руки і бачите на обкладинці:
"Ця книга доступна мовами:
- Українська (сторінка 1)
- English (сторінка 500)
- Deutsch (сторінка 1000)"
Це Master Playlist — він каже, які варіанти доступні.
Ви відкриваєте сторінку 1 (українська версія) і бачите зміст:
Розділ 1: сторінки 10-25
Розділ 2: сторінки 26-40
Розділ 3: сторінки 41-55
Це Media Playlist — список конкретних фрагментів.
Ви не читаєте всю книгу за раз. Ви читаєте Розділ 1 (15 сторінок), потім Розділ 2 (14 сторінок), і так далі.
Кожний розділ — це Segment (фрагмент відео).
📋 Головне меню
Master Playlist
Ви заходите в ресторан і отримуєте головне меню:
Меню:
- Англійською (сторінка 2)
- Французькою (сторінка 10)
- Китайською (сторінка 20)
::card{title="🍕 Розділ "Піца"" icon="i-lucide-pizza"} Media Playlist
Ви обираєте англійську версію і відкриваєте розділ "Піца":
Піца:
1. Маргарита
2. Пепероні
3. Чотири сири
::
🍽️ Конкретна страва
Segment
Ви обираєте "Маргарита" і отримуєте конкретну порцію їжі. Це і є сегмент — те, що ви реально "споживаєте".
Master Playlist — це текстовий файл UTF-8 з розширенням .m3u8. Формат базується на M3U (MP3 URL), але з додатковими тегами для HLS.
#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
#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 — це середня швидкість. Плеєр використовує ці значення для вибору якості.Практичний приклад:Media Playlist містить список конкретних сегментів для відтворення.
#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
TARGETDURATION=10, плеєр буферує мінімум 30 секунд (3 сегменти).#EXT-X-MEDIA-SEQUENCE:1523 ← перший сегмент має номер 1523
segment_01523.ts
segment_01524.ts
segment_01525.ts
#EXT-X-ENDLIST)#EXTINF:<duration>,[description]#EXTINF:10.500,This is chapter 1
segment_001.ts
TARGETDURATION/2 секунд.#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
#EXT-X-ENDLIST. Плеєр періодично оновлює цей файл (кожні 3 секунди, якщо TARGETDURATION=6), щоб отримати нові сегменти.MPEG-2 Transport Stream — класичний контейнер для HLS.
Структура:
┌─────────────┬─────────────┬─────────────┬─────────────┐
│ TS Packet │ TS Packet │ TS Packet │ TS Packet │
│ (188 bytes) │ (188 bytes) │ (188 bytes) │ (188 bytes) │
└─────────────┴─────────────┴─────────────┴─────────────┘
Кожен пакет:
Переваги:
cat seg1.ts seg2.ts > full.tsНедоліки:
Fragmented MP4 — сучасний контейнер, заснований на ISO Base Media File Format.
Структура:
init.mp4 (Header: кодеки, роздільна здатність, sample rate)
↓
segment_001.m4s (тільки відео/аудіо дані)
segment_002.m4s
segment_003.m4s
Переваги:
Недоліки:
init.mp4 (плеєр спершу завантажує header, потім сегменти)ffmpeg -i input.mp4 \
-c:v libx264 -c:a aac \
-f hls -hls_segment_type fmp4 \
-hls_playlist_type vod \
output.m3u8
Пояснення кроків:
Плеєр завжди починає з завантаження master.m3u8. Якщо файл закешований на CDN — відповідь приходить за 50-100 мс.
Плеєр не знає швидкості інтернету на старті. Тому обирає середню якість (зазвичай 720p або 480p). Агресивні плеєри (YouTube) починають з найвищої якості.
Обравши якість (наприклад, 720p), плеєр завантажує /720p/playlist.m3u8 — список конкретних сегментів.
Плеєр завантажує 3-5 сегментів (30-50 секунд відео) в буфер перед початком відтворення. Це захист від мікро-зависань.
Поки програє перший сегмент, плеєр вже завантажує 4-й, 5-й сегменти. Це конвеєр (pipelining).
При завантаженні кожного сегмента плеєр вимірює:
Швидкість = (Розмір сегмента в бітах) / (Час завантаження)
Приклад:
Кожні 3-5 сегментів плеєр аналізує:
Якщо (Середня швидкість > Поточний bitrate × 1.2):
Спробувати вищу якість
Якщо (Середня швидкість < Поточний bitrate × 0.8):
ТЕРМІНОВО знизити якість
Плеєр НЕ перезавантажується. Він просто:
/480p/playlist.m3u8)Користувач бачить зміну якості, але без паузи.
ABR — це алгоритм автоматичного вибору якості. Без нього HLS був би звичайним завантаженням відео.
// Це псевдокод, що показує логіку 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
}
}
🚀 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
Приклад:
Оптимальні значення:
🌐 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) на кожен сегмент.
Traditional HLS latency: 20-40 секунд
LL-HLS latency: 2-6 секунд (як у WebRTC!)
#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"
Напишемо плеєр на 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
...
Створимо 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
Що відбувається:
live.m3u8MEDIA-SEQUENCE збільшуєтьсяРезультат: Ви бачите, як плейліст live stream оновлюється в real-time!
Створимо 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>
Що показує демо:
Симптом:
Access to fetch at 'https://cdn.example.com/master.m3u8' from origin 'https://mysite.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
Причина: Сервер не віддає CORS headers.
Рішення:
Додайте в Nginx конфігурацію:
location ~ \.(m3u8|ts|m4s)$ {
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods 'GET, OPTIONS' always;
add_header Access-Control-Allow-Headers 'Range' always;
if ($request_method = OPTIONS) {
return 204;
}
}
Можливі причини:
curl -w "@curl-format.txt" https://cdn.example.com/segment.tsСимптом: Відео перемикається між 720p і 1080p кожні 10 секунд.
Причина: Нестабільна мережа або надто агресивний ABR алгоритм.
Рішення:
// Налаштуйте hls.js для більшої стабільності
const hls = new Hls({
abrEwmaDefaultEstimate: 1000000, // Початковий estimate (1 Mbps)
abrBandWidthFactor: 0.95, // Більш консервативний вибір (за замовчуванням 0.95)
abrBandWidthUpFactor: 0.7, // Повільніше підвищення якості
})
Симптом: Live стрім зупиняється після 5-го сегмента.
Причина: Плеєр припинив оновлювати playlist (побачив #EXT-X-ENDLIST).
Діагностика:
# Перевірте, чи є ENDLIST в playlist
curl https://cdn.example.com/live/playlist.m3u8 | grep ENDLIST
Рішення:
#EXT-X-ENDLIST з Live плейлістаПричина: Несумісний кодек.
Діагностика:
# Перевірте кодеки в плейлісті
curl https://cdn.example.com/master.m3u8 | grep CODECS
# Очікуваний результат:
# CODECS="avc1.64001f,mp4a.40.2"
# ^^^^^^^^^^^ ^^^^^^^^^
# H.264 AAC
Рішення:
Симптом:
Failed to load resource: the server responded with a status of 404 (Not Found)
https://cdn.example.com/segment_045.ts
Можливі причини:
Діагностика:
# Спробуйте завантажити сегмент вручну
curl -I https://cdn.example.com/segment_045.ts
# Якщо 404 → перевірте, чи файл існує на origin
ssh user@origin-server "ls -lh /var/www/hls/segment_045.ts"
🔍 Chrome DevTools Network Tab
Як використовувати:
.m3u8 та .ts файлиЩо дивитись:
bitrate × duration)📊 VLC Media Info
VLC показує детальну інформацію про стрім:
https://cdn.example.com/master.m3u8Що дивитись:
⚙️ 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 і отримайте звіт про помилки.
HTTP Live Streaming від Apple
Переваги:
Недоліки:
Dynamic Adaptive Streaming over HTTP від MPEG Industry
Переваги:
Недоліки:
Порівняльна таблиця:
| Характеристика | HLS | DASH |
|---|---|---|
| Розробник | Apple | MPEG |
| Формат Manifest | M3U8 (текст) | MPD (XML) |
| Нативна підтримка iOS | ✅ Так | ❌ Потрібен dash.js |
| Нативна підтримка Android | ⚠️ Частково | ✅ Так (ExoPlayer) |
| Латентність (традиційна) | 20-40 сек | 10-30 сек |
| Латентність (Low Latency) | 2-6 сек (LL-HLS) | 3-5 сек (LL-DASH) |
| DRM | FairPlay | Widevine, PlayReady |
| CDN підтримка | Відмінна | Дуже добра |
| Екосистема | Більша | Менша |
ffmpeg -i input.mp4 \
-c:v libx264 -c:a aac \
-f hls -hls_segment_type fmp4 \ ← fMP4 (CMAF-сумісний)
output_hls.m3u8
# Той самий набір .m4s файлів можна використати для DASH
| HLS | RTMP | |
|---|---|---|
| Протокол | HTTP/HTTPS | TCP (порт 1935) |
| Firewall friendly | ✅ Так (порт 80/443) | ❌ Блокується фаєрволами |
| CDN | ✅ Працює | ❌ Потрібен спеціальний сервер |
| Латентність | 20-40 сек (traditional) 2-6 сек (LL-HLS) | 2-5 сек |
| Підтримка браузерів | ✅ Всі (нативно або hls.js) | ❌ Помер з Flash |
| Use case | VOD, Live, DVR | Тільки Live (більше не використовується) |
Мета: Перетворити movie.mp4 на HLS з 3 варіантами якості.
Кроки:
Критерії успіху:
Мета: Створити production-ready Nginx конфігурацію.
Вимоги:
Перевірка:
curl -I https://your-domain.com/master.m3u8 | grep Cache-Control
# Має бути: Cache-Control: public, max-age=10
Даний buggy HLS:
https://test-broken-hls.example.com/master.m3u8
Проблеми:
Завдання:
Підказка: Перевірте CORS, bitrate mismatch, codec compatibility.
A: Ні, HLS має затримку 2-40 секунд (залежно від режиму). Для real-time комунікації потрібен WebRTC (латентність < 500 ms).
Use case HLS: Вебінари, де зворотній зв'язок не критичний. Use case WebRTC: Zoom, Google Meet, Discord calls.
A: Залежить від трафіку:
Приклад: 1000 глядачів, кожен дивиться 1 годину (2 Mbps якість):
1000 користувачів × 1 година × 2 Mbps = 2,000,000 Мбіт = 250 GB
Вартість: 250 GB × $0.04 = $10
A: Використовуйте комбінацію методів:
::
A: Так! Потрібен:
Приклад:
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 секунд
Рішення:
maxBufferLength: 15)::
🎯 Що таке HLS
📐 Архітектура
⚙️ Як створити HLS
ffmpeg -i input.mp4 \
-c:v libx264 -c:a aac \
-f hls -hls_time 6 \
output.m3u8
Для багатоякісного: кодуйте кілька варіантів + створіть master.m3u8
🔐 Безпека