Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9191e0258f | ||
|
|
cabcc1c2c2 | ||
|
|
077a3aa6be | ||
|
|
0d13d4184b | ||
|
|
1064c7953c |
15
CHANGELOG.md
15
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.4.0"
|
||||
__version__ = "1.4.1"
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user