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.
This commit is contained in:
imSp4rky
2026-04-28 09:28:21 -06:00
parent ffd67f15d8
commit 2f7a189c9c
3 changed files with 12 additions and 6 deletions

View File

@@ -359,9 +359,8 @@ class PlayReady:
key_hex = key if isinstance(key, str) else key.hex() key_hex = key if isinstance(key, str) else key.hex()
key_args.extend(["--key", f"{kid_hex}:{key_hex}"]) key_args.extend(["--key", f"{kid_hex}:{key_hex}"])
# Some services use a blank/zero default KID in the tenc box, # Fallback for tracks whose tenc default_KID is all-zero and whose real
# but the real KID for the license server. Add zero-KID fallback entries so # KID is signalled out-of-band: emit a zero-KID entry per content key.
# mp4decrypt can match when the file's default KID is all zeros.
zero_kid = "00" * 16 zero_kid = "00" * 16
existing_kids = {kid.hex if hasattr(kid, "hex") else str(kid).replace("-", "") for kid in self.content_keys} 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: if zero_kid not in existing_kids:

View File

@@ -296,9 +296,8 @@ class Widevine:
key_hex = key if isinstance(key, str) else key.hex() key_hex = key if isinstance(key, str) else key.hex()
key_args.extend(["--key", f"{kid_hex}:{key_hex}"]) key_args.extend(["--key", f"{kid_hex}:{key_hex}"])
# Some services use a blank/zero default KID in the tenc box, # Fallback for tracks whose tenc default_KID is all-zero and whose real
# but the real KID for the license server. Add zero-KID fallback entries so # KID is signalled out-of-band: emit a zero-KID entry per content key.
# mp4decrypt can match when the file's default KID is all zeros.
zero_kid = "00" * 16 zero_kid = "00" * 16
existing_kids = {kid.hex if hasattr(kid, "hex") else str(kid).replace("-", "") for kid in self.content_keys} 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: if zero_kid not in existing_kids:

View File

@@ -858,6 +858,14 @@ class HLS:
DOWNLOAD_CANCELLED.set() # skip pending track downloads DOWNLOAD_CANCELLED.set() # skip pending track downloads
progress(downloaded="[red]FAILED") progress(downloaded="[red]FAILED")
raise 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) encryption_data = (key, drm)
if DOWNLOAD_LICENCE_ONLY.is_set(): if DOWNLOAD_LICENCE_ONLY.is_set():