TL;DR
We package one CMAF asset encrypted with
cbcs, emit an HLS and a DASH manifest from the same segments, and play it back with Shaka Player so Widevine, PlayReady, and FairPlay all decrypt the same bytes. No more separatecencandcbcsencodes for the common case.
If you last set up DRM a few years ago, you probably remember packaging everything twice: a cenc copy for Widevine/PlayReady and a cbcs copy for FairPlay, because the cipher modes were incompatible. For current devices that is no longer necessary. Modern Widevine and PlayReady both decrypt cbcs, and CMAF lets HLS and DASH share one set of segments. Let's build the single-encode version.
We'll do it in four steps:
- Understand the cipher-mode constraint (30 seconds of theory).
- Package a CMAF asset with
cbcsusing Shaka Packager. - Wire up Shaka Player with EME and three license servers.
- Handle the FairPlay-specific bits that always bite first.
1. The one constraint that drives everything
There are three DRM systems and they map to devices, not to your preferences:
| DRM | Owner | Covers |
|---|---|---|
| Widevine | Android, Chrome, Chromecast, many smart TVs | |
| FairPlay | Apple | Safari (iOS/iPadOS/macOS), tvOS |
| PlayReady | Microsoft | Windows/Edge, Xbox, many connected TVs |
Historically Widevine and PlayReady used AES-128-CTR (the cenc scheme) and FairPlay used AES-128-CBC (the cbcs scheme). A cenc file literally cannot be decrypted by FairPlay; different cipher mode.
💡 The 2026 shortcut: encrypt once with
cbcs. Current Widevine and PlayReady clients accept it, FairPlay needs it. One encryption, three DRMs.⚠️ Old devices caveat: some legacy Widevine/PlayReady clients (old CTVs/set-top boxes) only do
cenc. Check your playback analytics before goingcbcs-only. Most apps shipping today can.
2. Package CMAF with cbcs using Shaka Packager
Shaka Packager is Google's open-source packager and speaks CMAF + cbcs natively. Grab a static build (packager-linux-x64) or run the Docker image.
First, encode your renditions as plain MP4 (this is your normal ABR ladder, no DRM yet):
# bash: a tiny 3-rung ladder for the demo
ffmpeg -i master.mov -c:v libx264 -preset medium -crf 23 -vf scale=1280:720 -an video_720.mp4
ffmpeg -i master.mov -c:v libx264 -preset medium -crf 26 -vf scale=854:480 -an video_480.mp4
ffmpeg -i master.mov -c:v aac -b:a 128k -vn audio.mp4
Now package to CMAF with cbcs encryption. For a real deployment you'd pull keys from a key server over SPEKE; for a local demo you can pass a raw key and key id (KID):
# bash: package.sh
KEY_ID=$(openssl rand -hex 16)
KEY=$(openssl rand -hex 16)
packager \
'in=video_720.mp4,stream=video,init_segment=out/720/init.mp4,segment_template=out/720/$Number$.m4s' \
'in=video_480.mp4,stream=video,init_segment=out/480/init.mp4,segment_template=out/480/$Number$.m4s' \
'in=audio.mp4,stream=audio,init_segment=out/audio/init.mp4,segment_template=out/audio/$Number$.m4s' \
--protection_scheme cbcs \
--enable_raw_key_encryption \
--keys "label=:key_id=${KEY_ID}:key=${KEY}" \
--generate_static_live_mpd \
--mpd_output out/manifest.mpd \
--hls_master_playlist_output out/master.m3u8
The important flags: --protection_scheme cbcs is what makes this asset playable by all three DRMs, and emitting both --mpd_output (DASH) and --hls_master_playlist_output (HLS) from the same run means both manifests point at the same .m4s segments. One set of bytes on disk.
# bash: what you get
$ tree out
out
├── 720/ init.mp4 1.m4s 2.m4s ...
├── 480/ init.mp4 1.m4s 2.m4s ...
├── audio/ init.mp4 1.m4s 2.m4s ...
├── manifest.mpd # DASH
└── master.m3u8 # HLS, same segments
💡 In production, replace
--enable_raw_key_encryption/--keyswith--enable_widevine_encryptionor a SPEKE key-server URL so a real multi-DRM service mints and stores your keys. Never ship raw keys in your packaging script.
3. Play it back with Shaka Player + EME
Browser DRM runs through Encrypted Media Extensions (EME). Shaka Player (4.x) abstracts EME and picks the right DRM for the current browser. You give it license-server URLs per system; it negotiates the rest.
<!-- index.html -->
<script src="https://cdn.jsdelivr.net/npm/shaka-player@4/dist/shaka-player.compiled.js"></script>
<video id="video" controls autoplay style="width:100%"></video>
// app.js
async function main() {
shaka.polyfill.installAll();
const video = document.getElementById('video');
const player = new shaka.Player(video);
player.configure({
drm: {
servers: {
'com.widevine.alpha': 'https://YOUR-LICENSE-SERVICE/widevine',
'com.microsoft.playready': 'https://YOUR-LICENSE-SERVICE/playready',
'com.apple.fps': 'https://YOUR-LICENSE-SERVICE/fairplay',
},
advanced: {
'com.apple.fps': {
// FairPlay needs the Apple-issued application certificate up front
serverCertificateUri: 'https://YOUR-CDN/fairplay.cer',
},
},
},
// Let Shaka use the same EME path for FairPlay instead of native HLS
streaming: { useNativeHlsForFairPlay: false },
});
player.addEventListener('error', (e) => console.error('shaka error', e.detail));
// DASH on most browsers; Shaka also reads the HLS manifest if you prefer
await player.load('/out/manifest.mpd');
console.log('playing, DRM negotiated:', player.drmInfo()?.keySystem);
}
main();
Shaka inspects the browser: Chrome/Android negotiate com.widevine.alpha, Edge/Windows can use com.microsoft.playready, Safari uses com.apple.fps. Same manifest, same segments, three key systems.
4. The FairPlay bits that always bite
FairPlay is the one that costs you a day, and none of it is the cryptography. Three things to know before you start:
1. You need an Apple-issued application certificate (.cer).
Enroll in the Apple Developer Program → request the
FairPlay Streaming deployment package. This is gated and slow.
2. The license exchange is SPC → CKC:
player generates an SPC (Server Playback Context) →
your key server returns a CKC (Content Key Context).
A multi-DRM license service implements the KSM for you.
3. Native Safari can also play FairPlay HLS with NO JS player:
<video src="master.m3u8"> works if you wire the
`webkitneedkey` / `encrypted` EME handlers. Shaka does this for you.
If you skip the certificate step and only test in Chrome, everything looks perfect, and then iOS playback fails at launch. Test on a real Apple device early.
Common errors and fixes
NO_LICENSE_SERVER_SPECIFIED (shaka 6001)
→ you encrypted with a key system you didn't add to drm.servers
LICENSE_REQUEST_FAILED (shaka 6007)
→ license server URL wrong, or CORS not allowed on the license endpoint
FAIRPLAY: keySystemAccess denied / no recognized key system
→ missing serverCertificateUri, or testing FairPlay outside Safari
Playback works in Chrome, black screen on old smart TV
→ that device only supports cenc; ship a cenc set for the long tail
What's next
You now have one cbcs CMAF asset serving Widevine, PlayReady, and FairPlay from a single encode. From here:
- Swap the raw-key demo for a real multi-DRM license service (SPEKE key provisioning) so keys are minted and rotated for you.
- Add hardware-security-level requirements (Widevine L1, HDCP) if you license premium content; studios specify these in contracts.
- Read the Shaka Player DRM config docs for persistent licenses and offline playback.
The same single-encode pattern works whether you package yourself with Shaka Packager or hand it to a managed video API. The cipher-mode war that forced the second encode is over; build like it.
Top comments (0)