Skip to content

MP4 to M3U8 Tutorial: FFmpeg HLS Slicing and AES-128 Encryption

Converting MP4 to M3U8 (HLS format) is essential for web streaming. HLS is the standard for adaptive bitrate streaming, supported by Safari natively and other browsers via hls.js. This tutorial covers FFmpeg commands for slicing, AES-128 encryption, and deployment.

What is HLS?

HLS (HTTP Live Streaming) is Apple's streaming protocol (RFC 8216). It works by:

  1. Encoding video at multiple bitrates/resolutions
  2. Splitting each version into small TS segments (2-10 seconds)
  3. Generating M3U8 playlist files describing the segments
  4. Serving everything over standard HTTP

Players download the M3U8, then request segments based on available bandwidth, switching quality as conditions change.

For M3U8 format details, see our M3U8 file complete guide.

Basic MP4 to M3U8 conversion

Stream copy (fastest, lossless)

bash
ffmpeg -i input.mp4 \
  -codec copy \
  -hls_time 6 \
  -hls_playlist_type vod \
  -hls_segment_filename "seg_%03d.ts" \
  playlist.m3u8

Parameters:

  • -codec copy: Copy streams without re-encoding (fastest, lossless)
  • -hls_time 6: 6-second segments
  • -hls_playlist_type vod: Video-on-demand playlist
  • -hls_segment_filename "seg_%03d.ts": Segment naming pattern (seg_000.ts, seg_001.ts, etc.)
  • playlist.m3u8: Output playlist file

This runs in seconds for most videos.

Re-encode to H.264

If source video isn't H.264/AAC (or you want to change quality):

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

Multi-bitrate HLS (adaptive streaming)

For adaptive bitrate streaming, generate multiple versions and a master playlist:

bash
ffmpeg -i input.mp4 \
  -map 0:v:0 -map 0:a:0 -map 0:v:0 -map 0:a:0 -map 0:v:0 -map 0:a:0 \
  -c:v:0 libx264 -b:v:0 800k -s:v:0 640x360 \
  -c:v:1 libx264 -b:v:1 2000k -s:v:1 1280x720 \
  -c:v:2 libx264 -b:v:2 5000k -s:v:2 1920x1080 \
  -c:a aac -b:a 128k \
  -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2" \
  -f hls \
  -hls_time 6 \
  -hls_playlist_type vod \
  -master_pl_name master.m3u8 \
  -hls_segment_filename "v%v/seg_%03d.ts" \
  "v%v/playlist.m3u8"

This creates:

  • master.m3u8: Master playlist referencing all versions
  • v0/playlist.m3u8: 360p playlist
  • v1/playlist.m3u8: 720p playlist
  • v2/playlist.m3u8: 1080p playlist

Segment duration guide

Use caseDurationProsCons
VOD6-10 secondsFewer files, better compressionHigher latency
Live streaming2-4 secondsLower latencyMore files, encoding overhead
LL-HLS1-2 secondsSub-2-second latencyRequires special server support
Mobile4-6 secondsBalance for cellular networks-

AES-128 encryption

AES-128 encryption provides basic content protection. Not as strong as DRM but enough to prevent casual downloading.

Generate encryption key

bash
# Generate 16-byte random key
openssl rand 16 > enc.key

# Generate 16-byte IV
openssl rand -hex 16

Create key info file

Create enc.keyinfo:

text
https://example.com/enc.key
enc.key
00000000000000000000000000000001

Format:

  • Line 1: URL where players will fetch the key
  • Line 2: Local path to key file (for FFmpeg to use)
  • Line 3: IV (hex string)

Encrypt HLS with FFmpeg

bash
ffmpeg -i input.mp4 \
  -codec copy \
  -hls_time 6 \
  -hls_playlist_type vod \
  -hls_segment_filename "seg_%03d.ts" \
  -hls_key_info_file enc.keyinfo \
  playlist.m3u8

The resulting M3U8 will contain:

text
#EXT-X-KEY:METHOD=AES-128,URI="https://example.com/enc.key",IV=0x00000000000000000000000000000001

Auto-generate key with FFmpeg

bash
ffmpeg -i input.mp4 \
  -codec copy \
  -hls_time 6 \
  -hls_playlist_type vod \
  -hls_segment_filename "seg_%03d.ts" \
  -hls_enc 1 \
  -hls_enc_key enc.key \
  -hls_enc_key_url https://example.com/enc.key \
  -hls_enc_iv 1234567890abcdef1234567890abcdef \
  playlist.m3u8

Live streaming HLS

