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.

Advertisement

What the packager does

The packaging step in a multi-DRM pipelineEncoderclear CMAF renditionsPackagerencrypt + manifestsOrigin / storageinit, segments, MPD, M3U8Key serviceCPIX / SPEKEDRM licence serversWidevine, PlayReady, FairPlayJob controltitle id, key policyQA stagedecrypt with test keysCDN + playersEME / nativerenditionsencrypted outputjobKIDs, keys, PSSHsame keyssample outputKeys travel from the key service to the packager and to the licence servers, never to storage or logs.The packager writes key IDs and DRM signalling; players fetch keys from the licence servers.
The packager requests keys for a title from a key service, encrypts each track with the right key, writes DRM signalling into init segments and manifests, and hands output to QA before the origin.

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.

Advertisement

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, avc1 or hvc1 becomes encv (audio becomes enca) and gains a sinf box holding the original format, the scheme (schm: cenc or cbcs) and a tenc box.
  • 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

StrategyStorage and CDNDevice reachWhen to choose
cbcs onlyOne copy of every segmentCurrent Apple devices, browsers and most current TVs and sticksDefault for new services
cenc onlyOne copyNo FairPlay; misses Apple native playbackServices without Apple native targets
Both, packaged aheadTwo copies; cache efficiency halves for shared titlesWidest, including older devices that only decrypt cencLarge legacy device fleets
Both, packaged on requestOne clear or mezzanine copy plus cachesWidestLong-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

SymptomLikely causeFix
Plays on browsers, fails on Apple devicesOutput is cenc, or no FairPlay key tagPackage cbcs and signal com.apple.streamingkeydelivery
Stalls on up-switch to HDHD key ID not signalled early, or licence refused HD and player did not filterSession keys in the multivariant playlist; filter renditions by key status
Green or garbled framesSubsample map wrong, often after re-muxingRe-package from clear source; decode-test with known keys
HD rendition decrypts with SD licenceUsage rule matched the wrong trackAssert key ID per rendition in QA
Live output stops at a boundaryNext key period not fetched in timePrefetch periods; alert on lookahead
Keys found in logsRaw keys on command lines or debug loggingCPIX 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

  1. Pick cbcs as your single output unless your device data shows a fleet that cannot decrypt it.
  2. Move key delivery to CPIX or SPEKE with keys encrypted in the document, and remove raw keys from scripts and logs.
  3. Define key usage rules per track class (audio, SD, HD, UHD) and assert every rendition matches exactly one.
  4. Signal every DRM system in DASH and HLS, including session keys in the HLS multivariant playlist.
  5. Add the structural QA check, a known-key decrypt and decode, and a device playback matrix before publishing.
  6. For live, set clear lead to a segment or two, enable rotation where your key source supports it, and alert on key lookahead.
Key takeaway: Multi-DRM packaging encrypts media once with Common Encryption and lets each DRM system deliver the same keys in its own way. The packager obtains keys and PSSH data from a key service over CPIX, encrypts samples with cbcs pattern encryption under per-track-class keys, records the details in tenc, pssh and senc boxes, and signals every system in DASH ContentProtection elements and HLS key tags. Keep keys out of logs, keep clear lead short, prefetch rotation periods for live, and prove every output with structural, cryptographic and playback checks before it reaches the origin.