Remove Cdm raw param, Improve PSSH.get_as_box()

The Cdm no longer requires you to specify if it's raw or not thanks to changes in PSSH.get_as_box() now supporting both dynamically.

It will parse the data and if its not a box, it will use the provided data in a newly crafted box.
This commit is contained in:
rlaphoenix 2022-07-30 02:21:19 +01:00
parent 8f7cacb10a
commit b5ac0f45a2
2 changed files with 53 additions and 35 deletions

View File

@ -60,7 +60,7 @@ class Cdm:
NUM_OF_SESSIONS = 0 NUM_OF_SESSIONS = 0
MAX_NUM_OF_SESSIONS = 50 # most common limit MAX_NUM_OF_SESSIONS = 50 # most common limit
def __init__(self, device: Device, pssh: Union[Container, bytes, str], raw: bool = False): def __init__(self, device: Device, pssh: Union[Container, bytes, str]):
""" """
Open a Widevine Content Decryption Module (CDM) session. Open a Widevine Content Decryption Module (CDM) session.
@ -69,9 +69,6 @@ class Cdm:
more device-specific information. more device-specific information.
pssh: Protection System Specific Header Box or Init Data. This should be a pssh: Protection System Specific Header Box or Init Data. This should be a
compliant mp4 pssh box, or just the init data (Widevine Cenc Header). compliant mp4 pssh box, or just the init data (Widevine Cenc Header).
raw: This should be set to True if the PSSH data provided is arbitrary data.
E.g., a PSSH Box where the init data is not a Widevine Cenc Header, or
is simply arbitrary data.
Devices have a limit on how many sessions can be open and active concurrently. Devices have a limit on how many sessions can be open and active concurrently.
The limit is different for each device and security level, most commonly 50. The limit is different for each device and security level, most commonly 50.
@ -92,10 +89,6 @@ class Cdm:
self.NUM_OF_SESSIONS += 1 self.NUM_OF_SESSIONS += 1
self.device = device self.device = device
self.init_data = pssh
if not raw:
# we only want the init_data of the pssh box
self.init_data = PSSH.get_as_box(pssh).init_data self.init_data = PSSH.get_as_box(pssh).init_data
self.session_id = get_random_bytes(16) self.session_id = get_random_bytes(16)

View File

@ -1,9 +1,11 @@
from __future__ import annotations from __future__ import annotations
import base64 import base64
import binascii
from typing import Union from typing import Union
from uuid import UUID from uuid import UUID
import construct
from construct import Container from construct import Container
from google.protobuf.message import DecodeError from google.protobuf.message import DecodeError
from lxml import etree from lxml import etree
@ -78,36 +80,59 @@ class PSSH:
return box return box
@staticmethod @staticmethod
def get_as_box(data: Union[Container, bytes, str]) -> Container: def get_as_box(data: Union[Container, bytes, str], strict: bool = False) -> Container:
""" """
Get the possibly arbitrary data as a parsed PSSH mp4 box. Get possibly arbitrary data as a parsed PSSH mp4 box.
If the data is just Widevine PSSH Data (init data) then it will be crafted
into a new PSSH mp4 box. Parameters:
If the data could not be recognized as a PSSH box of some form of encoding data: PSSH mp4 box, Widevine Cenc Header (init data), or arbitrary data to
it will raise a ValueError. parse or craft into a PSSH mp4 box.
strict: Do not return a PSSH box for arbitrary data. Require the data to be
at least a PSSH mp4 box, or a Widevine Cenc Header.
Raises:
ValueError: If the data is empty, or an unexpected type.
binascii.Error: If the data could not be decoded as Base64 if provided
as a string.
construct.ConstructError: If the data could not be parsed as a PSSH mp4 box
nor a Widevine Cenc Header while strict=True.
""" """
if not data:
raise ValueError("Data must not be empty.")
if isinstance(data, Container):
return data
if isinstance(data, str): if isinstance(data, str):
try:
data = base64.b64decode(data) data = base64.b64decode(data)
except (binascii.Error, binascii.Incomplete) as e:
raise binascii.Error(f"Could not decode data as Base64, {e}")
if isinstance(data, bytes): if isinstance(data, bytes):
if base64.b64encode(data).startswith(b"CAES"): # likely widevine pssh data try:
data = Box.parse(data)
except construct.ConstructError:
if strict:
try: try:
cenc_header = WidevinePsshData() cenc_header = WidevinePsshData()
cenc_header.ParseFromString(data) if cenc_header.MergeFromString(data) < len(data):
raise DecodeError()
except DecodeError: except DecodeError:
# not actually init data after all raise DecodeError(f"Could not parse data as a PSSH mp4 box nor a Widevine Cenc Header.")
pass
else: else:
data = cenc_header.SerializeToString()
data = Box.parse(Box.build(dict( data = Box.parse(Box.build(dict(
type=b"pssh", type=b"pssh",
version=0, version=0,
flags=0, flags=0,
system_ID=PSSH.SystemId.Widevine, system_ID=PSSH.SystemId.Widevine,
init_data=cenc_header.SerializeToString() init_data=data
))) )))
data = Box.parse(data) else:
if isinstance(data, Container): raise ValueError(f"Data is an unexpected type, expected bytes got {data!r}.")
return data return data
raise ValueError(f"Unrecognized PSSH data: {data!r}")
@staticmethod @staticmethod
def get_key_ids(box: Container) -> list[UUID]: def get_key_ids(box: Container) -> list[UUID]: