What you'll learn
Quick Answer
Video streaming doesn't send one big file — it cuts the video into short segments a few seconds each, encodes each segment at several different quality levels, and lets the player switch quality levels segment by segment based on current network speed. HLS (Apple's format) and DASH (the open international standard) both work this way; they mainly differ in manifest format and default container, and modern pipelines increasingly package one shared set of files that serve both.
Why You Can't Just Serve an .mp4 File
Serving a single video file over plain HTTP (progressive download) works, but breaks down for anything beyond a short clip. There's no way to switch quality mid-playback without restarting or re-buffering the whole thing, so a viewer whose network conditions change — walking from Wi-Fi onto mobile data, or a household Wi-Fi network getting congested — either stalls or stays stuck on whatever quality the page happened to pick at load time. Seeking can also be awkward depending on where the file's index data lives in the container. And if the connection drops partway through a large file, the player generally has to resume or restart the whole download rather than just fetching the next few seconds.
Adaptive streaming protocols solve this by splitting the video into many small, independently fetchable segments — typically a few seconds of video each — encoded at multiple quality levels, described by a small text manifest file. Because segments are just static files served over ordinary HTTP, any CDN or plain web server can host them; no special streaming server or protocol is required, which was one of the original selling points of HTTP-based streaming over older approaches like RTMP.
Segmenting Video for Real: An ffmpeg Demo
To make this concrete, we generated a real 12-second synthetic 1280x720 test video with ffmpeg (a built-in color-bar test pattern plus a sine-wave test tone — no external footage needed) and ran ffmpeg's HLS segmenter against it, asking for 4-second segments:
ffmpeg -i source_720p.mp4 -c copy -start_number 0 \
-hls_time 4 -hls_list_size 0 \
-hls_segment_filename "720p/seg_%03d.ts" 720p/prog.m3u8The real playlist this produced was not three clean 4-second segments. It was this:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:8
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:8.333333,
seg_000.ts
#EXTINF:3.666667,
seg_001.ts
#EXT-X-ENDLISTTwo segments, 8.33 seconds and 3.67 seconds — nowhere near the requested 4 seconds. The reason is a real, easy-to-miss detail: with -c copy, ffmpeg cannot cut a segment at an arbitrary point in time, only at a keyframe (a fully self-contained frame the decoder can start from). The source video had been encoded with libx264's default keyframe interval, which didn't happen to place a keyframe anywhere near the 4-second mark, so the segmenter rounded up to the next keyframe it actually had available — landing on roughly 8.3 seconds instead.
The fix is to control the keyframe placement at encode time, forcing a keyframe exactly every 4 seconds so the segmenter always has one to cut on:
ffmpeg -f lavfi -i testsrc=duration=12:size=1280x720:rate=30 \
-f lavfi -i sine=frequency=1000:duration=12 \
-c:v libx264 -g 120 -keyint_min 120 -sc_threshold 0 \
-force_key_frames "expr:gte(t,n_forced*4)" \
-c:a aac -shortest source_720p_keyed.mp4Re-running the same segmenting command against this re-encoded source produced exactly what we originally wanted:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:4.000000,
seg_000.ts
#EXTINF:4.000000,
seg_001.ts
#EXTINF:4.000000,
seg_002.ts
#EXT-X-ENDLISTWe independently confirmed this with ffprobe measuring the actual duration of each resulting .ts file rather than trusting the playlist's own claims: 4.02s, 4.05s, and 4.06s — matching within normal encoding rounding. The lesson generalizes: a segment duration you configure in a streaming pipeline is a request to the encoder, not a guarantee the segmenter can enforce on its own — the encoder has to actually place keyframes where the segmenter needs to cut.
The Adaptive Bitrate Ladder and the Multivariant Playlist
A real streaming pipeline doesn't produce just one quality level — it produces a "ladder" of several renditions (say, 720p and 360p) of the same content, each segmented independently but with matching keyframe placement, so the player can switch renditions exactly at a segment boundary without a decode gap. We re-encoded a 360p rendition of the same source using the identical 4-second forced-keyframe setting for exactly this reason, then segmented it the same way.
A top-level manifest — HLS calls it a multivariant playlist (older material calls it a "master playlist") — lists the available renditions so the player can choose. Here's the real one we generated:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2"
720p_fixed/prog.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=900000,RESOLUTION=640x360,CODECS="avc1.640015,mp4a.40.2"
360p_fixed/prog.m3u8One honest disclosure: the BANDWIDTH values above (2.8 Mbps and 900 kbps) are realistic, rounded figures typical of real 720p/360p production encodes, not the literal measured bitrate of our synthetic test clip — a computer-generated color-bar pattern compresses far more efficiently than real footage (ours measured well under 200 kbps), so using its literal numbers here would have been misleading rather than illustrative. Apple's own HLS authoring guidance recommends a 6-second target segment duration for typical VOD content; we used 4 seconds above to keep the demo compact, and both are legitimate production choices — real-world segment lengths commonly range from 2 to 10 seconds depending on the trade-off between switch responsiveness and request overhead.
DASH: The Non-Apple Standard
MPEG-DASH — formally ISO/IEC 23009-1 — is the international-standard sibling of HLS, and the default outside Apple's ecosystem: Android, smart TVs, and many desktop players support it. Apple's own App Store Review Guideline 2.5.7 requires apps that stream video over a cellular network for more than 10 minutes (or more than 5MB of data within five minutes) to use HLS specifically, which is a large part of why HLS remains mandatory in practice for native iOS apps regardless of what a backend also supports.
We generated a real DASH manifest with ffmpeg's DASH muxer against the same two-rendition source, and it produced an actual .mpd (Media Presentation Description) file with this real structure:
<MPD ... type="static" mediaPresentationDuration="PT12.0S" maxSegmentDuration="PT4.0S">
<Period id="0" start="PT0.0S">
<AdaptationSet id="0" contentType="video" ...>
<Representation id="0" bandwidth="2800000" width="1280" height="720">
<SegmentTemplate timescale="15360" initialization="init-stream$RepresentationID$.m4s"
media="chunk-stream$RepresentationID$-$Number%05d$.m4s" startNumber="1">
<SegmentTimeline>
<S t="0" d="128000" />
<S d="56320" />
</SegmentTimeline>
</SegmentTemplate>
</Representation>
<Representation id="2" bandwidth="800000" width="640" height="360">...</Representation>
</AdaptationSet>
<AdaptationSet id="1" contentType="audio">...</AdaptationSet>
</Period>
</MPD>Notice the real files ffmpeg wrote alongside that manifest: init-stream0.m4s, chunk-stream0-00001.m4s, chunk-stream0-00002.m4s, and so on for each representation. DASH's default container is fragmented MP4 (.m4s — an initialization segment plus a sequence of media chunks), whereas classic HLS defaults to MPEG-TS (.ts) segments like the ones we generated earlier. Structurally, DASH's manifest is more explicit and machine-oriented than HLS's line-based playlist format — Period, AdaptationSet, and Representation map directly onto "a section of the timeline," "a track type like video or audio," and "one specific quality/codec option for that track," all inside one XML document rather than a tree of separate .m3u8 files.
How the Player Decides Which Quality to Request
Neither HLS nor DASH standardizes exactly how a player should choose between renditions — that logic, called Adaptive Bitrate (ABR) selection, lives entirely in the player. Real-world players like hls.js, Shaka Player, and ExoPlayer generally blend two heuristics. A throughput-based approach measures how fast recent segments actually downloaded and estimates available bandwidth from that, picking the highest rendition whose bitrate comfortably fits under the estimate. A buffer-based approach instead watches how much video is already safely buffered ahead of the playhead: a healthy, growing buffer allows stepping up to a higher quality, while a shrinking buffer triggers an immediate step down regardless of what the throughput estimate says, since running out of buffer means a visible stall.
Most production players combine both, along with a deliberately conservative "startup rendition" choice — starting on the highest available quality risks an immediate stall before any buffer has built up, so players typically start lower and step up once they've confirmed the network can sustain it. This selection logic runs independently for every player watching the stream; nothing in the manifest tells the player which rendition to pick, only which ones exist.
One more distinction worth noting from our own generated files: both manifests we produced were for a fixed-length 12-second clip, so they're VOD (video-on-demand) manifests — visible in the real type="static" attribute in our MPD and the real #EXT-X-ENDLIST tag at the end of our HLS playlist, both of which say "this is the complete stream, nothing more is coming." A live stream instead continuously appends new segments to a playlist that never gets that end marker, and the player has to keep re-fetching the manifest itself to discover new segments as they're published.
CMAF: The Format That Unified Both
Historically, supporting both HLS and DASH meant encoding and packaging your content twice — once into MPEG-TS segments for HLS, once into fragmented MP4 for DASH — doubling both processing time and storage. The Common Media Application Format (CMAF, standardized as ISO/IEC 23000-19 in 2017) fixed this by standardizing fragmented MP4 as a shared packaging format that both HLS and DASH manifests can reference. In practice, this means a modern pipeline can encode and package a rendition ladder exactly once, producing the same kind of .m4s chunks our DASH demo generated, and then simply generate two lightweight manifests — an .m3u8 for HLS players and an .mpd for DASH players — that both point at the same underlying segment files on the same CDN. This is the core reason modern HLS increasingly uses fragmented MP4 instead of the older MPEG-TS segments too: it's what lets one set of encoded bytes serve both ecosystems.
