Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.
01Bagaimana cara kerja video streaming?
Video di-transcode sekali ke beberapa resolusi dan bitrate, lalu tiap versi dipotong menjadi segment berdurasi beberapa detik. Segment disimpan sebagai file biasa dan disajikan lewat HTTP, biasanya melalui CDN. Sebuah playlist mendaftarnya, dan player mengunduh satu segment sekali waktu sambil memilih kualitas yang sanggup ia pertahankan.
02Apa itu HLS dan apa isi file .m3u8?
HLS adalah HTTP Live Streaming, didefinisikan di RFC 8216. File .m3u8 adalah playlist teks UTF-8: master playlist mendaftar rendition yang tersedia beserta BANDWIDTH dan RESOLUTION, dan tiap media playlist mendaftar file segment berurutan dengan durasinya.
03Bagaimana adaptive bitrate streaming memilih kualitas?
Player mengukur seberapa cepat tiap segment terunduh, menghaluskannya menjadi estimasi throughput, lalu memilih rendition tertinggi yang bandwidth-nya masih muat di bawah estimasi itu dengan margin pengaman. Player juga mengawasi buffer dan turun satu anak tangga saat buffer menipis. Perpindahan hanya terjadi di batas segment.
04Berapa storage dan bandwidth yang dibutuhkan video streaming?
Tergantung ladder-nya, tetapi hitungannya sederhana: bitrate dikali durasi dibagi 8 menghasilkan byte. Dengan ladder empat anak tangga 5.128, 2.928, 1.528 dan 928 kbps, satu jam video memakan 4,73 GB, dan 10.000 penonton di 2.928 kbps butuh egress 29,28 Gbps. Egress, bukan storage, yang memaksa Anda memakai CDN.
05Apa bedanya live streaming dengan VOD?
VOD memakai playlist final yang berakhir dengan EXT-X-ENDLIST dan tidak pernah berubah, sehingga bisa di-cache lama. Live terus menambah segment dan player me-reload playlist, jadi masa cache-nya harus sekitar satu segment. Live juga meng-encode secara real time dan tertinggal beberapa durasi segment dari kamera.
Desain Sistem Video Streaming: HLS, CDN dan Adaptive Bitrate
Cara kerja video streaming dan cara mendesainnya: transcode ladder, segment dan manifest HLS, object storage, CDN, logika adaptive bitrate, signed URL, serta live versus VOD, lengkap dengan hitungan storage dan egress.
Video streaming bekerja dengan men-transcode setiap upload ke beberapa resolusi dan bitrate, memotong masing-masing menjadi segment berdurasi beberapa detik, menyimpannya sebagai file biasa di object storage, lalu menyajikannya lewat HTTP melalui CDN. Manifest mendaftar semua rendition, dan player mengukur kecepatan unduhnya lalu berpindah kualitas di antara segment.
Hal yang membuat video berbeda dari soal system design lain adalah bahwa tidak ada yang benar-benar di-stream seperti arti katanya. Tidak ada koneksi panjang yang mendorong frame. Yang ada hanya folder berisi file kecil, sebuah playlist teks, dan player yang terus meminta file berikutnya.
Post ini mendesain pipeline itu dari ujung ke ujung: upload, transcode ladder, segmenting, storage, CDN, logika kualitas di player, kontrol akses, dan varian live. Angkanya dihitung dari asumsi yang dinyatakan, bukan hasil pengukuran, dan playlist serta perintah ffmpeg dicek terhadap RFC 8216 dan dokumentasi ffmpeg. Saya menjalankan workload ERP dan POS di satu VPS, jadi saya juga menyebut di mana pendekatan itu berhenti bekerja.
Bagaimana sebenarnya cara kerja video streaming?
Platform streaming adalah pipeline file dengan client yang pintar di ujungnya. Video dikonversi sekali di awal menjadi banyak file kecil dengan beberapa level kualitas, lalu server HTTP biasa yang membagikannya. Alurnya seperti ini:
Upload: file asli masuk ke object storage, biasanya lewat presigned upload sehingga tidak melewati API server Anda.
Transcode: sebuah worker meng-encode file asli menjadi ladder berisi rendition, masing-masing dengan resolusi dan bitrate berbeda.
Segment: tiap rendition dipotong menjadi segment berdurasi beberapa detik, dan sebuah playlist mendaftarnya berurutan.
Store dan distribute: segment dan playlist ditulis ke object storage lalu diletakkan di belakang CDN, yang meng-cache-nya dekat dengan penonton.
Play: player mengunduh master playlist, memilih satu rendition, lalu mengambil segment satu per satu sambil memilih ulang rendition saat kondisi berubah.
Karena pengirimannya HTTP biasa, CDN tidak butuh logika khusus video. Satu segment hanyalah file yang bisa di-cache, dan itulah alasan desain ini bisa scale. Ada tiga format pengiriman yang perlu dibedakan:
Format
Cara beradaptasi
Trade-off
Progressive MP4
Tidak beradaptasi. Satu file, satu kualitas, diambil dengan range request.
Paling sederhana dan cukup untuk klip pendek, tetapi koneksi lambat akan macet atau Anda memilih satu kualitas untuk semua orang.
HLS
Playlist .m3u8 mendaftar rendition dan segment, player berpindah di antaranya. Didefinisikan di RFC 8216.
Diputar native di platform Apple dan lewat player JavaScript di tempat lain. Segment berupa MPEG-TS atau fragmented MP4.
MPEG-DASH
Manifest XML .mpd menjelaskan rendition, player berpindah di antaranya.
Standar terbuka dengan ide segment dan manifest yang sama, diputar di browser lewat Media Source Extensions.
Untuk produk kecil saya memilih HLS: satu format, satu set file, dan library player-nya sudah matang. Mengemas segment yang sama untuk DASH juga adalah optimasi belakangan, bukan kebutuhan hari pertama.
Berapa besar storage dan bandwidth yang dibutuhkan platform video?
Lakukan hitungan ini sebelum memilih infrastruktur apa pun, karena egress, bukan storage, yang menentukan desain. Input di bawah adalah asumsi untuk latihan desain, dan setiap baris diturunkan dari baris di atasnya.
Assumed inputs (a design exercise, not a measurement):
ladder, video + 128 kbps AAC audio:
1080p 5,000 + 128 = 5,128 kbps
720p 2,800 + 128 = 2,928 kbps
480p 1,400 + 128 = 1,528 kbps
360p 800 + 128 = 928 kbps
segment length = 6 s
catalogue = 1,000 hours of source video
concurrent viewers at peak = 10,000, all on the 720p rung
origin network port = 1 Gbps (assumed single VPS)
Storage (kbps x 3,600 s / 8 = kB per hour)
1080p 5,128 x 3,600 / 8 = 2,307,600 kB = 2.31 GB per hour
720p 2,928 x 3,600 / 8 = 1,317,600 kB = 1.32 GB per hour
480p 1,528 x 3,600 / 8 = 687,600 kB = 0.69 GB per hour
360p 928 x 3,600 / 8 = 417,600 kB = 0.42 GB per hour
whole ladder: 10,512 x 3,600 / 8 = 4,730,400 kB = 4.73 GB per hour of video
catalogue: 4.73 GB x 1,000 = 4.73 TB
Objects
segments per rendition per hour = 3,600 / 6 = 600
per hour of video = 600 x 4 renditions = 2,400 files
catalogue = 2,400 x 1,000 = 2,400,000 files
Egress at peak
10,000 x 2,928 kbps = 29,280,000 kbps = 29.28 Gbps
per hour of viewing = 10,000 x 1.3176 GB = 13,176 GB = about 13.2 TB
machines to push that at line rate from a 1 Gbps port = 29.28 / 1 -> 30
Origin load behind a CDN (origin sees only the misses)
95% hit ratio -> 5% x 29.28 Gbps = 1.464 Gbps (still over one port)
99% hit ratio -> 1% x 29.28 Gbps = 0.293 Gbps (fits)
Request rate for 10,000 viewers
6 s segments : 10,000 / 6 = 1,667 segment requests per second
2 s segments : 10,000 / 2 = 5,000 segment requests per second
Dua hasil yang penting. Storage relatif murah: seluruh katalog 1.000 jam muat di 4,73 TB, dan 2,4 juta file adalah masalah jumlah objek, bukan kapasitas. Egress adalah kendala sebenarnya: 10.000 penonton bersamaan butuh 29,28 Gbps, kira-kira tiga puluh port satu gigabit yang berjalan penuh terus-menerus.
Karena itu CDN bukan pilihan di sini. Bahkan dengan hit ratio 95 persen, origin masih menerima 1,464 Gbps, lebih dari satu port, jadi hit ratio yang dibutuhkan mendekati 99 persen. Segment membantu: segment video populer diminta ribuan penonton, jadi diambil dari origin sekali per lokasi edge lalu disajikan dari cache. Untuk cara edge caching bekerja, lihat post tentang cara kerja CDN.
Jangan menyajikan segment dari application server atau satu VPS lalu menyebutnya streaming. Hitungan di atas menunjukkan 10.000 penonton 720p butuh 29,28 Gbps, sedangkan satu port 1 Gbps hanya mampu sekitar 341 penonton seperti itu (1.000.000 kbps dibagi 2.928 kbps). Taruh object storage di belakang CDN sejak hari pertama.
Bagaimana cara men-transcode ke bitrate ladder?
Ladder adalah kumpulan rendition yang bisa dipilih player. Anak tangga di bawah adalah titik awal asumsi saya, bukan standar: 1080p di 5.000 kbps, 720p di 2.800, 480p di 1.400 dan 360p di 800, masing-masing dengan audio AAC 128 kbps. Sesuaikan dengan konten Anda, karena rekaman layar dan olahraga terkompresi sangat berbeda. Satu perintah ffmpeg bisa menghasilkan seluruh ladder beserta playlist-nya.
# One input, four renditions, HLS output. Assumes a 24 fps source.
# -g 144 = 6 s x 24 fps, so every segment starts on a keyframe.
# -sc_threshold 0 stops x264 inserting extra keyframes at scene cuts,
# which would shift segment boundaries differently per rendition.
ffmpeg -i input.mp4 \
-filter_complex "[0:v]split=4[a][b][c][d];[a]scale=-2:1080[v1080];[b]scale=-2:720[v720];[c]scale=-2:480[v480];[d]scale=-2:360[v360]" \
-map "[v1080]" -map "[v720]" -map "[v480]" -map "[v360]" \
-map 0:a:0 -map 0:a:0 -map 0:a:0 -map 0:a:0 \
-c:v libx264 -preset medium -g 144 -keyint_min 144 -sc_threshold 0 \
-b:v:0 5000k -maxrate:v:0 5350k -bufsize:v:0 7500k \
-b:v:1 2800k -maxrate:v:1 2996k -bufsize:v:1 4200k \
-b:v:2 1400k -maxrate:v:2 1498k -bufsize:v:2 2100k \
-b:v:3 800k -maxrate:v:3 856k -bufsize:v:3 1200k \
-c:a aac -b:a 128k \
-f hls -hls_time 6 -hls_playlist_type vod \
-hls_flags independent_segments -hls_segment_type mpegts \
-hls_segment_filename "out/%v/seg_%03d.ts" \
-master_pl_name master.m3u8 \
-var_stream_map "v:0,a:0,name:1080p v:1,a:1,name:720p v:2,a:2,name:480p v:3,a:3,name:360p" \
"out/%v/index.m3u8"
# Result: out/master.m3u8, out/1080p/index.m3u8, out/1080p/seg_000.ts ... and so on per rung.
Tiga detail menopang desainnya. Pertama, -g 144 bersama -sc_threshold 0 memaksa keyframe tiap 6 detik pada 24 fps, dan HLS muxer hanya memotong segment pada keyframe berikutnya setelah hls_time terlewati, sehingga interval keyframe harus sama dengan panjang segment atau segment tidak akan sepanjang yang Anda minta. Kedua, semua rendition harus dipotong pada waktu yang sama, supaya player bisa berpindah di batas segment tanpa lompatan terlihat. Ketiga, -maxrate dan -bufsize membatasi lonjakan bitrate yang harus dijanjikan atribut BANDWIDTH di playlist.
Transcoding adalah langkah yang mahal dan datang berkelompok, jadi perlakukan sebagai antrean job, bukan request handler. Taruh file asli di object storage, enqueue sebuah job, jalankan encoder di container worker, lalu tulis hasilnya kembali. Job yang gagal di-retry dari file asli dan tidak pernah menyentuh apa yang sedang ditonton orang.
Simpan file asli selamanya dan perlakukan setiap rendition sebagai cache darinya. Saat nanti Anda menambah ladder AV1 atau HEVC, atau mengubah panjang segment, Anda menjalankan ulang job dari sumber alih-alih meng-encode ulang rendition yang sudah terkompresi dan kehilangan kualitas dua kali.
Seperti apa manifest HLS (.m3u8) itu?
Ada dua jenis playlist. Master playlist mendaftar rendition, satu baris EXT-X-STREAM-INF untuk masing-masing, dan RFC 8216 mewajibkan atribut BANDWIDTH, yaitu peak segment bit rate dalam bit per detik. AVERAGE-BANDWIDTH, RESOLUTION, FRAME-RATE dan CODECS bersifat opsional tetapi layak diisi agar player bisa menyingkirkan rendition yang tidak bisa didecode.
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-STREAM-INF:BANDWIDTH=984000,AVERAGE-BANDWIDTH=928000,RESOLUTION=640x360,FRAME-RATE=24.000,CODECS="avc1.42c01e,mp4a.40.2"
360p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1626000,AVERAGE-BANDWIDTH=1528000,RESOLUTION=854x480,FRAME-RATE=24.000,CODECS="avc1.4d401e,mp4a.40.2"
480p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3124000,AVERAGE-BANDWIDTH=2928000,RESOLUTION=1280x720,FRAME-RATE=24.000,CODECS="avc1.64001f,mp4a.40.2"
720p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5478000,AVERAGE-BANDWIDTH=5128000,RESOLUTION=1920x1080,FRAME-RATE=24.000,CODECS="avc1.640028,mp4a.40.2"
1080p/index.m3u8
# BANDWIDTH = maxrate + 128k audio (peak). AVERAGE-BANDWIDTH = the target bitrate + audio.
# CODECS must match what the encoder really wrote: confirm with ffprobe, do not copy these blindly.
Setiap rendition punya media playlist sendiri: sebuah target duration, lalu durasi EXTINF dan URI untuk tiap segment. Segment terakhir lebih pendek karena video tidak habis dibagi 6 detik. Tag EXT-X-ENDLIST menandai presentasi yang sudah selesai, dan EXT-X-INDEPENDENT-SEGMENTS menjanjikan bahwa setiap segment dimulai pada keyframe.
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-INDEPENDENT-SEGMENTS
#EXTINF:6.000000,
seg_000.ts
#EXTINF:6.000000,
seg_001.ts
#EXTINF:6.000000,
seg_002.ts
#EXTINF:3.250000,
seg_003.ts
#EXT-X-ENDLIST
# Relative URIs resolve against THIS playlist's URL (RFC 8216), so the playlist and its
# segments can sit in one folder and move between hosts without editing a line.
# #EXT-X-ENDLIST tells the player the presentation is complete: no reloads needed.
Kedua playlist adalah file teks kecil, jadi cache juga, tetapi singkat jika bisa berubah. URI relatif bukan hiasan: RFC 8216 menyatakan URI relatif di-resolve terhadap URI playlist itu sendiri, sehingga satu folder utuh bisa dipindah antar bucket dan hostname CDN.
Bagaimana player memilih dan mengganti kualitas?
Adaptive bitrate adalah loop di sisi client, dan server tidak ikut campur. Setelah tiap segment, player mengukur seberapa cepat segment itu tiba, menghaluskannya menjadi estimasi throughput, lalu memilih rendition tertinggi yang BANDWIDTH-nya masih muat di bawah estimasi itu dengan margin pengaman. Aturan kedua mengawasi buffer: bila video yang ter-buffer terlalu sedikit, player langsung turun satu anak tangga, karena stall lebih buruk daripada gambar yang agak buram. Player nyata seperti hls.js dan Shaka lebih rumit, tetapi inilah intinya.
interface Rung { bandwidth: number; url: string } // BANDWIDTH from the master playlist
const SAFETY = 0.8; // assumed: use 80% of measured throughput, leave headroom
const PANIC_BUFFER_S = 5; // assumed: below this many seconds buffered, drop one rung now
let estimate = 0; // bits per second, smoothed
function onSegmentDownloaded(bytes: number, ms: number) {
const sample = (bytes * 8) / (ms / 1000);
// Exponentially weighted average: one slow segment nudges, not whipsaws, the estimate.
estimate = estimate === 0 ? sample : 0.7 * estimate + 0.3 * sample;
}
function pickRung(rungs: Rung[], current: number, bufferS: number): number {
// rungs are sorted ascending by bandwidth. Return the index to fetch the NEXT segment from.
if (bufferS < PANIC_BUFFER_S) return Math.max(0, current - 1);
let best = 0;
for (let i = 0; i < rungs.length; i++) {
if (rungs[i].bandwidth <= estimate * SAFETY) best = i;
}
return best;
}
// Worked example: measured 4,000,000 bps -> budget 3,200,000 bps.
// 1080p declares 5,478,000 (no), 720p declares 3,124,000 (yes) -> pick 720p.
// Switching only ever happens between segments, which is why they start on keyframes.
Perpindahan hanya terjadi di antara segment, itulah sebabnya ladder dipotong pada waktu yang identik dan segment dimulai pada keyframe. Segment yang lebih pendek membuat player bereaksi lebih cepat tetapi menambah request: pada 10.000 penonton, segment 2 detik berarti 5.000 request per detik dibanding 1.667 untuk segment 6 detik. Konstanta di kode adalah asumsi, jadi sesuaikan dengan data playback nyata, jangan percaya angka saya.
Bagaimana signed URL dan DRM melindungi video?
Ada dua perlindungan berbeda, dan orang sering mencampurnya. Access control menentukan siapa yang boleh mengunduh segment sama sekali: CDN memeriksa signature atau cookie berikut expiry, dan menolak sisanya. Enkripsi dan DRM menentukan apa yang bisa dilakukan pengunduh dengan byte-nya. HLS mendukung enkripsi AES-128 untuk segment lewat tag EXT-X-KEY, dan sistem DRM komersial menambahkan license server serta penanganan key berbasis hardware di atasnya. Dokumentasi CDN, misalnya CloudFront, membedakan signed URL untuk satu file dari signed cookie untuk banyak file.
// NestJS-style sketch: a short-lived token that covers a whole folder, not one file.
import { createHmac } from "node:crypto";
function playbackToken(videoId: string, userId: string, ttlSeconds = 4 * 3600): string {
const expires = Math.floor(Date.now() / 1000) + ttlSeconds;
const scope = "/v/" + videoId + "/"; // every rendition + segment below this path
const payload = scope + "|" + userId + "|" + expires;
const sig = createHmac("sha256", process.env.CDN_SIGNING_KEY!).update(payload).digest("hex");
return Buffer.from(payload + "|" + sig).toString("base64url");
}
// Wrong: sign each URL with a query string. The master playlist lists RELATIVE segment URIs,
// and a relative URI does not inherit the playlist's query string, so segments 403.
// Right: put the token in a cookie or a path prefix that every request under /v/<id>/ carries.
// Each CDN defines its own token format and validation; this only shows the shape of the idea.
Satu playlist merujuk puluhan segment, jadi signing per URL merepotkan. Token yang mencakup satu folder utuh, di cookie atau di path, adalah bentuk yang berhasil. Mulailah dengan akses bertanda tangan dan expiry pendek. Tambahkan DRM hanya bila kontrak atau pemegang hak mewajibkannya, karena DRM membawa license server, integrasi per platform dan biaya nyata, dan penonton yang bertekad tetap bisa merekam layar.
Apa bedanya live streaming dengan VOD?
Live memakai segment dan playlist yang sama, tetapi playlist terus bertambah dan encoder tidak pernah selesai. Bedanya ada pada timing dan pada apa yang bisa di-cache:
Aspek
VOD
Live
Media playlist
Lengkap dan tetap, diakhiri EXT-X-ENDLIST.
Tanpa ENDLIST selama live; player terus me-reload untuk segment baru.
Encoding
Job offline, selambat yang diperlukan.
Real time: tiap rendition harus ter-encode lebih cepat daripada video diputar.
Caching playlist
TTL panjang aman, karena tidak pernah berubah.
TTL harus kira-kira satu segment atau kurang, atau penonton melihat playlist basi.
Jeda dari waktu nyata
Tidak ada.
Minimal beberapa durasi segment, dihitung di bawah.
RFC 8216 menetapkan hitungannya. Server harus menerbitkan versi playlist baru tidak lebih awal dari setengah target duration dan tidak lebih lambat dari 1,5 target duration setelah versi sebelumnya, dan tidak boleh memperpendek playlist live di bawah tiga kali target duration. Dengan segment 6 detik itu berarti minimal 18 detik playlist, jadi penonton biasanya 18 detik atau lebih di belakang kamera. Memotong segment menjadi 2 detik membawanya mendekati 6 detik dengan harga request tiga kali lipat. Varian HLS latensi rendah memang ada, tetapi itu desain tersendiri yang lebih menuntut.
Desain mana yang sebaiknya dipilih tim kecil? Sebuah checklist
Kebanyakan produk tidak perlu membangun ini. Jika Anda men-stream beberapa jam video pelatihan ke beberapa ratus orang, layanan video terkelola adalah pilihan yang masuk akal. Jika Anda tetap membangunnya, inilah urutan yang akan saya ikuti:
Simpan upload asli di object storage dan jangan pernah menimpanya.
Jalankan transcoding sebagai job worker dalam antrean, satu input menghasilkan seluruh ladder, dengan interval keyframe sama dengan panjang segment.
Terbitkan HLS dulu, dengan panjang segment tetap, dan isi BANDWIDTH serta CODECS dari apa yang benar-benar dihasilkan encoder.
Taruh object storage di belakang CDN dan rencanakan hit ratio 99 persen, bukan 95, memakai puncak penonton Anda dikali bitrate anak tangga tertinggi.
Lindungi akses dengan token berexpiry di path atau cookie sebelum mempertimbangkan DRM.
Tambahkan live hanya saat dibutuhkan: ia mengubah encoding, TTL caching dan jeda sekaligus.
Di satu VPS, saya memakai mesin itu untuk API dan transcode worker, dan menaruh setiap byte yang menuju penonton di object storage dan CDN. VPS tidak sanggup melayani egress, seperti ditunjukkan hitungan tadi, tetapi cukup untuk mengorkestrasi job. Untuk sisi storage, lihat perbandingan cloud storage dan MinIO sebelumnya.
Video streaming adalah pipeline file dengan client yang pintar. Encode sekali menjadi ladder, potong setiap anak tangga pada keyframe yang sama, terbitkan segment sebagai objek biasa yang bisa di-cache, dan biarkan CDN menyerap egress sementara player memilih kualitasnya sendiri. Hitung egress lebih dulu, karena dialah, bukan storage, yang menentukan apakah desain ini muat di hardware Anda.