Skip to content
HLS & DASH

How to Convert MP4 to HLS with FFmpeg (Step by Step)

Convert MP4 to HLS with FFmpeg: single-quality and adaptive ladders, fMP4 segments, AES-128 encryption, and how to test and host the result.

How to Convert MP4 to HLS with FFmpeg (Step by Step)
On this page 15 sections

You have a folder of MP4 files and you want them to stream properly: quick to start, smooth on slow connections, and playable on phones, laptops and TVs. The standard way to do that is to convert MP4 to HLS. FFmpeg, the free command-line tool behind a large part of the video world, can do the whole job on its own.

This guide walks through it step by step, from the simplest one-line conversion to a full adaptive ladder with encryption, then shows how to test and host the result. You do not need to be an FFmpeg expert. You do need a terminal and a little patience.

What “converting to HLS” actually means

An MP4 is a single file. HLS is a set of files:

  • Short segments of video, usually a few seconds each.
  • A media playlist (.m3u8) listing the segments of one quality level.
  • Optionally, a master playlist listing several quality levels so the player can switch between them.

Converting an MP4 to HLS means cutting it into segments and writing those playlists. If you also want several quality levels, it means encoding the video more than once at different sizes. Our explainer on HLS streaming covers the concepts; here we focus on doing it.

Before you start

Install a recent FFmpeg build. On Linux use your package manager or a static build, on macOS brew install ffmpeg, on Windows download a build and add it to your PATH. Check it works:

ffmpeg -version

Then look at your source file:

ffprobe -v error -show_entries stream=codec_name,width,height,r_frame_rate -of compact input.mp4

Note the video codec, resolution and frame rate. They decide which of the recipes below fits.

Recipe 1: the fastest conversion, no re-encoding

If your MP4 already uses H.264 video and AAC audio, which most phone and camera exports do, you can split it without touching the picture:

ffmpeg -i input.mp4 -c copy -hls_time 6 -hls_playlist_type vod \
  -hls_segment_filename "out/seg_%03d.ts" out/index.m3u8

This runs in seconds and keeps the original quality. The catch is that FFmpeg can only cut where a keyframe already exists. If the camera placed keyframes irregularly, some segments will be longer than six seconds. For casual use that is fine. For a streaming platform it is not, because irregular segments upset players and break the target-duration rule. Recipe 2 fixes that.

Recipe 2: one quality level, clean segments

Re-encode with a fixed keyframe interval so every segment starts cleanly:

ffmpeg -i input.mp4 \
  -c:v libx264 -preset medium -crf 21 -pix_fmt yuv420p \
  -g 60 -keyint_min 60 -sc_threshold 0 \
  -c:a aac -b:a 128k -ac 2 \
  -hls_time 6 -hls_playlist_type vod -hls_segment_type fmp4 \
  -hls_segment_filename "out/seg_%03d.m4s" out/index.m3u8

What the key flags do:

  • -crf 21 sets a quality target. Lower numbers mean higher quality and bigger files.
  • -g 60 -keyint_min 60 -sc_threshold 0 forces a keyframe every 60 frames and stops extra ones at scene cuts. At 30 fps that is every two seconds, which divides evenly into six-second segments. At 25 fps use 50; at 60 fps use 120. Our guide to keyframes explains why this matters.
  • -hls_segment_type fmp4 writes fragmented MP4 segments instead of MPEG-TS, plus an init.mp4 file. Modern players prefer this, and it prepares you for CMAF and DRM later.

Recipe 3: a full adaptive ladder

One quality level will buffer for viewers on weak connections. Adaptive streaming needs several renditions and a master playlist. FFmpeg can produce all of them in one command:

ffmpeg -i input.mp4 -filter_complex \
 "[0:v]split=3[a][b][c];[a]scale=w=1920:h=1080[v1];[b]scale=w=1280:h=720[v2];[c]scale=w=640:h=360[v3]" \
 -map "[v1]" -c:v:0 libx264 -crf 21 -maxrate:v:0 5000k -bufsize:v:0 10000k \
 -map "[v2]" -c:v:1 libx264 -crf 22 -maxrate:v:1 3000k -bufsize:v:1 6000k \
 -map "[v3]" -c:v:2 libx264 -crf 23 -maxrate:v:2 800k -bufsize:v:2 1600k \
 -preset medium -g 60 -keyint_min 60 -sc_threshold 0 -pix_fmt yuv420p \
 -map a:0 -map a:0 -map a:0 -c:a aac -b:a 128k -ac 2 \
 -f hls -hls_time 6 -hls_playlist_type vod -hls_segment_type fmp4 \
 -master_pl_name master.m3u8 -hls_segment_filename "out/%v/seg_%03d.m4s" \
 -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2" out/%v/index.m3u8

It looks intimidating, but it follows a simple pattern. The filter splits the video into three copies and scales each one. Each -map line encodes one copy with its own quality target and bitrate cap. The audio is mapped three times so every rendition has sound. The -var_stream_map line pairs each video with its audio, and -master_pl_name writes the master playlist that points to all three.

Why cap the bitrate as well as setting CRF? CRF alone would let a difficult scene spike far above what a viewer’s connection can handle. The cap keeps each rendition inside the bandwidth budget the player expects. The bitrates here are a starting point; our encoding settings guide has a fuller ladder and explains how to tune it for your content.

If your source is smaller than 1080p, drop the top rendition. Upscaling adds file size without adding detail.

Recipe 4: adding AES-128 encryption

To stop casual downloading, HLS can encrypt each segment with AES-128. First create a key and a key info file:

