FFmpeg Recipe

Convert MP4 to HLS for adaptive streaming

Package an MP4 into an HLS playlist with FFmpeg. Single-bitrate gets you streaming in minutes; multi-bitrate (ABR) playlists adapt to each viewer's bandwidth.

Common HLS use cases

  • VOD streaming for SaaS apps, course platforms, and membership sites
  • Adaptive bitrate playback so mobile viewers don't buffer on slow networks
  • iOS-friendly playback (HLS is the native format for Safari and iOS apps)
  • CDN-friendly delivery — small segment files cache better than monolithic MP4s
  • Live-to-VOD pipelines where you record live streams and republish as on-demand HLS

Single-bitrate HLS (simplest setup)

Re-encode an MP4 into a single-rendition HLS playlist. Good for testing, internal tools, or any case where you don't need adaptive bitrates:

ffmpeg -i input.mp4 \
  -c:v libx264 -c:a aac \
  -hls_time 6 \
  -hls_playlist_type vod \
  -hls_segment_filename "segment_%03d.ts" \
  playlist.m3u8

-hls_time 6 sets segment length to ~6 seconds, -hls_playlist_type vod marks the playlist as on-demand (vs event or live), and -hls_segment_filename controls how segments are named. Output: playlist.m3u8 plus segment_000.ts, segment_001.ts, …

Faster: copy codecs without re-encoding

If your source is already H.264 video and AAC audio, skip the re-encode and just repackage:

ffmpeg -i input.mp4 \
  -c:v copy -c:a copy \
  -hls_time 6 \
  -hls_playlist_type vod \
  -hls_segment_filename "segment_%03d.ts" \
  playlist.m3u8

Runs in seconds instead of minutes because there's no transcoding. Use FFprobe first to confirm your source is H.264/AAC — if it isn't, you have to re-encode.

Multi-bitrate (ABR) HLS

Adaptive bitrate streaming generates multiple renditions and a master playlist that lets the player switch between them based on bandwidth. Three renditions (360p / 720p / 1080p) is a common starting point:

ffmpeg -i input.mp4 \
  -filter_complex "[0:v]split=3[v1][v2][v3]; \
    [v1]scale=640:360[v1out]; \
    [v2]scale=1280:720[v2out]; \
    [v3]scale=1920:1080[v3out]" \
  -map "[v1out]" -c:v:0 libx264 -b:v:0 800k  -profile:v:0 baseline \
  -map "[v2out]" -c:v:1 libx264 -b:v:1 2800k -profile:v:1 main \
  -map "[v3out]" -c:v:2 libx264 -b:v:2 5000k -profile:v:2 high \
  -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_flags independent_segments \
  -master_pl_name master.m3u8 \
  -hls_segment_filename "stream_%v/segment_%03d.ts" \
  -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2" \
  "stream_%v/playlist.m3u8"

split=3 duplicates the video stream three times so each scale filter can resize independently. -var_stream_map ties video and audio streams into rendition groups. -master_pl_name master.m3u8 writes the top-level manifest the player reads first.

What FFmpeg outputs

For the multi-bitrate command above, you get this directory layout:

master.m3u8
stream_0/playlist.m3u8
stream_0/segment_000.ts
stream_0/segment_001.ts
...
stream_1/playlist.m3u8
stream_1/segment_000.ts
...
stream_2/playlist.m3u8
stream_2/segment_000.ts
...

The player loads master.m3u8, picks a rendition, then loads that rendition's playlist.m3u8 and its segments. All paths in the master are relative, so the directory structure must be preserved when you upload to S3, R2, or your CDN origin.

Serving HLS correctly

MIME types matter. Configure your storage / CDN to serve:

  • .m3u8application/vnd.apple.mpegurl
  • .tsvideo/mp2t
  • .m4s (fMP4 segments) → video/iso.segment

CORS. If your player loads HLS from a different origin, set Access-Control-Allow-Origin on the manifest and segment responses.

CDN. Put a CDN (CloudFront, Cloudflare, Fastly) in front of the bucket. Segment requests are small but frequent — origin pulls add up fast without caching.

Auth / DRM. For paid content, use signed URLs (S3 / CloudFront / R2) or AES-128 segment encryption (-hls_key_info_file) for token-gated playback.

Choosing segment duration

Apple's HLS spec recommends 6-second segments. Trade-offs:

  • Shorter (2-4s): faster startup, more responsive ABR switching, more files to manage and more HTTP requests
  • Longer (8-10s): fewer files, better caching, slower channel-change and slower ABR switching
  • Default (6s): balanced, matches Apple's recommendation, works well for most VOD

Common mistakes

  • Misaligned keyframes across renditions: ABR switching only works at keyframes. Force a fixed GOP with -g 60 -keyint_min 60 -sc_threshold 0 (assuming 30fps and 2s GOP) so segment boundaries align across all renditions.
  • Wrong MIME type: if .m3u8 serves as text/plain, some players silently fail. Always set application/vnd.apple.mpegurl.
  • Missing independent_segments flag: without it, segments may have interdependencies that break ABR switching mid-stream.
  • Uploading files individually without preserving paths: master.m3u8 uses relative URLs to find renditions. Flatten the directory and playback breaks.
  • Not testing on iOS Safari: Safari's native HLS player is the strictest. If it works there, it usually works everywhere.

FFmpeg Micro API note: Direct HLS output from our API is on the roadmap. Today the API produces single-file outputs (MP4, WebM, MP3, etc.) so it's a good fit for the upstream transcode step before HLS packaging. For managed end-to-end HLS delivery with CDN and analytics, see the FFmpeg Micro vs Mux comparison for trade-offs.

Related recipes

Need MP4 transcoding before HLS packaging?

Use FFmpeg Micro to transcode and prep your source MP4s, then run the HLS packaging step on your own infrastructure or a managed streaming service. Start with 100 free minutes.