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 a decoder reset. 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.
If you want to watch one actually play, I keep a tiny hls.js sandbox at /lab pointed at a public Mux test stream. Open it with the Network tab up and you'll watch the master fetch, then a media playlist, then the segment requests — and see the rung change as ABR reacts. Or just curl the .m3u8 and read the shape above.
This post describes general, industry-standard technique. It is not based on, and does not describe, any specific employer's or client's system.