From 2f7a189c9c475c32cca8861465abfd6d3440dd17 Mon Sep 17 00:00:00 2001 From: imSp4rky Date: Tue, 28 Apr 2026 09:28:21 -0600 Subject: [PATCH] fix(hls): carry DRM keys forward across EXT-X-KEY rotation When the active EXT-X-KEY changes but no segments precede the new key (e.g. rotation at the first segment), no separate decrypt batch is flushed for the previous DRM and its content keys are lost. The merged file still contains samples encrypted under those keys, so the final mp4decrypt/shaka call decrypts them as garbage. Carry the previous DRM's content keys into the new DRM via setdefault so every key needed across the merged segments is present at decrypt time. Existing zero-KID fallback handling (PlayReady, Widevine) remains the disambiguator for tracks whose tenc default_KID is all-zero. --- unshackle/core/drm/playready.py | 5 ++--- unshackle/core/drm/widevine.py | 5 ++--- unshackle/core/manifests/hls.py | 8 ++++++++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/unshackle/core/drm/playready.py b/unshackle/core/drm/playready.py index bc9a713..253e4a5 100644 --- a/unshackle/core/drm/playready.py +++ b/unshackle/core/drm/playready.py @@ -359,9 +359,8 @@ class PlayReady: key_hex = key if isinstance(key, str) else key.hex() key_args.extend(["--key", f"{kid_hex}:{key_hex}"]) - # Some services use a blank/zero default KID in the tenc box, - # but the real KID for the license server. Add zero-KID fallback entries so - # mp4decrypt can match when the file's default KID is all zeros. + # Fallback for tracks whose tenc default_KID is all-zero and whose real + # KID is signalled out-of-band: emit a zero-KID entry per content key. zero_kid = "00" * 16 existing_kids = {kid.hex if hasattr(kid, "hex") else str(kid).replace("-", "") for kid in self.content_keys} if zero_kid not in existing_kids: diff --git a/unshackle/core/drm/widevine.py b/unshackle/core/drm/widevine.py index 82c6419..0bad0a6 100644 --- a/unshackle/core/drm/widevine.py +++ b/unshackle/core/drm/widevine.py @@ -296,9 +296,8 @@ class Widevine: key_hex = key if isinstance(key, str) else key.hex() key_args.extend(["--key", f"{kid_hex}:{key_hex}"]) - # Some services use a blank/zero default KID in the tenc box, - # but the real KID for the license server. Add zero-KID fallback entries so - # mp4decrypt can match when the file's default KID is all zeros. + # Fallback for tracks whose tenc default_KID is all-zero and whose real + # KID is signalled out-of-band: emit a zero-KID entry per content key. zero_kid = "00" * 16 existing_kids = {kid.hex if hasattr(kid, "hex") else str(kid).replace("-", "") for kid in self.content_keys} if zero_kid not in existing_kids: diff --git a/unshackle/core/manifests/hls.py b/unshackle/core/manifests/hls.py index a381ac8..8547565 100644 --- a/unshackle/core/manifests/hls.py +++ b/unshackle/core/manifests/hls.py @@ -858,6 +858,14 @@ class HLS: DOWNLOAD_CANCELLED.set() # skip pending track downloads progress(downloaded="[red]FAILED") raise + if ( + encryption_data + and isinstance(drm, (Widevine, PlayReady)) + and isinstance(encryption_data[1], type(drm)) + and getattr(encryption_data[1], "content_keys", None) + ): + for prev_kid, prev_key in encryption_data[1].content_keys.items(): + drm.content_keys.setdefault(prev_kid, prev_key) encryption_data = (key, drm) if DOWNLOAD_LICENCE_ONLY.is_set():