Add m3u playlists

This commit is contained in:
Nirvana
2026-02-13 18:30:06 +01:00
parent e08c6d6591
commit a11470fb76
2 changed files with 57 additions and 211 deletions
@@ -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)
@@ -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("<I", data[0:4])[0] # Little-endian uint32
data2 = struct.unpack("<H", data[4:6])[0] # Little-endian uint16
data3 = struct.unpack("<H", data[6:8])[0] # Little-endian uint16
data4 = data[8:16].hex() # Big-endian (raw bytes)
# Format as hex string
guid_hex = f"{data1:08x}{data2:04x}{data3:04x}{data4}"
return guid_hex.lower()
except Exception as e:
logger.debug(f"Failed to extract KID from tenc: {e}")
return []
@staticmethod
def is_track_encrypted(tenc_data: bytes) -> 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
except Exception:
return False