02. Live Streaming Platform — Low-Level Design¶
~18 min read · Part 3 of 4 (Overview → HLD → LLD → Q&A)
Data models¶
CREATE TABLE stream (
channel_id BIGINT PRIMARY KEY,
stream_key TEXT, -- broadcaster secret for ingest auth
status SMALLINT, -- live / offline
started_at TIMESTAMP,
ingest_node TEXT -- current ingest server
);
-- Live manifest (rolling, in edge/cache):
-- /live/{channel}/{rendition}/index.m3u8 = last K segments only
-- #EXT-X-MEDIA-SEQUENCE grows; old segments fall out of the window
-- Chat room (transient): room:{channel} pub/sub topic; optional ring buffer of last N msgs
-- Viewer count (approx): hll:{channel} = HyperLogLog / sampled counter
The live manifest is a sliding window — it lists only the last K segments (#EXT-X-MEDIA-SEQUENCE advances, old segments drop). That's the core difference from VOD's complete manifest: a live player always requests near the live edge, so only recent segments need to exist at the edge.
Component internals¶
Real-time segmenter — racing the clock¶
def on_encoded_frames(channel, rendition, frames):
buffer.append(frames)
if buffer.duration() >= SEG_SEC: # ~2s target
seg = package(buffer.flush())
n = seq.next(channel, rendition)
blob.put_live(channel, rendition, n, seg)
cdn.push_to_edges(channel, rendition, n, seg) # warm as produced
manifest.append_and_roll(channel, rendition, n, window=K)
The whole pipeline is a race against wall-clock: segment N must be produced, published, and pushed to edges in less time than it takes viewers to drain their buffer. Shorter SEG_SEC lowers latency but multiplies per-segment overhead (more requests, more manifest churn) — the central live tuning knob.
The latency fork — chunked HLS vs WebRTC¶
Chunked HLS/LL-HLS (this design):
latency ≈ segment_dur + player_buffer → ~2–6 s
scales to millions via ordinary CDN caching of segments
best for: large audiences, one-to-many, non-interactive
WebRTC:
latency < 500 ms (peer/SFU real-time transport)
needs an SFU (selective forwarding unit) fleet, not plain CDN caching
best for: interactive (auctions, betting, video calls), smaller fan-out
The choice is a genuine tradeoff, not a default. For the 5M-viewer esports final, chunked HLS is correct: a few seconds of latency is fine and CDN caching is what makes 20 Tbps affordable. For a sub-second interactive stream (a live auction), WebRTC via an SFU is worth its far higher per-viewer cost. Naming why you pick one is the senior move.
Chat fan-out — reduce, don't brute-force¶
def on_chat_message(channel, user, text):
if not rate_limiter.allow(user, channel): # slow mode / per-user cap
return reject(user)
msg = {user, text, ts}
room_pubsub.publish(channel, msg) # → all gateway shards for channel
def gateway_deliver(channel, msg):
conns = local_conns[channel]
if len(conns) > SAMPLE_THRESHOLD: # huge room
if not sample(msg): return # show a legible subset
for c in conns: c.send(msg)
The delivery product (50k msgs/s × 5M viewers) is physically impossible and pointless — no human reads 50k/s. So chat reduces fan-out: per-user rate limits cut the input, and on very large channels the gateway samples/batches what it forwards so each viewer sees a readable stream. Correctness for chat means "legible and timely," not "every message to everyone."
Core algorithm — approximate concurrent viewer count¶
join(channel, viewer_id):
hll[channel].add(viewer_id) # HyperLogLog: O(1), tiny memory, ~2% error
viewer_count(channel):
return hll[channel].estimate() # good enough for a live number
Millions joining at stream start would crush an exact counter. A HyperLogLog gives a viewer count within a couple percent using kilobytes — the right tradeoff, since "5.0M vs 5.02M viewers" is a display detail, not a decision input.
Sequence diagram — live segment race + chat¶
Broadcaster Ingest Transcode Packager Edge/CDN Viewer Chat gw PubSub
│ RTMP push │ │ │ │ │ │ │
├───────────▶│ frames ─▶│ encode ──▶│ seg N ────▶│ warm │ │ │
│ │ │ │ │◀ GET segN │ │ │
│ │ │ │ │── segN ──▶│ (plays) │ │
│ │ │ │ │ │ "GG!" ──▶ │ publish▶│
│ │ │ │ │ │◀── msg ── │◀───────┤ fan to shards
Concurrency and edge cases¶
- Encoder falling behind: shed the top rendition and/or lengthen segments briefly to regain real-time; never let the live edge drift unbounded.
- Broadcaster reconnect: ingest holds a short buffer and resumes the same
#EXT-X-MEDIA-SEQUENCEso viewers see a brief stall, not a broken stream. - Late chat joiner: an optional ring buffer of the last N messages gives context on join without replaying the whole room.
- Duplicate chat delivery across shards: dedup by message id at the gateway before sending to a connection.
- DVR seek during live: seeking back reads retained segments (VOD store); returning to live jumps to the manifest's newest sequence.
- Segment window eviction: a viewer whose buffer stalls past the window falls off the live edge and must re-join at the current sequence (can't request an evicted segment).
- Stream end → VOD: on
status=offline, retained segments are sealed into a complete manifest served by the OTT path.