For a VOD playlist you own, the fastest conversion is a stream copy: ffmpeg -i master.m3u8 -c copy -movflags +faststart output.mp4. It repackages the existing tracks into an MP4 container without recompressing a single frame, so quality is identical and the job usually finishes in seconds. You only need to transcode when the source codecs cannot live inside MP4, or when you want to change resolution or bitrate.
How it works
An .m3u8 master playlist is a text index of variant playlists, each of which is a text index of media segments. ffmpeg opens the playlist through the HLS demuxer, resolves the segment URLs, concatenates the elementary streams, and hands them to the MP4 muxer. With -c copy the encoded bitstream is passed through untouched; the container changes, the pixels do not.
This distinction matters for troubleshooting. If playback fails after a stream copy, the problem is usually container-level (timestamps, AAC in MPEG-TS, missing key URI), not video quality. If you see Unknown encoder or Codec not supported in container, the track must be re-encoded.
# Inspect what is actually inside the playlist before converting
ffprobe -v error -show_entries format=duration,format_name \
-show_entries stream=index,codec_name,codec_type,width,height \
-of default=noprint_wrappers=1 master.m3u8
A VOD playlist ends with #EXT-X-ENDLIST. A live playlist keeps growing and has no ENDLIST, so a plain -c copy command will follow the live edge until you stop it. Only run conversion jobs against content you are authorized to process, and prefer finished VOD assets.
Step-by-step: commands that cover real cases
1. Basic remux (local playlist or a URL you control).
ffmpeg -allowed_extensions ALL \
-i master.m3u8 \
-map 0:v:0 -map 0:a:0? \
-c copy -movflags +faststart \
output.mp4
-map 0:v:0 -map 0:a:0? picks the first video stream and the first audio stream if one exists, which prevents ffmpeg from trying to mux every subtitle or data track. -movflags +faststart relocates the MP4 index to the beginning of the file so web players can start before the whole file arrives.
2. Remote playlist with non-standard segment extensions or protocols.
ffmpeg -protocol_whitelist file,http,https,tcp,tls,crypto \
-allowed_extensions ALL \
-i https://cdn.example.com/vod/master.m3u8 \
-c copy -bsf:a aac_adtstoasc \
-movflags +faststart output.mp4
ffmpeg restricts protocols for playlists by default and rejects segments whose extension is not on its allow list. -protocol_whitelist is needed when the playlist references https segments combined with local or encrypted key files, and crypto must be present for AES-128 HLS. -bsf:a aac_adtstoasc converts AAC ADTS from MPEG-TS into the ASC format MP4 expects; modern ffmpeg builds often apply it automatically, but keeping it explicit avoids a silent audio failure on older builds.
3. Transcode when stream copy is impossible.
ffmpeg -i master.m3u8 \
-c:v libx264 -preset veryfast -crf 22 -pix_fmt yuv420p \
-c:a aac -b:a 128k \
-movflags +faststart output.mp4
Use this when the source uses a codec MP4 cannot carry, when you need a smaller file, or when variants are misaligned and produce timestamp warnings. It is slower because every frame is recompressed.
4. Verify the result.
ffprobe -v error -show_entries format=duration,size -of csv=p=0 output.mp4
ffmpeg -v error -i output.mp4 -f null - # full decode, prints errors only
If the reported duration is much shorter than the playlist duration, the download was truncated or a segment returned an error.
Common mistakes and parameters
| Flag | What it does | When to use it |
|---|---|---|
-c copy | Copies encoded tracks without recompressing | Default for any remux |
-movflags +faststart | Moves the MP4 moov atom to the front | Any file served over the web |
-allowed_extensions ALL | Permits segments with unusual extensions | Local playlists, tokenized segment names |
-protocol_whitelist | Allows the demuxer to open listed protocols | Remote playlists and AES-128 keys |
-bsf:a aac_adtstoasc | Fixes AAC carried in MPEG-TS | Older ffmpeg or silent audio |
-map 0:v:0 -map 0:a:0? | Selects one video and one audio track | Master playlists with many renditions |
-live_start_index | Chooses the segment offset for a live playlist | Only when converting a live recording |
| Symptom | Likely level | First thing to check |
|---|---|---|
| Output is tiny or empty | Network | Segment URLs return 200, not 403 |
| Video plays, no audio | Container | AAC bitstream filter |
| Duration shorter than expected | Playlist | Missing ENDLIST or truncated capture |
Codec not currently supported in container | Codec | Switch to transcode |
| Choppy playback with no decode errors | Timestamps | Discontinuity tags and variant alignment |
Troubleshooting
Protocol 'https' not on whitelist: add-protocol_whitelist file,http,https,tcp,tls,cryptobefore-i.Invalid data found when processing input: the URL returned HTML (a login or error page) rather than a playlist; confirm withcurl -Ithat the response is a playlist MIME type.Error opening key fileorUnable to open key: the#EXT-X-KEYURI must be reachable and return exactly 16 bytes; addcryptoto the whitelist and check the key path is absolute or correctly resolved.Non-monotonous DTSwarnings: usually a#EXT-X-DISCONTINUITYboundary or variants encoded with different GOP settings; transcode, or convert each variant separately.- Output has no audio: try
-bsf:a aac_adtstoasc, then confirm withffprobe -show_streamsthat the audio track was mapped at all. - Conversion runs forever: the playlist is live. Stop with
q, and use a finished VOD rendition instead.
Related reading
- Confirm the stream actually plays before converting it with the online HLS player.
- Check playlist syntax and variant structure with the stream inspector.
- Browse all HLS and ffmpeg guides for CORS, remux, and buffering fixes.
- How to Play m3u8 in Chrome (3 Working Methods) - set up hls.js playback in Chrome.
- How to Test an HLS Stream Online - run a full playlist, segment and header check.
- How to Fix hls.js CORS Errors - diagnose cross-origin and playlist request failures.