Архітектура платформи для прямих трансляцій
Ми часто стикаємося із завданням: компанія хоче запустити свій стрімінг-сервіс, схожий на Twitch, але з власними правилами монетизації та контенту. Типова проблема — затримка зростає при піку до 10 секунд, якість падає, а інфраструктура не витримує навантаження. За 5+ років ми розробили понад 15 streaming-проєктів, від невеликих educational-платформ до великих media. Розберемо, як побудувати надійну live-систему з нуля з урахуванням вимог до затримки, сумісності з пристроями та масштабування до тисяч глядачів. В основі вибору протоколів лежать такі критерії, як затримка, підтримка CDN та сумісність із браузерами.
Схема доставки та протоколи
Стрімер → Ingest → Transcoding → CDN → Viewer
Вхідний потік від стрімера — зазвичай RTMP (OBS, StreamLabs, XSplit всі вміють RTMP із коробки). На виході до глядача — HLS або DASH для браузера, WebRTC для ultra-low-latency (< 1 сек).
RTMP забезпечує низьку затримку на етапі прийому, але не підходить для доставки через блокування фаєрволами. HLS — стандарт де-факто, працює через HTTP і легко кешується на CDN.
OBS/FFMPEG → RTMP → Nginx-RTMP/SRS/Wowza → FFmpeg transcoding
↓
HLS segments → S3/CDN
WebRTC → Selective Forwarding Unit
Чому SRS кращий за інші ingest-сервери?
SRS (Simple Realtime Server) — опенсорс, Go, чудово тримає навантаження до 10K+ підключень на одному сервері. У наших проєктах SRS показав продуктивність на 30% вищу, ніж Wowza, при ідентичній конфігурації. Налаштування через один конфіг:
# srs.conf
listen 1935;
max_connections 1000;
daemon off;
http_server {
enabled on;
listen 8080;
dir ./objs/nginx/html;
}
vhost __defaultVhost__ {
# Хук: повідомляємо backend про початок/кінець трансляції
http_hooks {
enabled on;
on_publish http://api:8000/hooks/stream/start;
on_unpublish http://api:8000/hooks/stream/stop;
on_play http://api:8000/hooks/stream/view;
}
hls {
enabled on;
hls_path ./objs/nginx/html;
hls_fragment 2; # 2 секунди — баланс затримки та стабільності
hls_window 10; # 10 сегментів у вікні
}
transcode {
enabled on;
ffmpeg /usr/local/bin/ffmpeg;
engine hd {
enabled on;
vcodec libx264;
vbitrate 2000;
vfps 30;
vwidth 1280; vheight 720;
acodec aac;
abitrate 128;
output rtmp://localhost:1935/[app]/[stream]_720p;
}
engine sd {
enabled on;
vcodec libx264;
vbitrate 800;
vfps 30;
vwidth 854; vheight 480;
acodec aac;
abitrate 96;
output rtmp://localhost:1935/[app]/[stream]_480p;
}
}
}
Аутентифікація стрімера
Стрімер публікує потік за stream key. Не можна приймати RTMP від невідомих джерел:
# FastAPI: хук для SRS on_publish
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
class PublishHook(BaseModel):
action: str
app: str
stream: str # stream key від стрімера
param: str # query string
@app.post("/hooks/stream/start")
async def on_stream_start(hook: PublishHook):
# Валідуємо stream key
streamer = await db.fetchrow(
"SELECT id, user_id, is_active FROM stream_keys WHERE key = $1",
hook.stream
)
if not streamer or not streamer['is_active']:
raise HTTPException(status_code=403, detail="Invalid stream key")
# Запускаємо трансляцію в БД
await db.execute("""
INSERT INTO live_streams (user_id, stream_key_id, started_at, status)
VALUES ($1, $2, NOW(), 'live')
ON CONFLICT (stream_key_id) DO UPDATE SET started_at = NOW(), status = 'live'
""", streamer['user_id'], streamer['id'])
# Повідомляємо підписників через WebSocket
await notify_followers(streamer['user_id'], 'stream_started')
return {"code": 0} # SRS очікує code=0 для дозволу
Як оптимізувати завантаження HLS-сегментів у S3?
Моніторимо директорію із сегментами через cron або системний таймер. Для .m3u8 файлів встановлюємо Cache-Control: max-age=2, для .ts — max-age=86400, immutable. Використовуємо aws s3 cp з правильними заголовками.
Як працює чат у реальному часі?
Чат трансляції — обов'язковий елемент. WebSocket через Redis Pub/Sub з rate limiting та зберіганням ковзного вікна повідомлень:
// Node.js: WebSocket-сервер для чату
import { WebSocketServer } from 'ws';
import { createClient } from 'redis';
const wss = new WebSocketServer({ port: 3001 });
const redis = createClient({ url: process.env.REDIS_URL });
const redisSub = redis.duplicate();
await redis.connect();
await redisSub.connect();
interface ChatMessage {
type: 'message' | 'emote' | 'sub' | 'ban';
streamId: string;
userId: string;
username: string;
text: string;
badges: string[];
timestamp: number;
}
// Підписуємося на канал трансляції
wss.on('connection', (ws, req) => {
const streamId = new URL(req.url!, 'ws://x').searchParams.get('stream');
if (!streamId) return ws.close();
const channel = `chat:${streamId}`;
// Слухаємо Redis Pub/Sub для цього стріму
redisSub.subscribe(channel, (message) => {
if (ws.readyState === ws.OPEN) {
ws.send(message);
}
});
ws.on('message', async (data) => {
const msg: ChatMessage = JSON.parse(data.toString());
// Антиспам: rate limit на користувача
const key = `chat_limit:${msg.userId}:${streamId}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, 5);
if (count > 20) { // 20 повідомлень за 5 секунд — забагато
ws.send(JSON.stringify({ type: 'slowmode', waitMs: 5000 }));
return;
}
// Зберігаємо в Redis Stream (ковзне вікно 1000 повідомлень)
await redis.xAdd(`stream_chat:${streamId}`, '*', msg as any, {
TRIM: { strategy: 'MAXLEN', threshold: 1000 }
});
// Публікуємо всім підключеним
await redis.publish(channel, JSON.stringify(msg));
});
ws.on('close', () => {
redisSub.unsubscribe(channel);
});
});
Запис трансляцій у VOD
Після закінчення стріму склеюємо TS-сегменти та перекодовуємо з faststart для псевдострімінгу. Офіційна документація FFmpeg рекомендує використовувати прапорець -movflags +faststart для переміщення атома moov на початок файлу, що прискорює початок відтворення.
# Celery task: конвертація в VOD
@app.task
def process_vod(stream_id: int):
stream = LiveStream.objects.get(id=stream_id)
segments = sorted(
glob(f"/var/srs/hls/{stream.stream_key}/*.ts"),
key=lambda f: int(Path(f).stem.split('_')[-1])
)
concat_list = "/tmp/vod_concat.txt"
with open(concat_list, 'w') as f:
for s in segments: f.write(f"file '{s}'\n")
raw_mp4 = f"/tmp/vod_{stream_id}_raw.mp4"
subprocess.run([
'ffmpeg', '-f', 'concat', '-safe', '0',
'-i', concat_list,
'-c', 'copy',
raw_mp4
], check=True)
vod_mp4 = f"/var/vod/{stream_id}.mp4"
subprocess.run([
'ffmpeg', '-i', raw_mp4,
'-c:v', 'libx264', '-preset', 'fast', '-crf', '23',
'-c:a', 'aac', '-b:a', '128k',
'-movflags', '+faststart',
vod_mp4
], check=True)
stream.vod_path = vod_mp4
stream.status = 'ended'
stream.save()
Як налаштувати транскодинг у кілька якостей: покрокова інструкція
- Встановіть SRS та FFmpeg на сервер.
- У конфігу SRS увімкніть секцію
transcodeта вкажіть шляхи до FFmpeg. - Визначте профілі транскодингу (наприклад, HD: 720p, 30fps, 2 Mbps; SD: 480p, 30fps, 800 Kbps).
- Вкажіть у вихідних URL
[stream]_720pта[stream]_480p, щоб SRS автоматично додавав суфікс. - Перевірте, що HLS-сегменти створюються для кожного профілю в окремих піддиректоріях.
- Протестуйте з OBS, надсилаючи потік на RTMP ingest. Переконайтеся, що плеєр може перемикатися між quality levels.
Порівняння протоколів доставки
| Протокол | Затримка | Сумісність | Кешування CDN | Використання |
|---|---|---|---|---|
| RTMP | < 1 сек | Flash/старі плеєри | Ні | Ingest |
| HLS | 2-30 сек | HTML5, iOS, Android | Так | Доставка |
| DASH | 2-10 сек | HTML5, SmartTV | Так | Доставка |
| WebRTC | < 500 мс | Браузери, P2P | Ні | Інтерактив |
Порівняння популярних CDN для HLS-доставки
| CDN | Кешування HLS | Origin Shield | GeoDNS | Ціна за TB (орієнтовно) |
|---|---|---|---|---|
| Cloudflare | Так | Так | Так | $0.036 |
| AWS CloudFront | Так | Так | Так | $0.085 |
| Fastly | Так | Так | Так | $0.10 |
| Akamai | Так | Так | Так | >$0.15 |
Що входить у розробку платформи під ключ
- Архітектурне проектування: вибір протоколів, CDN, capacity planning.
- Ingest-сервер: налаштування SRS/Nginx-RTMP, транскодинг у кілька якостей.
- Бекенд: API для керування стрімами, аутентифікація, монетизація.
- Фронтенд: кастомізований плеєр, дашборд стрімера, чат.
- Інфраструктура: Docker-контейнеризація, автоскейлінг, моніторинг.
- Документація: повна технічна документація, інструкції для адміністраторів.
- Навчання: сесія для команди, підтримка 2 тижні після запуску.
- Гарантія: виправляємо баги протягом місяця безкоштовно.
Масштабування: багатосерверний ingest
Один ingest-сервер — SPOF. Для продакшну потрібен кластер з балансуванням:
DNS → Load Balancer (GeoDNS) → Ingest cluster
↓
Transcoding workers (GPU)
↓
HLS → S3 → CDN
Стрімери направляються на найближчий ingest за GeoDNS. Кожен ingest пише в загальне об'єктне сховище або реплікує сегменти синхронно. Зв'яжіться з нами для обговорення архітектури вашого проєкту — ми підберемо оптимальну конфігурацію.
Строки
MVP із RTMP-прийомом, HLS-доставкою, WebSocket-чатом та записом у VOD — 10–12 тижнів. Додавання транскодингу кількох якостей, gift-subscriptions, модерації чату, мобільного плеєра — ще 8–10 тижнів. Масштабування до 10k+ concurrent viewers, балансування ingest, CDN з origin shield — окремий етап.
Хочете запустити свій стрімінг-сервіс? Отримайте консультацію — оцінимо ваш проєкт за 2 дні. Пишіть.







