DEV Community

Mason K
Mason K

Posted on

HLS with fMP4 instead of MPEG-TS: FFmpeg commands, playlist diff, and the hev1 trap

TL;DR

We'll package one encode two ways with FFmpeg (MPEG-TS and fMP4), diff the playlists, then add an HEVC rung and catch the hev1 vs hvc1 tag problem with a one-line ffprobe check you can drop into CI.

📦 Code: github.com/USER/hls-fmp4 (replace before publishing)

HLS used to mean .ts files. Today the spec requires fragmented MP4 for HEVC, AV1 and several newer audio codecs, so if you plan to add any of them you'll want fMP4 in the pipeline anyway. #ffmpeg #video

Versions used: ffmpeg 6.1.1 with libx264 and libx265, ffprobe, and a Linux shell. Newer FFmpeg should behave the same, but re-check the tag defaults in step 5.

1. 🎥 Make a test clip

ffmpeg -y -f lavfi -i "testsrc2=size=1280x720:rate=24:duration=60" \
  -f lavfi -i "sine=frequency=440:duration=60" \
  -c:v libx264 -preset veryfast -pix_fmt yuv420p -c:a aac -shortest src.mp4
Enter fullscreen mode Exit fullscreen mode

2. 📦 Package as MPEG-TS

# ts.sh
mkdir -p ts
C="-c:v libx264 -preset veryfast -g 144 -keyint_min 144 -sc_threshold 0 -c:a aac -b:a 128k"
ffmpeg -y -i src.mp4 $C -f hls -hls_time 6 -hls_playlist_type vod \
  -hls_segment_type mpegts -hls_segment_filename 'ts/seg_%03d.ts' ts/index.m3u8
head -8 ts/index.m3u8
Enter fullscreen mode Exit fullscreen mode
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:6.000000,
seg_000.ts
#EXTINF:6.000000,
Enter fullscreen mode Exit fullscreen mode

💡 Tip: -g 144 -keyint_min 144 -sc_threshold 0 makes segments exactly 6 s at 24 fps. Without it the muxer cuts on the next keyframe, which can be 10 s away.

3. 📦 Package as fMP4

Same encode settings, two changed flags:

# fmp4.sh
mkdir -p fm
C="-c:v libx264 -preset veryfast -g 144 -keyint_min 144 -sc_threshold 0 -c:a aac -b:a 128k"
ffmpeg -y -i src.mp4 $C -f hls -hls_time 6 -hls_playlist_type vod \
  -hls_segment_type fmp4 -hls_fmp4_init_filename init.mp4 \
  -hls_segment_filename 'fm/seg_%03d.m4s' fm/index.m3u8
head -9 fm/index.m3u8
ls fm
Enter fullscreen mode Exit fullscreen mode
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MAP:URI="init.mp4"
#EXTINF:6.000000,
seg_000.m4s
#EXTINF:6.000000,
index.m3u8  init.mp4  seg_000.m4s  seg_001.m4s  ...
Enter fullscreen mode Exit fullscreen mode

Two differences to remember:

MPEG-TS fMP4
EXT-X-VERSION 3 7
Init segment none init.mp4 via EXT-X-MAP
Segment extension .ts .m4s

The RFC says each fMP4 segment must have an EXT-X-MAP tag, and EXT-X-MAP needs compatibility version 6 or higher (RFC 8216 3.3 and 4.3.2.5). FFmpeg set version 7 for us.

4. 📏 Compare sizes

