5 Commits

Author SHA1 Message Date
rlaphoenix
9191e0258f Update Changelog for v1.4.1 2022-08-17 17:26:57 +01:00
rlaphoenix
cabcc1c2c2 Bump to v1.4.1 2022-08-17 17:26:44 +01:00
rlaphoenix
077a3aa6be PSSH: Rework from_playready_pssh class method as normal method 2022-08-06 13:48:39 +01:00
rlaphoenix
0d13d4184b PSSH: Rework get_key_ids as key_ids property 2022-08-06 13:45:30 +01:00
rlaphoenix
1064c7953c PSSH: Rework overwrite_key_ids as set_key_ids method 2022-08-06 13:42:31 +01:00
4 changed files with 92 additions and 80 deletions

View File

@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.4.1] - 2022-08-17
Small patch release for some fixes to the PSSH classes recent face-lift.
### Changed
- `PSSH.overwrite_key_ids` static method is now an instance method named `set_key_ids` and works on the current
instance instead of making and returning a new one.
- `PSSH.get_key_ids` static method is now a property method named `key_ids`. This allows swift access to all the
Key IDs of the current access.
- `PSSH.from_playready_pssh` class method is now an instance method named `playready_to_widevine` and now converts
the current instances values directly. This allows you to more easily instance as any PSSH, then convert afterwards.
## [1.4.0] - 2022-08-06
This release is a face-lift for the PSSH class with a moderate amount of Cdm and Serve interface changes.
@@ -223,6 +236,8 @@ This release is primarily a maintenance release for `serve` functionality but so
Initial Release.
[1.4.1]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.4.1
[1.4.0]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.4.0
[1.3.1]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.3.1
[1.3.0]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.3.0
[1.2.1]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.2.1

View File

@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "pywidevine"
version = "1.4.0"
version = "1.4.1"
description = "Widevine CDM (Content Decryption Module) implementation in Python."
authors = ["rlaphoenix <rlaphoenix@pm.me>"]
license = "GPL-3.0-only"

View File

@@ -1 +1 @@
__version__ = "1.4.0"
__version__ = "1.4.1"

View File