bash
# From a live source (e.g., RTMP input)
ffmpeg -i rtmp://example.com/live/stream \
  -c:v libx264 -preset veryfast -b:v 2500k \
  -c:a aac -b:a 128k \
  -hls_time 4 \
  -hls_list_size 6 \
  -hls_flags delete_segments \
  -hls_segment_filename "live_%03d.ts" \
  live.m3u8
  • -hls_list_size 6: Keep only 6 segments in playlist (sliding window)
  • -hls_flags delete_segments: Delete old segments from disk
  • -hls_time 4: 4-second segments for lower latency

LL-HLS (Low-Latency HLS)

bash
ffmpeg -i input.mp4 \
  -c:v libx264 -crf 23 -preset veryfast -tune zerolatency \
  -c:a aac -b:a 128k \
  -hls_time 2 \
  -hls_playlist_type vod \
  -hls_segment_type fmp4 \
  -hls_fmp4_init_filename init.mp4 \
  -hls_segment_filename "seg_%03d.m4s" \
  -hls_flags independent_segments \
  playlist.m3u8
  • -hls_segment_type fmp4: Use fragmented MP4 (CMAF) instead of TS
  • -hls_flags independent_segments: Each segment is independently decodable
  • -tune zerolatency: Reduce encoding latency

HLS with subtitles

bash
ffmpeg -i input.mp4 -i subtitles.srt \
  -map 0:v -map 0:a -map 1:s \
  -c:v copy -c:a copy -c:s webvtt \
  -hls_time 6 \
  -hls_playlist_type vod \
  -hls_segment_filename "seg_%03d.ts" \
  -hls_subtitle_path "sub_%v_%03d.vtt" \
  playlist.m3u8

Deployment

Static file hosting

HLS works over standard HTTP. Upload M3U8 and TS files to any web server:

bash
# Upload to S3
aws s3 sync ./hls-output/ s3://your-bucket/videos/

# Set correct MIME types (most servers auto-detect, but verify)
# .m3u8 → application/vnd.apple.mpegurl
# .ts → video/mp2t

Nginx configuration

nginx
server {
    location /hls/ {
        types {
            application/vnd.apple.mpegurl m3u8;
            video/mp2t ts;
        }
        root /var/www;
        add_header Access-Control-Allow-Origin *;
        add_header Cache-Control "public, max-age=3600";
    }
}

CDN deployment

For production, use a CDN:

  • CloudFront: AWS CDN, integrates with S3
  • Cloudflare: Free tier, global network
  • Akamai: Enterprise CDN with HLS specialization

Browser playback

Safari (native support)

html
<video controls>
  <source src="https://example.com/playlist.m3u8" type="application/vnd.apple.mpegurl">
</video>

Other browsers (hls.js)

html
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<video controls id="video"></video>
<script>
  const video = document.getElementById('video');
  if (Hls.isSupported()) {
    const hls = new Hls();
    hls.loadSource('https://example.com/playlist.m3u8');
    hls.attachMedia(video);
  } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
    // Safari native
    video.src = 'https://example.com/playlist.m3u8';
  }
</script>

For hls.js details, see our HLS.js complete guide.

Common issues

IssueCauseSolution
Segments out of orderNaming pattern wrongUse %03d for zero-padded numbering
CORS errorsServer missing CORS headersAdd Access-Control-Allow-Origin: *
Audio missingMultiple audio tracksUse -map to select audio
Encryption failsKey info file format wrongCheck 3-line format
Live playlist not updatingWrong playlist typeRemove -hls_playlist_type vod
Segments too largeWrong -hls_timeUse 2-4 seconds for live
Poor qualitySource low qualityRe-encode with -crf 23

Summary

HLS is the standard for web streaming, and FFmpeg makes MP4 to M3U8 conversion straightforward:

  • Basic conversion: ffmpeg -i input.mp4 -codec copy -hls_time 6 -hls_playlist_type vod -hls_segment_filename "seg_%03d.ts" playlist.m3u8
  • Adaptive streaming: Multiple bitrates with master playlist
  • Encryption: AES-128 with key info file
  • Live streaming: Use -hls_list_size and delete_segments
  • Low latency: Use fMP4 segments and shorter duration

Quick reference:

  • VOD: -hls_playlist_type vod -hls_time 6
  • Live: -hls_list_size 6 -hls_flags delete_segments -hls_time 4
  • Encryption: -hls_key_info_file enc.keyinfo
  • LL-HLS: -hls_segment_type fmp4 -hls_time 2

For FFmpeg basics, see our FFmpeg Getting Started Tutorial.

References

Last updated: