CoolFace
Apppublic

sdv2500/progettojava

sourceHugging Faceupdated 10mo agoView on Hugging Face
0likes
drm_decrypter.py778 linesDownload Raw Back to utils
1import argparse2import struct3import sys4from typing import Optional, Union5 6from Crypto.Cipher import AES7from collections import namedtuple8import array9 10CENCSampleAuxiliaryDataFormat = namedtuple("CENCSampleAuxiliaryDataFormat", ["is_encrypted", "iv", "sub_samples"])11 12 13class MP4Atom:14    """15    Represents an MP4 atom, which is a basic unit of data in an MP4 file.16    Each atom contains a header (size and type) and data.17    """18 19    __slots__ = ("atom_type", "size", "data")20 21    def __init__(self, atom_type: bytes, size: int, data: Union[memoryview, bytearray]):22        """23        Initializes an MP4Atom instance.24 25        Args:26            atom_type (bytes): The type of the atom.27            size (int): The size of the atom.28            data (Union[memoryview, bytearray]): The data contained in the atom.29        """30        self.atom_type = atom_type31        self.size = size32        self.data = data33 34    def __repr__(self):35        return f"<MP4Atom type={self.atom_type}, size={self.size}>"36 37    def pack(self):38        """39        Packs the atom into binary data.40 41        Returns:42            bytes: Packed binary data with size, type, and data.43        """44        return struct.pack(">I", self.size) + self.atom_type + self.data45 46 47class MP4Parser:48    """49    Parses MP4 data to extract atoms and their structure.50    """51 52    def __init__(self, data: memoryview):53        """54        Initializes an MP4Parser instance.55 56        Args:57            data (memoryview): The binary data of the MP4 file.58        """59        self.data = data60        self.position = 061 62    def read_atom(self) -> Optional[MP4Atom]:63        """64        Reads the next atom from the data.65 66        Returns:67            Optional[MP4Atom]: MP4Atom object or None if no more atoms are available.68        """69        pos = self.position70        if pos + 8 > len(self.data):71            return None72 73        size, atom_type = struct.unpack_from(">I4s", self.data, pos)74        pos += 875 76        if size == 1:77            if pos + 8 > len(self.data):78                return None79            size = struct.unpack_from(">Q", self.data, pos)[0]80            pos += 881 82        if size < 8 or pos + size - 8 > len(self.data):83            return None84 85        atom_data = self.data[pos : pos + size - 8]86        self.position = pos + size - 887        return MP4Atom(atom_type, size, atom_data)88 89    def list_atoms(self) -> list[MP4Atom]:90        """91        Lists all atoms in the data.92 93        Returns:94            list[MP4Atom]: List of MP4Atom objects.95        """96        atoms = []97        original_position = self.position98        self.position = 099        while self.position + 8 <= len(self.data):100            atom = self.read_atom()101            if not atom:102                break103            atoms.append(atom)104        self.position = original_position105        return atoms106 107    def _read_atom_at(self, pos: int, end: int) -> Optional[MP4Atom]:108        if pos + 8 > end:109            return None110 111        size, atom_type = struct.unpack_from(">I4s", self.data, pos)112        pos += 8113 114        if size == 1:115            if pos + 8 > end:116                return None117            size = struct.unpack_from(">Q", self.data, pos)[0]118            pos += 8119 120        if size < 8 or pos + size - 8 > end:121            return None122 123        atom_data = self.data[pos : pos + size - 8]124        return MP4Atom(atom_type, size, atom_data)125 126    def print_atoms_structure(self, indent: int = 0):127        """128        Prints the structure of all atoms in the data.129 130        Args:131            indent (int): The indentation level for printing.132        """133        pos = 0134        end = len(self.data)135        while pos + 8 <= end:136            atom = self._read_atom_at(pos, end)137            if not atom:138                break139            self.print_single_atom_structure(atom, pos, indent)140            pos += atom.size141 142    def print_single_atom_structure(self, atom: MP4Atom, parent_position: int, indent: int):143        """144        Prints the structure of a single atom.145 146        Args:147            atom (MP4Atom): The atom to print.148            parent_position (int): The position of the parent atom.149            indent (int): The indentation level for printing.150        """151        try:152            atom_type = atom.atom_type.decode("utf-8")153        except UnicodeDecodeError:154            atom_type = repr(atom.atom_type)155        print(" " * indent + f"Type: {atom_type}, Size: {atom.size}")156 157        child_pos = 0158        child_end = len(atom.data)159        while child_pos + 8 <= child_end:160            child_atom = self._read_atom_at(parent_position + 8 + child_pos, parent_position + 8 + child_end)161            if not child_atom:162                break163            self.print_single_atom_structure(child_atom, parent_position, indent + 2)164            child_pos += child_atom.size165 166 167class MP4Decrypter:168    """169    Class to handle the decryption of CENC encrypted MP4 segments.170 171    Attributes:172        key_map (dict[bytes, bytes]): Mapping of track IDs to decryption keys.173        current_key (Optional[bytes]): Current decryption key.174        trun_sample_sizes (array.array): Array of sample sizes from the 'trun' box.175        current_sample_info (list): List of sample information from the 'senc' box.176        encryption_overhead (int): Total size of encryption-related boxes.177    """178 179    def __init__(self, key_map: dict[bytes, bytes]):180        """181        Initializes the MP4Decrypter with a key map.182 183        Args:184            key_map (dict[bytes, bytes]): Mapping of track IDs to decryption keys.185        """186        self.key_map = key_map187        self.current_key = None188        self.trun_sample_sizes = array.array("I")189        self.current_sample_info = []190        self.encryption_overhead = 0191 192    def decrypt_segment(self, combined_segment: bytes) -> bytes:193        """194        Decrypts a combined MP4 segment.195 196        Args:197            combined_segment (bytes): Combined initialization and media segment.198 199        Returns:200            bytes: Decrypted segment content.201        """202        data = memoryview(combined_segment)203        parser = MP4Parser(data)204        atoms = parser.list_atoms()205 206        atom_process_order = [b"moov", b"moof", b"sidx", b"mdat"]207 208        processed_atoms = {}209        for atom_type in atom_process_order:210            if atom := next((a for a in atoms if a.atom_type == atom_type), None):211                processed_atoms[atom_type] = self._process_atom(atom_type, atom)212 213        result = bytearray()214        for atom in atoms:215            if atom.atom_type in processed_atoms:216                processed_atom = processed_atoms[atom.atom_type]217                result.extend(processed_atom.pack())218            else:219                result.extend(atom.pack())220 221        return bytes(result)222 223    def _process_atom(self, atom_type: bytes, atom: MP4Atom) -> MP4Atom:224        """225        Processes an MP4 atom based on its type.226 227        Args:228            atom_type (bytes): Type of the atom.229            atom (MP4Atom): The atom to process.230 231        Returns:232            MP4Atom: Processed atom.233        """234        if atom_type == b"moov":235            return self._process_moov(atom)236        elif atom_type == b"moof":237            return self._process_moof(atom)238        elif atom_type == b"sidx":239            return self._process_sidx(atom)240        elif atom_type == b"mdat":241            return self._decrypt_mdat(atom)242        else:243            return atom244 245    def _process_moov(self, moov: MP4Atom) -> MP4Atom:246        """247        Processes the 'moov' (Movie) atom, which contains metadata about the entire presentation.248        This includes information about tracks, media data, and other movie-level metadata.249 250        Args:251            moov (MP4Atom): The 'moov' atom to process.252 253        Returns:254            MP4Atom: Processed 'moov' atom with updated track information.255        """256        parser = MP4Parser(moov.data)257        new_moov_data = bytearray()258 259        for atom in iter(parser.read_atom, None):260            if atom.atom_type == b"trak":261                new_trak = self._process_trak(atom)262                new_moov_data.extend(new_trak.pack())263            elif atom.atom_type != b"pssh":264                # Skip PSSH boxes as they are not needed in the decrypted output265                new_moov_data.extend(atom.pack())266 267        return MP4Atom(b"moov", len(new_moov_data) + 8, new_moov_data)268 269    def _process_moof(self, moof: MP4Atom) -> MP4Atom:270        """271        Processes the 'moov' (Movie) atom, which contains metadata about the entire presentation.272        This includes information about tracks, media data, and other movie-level metadata.273 274        Args:275            moov (MP4Atom): The 'moov' atom to process.276 277        Returns:278            MP4Atom: Processed 'moov' atom with updated track information.279        """280        parser = MP4Parser(moof.data)281        new_moof_data = bytearray()282 283        for atom in iter(parser.read_atom, None):284            if atom.atom_type == b"traf":285                new_traf = self._process_traf(atom)286                new_moof_data.extend(new_traf.pack())287            else:288                new_moof_data.extend(atom.pack())289 290        return MP4Atom(b"moof", len(new_moof_data) + 8, new_moof_data)291 292    def _process_traf(self, traf: MP4Atom) -> MP4Atom:293        """294        Processes the 'traf' (Track Fragment) atom, which contains information about a track fragment.295        This includes sample information, sample encryption data, and other track-level metadata.296 297        Args:298            traf (MP4Atom): The 'traf' atom to process.299 300        Returns:301            MP4Atom: Processed 'traf' atom with updated sample information.302        """303        parser = MP4Parser(traf.data)304        new_traf_data = bytearray()305        tfhd = None306        sample_count = 0307        sample_info = []308 309        atoms = parser.list_atoms()310 311        # calculate encryption_overhead earlier to avoid dependency on trun312        self.encryption_overhead = sum(a.size for a in atoms if a.atom_type in {b"senc", b"saiz", b"saio"})313 314        for atom in atoms:315            if atom.atom_type == b"tfhd":316                tfhd = atom317                new_traf_data.extend(atom.pack())318            elif atom.atom_type == b"trun":319                sample_count = self._process_trun(atom)320                new_trun = self._modify_trun(atom)321                new_traf_data.extend(new_trun.pack())322            elif atom.atom_type == b"senc":323                # Parse senc but don't include it in the new decrypted traf data and similarly don't include saiz and saio324                sample_info = self._parse_senc(atom, sample_count)325            elif atom.atom_type not in {b"saiz", b"saio"}:326                new_traf_data.extend(atom.pack())327 328        if tfhd:329            tfhd_track_id = struct.unpack_from(">I", tfhd.data, 4)[0]330            self.current_key = self._get_key_for_track(tfhd_track_id)331            self.current_sample_info = sample_info332 333        return MP4Atom(b"traf", len(new_traf_data) + 8, new_traf_data)334 335    def _decrypt_mdat(self, mdat: MP4Atom) -> MP4Atom:336        """337        Decrypts the 'mdat' (Media Data) atom, which contains the actual media data (audio, video, etc.).338        The decryption is performed using the current decryption key and sample information.339 340        Args:341            mdat (MP4Atom): The 'mdat' atom to decrypt.342 343        Returns:344            MP4Atom: Decrypted 'mdat' atom with decrypted media data.345        """346        if not self.current_key or not self.current_sample_info:347            return mdat  # Return original mdat if we don't have decryption info348 349        decrypted_samples = bytearray()350        mdat_data = mdat.data351        position = 0352 353        for i, info in enumerate(self.current_sample_info):354            if position >= len(mdat_data):355                break  # No more data to process356 357            sample_size = self.trun_sample_sizes[i] if i < len(self.trun_sample_sizes) else len(mdat_data) - position358            sample = mdat_data[position : position + sample_size]359            position += sample_size360            decrypted_sample = self._process_sample(sample, info, self.current_key)361            decrypted_samples.extend(decrypted_sample)362 363        return MP4Atom(b"mdat", len(decrypted_samples) + 8, decrypted_samples)364 365    def _parse_senc(self, senc: MP4Atom, sample_count: int) -> list[CENCSampleAuxiliaryDataFormat]:366        """367        Parses the 'senc' (Sample Encryption) atom, which contains encryption information for samples.368        This includes initialization vectors (IVs) and sub-sample encryption data.369 370        Args:371            senc (MP4Atom): The 'senc' atom to parse.372            sample_count (int): The number of samples.373 374        Returns:375            list[CENCSampleAuxiliaryDataFormat]: List of sample auxiliary data formats with encryption information.376        """377        data = memoryview(senc.data)378        version_flags = struct.unpack_from(">I", data, 0)[0]379        version, flags = version_flags >> 24, version_flags & 0xFFFFFF380        position = 4381 382        if version == 0:383            sample_count = struct.unpack_from(">I", data, position)[0]384            position += 4385 386        sample_info = []387        for _ in range(sample_count):388            if position + 8 > len(data):389                break390 391            iv = data[position : position + 8].tobytes()392            position += 8393 394            sub_samples = []395            if flags & 0x000002 and position + 2 <= len(data):  # Check if subsample information is present396                subsample_count = struct.unpack_from(">H", data, position)[0]397                position += 2398 399                for _ in range(subsample_count):400                    if position + 6 <= len(data):401                        clear_bytes, encrypted_bytes = struct.unpack_from(">HI", data, position)402                        position += 6403                        sub_samples.append((clear_bytes, encrypted_bytes))404                    else:405                        break406 407            sample_info.append(CENCSampleAuxiliaryDataFormat(True, iv, sub_samples))408 409        return sample_info410 411    def _get_key_for_track(self, track_id: int) -> bytes:412        """413        Retrieves the decryption key for a given track ID from the key map.414 415        Args:416            track_id (int): The track ID.417 418        Returns:419            bytes: The decryption key for the specified track ID.420        """421        if len(self.key_map) == 1:422            return next(iter(self.key_map.values()))423        key = self.key_map.get(track_id.pack(4, "big"))424        if not key:425            raise ValueError(f"No key found for track ID {track_id}")426        return key427 428    @staticmethod429    def _process_sample(430        sample: memoryview, sample_info: CENCSampleAuxiliaryDataFormat, key: bytes431    ) -> Union[memoryview, bytearray, bytes]:432        """433        Processes and decrypts a sample using the provided sample information and decryption key.434        This includes handling sub-sample encryption if present.435 436        Args:437            sample (memoryview): The sample data.438            sample_info (CENCSampleAuxiliaryDataFormat): The sample auxiliary data format with encryption information.439            key (bytes): The decryption key.440 441        Returns:442            Union[memoryview, bytearray, bytes]: The decrypted sample.443        """444        if not sample_info.is_encrypted:445            return sample446 447        # pad IV to 16 bytes448        iv = sample_info.iv + b"\x00" * (16 - len(sample_info.iv))449        cipher = AES.new(key, AES.MODE_CTR, initial_value=iv, nonce=b"")450 451        if not sample_info.sub_samples:452            # If there are no sub_samples, decrypt the entire sample453            return cipher.decrypt(sample)454 455        result = bytearray()456        offset = 0457        for clear_bytes, encrypted_bytes in sample_info.sub_samples:458            result.extend(sample[offset : offset + clear_bytes])459            offset += clear_bytes460            result.extend(cipher.decrypt(sample[offset : offset + encrypted_bytes]))461            offset += encrypted_bytes462 463        # If there's any remaining data, treat it as encrypted464        if offset < len(sample):465            result.extend(cipher.decrypt(sample[offset:]))466 467        return result468 469    def _process_trun(self, trun: MP4Atom) -> int:470        """471        Processes the 'trun' (Track Fragment Run) atom, which contains information about the samples in a track fragment.472        This includes sample sizes, durations, flags, and composition time offsets.473 474        Args:475            trun (MP4Atom): The 'trun' atom to process.476 477        Returns:478            int: The number of samples in the 'trun' atom.479        """480        trun_flags, sample_count = struct.unpack_from(">II", trun.data, 0)481        data_offset = 8482 483        if trun_flags & 0x000001:484            data_offset += 4485        if trun_flags & 0x000004:486            data_offset += 4487 488        self.trun_sample_sizes = array.array("I")489 490        for _ in range(sample_count):491            if trun_flags & 0x000100:  # sample-duration-present flag492                data_offset += 4493            if trun_flags & 0x000200:  # sample-size-present flag494                sample_size = struct.unpack_from(">I", trun.data, data_offset)[0]495                self.trun_sample_sizes.append(sample_size)496                data_offset += 4497            else:498                self.trun_sample_sizes.append(0)  # Using 0 instead of None for uniformity in the array499            if trun_flags & 0x000400:  # sample-flags-present flag500                data_offset += 4501            if trun_flags & 0x000800:  # sample-composition-time-offsets-present flag502                data_offset += 4503 504        return sample_count505 506    def _modify_trun(self, trun: MP4Atom) -> MP4Atom:507        """508        Modifies the 'trun' (Track Fragment Run) atom to update the data offset.509        This is necessary to account for the encryption overhead.510 511        Args:512            trun (MP4Atom): The 'trun' atom to modify.513 514        Returns:515            MP4Atom: Modified 'trun' atom with updated data offset.516        """517        trun_data = bytearray(trun.data)518        current_flags = struct.unpack_from(">I", trun_data, 0)[0] & 0xFFFFFF519 520        # If the data-offset-present flag is set, update the data offset to account for encryption overhead521        if current_flags & 0x000001:522            current_data_offset = struct.unpack_from(">i", trun_data, 8)[0]523            struct.pack_into(">i", trun_data, 8, current_data_offset - self.encryption_overhead)524 525        return MP4Atom(b"trun", len(trun_data) + 8, trun_data)526 527    def _process_sidx(self, sidx: MP4Atom) -> MP4Atom:528        """529        Processes the 'sidx' (Segment Index) atom, which contains indexing information for media segments.530        This includes references to media segments and their durations.531 532        Args:533            sidx (MP4Atom): The 'sidx' atom to process.534 535        Returns:536            MP4Atom: Processed 'sidx' atom with updated segment references.537        """538        sidx_data = bytearray(sidx.data)539 540        current_size = struct.unpack_from(">I", sidx_data, 32)[0]541        reference_type = current_size >> 31542        current_referenced_size = current_size & 0x7FFFFFFF543 544        # Remove encryption overhead from referenced size545        new_referenced_size = current_referenced_size - self.encryption_overhead546        new_size = (reference_type << 31) | new_referenced_size547        struct.pack_into(">I", sidx_data, 32, new_size)548 549        return MP4Atom(b"sidx", len(sidx_data) + 8, sidx_data)550 551    def _process_trak(self, trak: MP4Atom) -> MP4Atom:552        """553        Processes the 'trak' (Track) atom, which contains information about a single track in the movie.554        This includes track header, media information, and other track-level metadata.555 556        Args:557            trak (MP4Atom): The 'trak' atom to process.558 559        Returns:560            MP4Atom: Processed 'trak' atom with updated track information.561        """562        parser = MP4Parser(trak.data)563        new_trak_data = bytearray()564 565        for atom in iter(parser.read_atom, None):566            if atom.atom_type == b"mdia":567                new_mdia = self._process_mdia(atom)568                new_trak_data.extend(new_mdia.pack())569            else:570                new_trak_data.extend(atom.pack())571 572        return MP4Atom(b"trak", len(new_trak_data) + 8, new_trak_data)573 574    def _process_mdia(self, mdia: MP4Atom) -> MP4Atom:575        """576        Processes the 'mdia' (Media) atom, which contains media information for a track.577        This includes media header, handler reference, and media information container.578 579        Args:580            mdia (MP4Atom): The 'mdia' atom to process.581 582        Returns:583            MP4Atom: Processed 'mdia' atom with updated media information.584        """585        parser = MP4Parser(mdia.data)586        new_mdia_data = bytearray()587 588        for atom in iter(parser.read_atom, None):589            if atom.atom_type == b"minf":590                new_minf = self._process_minf(atom)591                new_mdia_data.extend(new_minf.pack())592            else:593                new_mdia_data.extend(atom.pack())594 595        return MP4Atom(b"mdia", len(new_mdia_data) + 8, new_mdia_data)596 597    def _process_minf(self, minf: MP4Atom) -> MP4Atom:598        """599        Processes the 'minf' (Media Information) atom, which contains information about the media data in a track.600        This includes data information, sample table, and other media-level metadata.601 602        Args:603            minf (MP4Atom): The 'minf' atom to process.604 605        Returns:606            MP4Atom: Processed 'minf' atom with updated media information.607        """608        parser = MP4Parser(minf.data)609        new_minf_data = bytearray()610 611        for atom in iter(parser.read_atom, None):612            if atom.atom_type == b"stbl":613                new_stbl = self._process_stbl(atom)614                new_minf_data.extend(new_stbl.pack())615            else:616                new_minf_data.extend(atom.pack())617 618        return MP4Atom(b"minf", len(new_minf_data) + 8, new_minf_data)619 620    def _process_stbl(self, stbl: MP4Atom) -> MP4Atom:621        """622        Processes the 'stbl' (Sample Table) atom, which contains information about the samples in a track.623        This includes sample descriptions, sample sizes, sample times, and other sample-level metadata.624 625        Args:626            stbl (MP4Atom): The 'stbl' atom to process.627 628        Returns:629            MP4Atom: Processed 'stbl' atom with updated sample information.630        """631        parser = MP4Parser(stbl.data)632        new_stbl_data = bytearray()633 634        for atom in iter(parser.read_atom, None):635            if atom.atom_type == b"stsd":636                new_stsd = self._process_stsd(atom)637                new_stbl_data.extend(new_stsd.pack())638            else:639                new_stbl_data.extend(atom.pack())640 641        return MP4Atom(b"stbl", len(new_stbl_data) + 8, new_stbl_data)642 643    def _process_stsd(self, stsd: MP4Atom) -> MP4Atom:644        """645        Processes the 'stsd' (Sample Description) atom, which contains descriptions of the sample entries in a track.646        This includes codec information, sample entry details, and other sample description metadata.647 648        Args:649            stsd (MP4Atom): The 'stsd' atom to process.650 651        Returns:652            MP4Atom: Processed 'stsd' atom with updated sample descriptions.653        """654        parser = MP4Parser(stsd.data)655        entry_count = struct.unpack_from(">I", parser.data, 4)[0]656        new_stsd_data = bytearray(stsd.data[:8])657 658        parser.position = 8  # Move past version_flags and entry_count659 660        for _ in range(entry_count):661            sample_entry = parser.read_atom()662            if not sample_entry:663                break664 665            processed_entry = self._process_sample_entry(sample_entry)666            new_stsd_data.extend(processed_entry.pack())667 668        return MP4Atom(b"stsd", len(new_stsd_data) + 8, new_stsd_data)669 670    def _process_sample_entry(self, entry: MP4Atom) -> MP4Atom:671        """672        Processes a sample entry atom, which contains information about a specific type of sample.673        This includes codec-specific information and other sample entry details.674 675        Args:676            entry (MP4Atom): The sample entry atom to process.677 678        Returns:679            MP4Atom: Processed sample entry atom with updated information.680        """681        # Determine the size of fixed fields based on sample entry type682        if entry.atom_type in {b"mp4a", b"enca"}:683            fixed_size = 28  # 8 bytes for size, type and reserved, 20 bytes for fixed fields in Audio Sample Entry.684        elif entry.atom_type in {b"mp4v", b"encv", b"avc1", b"hev1", b"hvc1"}:685            fixed_size = 78  # 8 bytes for size, type and reserved, 70 bytes for fixed fields in Video Sample Entry.686        else:687            fixed_size = 16  # 8 bytes for size, type and reserved, 8 bytes for fixed fields in other Sample Entries.688 689        new_entry_data = bytearray(entry.data[:fixed_size])690        parser = MP4Parser(entry.data[fixed_size:])691        codec_format = None692 693        for atom in iter(parser.read_atom, None):694            if atom.atom_type in {b"sinf", b"schi", b"tenc", b"schm"}:695                if atom.atom_type == b"sinf":696                    codec_format = self._extract_codec_format(atom)697                continue  # Skip encryption-related atoms698            new_entry_data.extend(atom.pack())699 700        # Replace the atom type with the extracted codec format701        new_type = codec_format if codec_format else entry.atom_type702        return MP4Atom(new_type, len(new_entry_data) + 8, new_entry_data)703 704    def _extract_codec_format(self, sinf: MP4Atom) -> Optional[bytes]:705        """706        Extracts the codec format from the 'sinf' (Protection Scheme Information) atom.707        This includes information about the original format of the protected content.708 709        Args:710            sinf (MP4Atom): The 'sinf' atom to extract from.711 712        Returns:713            Optional[bytes]: The codec format or None if not found.714        """715        parser = MP4Parser(sinf.data)716        for atom in iter(parser.read_atom, None):717            if atom.atom_type == b"frma":718                return atom.data719        return None720 721 722def decrypt_segment(init_segment: bytes, segment_content: bytes, key_id: str, key: str) -> bytes:723    """724    Decrypts a CENC encrypted MP4 segment.725 726    Args:727        init_segment (bytes): Initialization segment data.728        segment_content (bytes): Encrypted segment content.729        key_id (str): Key ID in hexadecimal format.730        key (str): Key in hexadecimal format.731    """732    key_map = {bytes.fromhex(key_id): bytes.fromhex(key)}733    decrypter = MP4Decrypter(key_map)734    decrypted_content = decrypter.decrypt_segment(init_segment + segment_content)735    return decrypted_content736 737 738def cli():739    """740    Command line interface for decrypting a CENC encrypted MP4 segment.741    """742    init_segment = b""743 744    if args.init and args.segment:745        with open(args.init, "rb") as f:746            init_segment = f.read()747        with open(args.segment, "rb") as f:748            segment_content = f.read()749    elif args.combined_segment:750        with open(args.combined_segment, "rb") as f:751            segment_content = f.read()752    else:753        print("Usage: python mp4decrypt.py --help")754        sys.exit(1)755 756    try:757        decrypted_segment = decrypt_segment(init_segment, segment_content, args.key_id, args.key)758        print(f"Decrypted content size is {len(decrypted_segment)} bytes")759        with open(args.output, "wb") as f:760            f.write(decrypted_segment)761        print(f"Decrypted segment written to {args.output}")762    except Exception as e:763        print(f"Error: {e}")764        sys.exit(1)765 766 767if __name__ == "__main__":768    arg_parser = argparse.ArgumentParser(description="Decrypts a MP4 init and media segment using CENC encryption.")769    arg_parser.add_argument("--init", help="Path to the init segment file", required=False)770    arg_parser.add_argument("--segment", help="Path to the media segment file", required=False)771    arg_parser.add_argument(772        "--combined_segment", help="Path to the combined init and media segment file", required=False773    )774    arg_parser.add_argument("--key_id", help="Key ID in hexadecimal format", required=True)775    arg_parser.add_argument("--key", help="Key in hexadecimal format", required=True)776    arg_parser.add_argument("--output", help="Path to the output file", required=True)777    args = arg_parser.parse_args()778    cli()