diff --git a/tests/core/test_dash_clearkey_licensing.py b/tests/core/test_dash_clearkey_licensing.py new file mode 100644 index 00000000..03a3bdb3 --- /dev/null +++ b/tests/core/test_dash_clearkey_licensing.py @@ -0,0 +1,84 @@ +"""Regression test for DASH ``org.w3.clearkey`` licensing in ``DASH.download_track``. + +The download path gated content-key licensing on ``isinstance(drm, (Widevine, +PlayReady))``, so a ClearKeyCENC track never reached its license callback, left +``content_keys`` empty, and aborted at ``drm.decrypt`` with "Cannot decrypt a +Track without any Content Keys". This drives the real ``download_track`` under +``DOWNLOAD_LICENCE_ONLY`` (returns straight after licensing, no segment fetch or +decrypt) and asserts the ClearKeyCENC drm is licensed. +""" + +from __future__ import annotations + +from functools import partial +from typing import Any +from uuid import UUID + +from unshackle.core.constants import DOWNLOAD_LICENCE_ONLY +from unshackle.core.drm.clearkey_cenc import ClearKeyCENC +from unshackle.core.manifests.dash import DASH +from unshackle.core.tracks.track import DownloadContext + +KID = UUID("9eb4050d-e44b-4802-932e-27d75083e266") +KEY = bytes.fromhex("ccd0064c43f7e9fcbaa9b12af3fd1f40") +LAURL = "https://license.example.test/clearkey" +MANIFEST_URL = "https://example.test/manifest.mpd" + +MPD = f""" + + + + + + {LAURL} + + + + + + + + +""" + + +class RecordingLicence: + """License callback stub: records its call and populates content keys.""" + + def __init__(self) -> None: + self.calls: list[tuple[Any, Any]] = [] + + def __call__(self, drm: Any, track_kid: Any = None) -> None: + self.calls.append((drm, track_kid)) + for kid in drm.kids: + drm.content_keys[kid] = KEY.hex() + + +def test_download_track_licenses_clearkey_cenc(tmp_path, monkeypatch) -> None: + track = DASH.from_text(MPD, MANIFEST_URL).to_tracks().videos[0] + # KID normally comes from the init segment; short-circuit the probe (offline). + monkeypatch.setattr(track, "get_key_id", lambda *args, **kwargs: KID) + + licence = RecordingLicence() + ctx = DownloadContext( + save_path=tmp_path / "video.mp4", + save_dir=tmp_path / "video_segments", + progress=partial(lambda **_: None), + license_widevine=licence, + cdm=None, + ) + + DOWNLOAD_LICENCE_ONLY.set() + try: + DASH.download_track(track=track, ctx=ctx) + finally: + DOWNLOAD_LICENCE_ONLY.clear() + + assert len(licence.calls) == 1, "ClearKeyCENC track was never licensed" + drm, track_kid = licence.calls[0] + assert isinstance(drm, ClearKeyCENC) + assert track_kid == KID + # with content keys populated, decrypt has what it needs + assert drm.content_keys == {KID: KEY.hex()} diff --git a/unshackle/core/manifests/dash.py b/unshackle/core/manifests/dash.py index 796d3bbf..119756a1 100644 --- a/unshackle/core/manifests/dash.py +++ b/unshackle/core/manifests/dash.py @@ -425,8 +425,8 @@ class DASH: if track.drm: track_kid = track_kid or track.get_key_id(url=segments[0][0], session=session) drm = track.get_drm_for_cdm(cdm) - if isinstance(drm, (Widevine, PlayReady)): - # license and grab content keys + if isinstance(drm, (Widevine, PlayReady, ClearKeyCENC)): + # license and grab content keys (ClearKeyCENC uses no CDM) try: if not license_widevine: raise ValueError("license_widevine func must be supplied to use DRM")