Multi-DRM packaging is the step that takes clear, encoded video and audio and produces a single set of encrypted segments and manifests that every major DRM system can unlock. It sits between the encoder and the origin, and most protected-playback failures that are not licence problems are born here: a missing key ID in a manifest, a rendition encrypted with the wrong key, a scheme that one device family cannot decrypt, or a key that ended up in a log file.
The Widevine, PlayReady and FairPlay article explains the three DRM systems, their licence exchanges and a basic packaging command. This article is about the packager as a system: how it obtains keys, exactly what encryption changes inside the files, how the manifests tell players what to do, how live streams rotate keys, and how to prove an output is correct before a single customer presses play.
What the packager does
Its inputs are encoded renditions, usually already fragmented MP4 or a mezzanine the packager will fragment, plus a key policy for the title. Its outputs are init segments, media segments, and manifests for each delivery format. The key point of multi-DRM is that encryption is done once, under the ISO Common Encryption standard (CENC), and the three DRM systems differ only in how a player obtains the key. Each DRM vendor supplies its own initialisation data, a PSSH box, identified by a system ID; the media bytes are the same for all of them.
That separation shapes the architecture. The packager never talks to licence servers. It talks to a key service, which generates or stores content keys and returns them with the DRM signalling for each system. The same keys are registered with the licence servers, so a player that later presents a key ID gets the matching key back.
Getting keys: CPIX and SPEKE
Passing keys on a command line is fine for a test and unacceptable in production. The interoperable format for exchanging key material between a key service and a packager is DASH-IF's Content Protection Information Exchange (CPIX), an XML document with four main lists: ContentKeyList (the keys, by key ID), DRMSystemList (one entry per key and DRM system, carrying the PSSH and format-specific signalling), ContentKeyPeriodList (time periods, for rotation) and ContentKeyUsageRuleList (which key applies to which track, using audio, video and bitrate filters). AWS's SPEKE is an API that carries CPIX between packagers and key providers; version 2 supports multiple keys per title.
A request for a title with separate audio, SD and HD keys looks roughly like this. The packager sends key IDs and filters; the key service returns the keys (encrypted to the packager) and fills in each PSSH. Exact attributes vary by profile and vendor, so validate against the spec your provider implements.
<cpix:CPIX contentId="title-8812" xmlns:cpix="urn:dashif:org:cpix"
xmlns:pskc="urn:ietf:params:xml:ns:keyprov:pskc">
<cpix:ContentKeyList>
<cpix:ContentKey kid="2b9b6d7e-..." commonEncryptionScheme="cbcs"/> <!-- audio -->
<cpix:ContentKey kid="7c1e04a2-..." commonEncryptionScheme="cbcs"/> <!-- SD video -->
<cpix:ContentKey kid="d3f5a9c0-..." commonEncryptionScheme="cbcs"/> <!-- HD video -->
</cpix:ContentKeyList>
<cpix:DRMSystemList>
<cpix:DRMSystem kid="7c1e04a2-..." systemId="edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"/>
<cpix:DRMSystem kid="7c1e04a2-..." systemId="9a04f079-9840-4286-ab92-e65be0885f95"/>
<cpix:DRMSystem kid="7c1e04a2-..." systemId="94ce86fb-07ff-4f43-adb8-93d2fa968ca2"/>
<!-- ...one DRMSystem per key and system; the response fills in PSSH and HLS signalling -->
</cpix:DRMSystemList>
<cpix:ContentKeyUsageRuleList>
<cpix:ContentKeyUsageRule kid="2b9b6d7e-..."><cpix:AudioFilter/></cpix:ContentKeyUsageRule>
<cpix:ContentKeyUsageRule kid="7c1e04a2-..."><cpix:VideoFilter maxPixels="589824"/></cpix:ContentKeyUsageRule>
<cpix:ContentKeyUsageRule kid="d3f5a9c0-..."><cpix:VideoFilter minPixels="589825"/></cpix:ContentKeyUsageRule>
</cpix:ContentKeyUsageRuleList>
</cpix:CPIX>Two design rules matter. Keys should be encrypted in transit with a document key, not only protected by TLS, so they are never in plaintext in request logs. And the usage rules must be complete: every track the packager will produce has to match exactly one rule, or the packager either fails or, worse, silently uses a default key.
What encryption writes into the files
CENC does not encrypt files; it encrypts samples, the coded frames inside media fragments, and records how in a handful of boxes. The fragmented MP4 internals article explains the box structure; here is what encryption adds.
- Sample entry renamed. In the init segment,
avc1orhvc1becomesencv(audio becomesenca) and gains asinfbox holding the original format, the scheme (schm:cencorcbcs) and atencbox. - tenc carries the defaults for the track: whether it is protected, the default key ID, the per-sample IV size, and for pattern schemes the crypt and skip block counts and, for cbcs, usually a constant IV.
- pssh boxes, one per DRM system, may sit in the init segment. Many services put them only in the manifest, so the licence request can be prepared before the init segment downloads.
- senc, saiz, saio in each fragment hold per-sample auxiliary information: the IV when it is not constant, and the subsample map that says which byte ranges are clear and which are encrypted.
The subsample map exists because video decoders and secure pipelines need to parse codec headers before decryption. For H.264 and HEVC the NAL unit headers and slice headers stay clear and only slice data is encrypted. The two schemes then differ. cenc uses AES-CTR over the protected ranges. cbcs uses AES-CBC with a pattern: for video, typically 1 encrypted 16-byte block followed by 9 clear blocks, repeated, and any partial block at the end of a range stays clear. Audio in cbcs is usually whole-sample encrypted without a pattern. The pattern reduces decryption work in constrained devices and is the scheme FairPlay requires, which is why cbcs is the default choice for a single multi-DRM output today.
Signalling in the manifests
Players decide which DRM to use from the manifest, so the signalling must name every system and every key. In DASH, each adaptation set carries a generic ContentProtection element with the scheme and default key ID, plus one element per DRM system, identified by its system ID as a URN. In HLS, each media playlist carries one EXT-X-KEY per key format. The HLS specification defines METHOD=SAMPLE-AES for sample encryption, which for fMP4 segments means cbcs, and SAMPLE-AES-CTR for segments encrypted with the cenc scheme.
<!-- DASH: in each AdaptationSet -->
<ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cbcs"
cenc:default_KID="7c1e04a2-..."/>
<ContentProtection schemeIdUri="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed">
<cenc:pssh>AAAA...</cenc:pssh> <!-- Widevine -->
</ContentProtection>
<ContentProtection schemeIdUri="urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95">
<cenc:pssh>AAAA...</cenc:pssh> <!-- PlayReady -->
</ContentProtection>
# HLS media playlist (fMP4 segments encrypted with cbcs)
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="skd://title-8812/hd",KEYFORMAT="com.apple.streamingkeydelivery",KEYFORMATVERSIONS="1"
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="data:text/plain;base64,AAAA...",KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed",KEYFORMATVERSIONS="1"
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="data:text/plain;charset=UTF-16;base64,...",KEYFORMAT="com.microsoft.playready",KEYFORMATVERSIONS="1"The multivariant playlist can repeat the same keys in EXT-X-SESSION-KEY tags, whose format is identical to EXT-X-KEY, so a player can start licence acquisition before it picks a rendition. When renditions use different keys, every key the player might switch to must be signalled somewhere it will see in time, and the player must drop renditions whose keys it was refused.
cenc, cbcs, or both
| Strategy | Storage and CDN | Device reach | When to choose |
|---|---|---|---|
| cbcs only | One copy of every segment | Current Apple devices, browsers and most current TVs and sticks | Default for new services |
| cenc only | One copy | No FairPlay; misses Apple native playback | Services without Apple native targets |
| Both, packaged ahead | Two copies; cache efficiency halves for shared titles | Widest, including older devices that only decrypt cenc | Large legacy device fleets |
| Both, packaged on request | One clear or mezzanine copy plus caches | Widest | Long-tail catalogues; see JIT packaging |
The dual-output cost is larger than doubled storage: CDN caches hold two variants of popular titles, so hit ratio falls and origin egress rises. Just-in-time packaging moves encryption to the origin edge and packages per request, trading compute and a more complex origin for one stored copy. Whichever you pick, track which device models actually request the cenc variant; when that number falls below your support threshold, the second copy can go.
Clear lead and key rotation
Clear lead leaves the first seconds of a title unencrypted so playback starts while the licence request is in flight. In Shaka Packager it is set with --clear_lead, which defaults to 5 seconds, and because the packager does not produce partially encrypted segments, every segment that starts inside the clear lead stays clear. Clear lead is a startup optimisation, not a security risk, but a lead longer than a trailer would be a gift; keep it to a segment or two.
Key rotation changes keys during a live stream so a leaked key exposes only one period. In CPIX, keys are bound to periods through ContentKeyPeriodList and period filters on the usage rules. In Shaka Packager, rotation is enabled by a non-zero --crypto_period_duration, which its documentation describes with Widevine key-server encryption; check your packager's documentation for which key sources support rotation. Operationally, rotation means the packager must fetch the next period's keys well before the boundary, players must receive new key IDs in time to request licences, and licence servers must hold keys for every period a viewer could rewind into. A key-service outage during a live event then stops output at the next boundary, so cache upcoming periods and alert on the lookahead margin.
Worked example and QA
Take a VOD title with one audio track and video renditions at 540p, 720p and 1080p, output once as cbcs CMAF with DASH and HLS manifests. Job control asks the key service for three keys: audio, SD (540p) and HD (720p and 1080p). The packager encrypts each track with its key, writes three DRM systems' signalling, and drops the output in a staging bucket. Nothing is published until QA passes.
QA has three layers. First, structure: parse every init segment and check the scheme, default key ID, IV settings and PSSH systems against what the job expected. The script below walks the boxes and prints them; a check that the 1080p track carries the HD key ID, not the SD one, catches the most expensive mistake in this pipeline, because playback would still work everywhere for anyone entitled to SD.
import struct, sys
SYSTEMS = {
"edef8ba979d64acea3c827dcd51d21ed": "Widevine",
"9a04f07998404286ab92e65be0885f95": "PlayReady",
"94ce86fb07ff4f43adb893d2fa968ca2": "FairPlay",
}
CONTAINERS = {b"moov", b"trak", b"mdia", b"minf", b"stbl", b"sinf", b"schi"}
def walk(buf, start, end, out):
i = start
while i + 8 <= end:
size, kind = struct.unpack(">I4s", buf[i:i + 8])
if size < 8:
break
if kind == b"stsd": # 8-byte full-box header, then entries
walk(buf, i + 16, i + size, out)
elif kind in (b"encv", b"enca"): # sample entry: skip its fixed fields
walk(buf, i + (86 if kind == b"encv" else 36), i + size, out)
elif kind in CONTAINERS:
walk(buf, i + 8, i + size, out)
elif kind == b"schm":
out.append(("scheme", buf[i + 12:i + 16].decode()))
elif kind == b"tenc":
version = buf[i + 8]
protected, iv_size = buf[i + 14], buf[i + 15]
pattern = (buf[i + 13] >> 4, buf[i + 13] & 15) if version else None
out.append(("tenc", protected, iv_size, buf[i + 16:i + 32].hex(), pattern))
elif kind == b"pssh":
out.append(("pssh", SYSTEMS.get(buf[i + 12:i + 28].hex(), "unknown")))
i += size
found = []
data = open(sys.argv[1], "rb").read()
walk(data, 0, len(data), found)
for item in found:
print(item)Second, cryptography: for titles packaged with test keys in a pre-production run, decrypt segments with Bento4's mp4decrypt and the known key, then decode the result; a clean decode proves the subsample map and pattern are right. Third, playback: an automated matrix of real devices per DRM and security level plays the first minute, an up-switch across key boundaries and a seek. Bento4's mp4dump is useful for reading boxes by hand when a check fails.
Failure modes
| Symptom | Likely cause | Fix |
|---|---|---|
| Plays on browsers, fails on Apple devices | Output is cenc, or no FairPlay key tag | Package cbcs and signal com.apple.streamingkeydelivery |
| Stalls on up-switch to HD | HD key ID not signalled early, or licence refused HD and player did not filter | Session keys in the multivariant playlist; filter renditions by key status |
| Green or garbled frames | Subsample map wrong, often after re-muxing | Re-package from clear source; decode-test with known keys |
| HD rendition decrypts with SD licence | Usage rule matched the wrong track | Assert key ID per rendition in QA |
| Live output stops at a boundary | Next key period not fetched in time | Prefetch periods; alert on lookahead |
| Keys found in logs | Raw keys on command lines or debug logging | CPIX with encrypted keys; scrub logs |
Operating the pipeline
Treat key IDs as data you own: store, per title and rendition, the key IDs, scheme and key period, so support can answer 'which key protects this file' without opening segments. Audit the key service: who requested keys for which title, when. Monitor packaging output by scheme and DRM system, licence error rates by key ID, and the share of plays per device that use each variant. Re-package titles when you change schemes rather than mixing schemes within a title, and keep forensic watermarking as a separate concern; DRM stops casual copying, watermarking traces leaks.
What to do next
- Pick cbcs as your single output unless your device data shows a fleet that cannot decrypt it.
- Move key delivery to CPIX or SPEKE with keys encrypted in the document, and remove raw keys from scripts and logs.
- Define key usage rules per track class (audio, SD, HD, UHD) and assert every rendition matches exactly one.
- Signal every DRM system in DASH and HLS, including session keys in the HLS multivariant playlist.
- Add the structural QA check, a known-key decrypt and decode, and a device playback matrix before publishing.
- For live, set clear lead to a segment or two, enable rotation where your key source supports it, and alert on key lookahead.