Ffmpeg drm
DRM and media encryption with ffmpeg: HLS AES-128 (-hls_key_info_file, -hls_enc, key rotation), HLS SAMPLE-AES, MPEG-DASH Common Encryption (CENC) with cenc_mp4 / cbcs schemes, ClearKey, fmp4 encrypted segments, cryptor protocol for playback. Use when the user asks to encrypt an HLS stream, set up AES-128 HLS, do sample-AES, package CENC DASH, rotate HLS keys, generate a key info file, protect VOD content, or configure ClearKey signaling.From its SKILL.md
npx -y skills add damionrashford/media-os --skill ffmpeg-drmAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 17 stars17 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
13.5 KB, as published. Nobody here has run it
ffmpeg-drm
Context: $ARGUMENTS
Quick start
- Encrypt HLS with AES-128: → Steps 1 → 2 → 3 → 4
- Package DASH CENC (ClearKey / Widevine / PlayReady-ready): → Steps 1 → 3 → 4
- Rotate keys mid-stream: → Step 3 with
-hls_flags periodic_rekey - Play back a
crypto:file: → Step 4 - Decrypt a segment for QA:
scripts/drm.py decrypt-segment ...
When to use
- You need to protect a VOD or live HLS stream with AES-128 full-segment encryption.
- You need to package MPEG-DASH with Common Encryption (CENC, cbcs, cenc-aes-ctr) for ClearKey, Widevine, PlayReady, or FairPlay-ready workflows.
- You need to generate HLS key info files, rotate keys, or signal ClearKey in an MPD.
- You need a reproducible, scripted encryption pipeline without a commercial packager.
- Don't use for: Widevine/PlayReady license server integration (ffmpeg can't emit PSSH boxes reliably — use Shaka Packager or Bento4), or FairPlay SAMPLE-AES-CTR signaling (use Apple
mediafilesegmenter).
Step 1 — Generate keys and IVs
A 16-byte (128-bit) AES key and a 16-byte IV. Both are binary; IVs are typically expressed as 32 hex chars.
# 16-byte key (binary file)
openssl rand 16 > enc.key
# or, stdlib-friendly:
dd if=/dev/urandom of=enc.key bs=16 count=1 2>/dev/null
# 32-hex-char IV (no "0x", no whitespace)
openssl rand -hex 16
# Key as hex (for ffmpeg -hls_enc_key / -encryption_key)
xxd -p enc.key # 32 hex chars
# KID for CENC (unique per content — distinct from the key)
openssl rand -hex 16
Or use the helper: scripts/drm.py gen-key --output enc.key and scripts/drm.py gen-iv.
Never commit keys to git. Add *.key and *.keyinfo to .gitignore. In production, fetch keys from a KMS or licensed DRM key server.
Step 2 — Build the HLS key info file
ffmpeg's HLS AES-128 mode reads a 3-line text file. Format is exact — deviation breaks encryption:
https://cdn.example.com/keys/enc.key
/absolute/path/to/enc.key
abcdef0123456789abcdef0123456789
- Line 1: Key URI the player fetches (goes into
EXT-X-KEY URI="..."in the playlist). - Line 2: Local path ffmpeg reads to encrypt — can be relative to the cwd used at launch, but absolute is safer.
- Line 3: IV in hex, no
0xprefix, no whitespace. Optional — if omitted, ffmpeg derives the IV from the segment media sequence number.
The server hosting line 1 must actually serve it with the right CORS headers and whatever auth your license scheme requires.
Step 3 — Package and encrypt
HLS AES-128 VOD (with key info file — recommended)
ffmpeg -i in.mp4 \
-c:v libx264 -c:a aac \
-f hls -hls_time 6 -hls_playlist_type vod \
-hls_key_info_file enc.keyinfo \
-hls_segment_filename 'seg_%03d.ts' \
out.m3u8
HLS AES-128 inline (no key info file)
ffmpeg -i in.mp4 \
-c:v libx264 -c:a aac \
-f hls -hls_time 6 \
-hls_enc 1 \
-hls_enc_key_url 'https://cdn.example.com/keys/enc.key' \
-hls_enc_key "$(xxd -p enc.key)" \
-hls_enc_iv "$(openssl rand -hex 16)" \
out.m3u8
Warning: -hls_enc 1 without -hls_enc_key / -hls_enc_key_url makes ffmpeg auto-generate a key and write it next to the playlist. Insecure default — always pass your own.
HLS key rotation (periodic rekey)
ffmpeg -i in.mp4 -c:v libx264 -c:a aac \
-f hls -hls_time 6 \
-hls_flags periodic_rekey \
-hls_key_info_file enc.keyinfo \
out.m3u8
When you update the enc.keyinfo file in place during a live ingest, ffmpeg will pick up the new key on the next segment boundary and emit a new EXT-X-KEY tag. ffmpeg's rotation is coarser than Shaka Packager's (no per-period labels); rewrite the keyinfo file on your cadence.
DASH Common Encryption (CENC) — cenc-aes-ctr
ffmpeg -i in.mp4 \
-c:v libx264 -c:a aac \
-encryption_scheme cenc-aes-ctr \
-encryption_key abcdef0123456789abcdef0123456789 \
-encryption_kid 11223344556677880011223344556677 \
-f dash \
-use_template 1 -use_timeline 1 \
-init_seg_name 'init-$RepresentationID$.m4s' \
-media_seg_name 'chunk-$RepresentationID$-$Number%05d$.m4s' \
manifest.mpd
-encryption_keyand-encryption_kidare both 32 hex chars (16 bytes each).- KID must be unique per content; the key is the AES secret the license server hands out.
- Emits CENC-compliant fmp4 segments and an MPD. For ClearKey playback, append a
<ContentProtection>ClearKey element (seereferences/schemes.md).
DASH CBCS (FairPlay / mixed-DRM compatible scheme)
ffmpeg -i in.mp4 -c:v libx264 -c:a aac \
-encryption_scheme cenc-aes-cbc \
-encryption_key abcdef0123456789abcdef0123456789 \
-encryption_kid 11223344556677880011223344556677 \
-f dash manifest.mpd
cenc-aes-cbc support depends on your ffmpeg build — verify with ffmpeg -h muxer=dash and ffmpeg -h muxer=mp4. For a fully CBCS-compliant FairPlay package you'll still typically need Shaka Packager.
Step 4 — Verify playback / decryption
Play an HLS AES-128 stream locally
ffplay -allowed_extensions ALL out.m3u8
ffplay fetches the key from line-1 URI of the EXT-X-KEY tag. If the URI is file:// or relative, ffplay needs -allowed_extensions ALL and a local HTTP server.
Decrypt one TS segment with openssl (QA)
# IV must match what ffmpeg used — line 3 of the keyinfo file
KEY_HEX=$(xxd -p enc.key)
IV_HEX=abcdef0123456789abcdef0123456789
openssl aes-128-cbc -d -K "$KEY_HEX" -iv "$IV_HEX" \
-in seg_000.ts -out seg_000.plain.ts
ffprobe seg_000.plain.ts # should show normal stream info
crypto: protocol playback (raw AES-128 file)
ffplay -i "crypto:encrypted.bin?key=$(xxd -p enc.key)&iv=abcdef0123456789abcdef0123456789"
Use for internally encrypted assets when a full HLS/DASH wrapper is overkill.
Available scripts
scripts/drm.py— subcommands:gen-key --output enc.key— 16 random bytes viasecrets.token_bytes.gen-iv— prints a 32-hex-char IV.hls-aes128 --input I --outdir D --key-url URL --key enc.key [--segments 6] [--rotate]— buildsenc.keyinfo, runs the ffmpeg HLS pipeline.dash-cenc --input I --outdir D --key HEX --kid HEX [--scheme cenc-aes-ctr|cenc-aes-cbc].decrypt-segment --input seg.ts --key enc.key --iv HEX --output plain.ts— shells out toopenssl aes-128-cbc.- Global flags:
--dry-run,--verbose(keys only printed with--verbose).
Reference docs
- Read
references/schemes.mdfor HLS vs CENC scheme comparison, MPD ContentProtection templates, browser DRM matrix, ClearKey demo, key rotation strategies, and Shaka/Bento4 handoff when ffmpeg is insufficient. - FFmpeg HLS options: https://ffmpeg.org/ffmpeg-formats.html#hls-2
- FFmpeg DASH CENC options: https://ffmpeg.org/ffmpeg-formats.html#dash-2
- FFmpeg crypto protocol: https://ffmpeg.org/ffmpeg-protocols.html#crypto
- Wiki: https://trac.ffmpeg.org/wiki/Encode/CommonEncryption
Gotchas
- Key info file format is exact. Three lines: URI / local-path / IV-hex. A blank line, CRLF mix, or a leading
0xon the IV silently breaks the encrypt or the player's decrypt. - IV is hex, no
0x, no colons, no whitespace — exactly 32 chars. - The key file must be exactly 16 bytes.
echo "somepassword" > enc.keyis wrong — that writes ASCII plus a newline. Useopenssl rand 16or thegen-keysubcommand. - Line 1 of keyinfo is what the player fetches. The origin serving that URL needs CORS (
Access-Control-Allow-Origin) and whatever auth the license flow requires. A 403 there = player can't decrypt. -hls_enc 1with no explicit key → ffmpeg auto-generates and writes a key next to the playlist. Never ship like that.- ffmpeg key rotation is coarse.
-hls_flags periodic_rekeyre-reads the keyinfo file on segment boundaries — you orchestrate the rotation by rewriting that file. Commercial packagers have per-period labels and time-based schedules; ffmpeg doesn't. - SAMPLE-AES and SAMPLE-AES-CTR (FairPlay) are not fully supported. ffmpeg can emit AES-128 full-segment HLS and CENC fmp4, but FairPlay's
EXT-X-KEY METHOD=SAMPLE-AESwithKEYFORMAT="com.apple.streamingkeydelivery"signaling needs Applemediafilesegmenteror Shaka Packager. - DASH CENC + Widevine / PlayReady: ffmpeg does not generate the PSSH boxes those systems require. Encrypt-then-repackage with Shaka Packager (
--enable_raw_key_encryption+ Widevine/PlayReady PSSH) or Bento4 (mp4encryptwith--pssh). - ClearKey works cross-browser (Chrome, Firefox, Edge, Safari 14+) and is fine for staging/internal. Not for commercial content — keys sit in the MPD.
-encryption_keyand-encryption_kidare 32 hex chars each (16 bytes). 64 hex would be 256-bit, which CENC does not use. Wrong length = silent no-op or "encryption key must be 16 bytes" error.- KID must be unique per asset. Reusing a KID across titles conflates license lookups at the key server.
- EXT-X-KEY tag is auto-generated from line 1 of the keyinfo file — don't try to hand-edit the
.m3u8afterward; the next ffmpeg run will overwrite it. - Never commit
.key,.keyinfo, or raw hex-key env files. Add them to.gitignoreand.dockerignore. - Rotate keys for long VOD. A single key for a 10-hour-per-day linear feed is an audit finding.
- Store keys in a KMS (AWS KMS, GCP KMS, HashiCorp Vault) or a licensed DRM server. Local
.keyfiles are fine for dev; never prod. - Test decryption on every build. A silent
-hls_encfallback (encrypting with a wrong key path) will mux cleanly and fail only at the player. Addopenssl aes-128-cbc -dround-trips to CI.
Examples
Example 1: Encrypt a VOD with AES-128 and a hosted key URL
Input: talk.mp4, key will be served from https://cdn.example.com/keys/talk.key.
# 1. generate
openssl rand 16 > talk.key
IV=$(openssl rand -hex 16)
# 2. keyinfo
cat > talk.keyinfo <<EOF
https://cdn.example.com/keys/talk.key
$(pwd)/talk.key
$IV
EOF
# 3. package
mkdir -p out
ffmpeg -i talk.mp4 -c:v libx264 -c:a aac \
-f hls -hls_time 6 -hls_playlist_type vod \
-hls_key_info_file talk.keyinfo \
-hls_segment_filename 'out/seg_%03d.ts' \
out/talk.m3u8
# 4. verify one segment
openssl aes-128-cbc -d -K $(xxd -p talk.key) -iv $IV \
-in out/seg_000.ts -out out/seg_000.plain.ts
ffprobe out/seg_000.plain.ts
Upload out/ to your CDN; upload talk.key to https://cdn.example.com/keys/talk.key behind whatever auth your license flow uses.
Example 2: DASH CENC with ClearKey for staging
KEY=$(openssl rand -hex 16)
KID=$(openssl rand -hex 16)
ffmpeg -i in.mp4 -c:v libx264 -c:a aac \
-encryption_scheme cenc-aes-ctr \
-encryption_key "$KEY" \
-encryption_kid "$KID" \
-f dash -use_template 1 -use_timeline 1 \
staging/manifest.mpd
# Patch ContentProtection into the MPD — see references/schemes.md
Base64 the hex KID and key per the ClearKey EME spec (references/schemes.md has the exact JSON).
Example 3: Decrypt a single segment for QA
scripts/drm.py decrypt-segment \
--input out/seg_000.ts \
--key talk.key \
--iv abcdef0123456789abcdef0123456789 \
--output /tmp/seg_000.plain.ts
ffprobe /tmp/seg_000.plain.ts
Troubleshooting
Error: Invalid key size / encryption key must be 16 bytes
Cause: key file is not 16 bytes (likely contains a trailing newline or ASCII text).
Solution: regenerate with openssl rand 16 > enc.key. Verify with wc -c enc.key → 16.
Error: Unable to open key file during ffmpeg
Cause: line 2 of the keyinfo file points to a path ffmpeg can't read (relative to wrong cwd, or permissions).
Solution: use absolute paths on line 2. chmod 600 enc.key.
Player loads the manifest but segments fail to decrypt
Cause: line 1 URI returns 403/CORS-blocked, or the IV in line 3 doesn't match what the player derives.
Solution: curl -I <line1-url> from the player origin — must be 200 with Access-Control-Allow-Origin. If omitting the IV line, ensure your player derives IV from media sequence number (standard HLS behavior); otherwise pin an explicit IV.
DASH MPD plays unencrypted or "no decryption key" error
Cause: MPD lacks a <ContentProtection> element, or the default_KID doesn't match -encryption_kid.
Solution: inject ContentProtection per references/schemes.md. default_KID must equal your --kid formatted with dashes (8-4-4-4-12).
-hls_flags periodic_rekey didn't rotate
Cause: keyinfo file wasn't rewritten on disk, or ffmpeg checked the mtime before your write.
Solution: write to a temp file then mv atomically. ffmpeg re-reads the keyinfo file at each segment boundary.
FairPlay player refuses the stream
Cause: ffmpeg emits AES-128 full-segment, not SAMPLE-AES with KEYFORMAT="com.apple.streamingkeydelivery".
Solution: use Apple mediafilesegmenter or Shaka Packager. ffmpeg alone cannot emit a FairPlay-compliant playlist.
Widevine / PlayReady player says "no PSSH"
Cause: ffmpeg doesn't write PSSH boxes for Widevine/PlayReady system IDs.
Solution: encrypt with ffmpeg to get raw CENC, then pass through Shaka Packager or mp4encrypt with --pssh to inject system-specific PSSH.