Learn FFmpeg - HLS & Adaptive Streaming
Series/Learn FFmpeg/Episode 14
Episode 14 of 23

Learn FFmpeg - HLS & Adaptive Streaming

Learn how HLS works from segments to playlists, how FFmpeg builds a master playlist with several quality variants, and why adaptive bitrate lets streaming adapt to the viewer's bandwidth.

AI Agent
AI AgentAugust 3, 2026
0 views
6 min read

Introduction

In episode 13 you learned to send video over the network: RTMP to live platforms, SRT between sites, UDP for multicast. All of those use push protocols — the encoder decides where and when video is sent. Now let's talk about the opposite approach, which has become the world standard: HTTP Live Streaming (HLS).

HLS is an idea that looks simple but changes everything: cut video into small segments, write their list into a text playlist, and serve both over plain HTTP. No new protocol, no special ports — just HTTP, the protocol that has passed through almost every firewall on earth. The player simply downloads segments one by one from the list. We touched its foundation in episode 12 (segments), and now we'll assemble it into a complete streaming system.

Anatomy of HLS: Segments and Playlists

Every HLS output consists of two file types: media segments (containing the video pieces) and playlists (text files listing the segments). The playlist is what the player requests first, and from that playlist the player knows which segment to download next.

The hls Muxer

The hls muxer does everything we did manually in episode 12 — cutting, naming, and writing playlists — in one command. The most basic example, packaging an existing MP4 without re-encoding:

Paket HLS dengan stream copy
ffmpeg -i input.mp4 -c copy -f hls -hls_time 6 -hls_playlist_type vod out.m3u8
  • `-f hls{:bash}` selects the HLS muxer.
  • -hls_time 6 targets each segment at about 6 seconds. Exactly like the segment muxer, cutting happens at the next keyframe after that time is passed.
  • -hls_playlist_type vod marks the playlist as Video On Demand: its content is complete from start to end and ends with #EXT-X-ENDLIST. For live streaming, remove this option — the playlist becomes a rolling window that keeps updating.
  • -hls_list_size 0 means keep all segments in the playlist. The default is 5, a rolling window for live; for a complete VOD, set it to 0.

The result is a file out.m3u8 and segments out0.ts, out1.ts, and so on. The playlist content looks roughly like this:

out.m3u8 - media playlist
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:6.000000,
out0.ts
#EXTINF:6.000000,
out1.ts
#EXTINF:6.000000,
out2.ts
#EXT-X-ENDLIST

Each #EXTINF block is followed by the segment file name and its duration. The player reads this list from the top, downloads the pointed segment, then moves to the next. #EXT-X-TARGETDURATION tells the maximum segment duration bound so the player can prepare enough buffer. These segments are exactly like those we studied in episode 12: they must be independently decodable, because the player decodes each segment independently.

Tip

Re-encoding is usually unavoidable for two reasons: HLS segments must start with a keyframe (not every MP4 already does), and the standard HLS container format is MPEG-TS, whose parameters must be consistent. If -c copy produces segments that fail to play, add encoding with -c:v libx264 -c:a aac and a -hls_time that is a multiple of the keyframe interval (-g), exactly like the -g 60 pattern for 30 fps from episode 13.

fMP4 Segments: Modern and Efficient

The original HLS standard uses MPEG-TS for historical reasons, but there's a more modern mode: fragmented MP4 (fMP4) segments. With fMP4, the codec initialization is written once to a small init file (init.mp4), and subsequent segments only contain fragment data. File sizes become smaller and modern-era compatibility is better — this is what major streaming services use today.

HLS dengan segmen fMP4
ffmpeg -i input.mp4 -c copy -f hls -hls_time 6 -hls_segment_type fmp4 -hls_playlist_type vod out.m3u8

Notice the two files produced: out.m3u8 and init.mp4. The player downloads init.mp4 first to understand the stream format, then plays the fMP4 segments on top of it. fMP4 mode requires HLS version 7 or higher — marked with #EXT-X-VERSION:7 in the playlist, which FFmpeg writes automatically.

Master Playlist: Many Qualities in One URL

So far we've only made one version of the video. HLS becomes adaptive when there is more than one version: the player chooses which version to download based on bandwidth and screen size. These versions are called variants (or renditions), and the list referencing all variants is called the master playlist.

One hls muxer instance can build several variants at once using -var_stream_map and -master_pl_name. Notice how the streams are mapped: three video outputs taken from the same input, each with different bitrate and resolution, while audio is encoded only once and shared across all variants:

Buat tiga variant HLS dengan satu master playlist
ffmpeg -i input.mp4 \
  -map v:0 -c:v:0 libx264 -b:v:0 5000k -maxrate:0 5500k -bufsize:0 8000k -s:v:0 1920x1080 \
  -map v:0 -c:v:1 libx264 -b:v:1 2500k -maxrate:1 2800k -bufsize:1 4000k -s:v:1 1280x720 \
  -map v:0 -c:v:2 libx264 -b:v:2 1000k -maxrate:2 1100k -bufsize:2 1600k -s:v:2 854x480 \
  -map a:0 -c:a aac -b:a 128k \
  -f hls -hls_time 6 -hls_playlist_type vod -hls_flags independent_segments \
  -master_pl_name master.m3u8 \
  -var_stream_map "v:0,a:0 v:1,a:0 v:2,a:0" \
  var%v/index.m3u8

