The short version: one ffmpeg command turns an MP4 into an HLS playlist plus a folder of segments. A working starting point for a video-on-demand file is:
ffmpeg -i input.mp4 \ -c:v libx264 -crf 21 -preset veryfast \ -c:a aac -b:a 128k \ -start_number 0 -hls_time 6 -hls_list_size 0 \ -f hls index.m3u8
This produces index.m3u8 and index0.ts, index1.ts and so on. Because -hls_list_size 0 keeps every segment in the playlist and ffmpeg appends #EXT-X-ENDLIST, the result is a VOD stream that players can seek through from start to finish.
-c:v libx264 / -c:a aac — re-encode into the codecs HLS requires for MPEG-TS segments. H.264 and AAC are the safe baseline across browsers and devices.-crf 21 -preset veryfast — quality and encoding speed. Lower CRF means better quality and larger files; 20–23 is a reasonable range for web delivery.-hls_time 6 — target segment duration in seconds, not a guarantee. ffmpeg only cuts on keyframes, so real segments may be longer.-hls_list_size 0 — unlimited entries in the playlist. Omit it for a live sliding window (the default is 5).-f hls — select the HLS muxer; the output filename ending in .m3u8 alone is not enough.The tempting shortcut is -c copy, which avoids re-encoding entirely. It only works if the input is already H.264/AAC and has keyframes at regular intervals. If keyframes are sparse, ffmpeg cannot split cleanly and you get segments of wildly uneven length or a playlist that stalls. When you control the source, force a keyframe interval first:
ffmpeg -i input.mp4 -c:v libx264 -g 48 -keyint_min 48 -sc_threshold 0 \ -c:a aac -hls_time 4 -hls_playlist_type vod -f hls index.m3u8
Here -g 48 at 24 fps puts a keyframe every 2 seconds, and -sc_threshold 0 disables scene-cut keyframes so cuts land predictably. -hls_playlist_type vod marks the output as VOD and makes ffmpeg write #EXT-X-ENDLIST for you.
A single rendition works, but adaptive bitrate is the point of HLS. Encode each variant, then write a master playlist that points at them:
#EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360 v360/index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=1800000,RESOLUTION=1280x720 v720/index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=4000000,RESOLUTION=1920x1080 v1080/index.m3u8
ffmpeg can also generate the master playlist automatically with -master_pl_name master.m3u8 plus -var_stream_map. Declare BANDWIDTH honestly: a player that is told a stream is cheaper than it really is will pick it on a slow connection and buffer constantly.
Modern HLS often uses fMP4 instead of MPEG-TS, which is also required if you want to share segments with DASH:
ffmpeg -i input.mp4 -c copy \ -hls_segment_type fmp4 -hls_fmp4_init_filename init.mp4 \ -hls_time 6 -hls_list_size 0 -f hls index.m3u8
The playlist then references an init.mp4 via #EXT-X-MAP and .m4s segment files. Serve both as video/mp4; a wrong MIME type here is a common cause of a stream that plays in one player but not another.
ffprobe index.m3u8 — confirms the playlist parses and the codecs are what you expect.curl -I https://your-domain/hls/index.m3u8 — confirms the MIME type and CORS headers.#EXT-X-ENDLIST or missing final segment usually shows up only at the end.#EXTINF entries. A mismatch means a segment was deleted while the playlist was still pointing at it.