Розробка платформи стриминга музики
Стриминг музики — це не просто «віддати mp3 через HTTP». Платформа, яка повинна витримувати навантаження, забезпечувати low-latency відтворення, керувати правами та монетизацією — інженерна задача з десятком нетривіальних вузлів. Нижче — архітектура та реалізація з реальними компромісами.
Протоколи доставки аудіо
Три варіанти, кожен під свою задачу.
Progressive download (псевдостриминг) — найпростіший. Файл віддається через звичайний HTTP з підтримкою Range-запитів. Браузер буферизує та відтворює. Підходить для невеликих бібліотек без суворих обмежень на скачування.
location /audio/ {
root /var/media;
add_header Accept-Ranges bytes;
add_header Cache-Control "no-store"; # для DRM
# X-Accel-Redirect якщо файли за авторизацією
}
HLS (HTTP Live Streaming) — стандарт для продакшну. Файл нарізається на сегменти по 5–10 секунд, клієнт підгружає по манівфесту. Дозволяє адаптивний бітрейт (ABR): клієнт переключається між 128/256/320 kbps залежно від каналу.
# FFmpeg: нарізка в HLS з трьома якостями
ffmpeg -i input.flac \
-filter_complex "[0:a]asplit=3[a1][a2][a3]" \
-map "[a1]" -codec:a aac -b:a 128k -vn \
-hls_time 6 -hls_list_size 0 \
-hls_segment_filename "out/128k_%03d.aac" out/128k.m3u8 \
-map "[a2]" -codec:a aac -b:a 256k -vn \
-hls_time 6 -hls_list_size 0 \
-hls_segment_filename "out/256k_%03d.aac" out/256k.m3u8 \
-map "[a3]" -codec:a aac -b:a 320k -vn \
-hls_time 6 -hls_list_size 0 \
-hls_segment_filename "out/320k_%03d.aac" out/320k.m3u8
Манівфест верхнього рівня (master.m3u8) перелічує варіанти, клієнт вибирає сам.
MPEG-DASH — альтернатива HLS, краще підтримує DRM через EME (Encrypted Media Extensions). Якщо потрібна захист контенту рівня лейблів — DASH + Widevine/FairPlay.
Архітектура обробки контенту
Загрузка треку — це пайплайн, а не просто зберігання файла.
Upload → Validation → Transcoding → Waveform → Fingerprint → CDN → DB
# Celery task: повний пайплайн обробки
from celery import chain
@app.task
def process_upload(track_id: int, raw_path: str):
pipeline = chain(
validate_audio.s(track_id, raw_path),
transcode_variants.s(), # 128/256/320 + HLS segments
generate_waveform.s(), # peaks.json для візуалізації
fingerprint_audio.s(), # AcoustID / Chromaprint
push_to_cdn.s(),
update_track_status.s('ready')
)
pipeline.delay()
@app.task
def transcode_variants(track_id: int, validated_path: str):
qualities = [
('128k', '128k', 'aac'),
('256k', '256k', 'aac'),
('320k', '320k', 'mp3'), # для офлайн-скачування
('lossless', None, 'flac'), # для hi-fi тиру
]
results = []
for name, bitrate, codec in qualities:
out = transcode(validated_path, bitrate, codec)
segment_hls(out, name, track_id)
results.append((name, out))
return track_id, results
Генерація волнової форми
Waveform — стандартний UI елемент. Бібліотека audiowaveform від BBC:
audiowaveform -i track.mp3 -o peaks.json \
--pixels-per-second 10 \
--bits 8
# peaks.json: { "bits": 8, "length": 1234, "data": [-12, 15, -8, 22, ...] }
На фронтенді — WaveSurfer.js або кастомний Canvas-рендер.
Система прав та ліцензування
Без управління правами платформу не запустити легально. Мінімальна модель:
CREATE TABLE tracks (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
duration_sec INT,
isrc CHAR(12), -- International Standard Recording Code
status TEXT DEFAULT 'processing'
);
CREATE TABLE track_rights (
track_id BIGINT REFERENCES tracks(id),
territory CHAR(2), -- ISO 3166-1 alpha-2, NULL = worldwide
right_type TEXT, -- 'stream', 'download', 'sync'
holder_id BIGINT,
expires_at TIMESTAMPTZ,
PRIMARY KEY (track_id, territory, right_type)
);
-- Перевірка права перед виданням URL
CREATE OR REPLACE FUNCTION can_stream(p_track_id BIGINT, p_territory CHAR(2))
RETURNS BOOLEAN AS $$
SELECT EXISTS (
SELECT 1 FROM track_rights
WHERE track_id = p_track_id
AND right_type = 'stream'
AND (territory IS NULL OR territory = p_territory)
AND (expires_at IS NULL OR expires_at > NOW())
);
$$ LANGUAGE sql STABLE;
Signed URLs та захист стрімів
Прямі посилання на файли віддавати нельзя — їх зберегти і поширять. Підписані URL з коротким TTL:
// Laravel: генерація підписаного URL через CDN (Cloudflare / AWS CloudFront)
class StreamController extends Controller
{
public function stream(Request $request, int $trackId): JsonResponse
{
$track = Track::findOrFail($trackId);
// Перевіримо територію за IP
$territory = $this->geoService->getCountry($request->ip());
if (!$track->canStream($territory)) {
return response()->json(['error' => 'not_available'], 451);
}
// Signed URL на 60 секунд — достатньо для початку буферизації
$url = $this->cdn->signedUrl(
path: "hls/{$trackId}/master.m3u8",
ttl: 60,
ip: $request->ip() // прив'язка до IP
);
// Логуємо прослуховування для роялті
StreamEvent::dispatch($trackId, $request->user()->id, now());
return response()->json(['url' => $url]);
}
}
Офлайн та Progressive Web App
Для мобільного PWA — кеширування через Service Worker:
// sw.js: кешируємо сегменти для офлайну
const AUDIO_CACHE = 'audio-v1';
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
if (url.pathname.includes('/hls/') && url.pathname.endsWith('.aac')) {
event.respondWith(
caches.open(AUDIO_CACHE).then(async cache => {
const cached = await cache.match(event.request);
if (cached) return cached;
const response = await fetch(event.request);
if (isInUserLibrary(url)) {
cache.put(event.request, response.clone());
}
return response;
})
);
}
});
Масштабування та CDN
HLS-сегменти — статичні файли, ідеально для CDN. Але: при пиковій навантаженості (новий релиз популярного виконавця) потрібен origin shield — проміжний кеш між CDN та origin, щоб не положити S3/хранилище.
User → CDN Edge (Cloudflare/CloudFront) → Origin Shield → S3/MinIO
Манівфести .m3u8 кешуються з коротким TTL (5–30 сек) — вони змінюються при live. Сегменти .aac/.ts кешуються агресивно (365 днів, immutable), тому що імена включають хеш.
Роялті та статистика
Для кожного прослуховування потрібно рахувати секунди (не просто факт відтворення). Threshold зазвичай 30 секунд по стандартах IFPI.
# Агрегація стрімів для роялті-звітів
@dataclass
class PlaybackEvent:
track_id: int
user_id: int
seconds_played: int
quality: str # '128k', '256k', 'lossless'
timestamp: datetime
def aggregate_streams(events: list[PlaybackEvent]) -> dict:
from collections import defaultdict
counts = defaultdict(int)
for e in events:
if e.seconds_played >= 30:
counts[e.track_id] += 1
return dict(counts)
Пошук по каталогу
Elasticsearch для повнотекстового пошуку з аналізом транслітерації та фонетики:
PUT /tracks
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "standard",
"fields": {
"phonetic": { "type": "text", "analyzer": "phonetic_analyzer" },
"autocomplete": { "type": "search_as_you_type" }
}
},
"artist": { "type": "text", "fields": { "keyword": { "type": "keyword" } } },
"genre": { "type": "keyword" },
"release_date": { "type": "date" },
"play_count": { "type": "long" }
}
}
}
Часові рамки
Базовий стриминг з каталогом, плеєром (HLS, waveform), плейлистами та авторизацією — 10–14 тижнів. Додавання системи прав, роялті-учету та DRM — ще 6–8 тижнів. Рекомендаційний рушій, офлайн-режим PWA, мобільні приложення — окремі треки, оцінюються незалежно.







