Music Streaming Platform Development
Music streaming is not just "serve mp3 via HTTP". Platform withstanding load, ensuring low-latency playback, managing rights and monetization — is engineering task with dozen non-trivial nodes. Below — architecture and implementation with real compromises.
Audio delivery protocols
Three options, each for its task.
Progressive download (pseudo-streaming) — simplest. File served via HTTP with Range-request support. Browser buffers and plays. Suits small libraries without strict download limits.
location /audio/ {
root /var/media;
add_header Accept-Ranges bytes;
add_header Cache-Control "no-store"; # for DRM
# X-Accel-Redirect if files need authorization
}
HLS (HTTP Live Streaming) — production standard. File cut into 5–10 second segments, client loads by manifest. Allows adaptive bitrate (ABR): client switches between 128/256/320 kbps by bandwidth.
# FFmpeg: cut to HLS with three qualities
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
Top-level manifest (master.m3u8) lists variants, client chooses.
MPEG-DASH — HLS alternative, better DRM support via EME (Encrypted Media Extensions). If label-level content protection needed — DASH + Widevine/FairPlay.
Content processing architecture
Track upload — pipeline, not just save file.
Upload → Validation → Transcoding → Waveform → Fingerprint → CDN → DB
# Celery task: full processing pipeline
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 for visualization
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'), # for offline download
('lossless', None, 'flac'), # for hi-fi tier
]
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 generation
Waveform — standard UI element. BBC's audiowaveform library:
audiowaveform -i track.mp3 -o peaks.json \
--pixels-per-second 10 \
--bits 8
# peaks.json: { "bits": 8, "length": 1234, "data": [-12, 15, -8, 22, ...] }
Frontend — WaveSurfer.js or custom Canvas render.
Rights and licensing system
Without rights management can't launch legally. Minimal model:
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)
);
-- Check rights before giving 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 and stream protection
Can't give direct file links — they'll be saved. Signed URLs with short TTL:
// Laravel: generate signed URL via CDN (Cloudflare / AWS CloudFront)
class StreamController extends Controller
{
public function stream(Request $request, int $trackId): JsonResponse
{
$track = Track::findOrFail($trackId);
// Check territory by IP
$territory = $this->geoService->getCountry($request->ip());
if (!$track->canStream($territory)) {
return response()->json(['error' => 'not_available'], 451);
}
// Signed URL 60 seconds — enough for buffering start
$url = $this->cdn->signedUrl(
path: "hls/{$trackId}/master.m3u8",
ttl: 60,
ip: $request->ip() // IP binding
);
// Log playback for royalties
StreamEvent::dispatch($trackId, $request->user()->id, now());
return response()->json(['url' => $url]);
}
}
Offline and Progressive Web App
For mobile PWA — caching via Service Worker:
// sw.js: cache segments for offline
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;
})
);
}
});
Scaling and CDN
HLS segments — static files, ideal for CDN. But: on peak load (new popular release) need origin shield — intermediate cache between CDN and origin to avoid overwhelming S3/storage.
User → CDN Edge (Cloudflare/CloudFront) → Origin Shield → S3/MinIO
Manifests .m3u8 cached short TTL (5–30 sec) — change on live. Segments .aac/.ts cached aggressively (365 days, immutable) because names include hash.
Royalties and stats
Each playback counted seconds (not just play fact). Threshold usually 30 seconds per IFPI.
# Stream aggregation for royalty reports
@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)
Catalog search
Elasticsearch for full-text with transliteration and phonetics:
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" }
}
}
}
Timeline
Basic streaming with catalog, player (HLS, waveform), playlists and auth — 10–14 weeks. Add rights system, royalty accounting and DRM — another 6–8 weeks. Recommendation engine, offline PWA, mobile apps — separate, estimated independently.







