Before a single frame of adaptive video reaches the screen, the player downloads a small text file: the manifest. Everything the player does — which quality to pick, where the segments live, whether it's live or on-demand — comes from reading it. For HLS, that file is an .m3u8 playlist, and once you can read one, a lot of "why is the video doing that?" stops being mysterious.
There are two kinds, and the player reads them in order.
The master playlist: the quality ladder#
The first file the player fetches is the master (or multivariant) playlist. It doesn't point at video — it points at other playlists, one per quality rung:
#EXTM3U #EXT-X-VERSION:3 #EXT-X-STREAM-INF:BANDWIDTH=1280000,RESOLUTION=720x480,CODECS="avc1.42c01e,mp4a.40.2" 480p.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=1280x720,CODECS="avc1.4d401f,mp4a.40.2" 720p.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=6000000,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2" 1080p.m3u8
Each #EXT-X-STREAM-INF line is one rung of the adaptive-bitrate ladder. BANDWIDTH is the number ABR logic watches: the player estimates your throughput and picks the highest rung it can sustain without stalling. CODECS lets it skip rungs the device can't decode; RESOLUTION lets it avoid rungs larger than the screen it's drawing to. That's the whole menu — and the player hasn't touched a byte of video yet.
The media playlist: the segments#
Pick a rung and you get a media playlist — the actual list of chunks:
#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:6 #EXT-X-MEDIA-SEQUENCE:0 #EXTINF:6.000, segment0.ts #EXTINF:6.000, segment1.ts #EXT-X-ENDLIST
#EXTINF is each segment's duration; #EXT-X-TARGETDURATION is the ceiling for any of them. The player downloads segments in order, appends them to a buffer, and plays. Switching quality just means fetching the next segment from a different media playlist — same timeline, different rung. That's why ABR can change resolution mid-playback without a visible reload. It only works because every rung is cut at the same segment boundaries, with a keyframe at the start of each segment — so the player can swap rungs at any boundary without restarting playback. Get that alignment wrong at encode time and switches stall or flash; it's the un-glamorous part that decides whether ABR feels smooth.
Live vs. on-demand: one line#
The difference between a movie and a live channel is mostly a single tag. #EXT-X-ENDLIST says "that's all the segments" — on-demand. Drop it, and the playlist is live: the player re-fetches it every few seconds to discover new segments as they're encoded, using #EXT-X-MEDIA-SEQUENCE to know where it left off. #EXT-X-DISCONTINUITY marks a break in the timeline — an ad insert, a stream splice — so the decoder resets cleanly instead of glitching.
That's the core of it: a ladder of qualities, a list of segments per quality, and a couple of tags that say live-or-not and where the seams are — all in plain text you can curl and read.
I skipped the parts that make it interesting at scale — fMP4/CMAF segments, alternate audio and subtitle tracks via #EXT-X-MEDIA, byte-range requests, and Low-Latency HLS — but the shape above is the spine underneath all of them.
Try it#
You don't need a player — the manifest is just text. Two curls walk the whole path a player takes. (On Windows, run these in WSL or use curl.exe — PowerShell's curl alias is a different tool.)
1. Fetch the master playlist — the quality ladder:
curl -s https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8
Expected: one #EXT-X-STREAM-INF per rung, each followed by that rung's playlist URL.
#EXTM3U #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2149280,CODECS="mp4a.40.2,avc1.64001f",RESOLUTION=1280x720,NAME="720" url_0/193039199_mp4_h264_aac_hd_7.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=246440,CODECS="mp4a.40.5,avc1.42000d",RESOLUTION=320x184,NAME="240" url_2/193039199_mp4_h264_aac_ld_7.m3u8 ...
Notice the ladder isn't sorted — order in the master carries no meaning; the player reads BANDWIDTH, not position.
2. Fetch one rung's media playlist — the segment list. Resolve the relative URL against the master's own directory (…/x36xhzz/), not the host root:
curl -s https://test-streams.mux.dev/x36xhzz/url_0/193039199_mp4_h264_aac_hd_7.m3u8
Expected: #EXTINF + .ts pairs, ending in #EXT-X-ENDLIST because this stream is VOD (drop that tag and it would be live).
#EXTM3U #EXT-X-VERSION:3 #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-TARGETDURATION:11 #EXTINF:10.000, url_462/193039199_mp4_h264_aac_hd_7.ts #EXTINF:10.000, url_463/193039199_mp4_h264_aac_hd_7.ts ... #EXT-X-ENDLIST
3. Watch a player choose between them. I keep a tiny hls.js sandbox at /lab on this same stream. Open it with the Network tab up: you'll see the master fetch first, then a media playlist, then the segment (.ts) requests — and, if you throttle the Network tab (e.g. Slow 3G), a rung switch as ABR steps down.
Written by Wilian Revejes — senior frontend / video engineer. More at /cv.
This post describes general, industry-standard technique. It is not based on, and does not describe, any specific employer's or client's system.