cat ts/*.ts | wc -c
cat fm/init.mp4 fm/*.m4s | wc -c
Enter fullscreen mode Exit fullscreen mode
17086568
16523545
Enter fullscreen mode Exit fullscreen mode

That's about 3 percent smaller for fMP4 on this clip. Don't generalize it: this is a synthetic test pattern, and real catalogues will differ. Run it on your own titles.

⚠️ Note: if you try to ffprobe a lone .m4s file you'll see trun track id unknown, no tfhd was found and error reading header. The media segments aren't playable without init.mp4. Probe the init segment, or concatenate init + segment first.

5. 🪤 Add an HEVC rung and hit the tag trap

Apple's authoring spec says HEVC in HLS must be in fMP4, so we're already in the right container. Now package it the naive way:

# hevc_naive.sh
ffmpeg -y -i src.mp4 -c:v libx265 -g 144 -keyint_min 144 -x265-params scenecut=0 \
  -c:a aac -f hls -hls_time 6 -hls_playlist_type vod \
  -hls_segment_type fmp4 -hls_fmp4_init_filename init_naive.mp4 \
  -hls_segment_filename 'hn_%03d.m4s' hn.m3u8
ffprobe -v error -select_streams v:0 \
  -show_entries stream=codec_name,codec_tag_string -of csv=p=0 init_naive.mp4
Enter fullscreen mode Exit fullscreen mode
hevc,hev1
Enter fullscreen mode Exit fullscreen mode

hev1 keeps parameter sets inside the samples. The spec recommends avc1, hvc1 and dvh1 over avc3, hev1 and dvhe (Apple authoring spec 1.10). Add the tag:

# hevc_fixed.sh
ffmpeg -y -i src.mp4 -c:v libx265 -tag:v hvc1 -g 144 -keyint_min 144 \
  -x265-params scenecut=0 -c:a aac -f hls -hls_time 6 -hls_playlist_type vod \
  -hls_segment_type fmp4 -hls_fmp4_init_filename init.mp4 \
  -hls_segment_filename 'hv_%03d.m4s' hv.m3u8
ffprobe -v error -select_streams v:0 \
  -show_entries stream=codec_name,codec_tag_string -of csv=p=0 init.mp4
Enter fullscreen mode Exit fullscreen mode
hevc,hvc1
Enter fullscreen mode Exit fullscreen mode

💡 Tip: the stream plays in desktop browsers either way, which is why this slips through. I did not test on Apple hardware here, so treat "may not play" as the risk, and check on a real device before shipping.

6. ✅ A tiny CI check

# scripts/check_tag.sh
set -euo pipefail
tag=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_tag_string -of csv=p=0 "$1")
case "$tag" in
  avc1|hvc1|dvh1) echo "OK: $tag" ;;
  *) echo "FAIL: codec tag is $tag, expected avc1/hvc1/dvh1"; exit 1 ;;
esac
Enter fullscreen mode Exit fullscreen mode
$ ./scripts/check_tag.sh init_naive.mp4
FAIL: codec tag is hev1, expected avc1/hvc1/dvh1
$ ./scripts/check_tag.sh init.mp4
OK: hvc1
Enter fullscreen mode Exit fullscreen mode

7. 🧭 Master playlist and CODECS

Apple's spec requires CODECS on every EXT-X-STREAM-INF. FFmpeg's -var_stream_map writes it for you:

ffmpeg -y -i src.mp4 -c:v libx264 -preset veryfast -c:a aac -f hls \
  -hls_segment_type fmp4 -hls_time 6 -master_pl_name master.m3u8 \
  -var_stream_map "v:0,a:0" -hls_segment_filename 'm_%v_%03d.m4s' m_%v.m3u8
grep STREAM-INF master.m3u8
Enter fullscreen mode Exit fullscreen mode
#EXT-X-STREAM-INF:BANDWIDTH=75900,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2"
Enter fullscreen mode Exit fullscreen mode

⚠️ Note: BANDWIDTH=75900 here is clearly too low for a real ladder; I did not set VOD playlist type or bitrates on this quick example. Always set BANDWIDTH from real measured bitrates before shipping.

What's next

  • Wire the tag check into your packaging tests for every rendition.
  • Add -g/-keyint_min alignment across rungs (see the segment audit post if you have it).
  • Read Apple's HLS authoring spec sections on video and playlists once; they're short.

Wrapping up: fMP4 is two flag changes away, and the HEVC tag is the one thing that bites. Which codec are you adding next?

Top comments (0)