openssl rand 16 > enc.key
echo "https://example.com/keys/lesson1.key" > enc.keyinfo
echo "enc.key" >> enc.keyinfo
openssl rand -hex 16 >> enc.keyinfo

The key info file has three lines: the URL players will fetch the key from, the local path of the key, and an initialisation vector. Then add one option to any recipe above:

-hls_key_info_file enc.keyinfo

Upload the key to the URL you listed, and protect that URL so only logged-in viewers can fetch it. Be clear about what this gives you: it keeps strangers out, but anyone who can play the stream can also fetch the key. For stronger protection you need DRM. The trade-offs are covered in AES-128 encryption vs DRM.

Testing the output

Before uploading, check three things.

Read the playlists. Open master.m3u8 and one index.m3u8 in a text editor, or paste them into our M3U8 analyzer. Make sure every rendition has RESOLUTION and CODECS attributes and that no segment is longer than the target duration.

Play it locally. Browsers will not load HLS from file:// paths, so serve the folder:

cd out && python3 -m http.server 8000

Then open http://localhost:8000/master.m3u8 in the HLS player. Use its quality menu to lock each rendition in turn and confirm all three play.

Check sizes. Add up the folder size and compare it with the original MP4. For a 1080p source with three renditions, the total is often similar to or smaller than the source, because the source was encoded at a high camera bitrate. The bitrate calculator helps you predict sizes before you encode a whole library.

Hosting the result

HLS files are ordinary static files, so any web server or object storage can host them. For real audiences, put them behind a CDN. Three settings matter:

  1. CORS headers, so players on your website can load them. At minimum Access-Control-Allow-Origin for your domain.
  2. Correct MIME types: application/vnd.apple.mpegurl for .m3u8, video/mp4 for .m4s and init.mp4, video/mp2t for .ts.
  3. Long cache times for segments and on-demand playlists, since they never change.

Our guide to video CDNs covers caching in more detail.

Common mistakes

  • Forgetting -sc_threshold 0. Without it, scene cuts insert extra keyframes, and segments come out uneven.
  • Using -c copy on files with sparse keyframes, producing 20-second segments that break the target-duration rule.
  • Mismatched audio. If renditions have different audio settings, some players click or drop audio when switching quality.
  • Uploading only the master playlist. The player needs every media playlist, every segment and every init.mp4.
  • Relative paths broken by folder changes. Keep the output folder structure exactly as FFmpeg wrote it.

Batch converting a whole library

Once one file works, a short loop converts a folder. In bash:

for f in *.mp4; do
  name="${f%.mp4}"
  mkdir -p "hls/$name"
  ffmpeg -i "$f" [your chosen recipe options] "hls/$name/index.m3u8"
done

Run it overnight on a spare machine or a cloud server. Encoding is CPU-heavy, and a library of long videos can take hours. Hardware encoders such as NVENC or Quick Sync speed things up considerably, at a small cost in efficiency.

When to stop using FFmpeg directly

FFmpeg is perfect for learning, small libraries and custom pipelines. As you grow, you may want a dedicated packager such as Shaka Packager or Bento4 to produce HLS and DASH from the same files, handle DRM, and keep packaging separate from encoding. Hosted video platforms and cloud encoders go further and do everything, including thumbnails and captions, behind an upload button. The concepts you learned here carry straight over.

How long does it take?

Encoding time depends on your processor, the preset and the number of renditions. As a rough guide on a modern eight-core desktop, a one-hour 1080p lecture takes a few minutes with Recipe 1, because nothing is re-encoded. Recipe 2 at the “medium” preset runs at a few times real speed, so roughly 15 to 25 minutes. Recipe 3, with three renditions, might take 30 to 60 minutes.

If that is too slow, you have three levers. Use a faster preset such as “veryfast”, accepting slightly larger files. Use a hardware encoder: on an NVIDIA card, replace libx264 with h264_nvenc and swap -crf for -cq. Or split the work across several machines, since each file is independent.

Adding subtitles

If your MP4 has a separate caption file, you can publish it alongside the HLS stream so viewers can switch captions on. The cleanest way is to convert the captions to WebVTT and reference them from the master playlist as a subtitle group. Our guide to adding subtitles to HLS shows the exact playlist lines, and the subtitle converter turns an SRT file into WebVTT in your browser.

A quick sanity check before you publish

Open the master playlist, pick the lowest rendition and play it on your phone over mobile data. If that looks acceptable and starts quickly, most of your viewers will be fine. If it buffers, lower its bitrate further or add a smaller rung. It is the rendition people on weak connections depend on, and they are the ones least likely to complain before leaving.

Summary

To convert MP4 to HLS with FFmpeg, you cut the video into short segments and write playlists. Use -c copy for a quick single-quality conversion, re-encode with fixed keyframes for clean segments, and build a three-rendition ladder with a master playlist for real adaptive streaming. Add AES-128 if you need basic protection, test the result in an HLS player, and host it behind a CDN with proper CORS and MIME types. That is everything a small streaming setup needs, from a single command-line tool.

Frequently asked questions

Can FFmpeg convert MP4 to HLS without re-encoding?

Yes, if the MP4 already uses H.264 or HEVC with AAC audio and has regular keyframes. Use -c copy. Segments will be cut at existing keyframes, so their lengths may vary.

What segment length should I use?

Six seconds is a good default for on-demand video. Use two seconds if you care about fast startup or plan to use the same settings for live streams.

Should I use .ts or fMP4 segments?

Use fMP4 for new projects. It works on all modern players, lets HLS and DASH share files, and is required for most DRM setups.

Keep reading