Principles and Practical Implementation of Low-Latency HLS (LL-HLS) Live Streaming

Category: Technology & Advanced


A while ago, I wanted to set up a small live stream for my friends, so I called a few buddies to watch a game together. Initially, I used the RTMP + Flash setup, but nowadays browsers have abandoned Flash, and it’s a hassle to install plugins. Then I switched to HLS, which improved compatibility, but latency shot up to over ten seconds — the chat messages were half a goal behind the video, a totally frustrating experience. So I started looking into ways to reduce HLS latency and came across LL-HLS (Low-Latency HLS).

To be honest, when LL-HLS first came out, I was skeptical — it felt like Apple was just promising vaporware. But after actually trying it out, I found that latency could indeed be reduced to 3–5 seconds, comparable to the old RTMP setup, and it’s entirely based on HTTP, so no need for weird ports and it works well with CDNs. Let me share some of my understanding and the pitfalls I encountered.

Core Idea of LL-HLS: Chopping Segments into Smaller Pieces

Traditional HLS has high latency because data is organized in ts or m4s segments, typically 6 seconds each, and with player buffering, latency easily reaches 10–15 seconds. LL-HLS’s clever trick is to further chop each segment into smaller Partial Segments (chunks), e.g., 0.5 seconds each. An #EXT-X-PART tag is added to the m3u8 to mark these partial segments, and the segment header is rendered early, allowing the player to start downloading chunks before the entire segment is generated.

Another key feature is Preload Hint. The server adds a special #EXT-X-PRELOAD-HINT tag in the m3u8, telling the player, “I’ll have that unfinished block ready for you soon.” The player then continuously fetches this data using HTTP long polling or chunked transfer. As soon as the server has a new chunk, the player gets the update immediately. This reduces waiting time to milliseconds.

Apple also recommends using HTTP/2 push or multiplexing, but in my tests, as long as the chunks are small enough, HTTP/1.1 long polling works fine too, just with a few more connections.

Practical Implementation: Pitfalls from Server to Player

The principle seems straightforward, but there are many details to manage when actually setting it up.

Server Side: The commonly used Nginx RTMP module (nginx-rtmp-module) can produce HLS segments, but its support for LL-HLS is limited (older versions basically don’t support it). I later switched to Nginx’s official HLS module with patches, or directly used open-source solutions like lal (a Go streaming server written by a great developer) that natively supports LL-HLS. You can also push streams using ffmpeg with -hls_init_time 0.5 and -hls_playlist_type event, but ffmpeg’s LL-HLS support is still being improved, and sometimes the segment timing is off.

Chunk Size vs. Latency Trade-off: Setting the Partial Segment duration too small (e.g., 0.2s) greatly increases server processing pressure and network overhead; latency decreases but buffering increases. I found 0.5s to be a good balance. Combined with preload hints, overall latency can be controlled at 3–4 seconds, which is barely noticeable for average users. This parameter needs to be configured in the server-side segmenter. For example, in lal, set fragment_duration = 1000 (milliseconds) and part_duration = 500.

Client Support: This is the trickiest part. Safari and iOS HLS natively support LL-HLS out of the box. But other browsers require third-party players. hls.js added LL-HLS support starting from version 1.0, but there have been bugs during version upgrades, such as not parsing #EXT-X-PRELOAD-HINT at all. During testing, I often used the M3U8Player (https://m3u8player.link/) online player — just drop an LL-HLS m3u8 link in and see the latency performance. It’s also convenient for friends to verify, a handy shortcut tool.

Another pitfall is time synchronization. LL-HLS heavily depends on time synchronization between server and client, because the preload hint carries SN (sequence number) and PART information. If the time offset is too large, the player may misjudge which block is the latest, leading to duplicate requests or never catching up. In my environment, I used NTP to lock the server time, and since clients are usually browsers, natural time sync is generally fine.

Caching Pitfall: If CDN caching is not disabled, chunks will be cached, and the player will always get the oldest chunk, causing latency to shoot back to over ten seconds. So I set up an API to automatically add random parameters to the m3u8 filenames and configured the CDN to avoid caching m3u8 and the most recent few seconds of ts/m4s files. This is easily overlooked.

Conclusion (Nothing much to conclude really)

After going through all the trouble, LL-HLS can indeed reduce HLS latency to a usable level, but the deployment cost is significantly higher than regular HLS. However, the benefit is that compatibility remains good, and major cloud service providers are now supporting it. If you’re also doing live streaming and don’t want to deal with the complex signaling of WebRTC, LL-HLS is definitely worth trying. Next, I plan to create an automated script on the server to generate LL-HLS streams with one click, so I won’t have to hear my friends complain about latency when I invite them to watch games again.