Архітектура платформи для прямих трансляцій
Ми часто стикаємося із завданням: компанія хоче запустити свій стрімінг-сервіс, схожий на 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 дні. Пишіть.







