What you’ll learn
- The difference between classic HLS and Low-Latency HLS (LL-HLS)
- The “partial segment” concept that makes LL-HLS possible
- FFmpeg commands that produce LL-HLS output (
-hls_time 1,-hls_flags,-hls_segment_type fmp4, …) - What
EXT-X-SERVER-CONTROL,EXT-X-PART-INF,EXT-X-PRELOAD-HINTactually do - Player compatibility (iOS / macOS native / hls.js) and how to measure real glass-to-glass latency
Tested version: FFmpeg 7.1 (verified on ubuntu-latest via CI) OS: Windows / macOS / Linux
What is LL-HLS?
Classic HLS delivers content in segments (typically 6 seconds), so it has historically suffered from 15–30 seconds of glass-to-glass latency.
LL-HLS (Low-Latency HLS) is an Apple extension published in 2020 that can push the end-to-end latency down to 2–6 seconds through:
| Mechanism | Summary |
|---|---|
| Partial Segments | Break regular .ts / .m4s segments into 200–500 ms “parts” that are exposed in the playlist before the full segment is ready |
| Blocking Playlist Reload | When the playlist isn’t updated yet, the server holds the HTTP response until a new part is available |
| Preload Hint | Announce the next upcoming part URL in the playlist so the player can pre-request it |
FFmpeg 7.0+ supports the core pieces (short segments, independent segments, program date-time). Full LL-HLS deployment additionally needs a CDN that can actually do blocking responses, or a purpose-built packager after FFmpeg.
Prerequisite: fixed keyframe interval / GOP
LL-HLS only works if you pin the IDR keyframe interval to the segment length. Without this, segments don’t align to keyframes and you’ll get split failures or long first-frame delay.
ffmpeg -i input.mp4 -c:v libx264 -preset veryfast -tune zerolatency -profile:v baseline -level 3.1 -pix_fmt yuv420p -g 30 -keyint_min 30 -sc_threshold 0 -b:v 2500k -c:a aac -b:a 128k -f hls -hls_time 1 -hls_list_size 10 -hls_flags independent_segments+delete_segments -hls_segment_type mpegts -hls_segment_filename "/tmp/seg_%04d.ts" /tmp/playlist.m3u8
Key choices:
-g 30+-keyint_min 30+-sc_threshold 0→ force an IDR every 30 frames (= 1s at 30 fps)-tune zerolatency→ disables B-frames and shrinks encoder pipeline-profile:v baseline→ maximum device compatibility (iOS / Android)-pix_fmt yuv420p→ satisfies baseline’s 4:2:0 chroma requirement explicitly-hls_time 1→ target 1 s segments-hls_flags independent_segments→ advertises in the playlist that each segment can decode standalone
Minimal LL-HLS output
ffmpeg -i input.mp4 -c:v libx264 -preset veryfast -tune zerolatency -g 30 -keyint_min 30 -sc_threshold 0 -c:a aac -f hls -hls_time 1 -hls_list_size 20 -hls_flags independent_segments+delete_segments+program_date_time -master_pl_name master.m3u8 -hls_segment_filename "/tmp/seg_%04d.ts" /tmp/stream.m3u8
Produces:
/tmp/stream.m3u8— media playlist/tmp/seg_0000.ts,seg_0001.ts, … — 1-second segments/tmp/master.m3u8— master playlist
Important LL-HLS options
| Option | Recommended | Purpose |
|---|---|---|
-hls_time | 1 | Target segment duration (seconds); 1 s or less is typical for LL-HLS |
-hls_list_size | 10–20 | Max number of segments retained in the playlist |
-hls_flags independent_segments | required | Signals that every segment is standalone-decodable |
-hls_flags delete_segments | recommended | Prevents disk bloat during live streaming |
-hls_flags program_date_time | recommended | Adds EXT-X-PROGRAM-DATE-TIME (useful for sync + seeking) |
-hls_segment_type | mpegts or fmp4 | If fmp4, also specify -hls_fmp4_init_filename |
-g / -keyint_min | fps × hls_time | Keeps keyframes at segment boundaries |
-sc_threshold | 0 | Disables scene-change keyframes (keeps interval fixed) |
-tune zerolatency | recommended | libx264’s low-latency tuning |
fMP4-based LL-HLS (preferred)
Apple’s spec effectively expects fragmented MP4:
ffmpeg -i input.mp4 -c:v libx264 -preset veryfast -tune zerolatency -g 30 -keyint_min 30 -sc_threshold 0 -c:a aac -f hls -hls_time 1 -hls_list_size 20 -hls_segment_type fmp4 -hls_fmp4_init_filename "init.mp4" -hls_flags independent_segments+delete_segments+program_date_time -hls_segment_filename "/tmp/seg_%04d.m4s" /tmp/stream.m3u8
init.mp4 is the initialization segment (contains the movie box only) and must be fetched by the player before any media segment.
Sample LL-HLS playlist
The above command produces a stream.m3u8 roughly like:
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:1
#EXT-X-MEDIA-SEQUENCE:42
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MAP:URI="init.mp4"
#EXTINF:1.000000,
seg_0042.m4s
#EXTINF:1.000000,
seg_0043.m4s
#EXTINF:1.000000,
seg_0044.m4s
A fully LL-HLS-compliant manifest additionally needs tags like these (usually emitted by a CDN or a dedicated packager downstream of FFmpeg):
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=3.0
#EXT-X-PART-INF:PART-TARGET=0.33
#EXT-X-PART:DURATION=0.33,URI="seg_0044.1.m4s"
#EXT-X-PART:DURATION=0.33,URI="seg_0044.2.m4s"
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="seg_0044.3.m4s"
FFmpeg doesn’t emit partial-segment tags directly, so pair it with something like Shaka Packager or Apple’s mediastreamsegmenter when you need the full LL-HLS experience.
Delivering LL-HLS from a live source
Taking an RTMP or UDP feed and writing LL-HLS:
ffmpeg -i rtmp://localhost/live/stream -c:v libx264 -preset ultrafast -tune zerolatency -g 30 -keyint_min 30 -sc_threshold 0 -b:v 3000k -c:a aac -b:a 128k -f hls -hls_time 1 -hls_list_size 10 -hls_flags delete_segments+independent_segments+program_date_time -hls_segment_filename "/tmp/live_seg_%04d.ts" /tmp/live_stream.m3u8
-preset ultrafastminimizes encoder latency-hls_list_size 10keeps a 10-second window available- Serve the output directory over HTTP and point the player at
live_stream.m3u8
Note: this example uses an
rtmp://URL as the input, so it is automatically skipped by the verify-ffmpeg CI (network required). Validate locally against your own RTMP source.
Player compatibility
| Player | LL-HLS support | Notes |
|---|---|---|
| iOS 14+ / tvOS 14+ Safari | ✅ Full | Native AVPlayer |
| macOS Safari 14+ | ✅ Full | Same AVPlayer pipeline |
| hls.js 1.4+ | ⚠️ Partial | Requires lowLatencyMode: true and a CDN that implements blocking reload |
| ExoPlayer (Android) | ⚠️ Partial | Version-dependent |
| dash.js / Shaka Player | ❌ | Use LL-DASH on the DASH side |
Measuring glass-to-glass latency
A quick method:
- Burn a timestamp on the sender (OBS timer source or FFmpeg
drawtext) - Shoot both the sender’s screen and the viewer’s screen in a single photo
- Subtract the two visible timestamps
FFmpeg example for burning in local time:
ffmpeg -i input.mp4 -vf "drawtext=text='%{localtime}':fontcolor=white:fontsize=36:x=10:y=10:box=1:[email protected]" -c:v libx264 -preset veryfast -tune zerolatency -g 30 -c:a aac -f hls -hls_time 1 -hls_list_size 10 -hls_flags delete_segments+independent_segments -hls_segment_filename "/tmp/burnin_%04d.ts" /tmp/burnin.m3u8
Note: drawtext requires a font file. On some environments you must additionally pass fontfile=/path/to/font.ttf.
Common issues
Segment boundaries don’t align with keyframes
Warnings like pkt->duration = 0, maybe the hls segment duration will not precise usually mean the GOP length doesn’t match hls_time. Make sure -g equals fps × hls_time exactly.
iOS Safari can’t play the stream
Native LL-HLS playback needs EXT-X-INDEPENDENT-SEGMENTS in the playlist and proper CORS (Access-Control-Allow-Origin: *) on your server.
hls.js feels slow
You must set lowLatencyMode: true on the hls.js config, and your CDN must return blocking responses when CAN-BLOCK-RELOAD=YES is advertised — otherwise you’re back to classic-HLS latency.
CPU is maxed out
Try -preset ultrafast, or switch to a hardware encoder (h264_nvenc, h264_videotoolbox, h264_qsv). Forcing IDRs every second raises CPU quite a bit versus classic HLS.
Latency comparison (rough field numbers)
| Setup | Segment length | Typical latency | Notes |
|---|---|---|---|
| Classic HLS (VOD-style) | 6 s | 18–30 s | hls_time 6 + 5-segment buffer |
| Short-segment HLS | 2 s | 8–12 s | hls_time 2 |
| LL-HLS (this article) | 1 s | 3–6 s | hls_time 1 + blocking reload |
| LL-HLS + partials | 1 s + 0.33 s parts | 2–3 s | Full spec (needs dedicated packager) |
| WebRTC | — | 0.1–0.5 s | Different use case |
Related
Frequently Asked Questions
Does hls_time 1 alone make it LL-HLS?
No. hls_time 1 just means 1-second segments. Real LL-HLS (2–3 s latency) requires partial-segment manifest tags and a server that can serve blocking responses. FFmpeg-only will give you short-segment HLS (~5 s latency), which is already good for many use cases.
Can I play LL-HLS anywhere but iOS?
hls.js 1.4+ partially supports it with lowLatencyMode: true. Android ExoPlayer and various smart-TV players have spotty support. On UWP / Chromecast, LL-DASH is often more practical.
Do 1-second segments play poorly with CDNs?
You’ll be issuing ~6× more HTTP requests (6 s → 1 s). Pick a CDN with favorable request pricing, or one with official LL-HLS tuning (CloudFront, Fastly, Cloudflare).
How do I reduce CPU?
Try -preset ultrafast, a hardware encoder (h264_nvenc, h264_videotoolbox, h264_qsv), or if your input is already H.264, use -c:v copy and skip transcoding entirely.
Is LL-HLS useful for VOD?
No. LL-HLS is a live-streaming technology. For on-demand delivery use classic HLS (6-second segments, full list retained). Using LL-HLS for VOD only multiplies CDN load with no viewer benefit.
Test environment: ffmpeg 7.1 / Ubuntu 24.04 (GitHub Actions) Primary sources: ffmpeg.org/ffmpeg-formats.html#hls-1 / Apple Developer — Enabling Low-Latency HLS