All posts
HLSStreaming19 Jun 2025 · 3 min read

How an HLS stream is put together

JPJean Perez

Point a video player at an HLS stream and it never touches a video file. It requests a .m3u8 playlist, then a second playlist, then a run of small segment files, one after another, for as long as playback lasts.

Everything about how HLS behaves follows from that shape. A stream is a tree of text files pointing at binary ones, and the format of those binary files decides how much a tool can learn from any single one of them.

Two levels of playlist

A player fetches two kinds of playlist before it asks for a single byte of video. The master playlist lists the renditions a stream is available in, one line per quality, with the bitrate and resolution a player needs to pick a starting point and switch later if bandwidth changes.

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=5128000,RESOLUTION=1920x1080
1080p/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2928000,RESOLUTION=1280x720
720p/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1528000,RESOLUTION=854x480
480p/playlist.m3u8

BANDWIDTH is the peak bits-per-second a player should expect for that rendition, combined audio and video. RESOLUTION is there so a player can skip a rendition outright if it's rendering into a small window. Neither line contains a segment. Each one points at a second playlist.

A player doesn't commit to one of those renditions for the whole session. It watches how fast its own segment downloads are completing, compares that to how much video time each segment buys, and switches to a higher or lower line in the master playlist as often as conditions change. That's the whole mechanism behind a stream that adapts to a shaky connection. The player already has the master playlist; switching quality just means requesting segments from a different media playlist.

That second playlist, the media playlist, is where the segments actually live:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:6.000,
segment000.ts
#EXTINF:6.000,
segment001.ts

EXT-X-TARGETDURATION is the longest any segment in the playlist can be, rounded to whole seconds; each EXTINF line gives the exact duration of the segment that follows it. A live stream keeps appending lines to this file as new segments finish encoding. A video-on-demand stream writes the whole thing once.

What a segment can tell you by itself

HLS segments come in two container formats, and the difference changes what a tool can learn from one segment in isolation.

An MPEG-TS segment (.ts) is self-describing. It's built from small fixed-size packets, and among them are program tables that repeat periodically and carry the codec and stream layout for everything in that segment. Hand a .ts segment to a prober with nothing else and it has what it needs.

A fragmented MP4 segment (.m4s) doesn't. fMP4 is built on the same ISO BMFF box structure as a regular .mp4 file, and the box that carries the decoder configuration, the resolution, codec profile, and reference-frame settings, is moov. That box lives once, in a separate initialization segment, and isn't repeated in the fragments that follow. Each media fragment is a moof box (timing and where each sample sits) plus an mdat box (the compressed samples), with nothing that says how to decode them.

Probing an fMP4 segment correctly means fetching the init segment first and handing both to the prober together, in order:

GET init-segment.mp4   # moov box: codec config, resolution, profile
GET segment003.m4s     # moof + mdat, no codec config
→ concatenate init + media, then probe the combined bytes

Skip the init segment and a prober sees samples it can't interpret. That's by design: fMP4 sends the decoder configuration once and reuses it, which keeps every fragment on a long or live stream smaller.

Why BeemMeUp started here

BeemMeUp's first working version took an uploaded file, ran it through a transcode ladder to produce an HLS master playlist with a media playlist and segment set per rendition, then probed every one of those segments and surfaced its codec, bitrate, and container details individually rather than one summary for the whole file. Getting that right for fMP4 meant doing the init-plus-media fetch above, because probing the media segment alone gave nothing usable.

Within a day the project's description changed from a streaming service to an analysis tool. The useful part was showing exactly what a player sees when it requests a playlist and a segment.

That's still the shape of the thing today: a master playlist, a set of media playlists underneath it, and segments that are only as self-describing as their container lets them be. Once you can read that structure by hand, a player's request log stops being a black box and starts being a playlist you already know how to parse.