Ahosting Logo
Knowledge Base

How to Set Up HLS Streaming with FFmpeg

HLS delivers video as a series of small files plus a playlist listing them. The player downloads the playlist, then fetches segments in order. Because it is ordinary HTTP, it needs no streaming server: any web server that can serve files can serve HLS, and that is the reason it became the default way to deliver video on the web.

This covers producing a single-quality stream, then a multi-bitrate ladder that lets the player adapt to the viewer's connection, and the two configuration mistakes that cause most HLS problems.

The simplest working stream

ffmpeg -i input.mp4 \
 -c:v libx264 -crf 21 -preset veryfast \
 -c:a aac -b:a 128k \
 -hls_time 6 \
 -hls_playlist_type vod \
 -hls_segment_filename "seg_%03d.ts" \
 playlist.m3u8

That produces playlist.m3u8 and a set of seg_000.ts files. Upload the whole directory and point a player at the .m3u8.

-hls_time 6 asks for roughly six-second segments. Shorter segments start playback sooner and adapt faster; longer ones mean fewer requests. Between four and six seconds suits most on-demand content, and it is a request rather than a guarantee, segments break at keyframes, so actual lengths vary unless you force keyframe placement.

-hls_playlist_type vod marks the playlist complete, so the player knows the full duration and can seek anywhere. Omit it for live, where the playlist keeps growing.

Force keyframes so segments are consistent

This is the first of the two mistakes worth avoiding. Without regular keyframes, FFmpeg cannot cut where you asked, and segment lengths drift, which matters enormously once you have more than one quality level.

ffmpeg -i input.mp4 \
 -c:v libx264 -crf 21 -preset veryfast \
 -g 48 -keyint_min 48 -sc_threshold 0 \
 -c:a aac -b:a 128k \
 -hls_time 2 -hls_playlist_type vod \
 -hls_segment_filename "seg_%03d.ts" playlist.m3u8

-g 48 places a keyframe every 48 frames; two seconds at 24fps. -sc_threshold 0 stops the encoder inserting extra keyframes at scene changes, which would otherwise make the spacing irregular.

Set the keyframe interval to divide evenly into your segment length. Six-second segments at 24fps means -g 48 with -hls_time 6, or -g 144; anything that does not divide cleanly produces segments of inconsistent length.

Why keyframe alignment decides whether a ladder worksEvery quality level is encoded separatelyand each one places its own keyframesAligned keyframessegments start at the same instant in every levelUnaligned keyframesthe player cannot switch cleanly, so it stutters or refusesForce the keyframe interval explicitly on every level of the ladder. Left to the encoder, they will not match.

An adaptive bitrate ladder

Multiple quality levels let the player pick one that fits the viewer's connection and change during playback. This produces three levels and a master playlist:

ffmpeg -i input.mp4 \
 -filter_complex "[0:v]split=3[v1][v2][v3]; \
 [v1]scale=w=1920:h=1080[v1out]; \
 [v2]scale=w=1280:h=720[v2out]; \
 [v3]scale=w=854:h=480[v3out]" \
 -map "[v1out]" -c:v:0 libx264 -b:v:0 5000k -maxrate:v:0 5350k -bufsize:v:0 7500k \
 -map "[v2out]" -c:v:1 libx264 -b:v:1 2800k -maxrate:v:1 2996k -bufsize:v:1 4200k \
 -map "[v3out]" -c:v:2 libx264 -b:v:2 1400k -maxrate:v:2 1498k -bufsize:v:2 2100k \
 -map a:0 -map a:0 -map a:0 -c:a aac -b:a 128k -ac 2 \
 -g 48 -keyint_min 48 -sc_threshold 0 \
 -f hls -hls_time 6 -hls_playlist_type vod \
 -hls_segment_filename "v%v/seg_%03d.ts" \
 -master_pl_name master.m3u8 \
 -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2" \
 "v%v/playlist.m3u8"

Each level gets its own directory. The player loads master.m3u8, which lists all three, and chooses.

Note that -g 48 -keyint_min 48 -sc_threshold 0 applies to every level, which is what keeps the segment boundaries aligned. Without that, switching quality mid-playback stalls or glitches.

Three levels is usually enough. Each one multiplies encoding time and storage, and viewers cannot tell the difference between five levels and three.

Serving it correctly

This is the second common mistake, and it produces a stream that is perfectly encoded and will not play.

