agentsclimarketplace

Ffmpeg drm

Skill damionrashford/media-os/skills/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

Install
npx -y skills add damionrashford/media-os --skill ffmpeg-drm

Assembled 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
  1. Line 1: Key URI the player fetches (goes into EXT-X-KEY URI="..." in the playlist).
  2. Line 2: Local path ffmpeg reads to encrypt — can be relative to the cwd used at launch, but absolute is safer.
  3. Line 3: IV in hex, no 0x prefix, 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_key and -encryption_kid are 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 (see references/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 via secrets.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] — builds enc.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 to openssl aes-128-cbc.
    • Global flags: --dry-run, --verbose (keys only printed with --verbose).

Reference docs

Gotchas

  • Key info file format is exact. Three lines: URI / local-path / IV-hex. A blank line, CRLF mix, or a leading 0x on 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.key is wrong — that writes ASCII plus a newline. Use openssl rand 16 or the gen-key subcommand.
  • 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 1 with 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_rekey re-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-AES with KEYFORMAT="com.apple.streamingkeydelivery" signaling needs Apple mediafilesegmenter or 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 (mp4encrypt with --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_key and -encryption_kid are 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 .m3u8 afterward; the next ffmpeg run will overwrite it.
  • Never commit .key, .keyinfo, or raw hex-key env files. Add them to .gitignore and .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 .key files are fine for dev; never prod.
  • Test decryption on every build. A silent -hls_enc fallback (encrypting with a wrong key path) will mux cleanly and fail only at the player. Add openssl aes-128-cbc -d round-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.

Keep looking

Skills are one crate of 628,971. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.