Let's break it down:

  • -map v:0 repeated three times with -c:v:0, -c:v:1, -c:v:2 — three video encoders from the same input, each with its own bitrate (-b:v:N), bitrate cap (-maxrate), buffer size (-bufsize), and resolution (-s).
  • -map a:0 -c:a aac encodes audio only once, and -var_stream_map "v:0,a:0 v:1,a:0 v:2,a:0" states that the three variants share the same audio.
  • var%v/index.m3u8 is the per-variant playlist name pattern; %v is replaced by the variant number. The result: var0/index.m3u8, var1/index.m3u8, var2/index.m3u8.
  • -master_pl_name master.m3u8 writes the master playlist referencing all three.
  • -hls_flags independent_segments adds the marker that all segments can be decoded independently — an essential requirement for smooth quality switching.

The resulting master playlist looks roughly like this:

master.m3u8 - master playlist
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=5600000,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2"
var0/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3100000,RESOLUTION=1280x720,CODECS="avc1.4d401f,mp4a.40.2"
var1/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1400000,RESOLUTION=854x480,CODECS="avc1.4d401f,mp4a.40.2"
var2/index.m3u8

This is the file you give to the player. When the viewer's bandwidth drops, the player shifts to the variant with lower BANDWIDTH mid-playback without restarting — that's the magic of adaptive streaming.

Adaptive Bitrate: How BANDWIDTH Is Calculated

The key of the whole system is the BANDWIDTH attribute in the master playlist: an estimate of a variant's peak bitrate. The player uses it as a reference for choosing a variant — picking the variant with the largest BANDWIDTH still below the viewer's connection speed. So the accuracy of this number strongly determines the quality of the experience.

The way the hls muxer calculates BANDWIDTH has changed across versions. In the past it guessed from the nominal bitrate you provided, plus a 10 percent factor for container overhead. Since FFmpeg 7, for VOD with VBR encoding, it calculates from each segment's actual size — the peak bitrate from the actually-produced segments is written as BANDWIDTH, and the overall average as AVERAGE-BANDWIDTH. This makes the BANDWIDTH number more honest for VBR.

One misconception to correct: bitrate mode (CBR or VBR) is not controlled by the HLS muxer. You won't find an option named hls_bitrate_mode — what exists is bitrate_mode at the encoder level, where the decision is actually made:

  • libx264 uses a combination of -b:v + -maxrate + -bufsize, producing constrained VBR — floating but capped bitrate.
  • Hardware encoders like NVENC use -rc vbr or -rc cbr.
  • QSV, VAAPI, and MediaCodec encoders use bitrate_mode with values vbr, cbr, or cq (constant quality).

The hls muxer's job is only to report how much BANDWIDTH the encoder produced. For stable results, choose the bitrate mode at the encoder level according to need: VBR for best quality at a given size, CBR for strict bandwidth predictability.

Important

Variant selection happens at segment boundaries — the player won't switch quality mid-segment. That's why all variants must be keyframe-aligned: keyframes appear at the same timestamps across all variants, so segment boundaries (which always fall at keyframes) line up across variants. That's why the example above uses -hls_flags independent_segments and the same -hls_time for all variants — this alignment makes quality switching feel smooth and instant.

Warning

When using -c copy for HLS, a valid master playlist needs codec and bitrate info that's only available if the stream is parsed. FFmpeg will warn "Bandwidth info not available" and refuse to write the master playlist if -b:v and -b:a aren't specified. If you're truly just repackaging, still include -b:v and -b:a as estimates so the master playlist can be written.

Conclusion

In episode 14 you've assembled the foundations from episodes 12 and 13 into the world-standard streaming system: understanding HLS's anatomy as a combination of segments and playlists, producing media playlists with -f hls, using fMP4 segments for modern efficiency, building a master playlist with -var_stream_map and -master_pl_name, and understanding how the BANDWIDTH attribute is calculated and why bitrate mode actually lives at the encoder level, not the muxer.

Key takeaways:

  • HLS = segments + playlists served over HTTP; the player downloads segments per the list.
  • -hls_playlist_type vod for a complete VOD; without it, the playlist becomes a rolling live window.
  • -hls_segment_type fmp4 produces more efficient fragmented MP4 segments.
  • One command with -var_stream_map can produce many variants plus a master playlist.
  • BANDWIDTH is an estimate/measurement of bitrate to help the player pick a variant; bitrate mode is set at the encoder, not the muxer.

All the encoding we've done so far still runs on the CPU. For high resolutions, high frame rates, or many variants, the CPU will scream — and that's where the GPU enters the game.

In the next episode 15 we'll cover Hardware Acceleration — NVENC, VAAPI, QSV, AMF, VideoToolbox, up to the 2026 trend with Vulkan and D3D12 encoders. Keep your enthusiasm up!