The server must send the right MIME types. In .htaccess:

AddType application/vnd.apple.mpegurl .m3u8
AddType video/MP2T .ts

If the player is on a different domain, CORS headers are also required, or the browser blocks the segment requests.

Do not cache the playlist aggressively while a stream is live. The player needs to see it change. Segments themselves never change once written and can be cached indefinitely.

Live streaming

For live, drop the VOD playlist type and keep a rolling window of segments:

ffmpeg -i rtmp://source/stream \
 -c:v libx264 -preset veryfast -b:v 2500k \
 -g 48 -keyint_min 48 -sc_threshold 0 \
 -c:a aac -b:a 128k \
 -f hls -hls_time 4 -hls_list_size 6 \
 -hls_flags delete_segments \
 -hls_segment_filename "live_%03d.ts" live.m3u8

-hls_list_size 6 keeps six segments in the playlist and delete_segments removes older files, so disk use stays bounded rather than growing until the volume fills.

Latency is roughly segment length multiplied by how many the player buffers, typically three. Four-second segments means twelve to twenty seconds behind live, which is normal for HLS and not something to tune away with very short segments; that trades latency for a request storm.

HLS or DASH

HLS has the widest device support, particularly on Apple platforms where it is the only option that works natively. DASH is codec-agnostic and slightly more flexible.

If you are delivering one format, deliver HLS. Creating DASH streams goes into the alternative and when it is worth producing both.

When it does not play

Nothing happens and the console shows a MIME type error. Add the AddType lines above.

Plays but will not switch quality. Keyframes are not aligned across levels. Re-encode with forced keyframes.

404 on segments. The paths in the playlist do not match where the files sit. Check -hls_segment_filename against your directory layout.

Plays on desktop, not on iOS. Almost always audio: use AAC, two channels, at a standard sample rate.

The disk filled during a live stream. -hls_flags delete_segments was missing.

If encoding the ladder is too slow to keep up, FFmpeg performance optimization covers preset and threading choices. Ahosting's FFmpeg hosting ships FFmpeg and FFprobe pre-installed with HLS and DASH ready, from $16.79/month.

For sending to a streaming platform rather than serving the stream yourself, How to Stream to RTMP with FFmpeg deals with the ingest side.

Each rendition in the ladder needs a rate-control decision of its own, and a peak ceiling matters more here than anywhere else. See CRF, Two-Pass and Bitrate: What to Use When.

Check the playlist before blaming the player

A stream that will not play is diagnosed from the text files, which are readable and usually say what is wrong.

curl -s https://example.com/stream/master.m3u8
curl -s https://example.com/stream/720p/index.m3u8 | head -12
curl -sI https://example.com/stream/720p/segment0.ts | head -1

Read the master playlist first. Each variant line must carry a bandwidth value and a resolution, and the address that follows must be fetchable. A relative address that resolves to the wrong directory is the single most common fault, and it is visible immediately by requesting it yourself.

Then confirm one segment actually downloads. A playlist that lists segments which return a not found error produces a player that spins forever with no error message.

Segment length is a trade off, not a default

The value chosen decides both how quickly playback starts and how well it adapts, and copying a number from an example is how streams end up feeling wrong.

Short segments start faster and let the player change quality sooner, at the cost of more files and more requests. Long segments are more efficient and slower to react, so a viewer whose connection degrades stays on the wrong quality longer.

ffprobe -v error -show_entries format=duration -of csv=p=0 stream/720p/segment0.ts
grep -c '^#EXTINF' stream/720p/index.m3u8

Confirm the actual segment duration matches what you asked for. If it does not, the keyframe interval and the segment length disagree, and the encoder is cutting where it can rather than where you specified.

The ladder should match the audience

Every extra quality level multiplies encoding time and storage, and levels nobody selects are pure cost.

A ladder needs a rung low enough for a poor mobile connection and one high enough for the best quality you intend to offer, with steps large enough to be worth switching between. Steps closer than roughly one and a half times apart produce switching that gains nothing.

awk -F, '/BANDWIDTH/{for(i=1;i<=NF;i++) if($i ~ /BANDWIDTH/) print $i}' stream/master.m3u8

Read the bandwidth values as a list and check the spacing. Four well spaced levels serve an audience better than seven bunched together, and cost roughly half as much to produce. Running FFmpeg jobs in parallel deals with the encoding cost.