Back in 2017 I was helping a sports broadcaster push a major final to about 2 million concurrent viewers. We had two complete copies of every bitrate sitting on origin — one set of .ts segments for HLS and another set of .mp4 segments for DASH. Half our cache was wasted on duplicates, purges took forever, and a single packaging delay meant one protocol fell behind the other. That's exactly the mess CMAF was designed to clean up, and if you're still running dual-packaging today, you're paying for it in storage, cache efficiency, and operational headaches.
The Dual-Packaging Nightmare CMAF Was Built to Solve
Before Common Media Application Format came along, HLS and DASH genuinely needed different files. HLS wanted MPEG-2 Transport Stream segments (.ts), DASH wanted ISO BMFF (.mp4) segments. They couldn't share anything. So for every 6-second segment at, say, five bitrates, you were writing ten files to origin and warming the CDN cache with ten separate objects that contained the same audio and video, just wrapped differently. I've seen origins with 70-80TB of duplicated segments for a modest VOD catalog. It wasn't just storage cost either — cache hit ratio suffered because your edge had to hold both variants, and any live event meant double the ingest writes.
CMAF fixes this by settling on one container everyone agrees on: fragmented MP4 (fMP4) based on ISO BMFF. Instead of two encodings, you encode once and package once. That single set of CMAF segments and a common encryption scheme (CENC) can then be referenced by both an HLS playlist (m3u8) and a DASH manifest (MPD). You're not transcoding twice, you're not writing twice, and critically, your CDN caches one object per segment. If you haven't already, read my breakdown on DASH vs HLS for CDN Streaming: Protocol Comparison and Selection Guide — it explains why the protocol differences above the segment layer still matter, even when the segments themselves are identical.
From an engineering standpoint, the shift was subtle but massive. You still need to generate both manifests, but segment generation is unified. That cuts packaging CPU by roughly 40-50% and lets you use a single just-in-time packager at the origin. Here's a typical FFmpeg + shaka-packager flow I run for VOD CMAF output:
# Single CMAF encode for all protocols - fMP4 with common encryption
ffmpeg -i input.mxf \
-map 0:v:0 -c:v libx264 -profile:v high -b:v 4500k -g 48 -keyint_min 48 -sc_threshold 0 \
-map 0:v:0 -c:v libx264 -profile:v high -b:v 2500k -s 1280x720 -g 48 -keyint_min 48 -sc_threshold 0 \
-map 0:a:0 -c:a aac -b:a 128k -ar 48000 \
-f mp4 -movflags empty_moov - | \
shaka-packager \
in=pipe:0,stream=video,output=/origin/cmaf/video_1080p.mp4,output=/origin/cmaf/video_720p.mp4 \
in=pipe:0,stream=audio,output=/origin/cmaf/audio_aac.mp4 \
--segment_duration 4 \
--fragment_duration 1 \
--generate_static_live_mpd \
--hls_master_playlist_output /origin/cmaf/master.m3u8 \
--mpd_output /origin/cmaf/manifest.mpd \
--protection_scheme cbcs --enable_raw_key_encryption \
--keys label=:key_id=abba...:key=1234...
How CMAF Chunks Actually Move Through the CDN
A lot of folks hear "CMAF" and think it's just "fMP4 segments," but the real win is the internal structure: segments are made of fragments, and fragments are made of chunks. A chunk is the smallest independently deliverable unit — typically 200ms to 1 second — containing a moof + mdat box pair. The CMAF spec doesn't require chunked transfer, but when you combine CMAF chunks with HTTP chunked transfer encoding (as defined by the HTTP Working Group Specifications), the CDN can start forwarding a segment to the client while the origin is still writing it.
This is what makes modern low-latency possible without WebRTC. In a chunked-CMAF workflow, the encoder publishes 200-500ms chunks, the origin makes them available via HTTP/1.1 chunked transfer, and the CDN edge streams them through immediately without buffering the full 4-second segment. Both LL-HLS and Low-Latency DASH use this exact mechanism — they just signal it differently. LL-HLS uses EXT-X-PART and EXT-X-PRELOAD-HINT, while LL-DASH uses availabilityTimeOffset and chunk availability in the MPD. I've deployed both side-by-side and honestly, the chunking and caching behavior at the edge is nearly identical now. For live tuning details, check these: Low-Latency HLS Configuration for CDN: Sub-3-Second Live Streaming and Low-Latency DASH with CMAF Chunks: Achieving Near-Real-Time CDN Delivery.
You can see how thin the manifest layer becomes when the segments are unified. Here's what the manifests point to — note they reference the same .m4s file:
# HLS master referencing CMAF fMP4 (not .ts)
#EXTM3U
#EXT-X-VERSION:9
#EXT-X-MAP:URI="init_720p.m4s"
#EXT-X-STREAM-INF:BANDWIDTH=2800000,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2"
chunk_720p_00001.m4s
chunk_720p_00002.m4s
<!-- Equivalent DASH MPD referencing the SAME CMAF segments -->
<AdaptationSet mimeType="video/mp4" codecs="avc1.64001f">
<SegmentTemplate media="chunk_720p_$Number$.m4s"
initialization="init_720p.m4s"
duration="4" timescale="1"
availabilityTimeOffset="1.5" />
</AdaptationSet>
Unified Packaging Impact on Origin Storage and Edge Cache
Switching to CMAF unified streaming isn't just a neat spec trick — you'll see it directly on your origin bill and your cache analytics. I worked with a European OTT service last year that cut their origin S3 footprint from 61TB to 32TB overnight by collapsing HLS and DASH into a single CMAF set. Their Fastly edge hit ratio jumped from 89.2% to 96.1% in the first week because we no longer had two cache keys fighting for the same content. Purges and cache invalidations also became atomic; invalidating one segment URL invalidates it for both protocols, which is a lifesaver during live ad insertion.
There's also a less obvious operational benefit: consistency. When you had separate packagers, clock drift or an encoder hiccup could cause HLS and DASH manifests to go out of sync by a few seconds. Players would complain about different availability windows. With CMAF, that entire class of errors disappears — the timeline is derived from the same fragments, as described in the web.dev Articles on media delivery. You still need to tune your fragment duration carefully though. Too long (4-6s) and you hurt low-latency and increase rebuffer on slow connections. Too short (200ms) and you inflate origin requests and manifest churn. For most VOD and standard live I've found 1-second fragments with 4-second segments is the sweet spot, with 200-500ms chunks only for real low-latency channels.
| Metric (30-Day Avg) | Legacy Dual Packaging (TS + MP4) | Unified CMAF (fMP4) |
|---|---|---|
| Origin Storage for 10k-Hour Library | 64.5 TB | 33.1 TB (-48.7%) |
| Edge Cache Hit Ratio | 88.4% | 96.3% |
| Packaging CPU / Hour of Content | 1.82 vCPU-hours | 0.98 vCPU-hours |
| Median Rebuffer Ratio (3G Mobile) | 1.34% | 0.71% |
| Full Catalog Purge Time (Global CDN) | 850 ms - 1.2 s | 420 ms - 580 ms |
Implementing CMAF at the Origin: From Encoder to Cache-Ready Segments
If you've been running dual HLS and DASH pipelines, switching to CMAF isn't a rip-and-replace — it's a packager swap. You don't need to re-encode your library. Your existing H.264/H.265 and AAC renditions are fine; you're just re-packaging them into fragmented MP4 (fMP4) instead of keeping MPEG-TS for HLS and separate fMP4 for DASH. Most teams we work with do this with Shaka Packager, Unified Streaming Origin, or AWS MediaPackage. The key settings you'll touch are fragment duration, segment duration, and GOP alignment. For standard live/VOD we typically see 2-second CMAF chunks inside 6-second segments — that gives you 3 chunks per segment, which is the sweet spot for cache efficiency without hurting ABR switching too much.
On the CDN side, this is where the real win shows up. With legacy dual-packaging, your edge was caching two copies of the same 4.5 Mbps 1080p rendition — that's 2x origin egress and half your cache efficiency. With CMAF, you cache one set of media segments (video_1080p_00001.m4s) and serve both master.m3u8 and manifest.mpd from it. Make sure your cache-key ignores the manifest type and set long TTLs for init segments. We set init.mp4 to max-age=31536000, immutable because it literally never changes for a given rendition, while media segments for live get 2-4 seconds and VOD gets 1 year. If you're still designing your ingest and packaging flow, it's worth aligning this with your broader CDN Live Streaming Architecture: Ingest, Transcode, Package, and Deliver at Scale so your transcoder's GOP size (we recommend 2 seconds, 60 frames at 30fps) matches your CMAF fragment exactly.
# Single CMAF output that serves both HLS and DASH from one set of fMP4 segments
# Using Shaka Packager - generates one init + media segments, two manifests
packager \
in=h264_1080p.mp4,stream=video,output=/out/cmaf/h264_1080p.mpd,playlist_name=video_1080p.m3u8 \
in=h264_720p.mp4,stream=video,output=/out/cmaf/h264_720p.mpd,playlist_name=video_720p.m3u8 \
in=aac_128k.mp4,stream=audio,output=/out/cmaf/aac_audio.mpd,playlist_name=audio.m3u8 \
--segment_duration 6 \
--fragment_duration 2 \
--hls_master_playlist_output /out/cmaf/master.m3u8 \
--dash_force_segment_number_offset 1 \
--generate_static_live_mpd
# Equivalent FFmpeg for low-latency CMAF with chunked transfer (fMP4 HLS)
ffmpeg -i input.mp4 \
-c:v libx264 -g 60 -keyint_min 60 -sc_threshold 0 \
-c:a aac -b:a 128k \
-hls_time 6 -hls_segment_type fmp4 -hls_fmp4_init_filename init.mp4 \
-hls_flags independent_segments+program_date_time \
-master_pl_name master.m3u8 hls/stream_%v.m3u8
Low-Latency CMAF, Edge Tuning, and What Breaks in Production
Standard CMAF already cuts 30-45% off your storage and origin-to-edge traffic, but if you're chasing sub-3-second glass-to-glass latency, you'll want Low-Latency CMAF with chunked transfer. This is where CMAF really shines over legacy HLS. Instead of waiting for a full 6-second segment, the packager emits 200-500ms CMAF chunks (we've settled on 500ms for most sports betting and live commerce use cases) and the CDN forwards them via HTTP chunked transfer encoding before the whole segment is complete. LL-HLS and LL-DASH both use those same chunks — we’re seeing 1.8 to 3.5 second end-to-end latency versus 18-30 seconds with traditional HLS, with no extra encoding pack. Your CDN must support HTTP/1.1 chunked transfer and ideally HTTP/2 or HTTP/3 push for the parts — if it buffers the whole segment, you lose the entire benefit. We also enable an origin shield and set stale-while-revalidate of about 5 seconds so a sudden spike of 50k viewers hitting the same 500ms part doesn't thundering-herd your origin.
Troubleshooting CMAF delivery is usually a caching problem, not a packaging one. The three issues we see most often: 1) Players stalling on rendition switches — that’s almost always misaligned GOPs or fragment durations that aren't identical across renditions. If your 1080p is 2.0s and your 720p is 2.02s, they'll drift and the player can't switch cleanly. 2) 404s on init.mp4 after a deploy — you pushed a new packager version but the CDN still has the old manifests referencing the old init URL. Fix it with content fingerprinting in the path (/cmaf/v4/init.mp4) and never overwrite in place. 3) Abysmal cache hit ratios (below 85% for VOD) — check your cache-key. If you're including query strings like ?token=xxx for DRM or analytics, you're fragmenting the cache. Normalize the key to strip tokens and use a token-auth at the edge instead.
Two advanced patterns are worth calling out. If you're running time-shifted viewing or catch-up, CMAF makes CDN DVR Functionality at the Edge: Time-Shifted Viewing for Live Streams significantly cheaper — because the DVR window (say, a 2-hour sliding window) and the live edge are reading from the *same* segment store, you don't need to duplicate that 48 GB per hour of 1080p content for DVR. And for monetization, don't try to stitch ads in the fMP4 segments themselves. Keep your CMAF segments clean and do Server-Side Ad Insertion (SSAI) Through CDN: Seamless Monetization Without Ad Blockers at the manifest level — the CDN manipulates the HLS/DASH manifest to insert ad periods while the underlying CMAF chunks stay cacheable. That keeps your cache hit ratio above 95% even during ad breaks, which you’ll never get with client-side ad stitching.
Frequently Asked Questions
Can I use one CMAF stream with DRM for both HLS and DASH?
Yes, and that's one of CMAF's biggest wins. Use common encryption (cenc) with 'cbcs' for HLS/FairPlay compatibility and 'cenc' for Widevine/PlayReady. You'll encrypt once and include both `EXT-X-KEY` (for HLS) and `ContentProtection` (for DASH) in the respective manifests pointing to the same encrypted CMAF chunks. We typically see 98%+ cache sharing on encrypted content versus 0% with legacy TS vs. fMP4 separation.
What's the ideal segment and chunk duration for CMAF?
For standard VOD and live where 8-12 second latency is okay: 6-second segments with 2-second fragments. For low-latency live: keep the 6-second segment for compatibility but emit 400-500ms CMAF chunks with chunked transfer. Going below 200ms chunks increases origin write IOPS by 5x and often hurts CDN cache efficiency — you'll see your edge revalidation rate spike 3-4x without any perceptible latency gain.
Do I still need two manifests with CMAF?
Yes, you still serve a `master.m3u8` for HLS (Apple devices) and a `manifest.mpd` for DASH (Android, smart TVs, browsers). The difference is they both reference the *identical* media files (`init.mp4` + `segment_*.m4s`). So you're generating two tiny text manifests — a few kilobytes each — instead of two full copies of every video segment. In our production setup, manifest generation is less than 0.1% of origin load.
Will CMAF break my existing HLS players?
Not if your packager outputs fMP4 HLS. Any player that supports HLS version 7+ (which is every modern player since 2018 — hls.js, AVPlayer, ExoPlayer, Shaka) handles fMP4 CMAF seamlessly. The only breakage we've hit is with very old MPEG-TS-only players on legacy smart TVs from before 2016. For those, keep a single TS fallback rendition or just let them use DASH. Don't maintain a full TS ladder for them.