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-HINT actually 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:

MechanismSummary
Partial SegmentsBreak regular .ts / .m4s segments into 200–500 ms “parts” that are exposed in the playlist before the full segment is ready
Blocking Playlist ReloadWhen the playlist isn’t updated yet, the server holds the HTTP response until a new part is available
Preload HintAnnounce 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

OptionRecommendedPurpose
-hls_time1Target segment duration (seconds); 1 s or less is typical for LL-HLS
-hls_list_size10–20Max number of segments retained in the playlist
-hls_flags independent_segmentsrequiredSignals that every segment is standalone-decodable
-hls_flags delete_segmentsrecommendedPrevents disk bloat during live streaming
-hls_flags program_date_timerecommendedAdds EXT-X-PROGRAM-DATE-TIME (useful for sync + seeking)
-hls_segment_typempegts or fmp4If fmp4, also specify -hls_fmp4_init_filename
-g / -keyint_minfps × hls_timeKeeps keyframes at segment boundaries
-sc_threshold0Disables scene-change keyframes (keeps interval fixed)
-tune zerolatencyrecommendedlibx264’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 ultrafast minimizes encoder latency
  • -hls_list_size 10 keeps 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

PlayerLL-HLS supportNotes
iOS 14+ / tvOS 14+ Safari✅ FullNative AVPlayer
macOS Safari 14+✅ FullSame AVPlayer pipeline
hls.js 1.4+⚠️ PartialRequires lowLatencyMode: true and a CDN that implements blocking reload
ExoPlayer (Android)⚠️ PartialVersion-dependent
dash.js / Shaka PlayerUse LL-DASH on the DASH side

Measuring glass-to-glass latency

A quick method:

  1. Burn a timestamp on the sender (OBS timer source or FFmpeg drawtext)
  2. Shoot both the sender’s screen and the viewer’s screen in a single photo
  3. 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)

SetupSegment lengthTypical latencyNotes
Classic HLS (VOD-style)6 s18–30 shls_time 6 + 5-segment buffer
Short-segment HLS2 s8–12 shls_time 2
LL-HLS (this article)1 s3–6 shls_time 1 + blocking reload
LL-HLS + partials1 s + 0.33 s parts2–3 sFull spec (needs dedicated packager)
WebRTC0.1–0.5 sDifferent use case


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