From a11470fb76790176a7fb7871e6b6536a5e791b26 Mon Sep 17 00:00:00 2001 From: Nirvana Date: Fri, 13 Feb 2026 18:30:06 +0100 Subject: [PATCH] Add m3u playlists --- .../base/models/drm/constants.py | 46 ++-- .../base/models/drm/tenc_parser.py | 222 ++++-------------- 2 files changed, 57 insertions(+), 211 deletions(-) diff --git a/lib/streaming_providers/base/models/drm/constants.py b/lib/streaming_providers/base/models/drm/constants.py index 8840d0d..07764a1 100644 --- a/lib/streaming_providers/base/models/drm/constants.py +++ b/lib/streaming_providers/base/models/drm/constants.py @@ -70,38 +70,26 @@ class PSSHOffsets: class TencOffsets: """ - Track Encryption Box (tenc) Binary Structure Offsets + tenc Box Structure (based on real-world implementation) - tenc Box Format (ISO/IEC 23001-7): - [0-3] Box size (uint32) - [4-7] Box type ('tenc') - [8] Version (uint8) - [9-11] Flags (uint24) - [12-15] Reserved (uint24) + default_crypt_byte_block (uint8) - - Version 0: - [16] Reserved (uint8) - [17] default_is_protected (uint8) - [18] default_per_sample_IV_size (uint8) - [19-34] default_KID (16 bytes) - - Version 1: - [16] default_constant_IV_size (uint8) - [17+] default_constant_IV (if size > 0) - [...] default_KID (16 bytes, after IV) + Data section (after 8-byte header): + [0-3] version + flags (uint32) + [4-6] reserved (3 bytes) + [7] default_is_protected (uint8) + [8] default_per_sample_IV_size (uint8) + [9-24] default_KID (16 bytes) """ - BOX_SIZE = 0 - BOX_TYPE = 4 - BOX_TYPE_END = 8 - VERSION = 8 + # Data section offsets + VERSION_FLAGS = 0 + VERSION_FLAGS_END = 4 + IS_PROTECTED = 7 + IV_SIZE = 8 + KID_START = 9 + KID_END = 25 # 9 + 16 = 25 - # Version 0 specific - V0_IS_PROTECTED = 17 - V0_IV_SIZE = 18 - V0_KID_START = 19 - V0_KID_END = 35 - - MIN_TENC_V0_SIZE = 35 # Minimum valid V0 tenc box + # Minimum data size (not including 8-byte header) + MIN_TENC_DATA_SIZE = 24 # Up to byte 23, but KID needs byte 24 + MIN_TENC_TOTAL_SIZE = 32 # 8 header + 24 data # DRM System UUID Mappings (normalized, no hyphens) diff --git a/lib/streaming_providers/base/models/drm/tenc_parser.py b/lib/streaming_providers/base/models/drm/tenc_parser.py index 157ef99..1ec70ef 100644 --- a/lib/streaming_providers/base/models/drm/tenc_parser.py +++ b/lib/streaming_providers/base/models/drm/tenc_parser.py @@ -1,217 +1,75 @@ """ tenc Box Parser -Handles parsing of Track Encryption (tenc) boxes for extracting Key IDs -when PSSH boxes don't contain them (version 0 PSSH). +Simplified parser based on working implementation that successfully +handles real-world streams without strict ISO spec compliance. """ -import struct - -from .constants import TencOffsets, MAX_TENC_SIZE, KID_SIZE_BYTES -from .exceptions import InvalidTencError -from .utils import bytes_to_uuid_hex +from ...utils import logger class TencParser: - """ - Parser for tenc (Track Encryption) boxes. - - Used as fallback when PSSH version 0 doesn't contain Key IDs. - According to ISO/IEC 23001-7, tenc boxes contain default encryption - information including the default Key ID. - """ + """Parser for tenc (Track Encryption) boxes.""" @staticmethod def extract_kids_from_tenc(tenc_data: bytes) -> list[str]: """ Extract Key IDs from tenc box data. - This is typically used as a fallback when PSSH version 0 boxes - don't contain Key IDs directly. + Based on working code that extracts KID from bytes 9-25 + after validating is_protected at byte 7. Args: - tenc_data: Raw tenc box bytes + tenc_data: Raw tenc box data (starting after 8-byte header) Returns: - List of normalized Key IDs (32 hex chars, no hyphens) - - Raises: - InvalidTencError: If tenc box is malformed + List containing single Key ID if found, empty list otherwise """ if not tenc_data: - raise InvalidTencError("tenc box data cannot be empty") - - # Check size limits - if len(tenc_data) > MAX_TENC_SIZE: - raise InvalidTencError( - f"tenc box too large: {len(tenc_data)} bytes (max: {MAX_TENC_SIZE})" - ) - - # Validate minimum size for version 0 - if len(tenc_data) < TencOffsets.MIN_TENC_V0_SIZE: - raise InvalidTencError( - f"tenc box too small: {len(tenc_data)} bytes " - f"(minimum: {TencOffsets.MIN_TENC_V0_SIZE})" - ) - - # Validate box type - box_type = tenc_data[TencOffsets.BOX_TYPE:TencOffsets.BOX_TYPE_END] - if box_type != b"tenc": - raise InvalidTencError( - f"Not a tenc box: type is '{box_type.decode('ascii', errors='ignore')}'" - ) - - # Parse version - version = tenc_data[TencOffsets.VERSION] - - # Extract KID based on version - if version == 0: - return TencParser._extract_kid_from_v0_tenc(tenc_data) - elif version == 1: - return TencParser._extract_kid_from_v1_tenc(tenc_data) - else: - raise InvalidTencError(f"Unsupported tenc version: {version}") - - @staticmethod - def _extract_kid_from_v0_tenc(tenc_data: bytes) -> list[str]: - """ - Extract Key ID from version 0 tenc box. - - Version 0 structure: - [16] Reserved (uint8) - [17] default_is_protected (uint8) - [18] default_per_sample_IV_size (uint8) - [19-34] default_KID (16 bytes) - - Args: - tenc_data: Raw tenc box bytes - - Returns: - List containing single normalized Key ID - - Raises: - InvalidTencError: If tenc structure is invalid - """ - if len(tenc_data) < TencOffsets.V0_KID_END: - raise InvalidTencError( - f"tenc v0 box truncated: {len(tenc_data)} bytes " - f"(need at least {TencOffsets.V0_KID_END})" - ) - - # Check if track is protected - is_protected = tenc_data[TencOffsets.V0_IS_PROTECTED] - if is_protected == 0: - # Unencrypted track, no KID return [] - # Extract KID bytes - kid_bytes = tenc_data[TencOffsets.V0_KID_START:TencOffsets.V0_KID_END] + # Size check based on working code + if len(tenc_data) < 24: + logger.debug(f"tenc data too small: {len(tenc_data)} bytes (need 24)") + return [] - # tenc KIDs are typically in UUID/GUID format (mixed-endian) try: - kid_hex = TencParser._parse_guid_bytes(kid_bytes) - except Exception: - # Fallback to raw hex if GUID parsing fails - kid_hex = bytes_to_uuid_hex(kid_bytes) + # Version and flags are at start but we don't really need them + # version_flags = struct.unpack(">I", tenc_data[0:4])[0] + # version = (version_flags >> 24) & 0xFF - return [kid_hex] + # Check if track is protected (byte 7) + if len(tenc_data) > 7: + is_protected = tenc_data[7] + if is_protected == 0: + logger.debug("Track not encrypted (is_protected=0)") + return [] - @staticmethod - def _extract_kid_from_v1_tenc(tenc_data: bytes) -> list[str]: - """ - Extract Key ID from version 1 tenc box. + # Extract KID from bytes 9-24 + if len(tenc_data) >= 25: + kid_bytes = tenc_data[9:25] - Version 1 structure: - [16] default_constant_IV_size (uint8) - [17+] default_constant_IV (if IV_size > 0) - [...] default_KID (16 bytes, after IV) + # Working code uses simple hexlify + kid_hex = kid_bytes.hex().lower() - Args: - tenc_data: Raw tenc box bytes + logger.debug(f"Extracted KID: {kid_hex[:8]}...") + return [kid_hex] + else: + logger.debug(f"tenc data too short for KID: {len(tenc_data)} bytes") + return [] - Returns: - List containing single normalized Key ID - - Raises: - InvalidTencError: If tenc structure is invalid - """ - # Read IV size - if len(tenc_data) < 17: - raise InvalidTencError("tenc v1 box too small to read IV size") - - iv_size = tenc_data[16] - - # KID starts after IV - kid_start = 17 + iv_size - kid_end = kid_start + KID_SIZE_BYTES - - if len(tenc_data) < kid_end: - raise InvalidTencError( - f"tenc v1 box truncated: {len(tenc_data)} bytes " - f"(need at least {kid_end} for KID)" - ) - - # Extract KID bytes - kid_bytes = tenc_data[kid_start:kid_end] - - # tenc KIDs are typically in UUID/GUID format - try: - kid_hex = TencParser._parse_guid_bytes(kid_bytes) - except Exception: - kid_hex = bytes_to_uuid_hex(kid_bytes) - - return [kid_hex] - - @staticmethod - def _parse_guid_bytes(data: bytes) -> str: - """ - Parse GUID/UUID bytes with mixed-endian format (Microsoft GUID). - - GUID byte order: - - Data1 (4 bytes): little-endian - - Data2 (2 bytes): little-endian - - Data3 (2 bytes): little-endian - - Data4 (8 bytes): big-endian - - Args: - data: 16 bytes of GUID data - - Returns: - 32-character hex string (normalized, no hyphens) - """ - if len(data) != 16: - raise ValueError(f"GUID bytes must be 16 bytes (got {len(data)})") - - # Parse with mixed-endian format (Microsoft GUID) - data1 = struct.unpack(" bool: - """ - Check if track is encrypted based on tenc box. - - Args: - tenc_data: Raw tenc box bytes - - Returns: - True if track is encrypted, False otherwise - """ - if not tenc_data or len(tenc_data) < TencOffsets.V0_IS_PROTECTED + 1: + """Check if track is encrypted based on is_protected flag.""" + if not tenc_data or len(tenc_data) < 8: return False - version = tenc_data[TencOffsets.VERSION] - - if version == 0: - is_protected = tenc_data[TencOffsets.V0_IS_PROTECTED] + try: + is_protected = tenc_data[7] if len(tenc_data) > 7 else 0 return is_protected != 0 - - # Version 1+ tracks are assumed encrypted if tenc box exists - return True \ No newline at end of file + except Exception: + return False \ No newline at end of file