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 contains no video, only pointers to 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," which means 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. The shape above is the spine underneath all of them.
Try it#
You don't need a player, since 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, because the player picks by BANDWIDTH.
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. Throttle the Network tab (e.g. Slow 3G) and you'll also see a rung switch as ABR steps down.
Written by Wilian Revejes, frontend engineer (OTT / Smart TV & video). 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.