Files
unshackle/tests/core/test_pr_from_kid.py
imSp4rky 4422c975c3 feat(drm): add FairPlay->PlayReady bridge for cbcs HLS
FairPlay HLS (SAMPLE-AES/cbcs) ships a content KID in its skd:// key but no PlayReady/Widevine PSSH. Synthesize a PlayReady header from that KID so a PlayReady CDM can license and decrypt it whenever the backend is multi-DRM.

- FairPlay DRM (PlayReady subclass) + FairPlay.from_kid; fairplay_kid_from_skd extracts the KID from GUID / ?KID= / 32-hex skd forms (core/drm/fairplay.py)
- build_pr_header_from_kid synthesizes a WRMHEADER/PRO (cbcs->AESCBC v4.3, cenc->AESCTR v4.0); PlayReady.from_track falls back to a tenc KID when no PSSH
- Service.get_fairplay_license hook (defaults to get_playready_license)
- dl.py routes FairPlay tracks to get_fairplay_license through the PlayReady CDM
- EXAMPLE service + config.yaml document the bridge end to end
- tests for the synthesized header and skd KID extraction
2026-06-18 16:05:02 +00:00

59 lines
2.0 KiB
Python

"""Tests for the FairPlay->PlayReady KID bridge.
FairPlay/cbcs HLS ships a tenc DEFAULT_KID but no PlayReady/Widevine PSSH.
``build_pr_header_from_kid`` synthesizes a PlayReady Object (PRO) from that bare
KID so a PlayReady license can be requested keyed on it. These pins ensure the
synthesized header parses back through pyplayready and yields the same KID.
"""
from __future__ import annotations
from uuid import UUID
import pytest
from pyplayready.system.pssh import PSSH
from unshackle.core.drm.playready import PlayReady, build_pr_header_from_kid
@pytest.mark.parametrize(
"kid",
[
UUID("279926a3-d9b9-4b6f-8b2c-1a2b3c4d5e6f"),
UUID("00000000-0000-0000-0000-000000000001"),
UUID("ffffffff-ffff-ffff-ffff-ffffffffffff"),
],
)
def test_synth_header_parses_and_round_trips_kid(kid: UUID) -> None:
pssh_b64 = build_pr_header_from_kid(kid)
# pyplayready parses the synthesized PRO as a single WRMHEADER record.
parsed = PSSH(pssh_b64)
assert len(parsed.wrm_headers) == 1
# PlayReady DRM object recovers the original KID from the synthesized header.
drm = PlayReady(pssh=parsed, pssh_b64=pssh_b64)
assert [k.hex for k in drm.kids] == [kid.hex]
def test_synth_header_is_v4_0_aesctr() -> None:
import base64
pssh_b64 = build_pr_header_from_kid(UUID("279926a3-d9b9-4b6f-8b2c-1a2b3c4d5e6f"))
xml = base64.b64decode(pssh_b64).decode("utf-16-le", errors="ignore")
assert "PlayReadyHeader" in xml
assert 'version="4.0.0.0"' in xml
assert "<ALGID>AESCTR</ALGID>" in xml
def test_cbcs_header_v4_3_round_trips_kid() -> None:
kid = UUID("279926a3-d9b9-4b6f-8b2c-1a2b3c4d5e6f")
pssh_b64 = build_pr_header_from_kid(kid, scheme="cbcs", version="4.3")
drm = PlayReady(pssh=PSSH(pssh_b64), pssh_b64=pssh_b64)
assert [k.hex for k in drm.kids] == [kid.hex]
def test_cbcs_requires_v4_3() -> None:
with pytest.raises(ValueError, match="4.3"):
build_pr_header_from_kid(UUID(int=1), scheme="cbcs", version="4.0")