@@ -96,7 +96,7 @@ class PSSH:
self.version = box.version
self.flags = box.flags
self.system_id = box.system_ID
self.key_ids = box.key_IDs
self.__key_ids = box.key_IDs
self.init_data = box.init_data
@classmethod
@@ -175,84 +175,38 @@ class PSSH:
init_data=[init_data, b""][init_data is None]
)))
pssh = cls(box)
if key_ids and version == 0:
PSSH.overwrite_key_ids(box, [UUID(bytes=x) for x in key_ids])
pssh.set_key_ids([UUID(bytes=x) for x in key_ids])
return cls(box)
return pssh
@classmethod
def from_playready_pssh(cls, box: Container) -> PSSH:
@property
def key_ids(self) -> list[UUID]:
"""
Convert a PlayReady PSSH Box to a Widevine PSSH Box.
Note: The resulting Widevine PSSH will likely not be usable for Licensing. This
is because there is some data for a Widevine Cenc Header that is missing from a
PlayReady PSSH Box.
This converted PSSH will only be useful for it's Key IDs, so realistically only
for matching Key IDs with a Track. As a fallback.
"""
if box.type != b"pssh":
raise ValueError(f"Box must be a PSSH box, not {box.type}")
if box.system_ID != PSSH.SystemId.PlayReady:
raise ValueError(f"This is not a PlayReady PSSH Box, {box.system_ID}")
key_ids = PSSH.get_key_ids(box)
cenc_header = WidevinePsshData()
cenc_header.algorithm = 1 # 0=Clear, 1=AES-CTR
for key_id in key_ids:
cenc_header.key_ids.append(key_id.bytes)
if box.version == 1:
# ensure both cenc header and box has same Key IDs
# v1 uses both this and within init data for basically no reason
box.key_IDs = key_ids
box.init_data = cenc_header.SerializeToString()
box.system_ID = PSSH.SystemId.Widevine
return cls(box)
def dump(self) -> bytes:
"""Export the PSSH object as a full PSSH box in bytes form."""
return Box.build(dict(
type=b"pssh",
version=self.version,
flags=self.flags,
system_ID=self.system_id,
key_IDs=self.key_ids,
init_data=self.init_data
))
def dumps(self) -> str:
"""Export the PSSH object as a full PSSH box in base64 form."""
return base64.b64encode(self.dump()).decode()
@staticmethod
def get_key_ids(box: Container) -> list[UUID]:
"""
Get Key IDs from a PSSH Box from within the Box or Init Data where possible.
Get all Key IDs from within the Box or Init Data, wherever possible.
Supports:
- Version 1 Boxes
- Widevine Headers
- PlayReady Headers (4.0.0.0->4.3.0.0)
"""
if box.version == 1 and box.key_IDs:
return box.key_IDs
if self.version == 1 and self.__key_ids:
return self.__key_ids
if box.system_ID == PSSH.SystemId.Widevine:
init = WidevinePsshData()
init.ParseFromString(box.init_data)
if self.system_id == PSSH.SystemId.Widevine:
# TODO: What if its not a Widevine Cenc Header but the System ID is set as Widevine?
cenc_header = WidevinePsshData()
cenc_header.ParseFromString(self.init_data)
return [
# the key_ids value may or may not be hex underlying
UUID(bytes=key_id) if len(key_id) == 16 else UUID(hex=key_id.decode())
for key_id in init.key_ids
for key_id in cenc_header.key_ids
]
if box.system_ID == PSSH.SystemId.PlayReady:
xml_string = box.init_data.decode("utf-16-le")
if self.system_id == PSSH.SystemId.PlayReady:
xml_string = self.init_data.decode("utf-16-le")
# some of these init data has garbage(?) in front of it
xml_string = xml_string[xml_string.index("<"):]
xml = etree.fromstring(xml_string)
@@ -270,27 +224,70 @@ class PSSH:
for key_id in key_ids
]
raise ValueError(f"Unsupported Box {box!r}")
raise ValueError(f"This PSSH is not supported by key_ids() property, {self.dumps()}")
@staticmethod
def overwrite_key_ids(box: Container, key_ids: list[UUID]) -> Container:
"""Overwrite all Key IDs in PSSH box with the specified Key IDs."""
if box.system_ID != PSSH.SystemId.Widevine:
raise ValueError(f"Only Widevine PSSH Boxes are supported, not {box.system_ID}.")
def dump(self) -> bytes:
"""Export the PSSH object as a full PSSH box in bytes form."""
return Box.build(dict(
type=b"pssh",
version=self.version,
flags=self.flags,
system_ID=self.system_id,
key_IDs=self.key_ids,
init_data=self.init_data
))
if box.version == 1 or box.key_IDs:
# only use key_IDs if version is 1, or it's already being used
def dumps(self) -> str:
"""Export the PSSH object as a full PSSH box in base64 form."""
return base64.b64encode(self.dump()).decode()
def playready_to_widevine(self) -> None:
"""
Convert PlayReady PSSH data to Widevine PSSH data.
There's only a limited amount of information within a PlayReady PSSH header that
can be used in a Widevine PSSH Header. The converted data may or may not result
in an accepted PSSH. It depends on what the License Server is expecting.
"""
if self.system_id != PSSH.SystemId.PlayReady:
raise ValueError(f"This is not a PlayReady PSSH, {self.system_id}")
cenc_header = WidevinePsshData()
cenc_header.algorithm = 1 # 0=Clear, 1=AES-CTR
cenc_header.key_ids[:] = [x.bytes for x in self.key_ids]
if self.version == 1:
# ensure both cenc header and box has same Key IDs
# v1 uses both this and within init data for basically no reason
self.__key_ids = self.key_ids
self.init_data = cenc_header.SerializeToString()
self.system_id = PSSH.SystemId.Widevine
def set_key_ids(self, key_ids: list[UUID]) -> None:
"""Overwrite all Key IDs with the specified Key IDs."""
if self.system_id != PSSH.SystemId.Widevine:
# TODO: Add support for setting the Key IDs in a PlayReady Header
raise ValueError(f"Only Widevine PSSH Boxes are supported, not {self.system_id}.")
if not isinstance(key_ids, list):
raise TypeError(f"Expecting key_ids to be a list, not {key_ids!r}")
if not all(isinstance(x, UUID) for x in key_ids):
not_uuid = [x for x in key_ids if not isinstance(x, UUID)]
raise TypeError(f"All Key IDs in key_ids must be a {UUID}, not {not_uuid}")
if self.version == 1 or self.__key_ids:
# only use v1 box key_ids if version is 1, or it's already being used
# this is in case the service stupidly expects it for version 0
box.key_IDs = key_ids
self.__key_ids = key_ids
init = WidevinePsshData()
init.ParseFromString(box.init_data)
cenc_header = WidevinePsshData()
cenc_header.ParseFromString(self.init_data)
init.key_ids[:] = [
cenc_header.key_ids[:] = [
key_id.bytes
for key_id in key_ids
]
box.init_data = init.SerializeToString()
return box
self.init_data = cenc_header.SerializeToString()