""" ISO base media file format / Apple Quicktime Movie parser. Documents: - Parsing and Writing QuickTime Files in Java (by Chris Adamson, 02/19/2003) http://www.onjava.com/pub/a/onjava/2003/02/19/qt_file_format.html - QuickTime File Format (official technical reference) http://developer.apple.com/documentation/QuickTime/QTFF/qtff.pdf - Apple QuickTime: http://wiki.multimedia.cx/index.php?title=Apple_QuickTime - File type (ftyp): http://www.ftyps.com/ - MPEG4 standard http://neuron2.net/library/avc/c041828_ISO_IEC_14496-12_2005%28E%29.pdf Author: Victor Stinner, Robert Xiao Creation: 2 august 2006 """ from hachoir_py3.parser import Parser from hachoir_py3.parser.common.win32 import GUID from hachoir_py3.field import (ParserError, FieldSet, MissingField, Enum, Bit, NullBits, Bits, UInt8, Int16, UInt16, UInt24, Int32, UInt32, Int64, UInt64, TimestampMac32, String, PascalString8, PascalString16, CString, RawBytes, NullBytes) from hachoir_py3.field.timestamp import timestampFactory from hachoir_py3.core.endian import BIG_ENDIAN from hachoir_py3.core.text_handler import textHandler from hachoir_py3.core.tools import MAC_TIMESTAMP_T0, timedelta # ISO/IEC 14496-1:2010 8.3.3 class InstanceLength(Bits): def __init__(self, parent, name, description=None): Bits.__init__(self, parent, name, 8, description) stream = self._parent.stream addr = self.absolute_address size = 8 byte = stream.readBits(addr, 8, BIG_ENDIAN) value = byte & 0x7F while byte & 0x80: addr += 8 size += 8 byte = stream.readBits(addr, 8, BIG_ENDIAN) value = (value << 7) + (byte & 0x7F) self._size = size self.createValue = lambda: value # ISO/IEC 14496-1:2010 7.2.6.5 ES_DescrTag = 0x03 def ESDescriptor(self): yield UInt16(self, "ES_ID") yield Bit(self, "streamDependenceFlag") yield Bit(self, "URL_Flag") yield Bit(self, "OCRstreamFlag") yield Bits(self, "streamPriority", 5) if self["streamDependenceFlag"].value: yield UInt16(self, "dependsOn_ES_ID") if self["URL_Flag"].value: yield PascalString8(self, "URL") if self["OCRstreamFlag"].value: yield UInt16(self, "OCR_ES_Id") yield Descriptor(self, "decConfigDescr", restrict=DecoderConfigDescriptor) # TODO while not self.eof: yield Descriptor(self, "descr[]") # ISO/IEC 14496-1:2010 7.2.6.6 DecoderConfigDescrTag = 0x04 def DecoderConfigDescriptor(self): yield UInt8(self, "objectTypeIndication") yield Bits(self, "streamType", 6) yield Bit(self, "upStream", 1) yield NullBits(self, "reserved", 1) yield UInt24(self, "bufferSizeDB") yield UInt32(self, "maxBitrate") yield UInt32(self, "avgBitrate") # TODO while not self.eof: yield Descriptor(self, "descr[]") # ISO/IEC 14496-1:2010 7.2.2.2 class Descriptor(FieldSet): # TODO: this is very annoying to represent without backtracking handlers = { DecoderConfigDescrTag: DecoderConfigDescriptor, ES_DescrTag: ESDescriptor, } def __init__(self, parent, name, description=None, restrict=None): FieldSet.__init__(self, parent, name, description) self.restrict = restrict field = self["sizeOfInstance"] self._size = field.address + field.size + field.value * 8 def createFields(self): yield UInt8(self, "tag") yield InstanceLength(self, "sizeOfInstance") handler = self.handlers.get(self["tag"].value) if self.restrict and handler != self.restrict: raise ParserError("invalid descriptor") if handler: yield from handler(self) else: yield RawBytes(self, "data", self["sizeOfInstance"].value) def timestampMac64(value): if not isinstance(value, (float, int)): raise TypeError("an integer or float is required") return MAC_TIMESTAMP_T0 + timedelta(seconds=value) TimestampMac64 = timestampFactory("TimestampMac64", timestampMac64, 64) def fixedFloatFactory(name, int_bits, float_bits, doc): size = int_bits + float_bits class Float(FieldSet): static_size = size __doc__ = doc def createFields(self): yield Bits(self, "int_part", int_bits) yield Bits(self, "float_part", float_bits) def createValue(self): return self["int_part"].value + float(self["float_part"].value) / (1 << float_bits) klass = Float klass.__name__ = name return klass QTFloat16 = fixedFloatFactory("QTFloat32", 8, 8, "8.8 fixed point number") QTFloat32 = fixedFloatFactory("QTFloat32", 16, 16, "16.16 fixed point number") QTFloat2_30 = fixedFloatFactory( "QTFloat2_30", 2, 30, "2.30 fixed point number") class AtomList(FieldSet): def createFields(self): while not self.eof: yield Atom(self, "atom[]") # ISO/IEC 14496-12:2012 8.3.2 class TrackHeader(FieldSet): def createFields(self): yield UInt8(self, "version", "Version (0 or 1)") yield NullBits(self, "flags", 20) yield Bit(self, "is_in_poster") yield Bit(self, "is_in_preview", "Is this track used when previewing the presentation?") yield Bit(self, "is_in_movie", "Is this track used in the presentation?") yield Bit(self, "is_enabled", "Is this track enabled?") if self['version'].value == 0: # 32-bit version yield TimestampMac32(self, "creation_date", "Creation time of this track") yield TimestampMac32(self, "lastmod_date", "Last modification time of this track") yield UInt32(self, "track_id", "Unique nonzero identifier of this track within the presentation") yield NullBytes(self, "reserved[]", 4) yield UInt32(self, "duration", "Length of track, in movie time-units") elif self['version'].value == 1: # 64-bit version yield TimestampMac64(self, "creation_date", "Creation time of this track") yield TimestampMac64(self, "lastmod_date", "Last modification time of this track") yield UInt32(self, "track_id", "Unique nonzero identifier of this track within the presentation") yield NullBytes(self, "reserved[]", 4) yield UInt64(self, "duration", "Length of track, in movie time-units") yield NullBytes(self, "reserved[]", 8) yield Int16(self, "video_layer", "Middle layer is 0; lower numbers are closer to the viewer") yield Int16(self, "alternate_group", "Group ID that this track belongs to (0=no group)") yield QTFloat16(self, "volume", "Track relative audio volume (1.0 = full)") yield NullBytes(self, "reserved[]", 2) yield QTFloat32(self, "geom_a", "Width scale") yield QTFloat32(self, "geom_b", "Width rotate") yield QTFloat2_30(self, "geom_u", "Width angle") yield QTFloat32(self, "geom_c", "Height rotate") yield QTFloat32(self, "geom_d", "Height scale") yield QTFloat2_30(self, "geom_v", "Height angle") yield QTFloat32(self, "geom_x", "Position X") yield QTFloat32(self, "geom_y", "Position Y") yield QTFloat2_30(self, "geom_w", "Divider scale") yield QTFloat32(self, "frame_size_width") yield QTFloat32(self, "frame_size_height") class TrackReferenceType(FieldSet): def createFields(self): while not self.eof: yield UInt32(self, "track_id[]", "Referenced track ID") class Handler(FieldSet): def createFields(self): yield UInt8(self, "version", "Version") yield NullBits(self, "flags", 24) yield String(self, "creator", 4) yield String(self, "subtype", 4) yield String(self, "manufacturer", 4) yield UInt32(self, "res_flags") yield UInt32(self, "res_flags_mask") if self.root.is_mpeg4: yield CString(self, "name", charset="UTF-8") else: yield PascalString8(self, "name") class LanguageCode(FieldSet): static_size = 16 MAC_LANG = { 0: 'English', 1: 'French', 2: 'German', 3: 'Italian', 4: 'Dutch', 5: 'Swedish', 6: 'Spanish', 7: 'Danish', 8: 'Portuguese', 9: 'Norwegian', 10: 'Hebrew', 11: 'Japanese', 12: 'Arabic', 13: 'Finnish', 14: 'Greek', 15: 'Icelandic', 16: 'Maltese', 17: 'Turkish', 18: 'Croatian', 19: 'Traditional Chinese', 20: 'Urdu', 21: 'Hindi', 22: 'Thai', 23: 'Korean', 24: 'Lithuanian', 25: 'Polish', 26: 'Hungarian', 27: 'Estonian', 28: 'Latvian', 29: 'Lappish', 30: 'Faeroese', 31: 'Farsi', 32: 'Russian', 33: 'Simplified Chinese', 34: 'Flemish', 35: 'Irish', 36: 'Albanian', 37: 'Romanian', 38: 'Czech', 39: 'Slovak', 40: 'Slovenian', 41: 'Yiddish', 42: 'Serbian', 43: 'Macedonian', 44: 'Bulgarian', 45: 'Ukrainian', 46: 'Byelorussian', 47: 'Uzbek', 48: 'Kazakh', 49: 'Azerbaijani', 50: 'AzerbaijanAr', 51: 'Armenian', 52: 'Georgian', 53: 'Moldavian', 54: 'Kirghiz', 55: 'Tajiki', 56: 'Turkmen', 57: 'Mongolian', 58: 'MongolianCyr', 59: 'Pashto', 60: 'Kurdish', 61: 'Kashmiri', 62: 'Sindhi', 63: 'Tibetan', 64: 'Nepali', 65: 'Sanskrit', 66: 'Marathi', 67: 'Bengali', 68: 'Assamese', 69: 'Gujarati', 70: 'Punjabi', 71: 'Oriya', 72: 'Malayalam', 73: 'Kannada', 74: 'Tamil', 75: 'Telugu', 76: 'Sinhalese', 77: 'Burmese', 78: 'Khmer', 79: 'Lao', 80: 'Vietnamese', 81: 'Indonesian', 82: 'Tagalog', 83: 'MalayRoman', 84: 'MalayArabic', 85: 'Amharic', 86: 'Tigrinya', 88: 'Somali', 89: 'Swahili', 90: 'Ruanda', 91: 'Rundi', 92: 'Chewa', 93: 'Malagasy', 94: 'Esperanto', 128: 'Welsh', 129: 'Basque', 130: 'Catalan', 131: 'Latin', 132: 'Quechua', 133: 'Guarani', 134: 'Aymara', 135: 'Tatar', 136: 'Uighur', 137: 'Dzongkha', 138: 'JavaneseRom', } def fieldHandler(self, field): if field.value == 0: return ' ' return chr(field.value + 0x60) def createFields(self): value = self.stream.readBits(self.absolute_address, 16, self.endian) if value < 1024: yield Enum(UInt16(self, "lang"), self.MAC_LANG) else: yield NullBits(self, "padding[]", 1) yield textHandler(Bits(self, "lang[0]", 5), self.fieldHandler) yield textHandler(Bits(self, "lang[1]", 5), self.fieldHandler) yield textHandler(Bits(self, "lang[2]", 5), self.fieldHandler) def createValue(self): if 'lang' in self: return self['lang'].display return self['lang[0]'].display + self['lang[1]'].display + self['lang[2]'].display class MediaHeader(FieldSet): def createFields(self): yield UInt8(self, "version", "Version (0 or 1)") yield NullBits(self, "flags", 24) if self['version'].value == 0: # 32-bit version yield TimestampMac32(self, "creation_date", "Creation time of this media") yield TimestampMac32(self, "lastmod_date", "Last modification time of this media") yield UInt32(self, "time_scale", "Number of time-units per second") yield UInt32(self, "duration", "Length of media, in time-units") elif self['version'].value == 1: # 64-bit version yield TimestampMac64(self, "creation_date", "Creation time of this media") yield TimestampMac64(self, "lastmod_date", "Last modification time of this media") yield UInt32(self, "time_scale", "Number of time-units per second") yield UInt64(self, "duration", "Length of media, in time-units") yield LanguageCode(self, "language") yield Int16(self, "quality") class VideoMediaHeader(FieldSet): GRAPHICSMODE = { 0: ('Copy', "Copy the source image over the destination"), 0x20: ('Blend', "Blend of source and destination; blending factor is controlled by op color"), 0x24: ('Transparent', "Replace destination pixel with source pixel if the source pixel is not the op color"), 0x40: ('Dither copy', "Dither image if necessary, else copy"), 0x100: ('Straight alpha', "Blend of source and destination; blending factor is controlled by alpha channel"), 0x101: ('Premul white alpha', "Remove white from each pixel and blend"), 0x102: ('Premul black alpha', "Remove black from each pixel and blend"), 0x103: ('Composition', "Track drawn offscreen and dither copied onto screen"), 0x104: ('Straight alpha blend', "Blend of source and destination; blending factor is controlled by combining alpha channel and op color") } def graphicsDisplay(self, field): if field.value in self.GRAPHICSMODE: return self.GRAPHICSMODE[field.value][0] return hex(field.value) def graphicsDescription(self, field): if field.value in self.GRAPHICSMODE: return self.GRAPHICSMODE[field.value][1] return "" def createFields(self): yield UInt8(self, "version", "Version") yield Bits(self, "flags", 24, "Flags (=1)") graphics = UInt16(self, "graphicsmode") graphics.createDisplay = lambda: self.graphicsDisplay(graphics) graphics.createDescription = lambda: self.graphicsDescription(graphics) yield graphics yield UInt16(self, "op_red", "Red value for graphics mode") yield UInt16(self, "op_green", "Green value for graphics mode") yield UInt16(self, "op_blue", "Blue value for graphics mode") class SoundMediaHeader(FieldSet): def createFields(self): yield UInt8(self, "version", "Version") yield NullBits(self, "flags", 24) yield QTFloat16(self, "balance") yield UInt16(self, "reserved[]") class HintMediaHeader(FieldSet): def createFields(self): yield UInt8(self, "version", "Version") yield NullBits(self, "flags", 24) yield UInt16(self, "max_pdu_size") yield UInt16(self, "avg_pdu_size") yield UInt32(self, "max_bit_rate") yield UInt32(self, "avg_bit_rate") yield UInt32(self, "reserved[]") # ISO/IEC 14496-12:2012 8.7.2 class DataEntryUrl(FieldSet): def createFields(self): yield UInt8(self, "version", "Version") yield NullBits(self, "flags", 23) yield Bit(self, "is_same_file", "Is the reference to this file?") if not self['is_same_file'].value: yield CString(self, "location") class DataEntryUrn(FieldSet): def createFields(self): yield UInt8(self, "version", "Version") yield NullBits(self, "flags", 23) yield Bit(self, "is_same_file", "Is the reference to this file?") if not self['is_same_file'].value: yield CString(self, "name") yield CString(self, "location") class DataReference(FieldSet): def createFields(self): yield UInt8(self, "version", "Version") yield NullBits(self, "flags", 24) yield UInt32(self, "count") for i in range(self['count'].value): yield Atom(self, "atom[]") # ISO/IEC 14496-12:2012 8.6.5 class EditList(FieldSet): def createFields(self): yield UInt8(self, "version", "Version (0 or 1)") yield NullBits(self, "flags", 24) yield UInt32(self, "count") version = self['version'].value if version == 0: UInt, Int = UInt32, Int32 elif version == 1: UInt, Int = UInt64, Int64 else: raise ParserError("elst version %d not supported" % version) for i in range(self['count'].value): yield UInt(self, "duration[]", "Duration of this edit segment") yield Int(self, "time[]", "Starting time of this edit segment within the media (-1 = empty edit)") yield QTFloat32(self, "play_speed[]", "Playback rate (0 = dwell edit, 1 = normal playback)") class Load(FieldSet): def createFields(self): yield UInt32(self, "start") yield UInt32(self, "length") # PreloadAlways = 1 or TrackEnabledPreload = 2 yield UInt32(self, "flags") # KeepInBuffer = 0x00000004; HighQuality = 0x00000100; SingleFieldVideo = 0x00100000 yield UInt32(self, "hints") # ISO/IEC 14496-12:2012 8.2.2 class MovieHeader(FieldSet): def createFields(self): yield UInt8(self, "version", "Version (0 or 1)") yield NullBits(self, "flags", 24) if self['version'].value == 0: # 32-bit version yield TimestampMac32(self, "creation_date", "Creation time of this presentation") yield TimestampMac32(self, "lastmod_date", "Last modification time of this presentation") yield UInt32(self, "time_scale", "Number of time-units per second") yield UInt32(self, "duration", "Length of presentation, in time-units") elif self['version'].value == 1: # 64-bit version yield TimestampMac64(self, "creation_date", "Creation time of this presentation") yield TimestampMac64(self, "lastmod_date", "Last modification time of this presentation") yield UInt32(self, "time_scale", "Number of time-units per second") yield UInt64(self, "duration", "Length of presentation, in time-units") yield QTFloat32(self, "play_speed", "Preferred playback speed (1.0 = normal)") yield QTFloat16(self, "volume", "Preferred playback volume (1.0 = full)") yield NullBytes(self, "reserved[]", 10) yield QTFloat32(self, "geom_a", "Width scale") yield QTFloat32(self, "geom_b", "Width rotate") yield QTFloat2_30(self, "geom_u", "Width angle") yield QTFloat32(self, "geom_c", "Height rotate") yield QTFloat32(self, "geom_d", "Height scale") yield QTFloat2_30(self, "geom_v", "Height angle") yield QTFloat32(self, "geom_x", "Position X") yield QTFloat32(self, "geom_y", "Position Y") yield QTFloat2_30(self, "geom_w", "Divider scale") yield UInt32(self, "preview_start") yield UInt32(self, "preview_length") yield UInt32(self, "still_poster") yield UInt32(self, "sel_start") yield UInt32(self, "sel_length") yield UInt32(self, "current_time") yield UInt32(self, "next_track_ID", "Value to use as the track ID for the next track added") # ISO/IEC 14496-12:2012 4.3 class FileType(FieldSet): def createFields(self): yield String(self, "brand", 4, "Major brand") yield UInt32(self, "version", "Version") while not self.eof: yield String(self, "compat_brand[]", 4, "Compatible brand") # ISO/IEC 14496-12:2012 8.8.5 class MovieFragmentHeader(FieldSet): def createFields(self): yield UInt8(self, "version", "Version") yield NullBits(self, "flags", 24) yield UInt32(self, "sequence_number") # ISO/IEC 14496-12:2012 8.8.7 class TrackFragmentHeaderBox(FieldSet): def createFields(self): yield UInt8(self, "version", "Version") yield UInt24(self, "flags") flags = self["flags"].value yield UInt32(self, "track_ID") if flags & 0x1: yield UInt64(self, "base_data_offset") if flags & 0x2: yield UInt32(self, "sample_description_index") if flags & 0x8: yield UInt32(self, "default_sample_duration") if flags & 0x10: yield UInt32(self, "default_sample_size") if flags & 0x20: yield UInt32(self, "default_sample_flags") # ISO/IEC 14496-12:2012 8.8.8 class TrackRunSample(FieldSet): def createFields(self): flags = self["../flags"].value if flags & 0x100: yield UInt32(self, "sample_duration") if flags & 0x200: yield UInt32(self, "sample_size") if flags & 0x400: yield UInt32(self, "sample_flags") if flags & 0x800: yield UInt32(self, "sample_composition_time_offset") class TrackRunBox(FieldSet): def createFields(self): yield UInt8(self, "version", "Version") yield UInt24(self, "flags") flags = self["flags"].value yield UInt32(self, "sample_count") if flags & 0x1: yield UInt32(self, "data_offset") if flags & 0x4: yield UInt32(self, "first_sample_flags") for i in range(self["sample_count"].value): yield TrackRunSample(self, "sample[]") # ISO/IEC 14496-12:2012 8.8.10 class TrackFragmentRandomAccess(FieldSet): def createFields(self): yield UInt8(self, "version", "Version") yield NullBits(self, "flags", 24) yield UInt32(self, "track_id") yield NullBits(self, "reserved", 26) yield Bits(self, "length_size_of_traf_num", 2) yield Bits(self, "length_size_of_trun_num", 2) yield Bits(self, "length_size_of_sample_num", 2) yield UInt32(self, "number_of_entry") for i in range(self['number_of_entry'].value): if self['version'].value == 1: yield UInt64(self, "time[%i]" % i) yield UInt64(self, "moof_offset[%i]" % i) else: yield UInt32(self, "time[%i]" % i) yield UInt32(self, "moof_offset[%i]" % i) if self['length_size_of_traf_num'].value == 3: yield UInt64(self, "traf_number[%i]" % i) elif self['length_size_of_traf_num'].value == 2: yield UInt32(self, "traf_number[%i]" % i) elif self['length_size_of_traf_num'].value == 1: yield UInt16(self, "traf_number[%i]" % i) else: yield UInt8(self, "traf_number[%i]" % i) if self['length_size_of_trun_num'].value == 3: yield UInt64(self, "trun_number[%i]" % i) elif self['length_size_of_trun_num'].value == 2: yield UInt32(self, "trun_number[%i]" % i) elif self['length_size_of_trun_num'].value == 1: yield UInt16(self, "trun_number[%i]" % i) else: yield UInt8(self, "trun_number[%i]" % i) if self['length_size_of_sample_num'].value == 3: yield UInt64(self, "sample_number[%i]" % i) elif self['length_size_of_sample_num'].value == 2: yield UInt32(self, "sample_number[%i]" % i) elif self['length_size_of_sample_num'].value == 1: yield UInt16(self, "sample_number[%i]" % i) else: yield UInt8(self, "sample_number[%i]" % i) # ISO/IEC 14496-12:2012 8.8.11 class MovieFragmentRandomAccessOffset(FieldSet): def createFields(self): yield UInt8(self, "version", "Version") yield NullBits(self, "flags", 24) yield UInt32(self, "size") def findHandler(self): ''' find the handler corresponding to this fieldset ''' while self: if self.name in ('media', 'tags'): break self = self.parent else: return None for atom in self: if atom['tag'].value == 'hdlr': return atom['hdlr'] return None class METATAG(FieldSet): def createFields(self): yield UInt8(self, "unk[]", "0x80 or 0x00") yield PascalString16(self, "tag_name", charset='UTF-8') yield UInt16(self, "unk[]", "0x0001") yield UInt16(self, "unk[]", "0x0000") yield PascalString16(self, "tag_value", charset='UTF-8') class META(FieldSet): def createFields(self): # This tag has too many variant forms. if '/tags/' in self.path: yield UInt32(self, "count") for i in range(self['count'].value): yield METATAG(self, "tag[]") elif self.stream.readBits(self.absolute_address, 32, self.endian) == 0: yield UInt8(self, "version") yield Bits(self, "flags", 24) yield AtomList(self, "tags") else: yield AtomList(self, "tags") class Item(FieldSet): def createFields(self): yield UInt32(self, "size") yield UInt32(self, "index") yield Atom(self, "value") class KeyList(FieldSet): def createFields(self): yield UInt8(self, "version") yield NullBits(self, "flags", 24) yield UInt32(self, "count") for i in range(self['count'].value): yield Atom(self, "key[]") class ItemList(FieldSet): def createFields(self): handler = findHandler(self) if handler is None: raise ParserError("ilst couldn't find metadata handler") if handler['subtype'].value == 'mdir': while not self.eof: yield Atom(self, "atom[]") elif handler['subtype'].value == 'mdta': while not self.eof: yield Item(self, "item[]") class NeroChapters(FieldSet): def createFields(self): yield UInt8(self, "version") yield NullBits(self, "flags", 24) yield UInt32(self, "unknown") yield UInt8(self, "count", description="Number of chapters") for i in range(self['count'].value): yield UInt64(self, "chapter_start[]") yield PascalString8(self, "chapter_name[]", charset='UTF-8') # ISO/IEC 14496-12:2012 8.6.1.2 class SampleDecodeTimeTable(FieldSet): def createFields(self): yield UInt8(self, "version") yield NullBits(self, "flags", 24) yield UInt32(self, "count", description="Total entries in sample time table") for i in range(self['count'].value): yield UInt32(self, "sample_count[]", "Number of consecutive samples with this delta") yield UInt32(self, "sample_delta[]", "Decode time delta since last sample, in time-units") class SampleCompositionTimeTable(FieldSet): def createFields(self): yield UInt8(self, "version") yield NullBits(self, "flags", 24) yield UInt32(self, "count", description="Total entries in sample time table") for i in range(self['count'].value): yield UInt32(self, "sample_count[]", "Number of consecutive samples with this offset") yield UInt32(self, "sample_offset[]", "Difference between decode time and composition time of this sample, in time-units") class ChunkOffsetTable(FieldSet): def createFields(self): yield UInt8(self, "version") yield NullBits(self, "flags", 24) yield UInt32(self, "count", description="Total entries in offset table") for i in range(self['count'].value): yield UInt32(self, "chunk_offset[]") class ChunkOffsetTable64(FieldSet): def createFields(self): yield UInt8(self, "version") yield NullBits(self, "flags", 24) yield UInt32(self, "count", description="Total entries in offset table") for i in range(self['count'].value): yield UInt64(self, "chunk_offset[]") # ISO/IEC 14496-14:2003 5.6 class ESDBox(FieldSet): def createFields(self): yield UInt8(self, "version", "Version") yield NullBits(self, "flags", 24) yield Descriptor(self, "ES", restrict=ESDescriptor) # ETSI TS 102 366 v1.2.1 F.6 class EC3SpecificBoxSubstream(FieldSet): def createFields(self): yield Bits(self, "fscod", 2) yield Bits(self, "bsid", 5) yield Bits(self, "bsmod", 5) yield Bits(self, "acmod", 3) yield Bits(self, "lfeon", 1) yield NullBits(self, "reserved", 3) yield Bits(self, "num_dep_sub", 4) if self["num_dep_sub"].value: yield Bits(self, "chan_loc", 9) else: yield NullBits(self, "reserved2", 1) class EC3SpecificBox(FieldSet): def createFields(self): yield Bits(self, "data_rate", 13) yield Bits(self, "num_ind_sub", 3) for i in range(self["num_ind_sub"].value + 1): yield EC3SpecificBoxSubstream(self, "sub[]") # ISO/IEC 14496-15:2014 5.3.3.1 class AVCDecoderConfigurationRecord(FieldSet): def createFields(self): yield UInt8(self, "configurationVersion") yield UInt8(self, "AVCProfileIndication") yield UInt8(self, "profile_compatibility") yield UInt8(self, "AVCLevelIndication") yield NullBits(self, "reserved[]", 6) yield Bits(self, "lengthSizeMinusOne", 2) yield NullBits(self, "reserved[]", 3) yield Bits(self, "numOfSequenceParameterSets", 5) for i in range(self["numOfSequenceParameterSets"].value): yield PascalString16(self, "sequenceParameterSetNALUnit[]") yield UInt8(self, "numOfPictureParameterSets") for i in range(self["numOfPictureParameterSets"].value): yield PascalString16(self, "pictureParameterSetNALUnit[]") if self['AVCProfileIndication'].value in (100, 110, 122, 144) and not self.eof: yield NullBits(self, "reserved[]", 6) yield Bits(self, "chroma_format", 2) yield NullBits(self, "reserved[]", 5) yield Bits(self, "bit_depth_luma_minus8", 3) yield NullBits(self, "reserved[]", 5) yield Bits(self, "bit_depth_chroma_minus8", 3) yield UInt8(self, "numOfSequenceParameterSetExt") for i in range(self["numOfSequenceParameterSetExt"].value): yield PascalString16(self, "sequenceParameterSetExtNALUnit[]") # ISO/IEC 14496-15:2014 5.4.2.1 class MPEG4BitRateBox(FieldSet): def createFields(self): yield UInt32(self, "bufferSizeDB") yield UInt32(self, "maxBitrate") yield UInt32(self, "avgBitrate") class AVCConfigurationBox(FieldSet): def createFields(self): yield AVCDecoderConfigurationRecord(self, "AVCConfig") # ISO/IEC 14496-12:2012 8.5.2.2 def VisualSampleEntry(self): yield UInt16(self, "version") yield UInt16(self, "revision_level") yield RawBytes(self, "vendor_id", 4) yield UInt32(self, "temporal_quality") yield UInt32(self, "spatial_quality") yield UInt16(self, "width", "Width (pixels)") yield UInt16(self, "height", "Height (pixels)") yield QTFloat32(self, "horizontal_resolution", "Horizontal resolution in DPI") yield QTFloat32(self, "vertical resolution", "Vertical resolution in DPI") yield UInt32(self, "data_size") yield UInt16(self, "frame_count") yield UInt8(self, "compressor_name_length") yield String(self, "compressor_name", 31, strip='\0') yield UInt16(self, "depth", "Bit depth of image") yield Int16(self, "unknown") def AudioSampleEntry(self): yield NullBytes(self, "reserved[]", 8) yield UInt16(self, "channels", "Number of audio channels") yield UInt16(self, "samplesize", "Sample size in bits") yield UInt16(self, "unknown") yield NullBytes(self, "reserved[]", 2) yield QTFloat32(self, "samplerate", "Sample rate in Hz") class SampleEntry(FieldSet): def createFields(self): yield UInt32(self, "size") yield RawBytes(self, "format", 4, "Data Format (codec)") yield NullBytes(self, "reserved[]", 6, "Reserved") yield UInt16(self, "data_reference_index") handler = findHandler(self) if not handler: raise ParserError("stsd couldn't find track handler") if handler['subtype'].value == 'soun': yield from AudioSampleEntry(self) elif handler['subtype'].value == 'vide': yield from VisualSampleEntry(self) elif handler['subtype'].value == 'hint': # Hint sample entry pass if self["format"].value in (b"enca", b"encv", b"mp4a", b"mp4v", b"mp4s", b"avc1"): # MP4VisualSampleEntry, MP4AudioSampleEntry, MpegSampleEntry, EC3SampleEntry, AVCSampleEntry... # all just have appended atoms while not self.eof: yield Atom(self, "atom[]") else: size = self['size'].value - self.current_size // 8 if size > 0: yield RawBytes(self, "extra_data", size) # ISO/IEC 14496-12:2012 8.5.2 class SampleDescription(FieldSet): def createFields(self): yield UInt8(self, "version") yield NullBits(self, "flags", 24) yield UInt32(self, "count", description="Total entries in table") for i in range(self['count'].value): yield SampleEntry(self, "sample_entry[]") # ISO/IEC 14496-12:2012 8.6.2 class SyncSampleTable(FieldSet): def createFields(self): yield UInt8(self, "version") yield NullBits(self, "flags", 24) yield UInt32(self, "count", description="Number of sync samples") for i in range(self['count'].value): yield UInt32(self, "sample_number[]") class SampleSizeTable(FieldSet): def createFields(self): yield UInt8(self, "version") yield NullBits(self, "flags", 24) yield UInt32(self, "uniform_size", description="Uniform size of each sample (0 if non-uniform)") yield UInt32(self, "count", description="Number of samples") if self['uniform_size'].value == 0: for i in range(self['count'].value): yield UInt32(self, "sample_size[]") class CompactSampleSizeTable(FieldSet): def createFields(self): yield UInt8(self, "version") yield NullBits(self, "flags", 24) yield NullBits(self, "reserved[]", 24) yield UInt8(self, "field_size", "Size of each entry in this table, in bits") yield UInt32(self, "count", description="Number of samples") bitsize = self['field_size'].value for i in range(self['count'].value): yield Bits(self, "sample_size[]", bitsize) if self.current_size % 8 != 0: yield NullBits(self, "padding[]", 8 - (self.current_size % 8)) # ISO/IEC 14496-12:2012 8.7.4 class SampleToChunkTable(FieldSet): def createFields(self): yield UInt8(self, "version") yield NullBits(self, "flags", 24) yield UInt32(self, "count", description="Number of samples") for i in range(self['count'].value): yield UInt32(self, "first_chunk[]") yield UInt32(self, "samples_per_chunk[]") yield UInt32(self, "sample_description_index[]") # ISO/IEC 14496-12:2012 8.12.1 class ProtectionSchemeInfoBox(FieldSet): def createFields(self): yield Atom(self, "original_format") if not self.eof: yield Atom(self, "scheme_type_box") if not self.eof: yield Atom(self, "info") # ISO/IEC 14496-12:2012 8.12.2 class OriginalFormatBox(FieldSet): def createFields(self): yield RawBytes(self, "data_format", 4) # ISO/IEC 14496-12:2012 8.12.5 class SchemeTypeBox(FieldSet): def createFields(self): yield UInt8(self, "version") yield UInt24(self, "flags") yield RawBytes(self, "scheme_type", 4) yield UInt32(self, "scheme_version") if self["flags"].value & 0x1: yield CString(self, "scheme_uri") # ISO/IEC 14496-12:2012 8.12.6 class SchemeInformationBox(FieldSet): def createFields(self): yield Atom(self, "scheme_specific_data") # ISO/IEC 14496-12:2012 8.16.3 class SegmentIndexBoxReference(FieldSet): def createFields(self): yield Bit(self, "reference_type") yield Bits(self, "referenced_size", 31) yield UInt32(self, "subsegment_duration") yield Bit(self, "starts_with_SAP") yield Bits(self, "SAP_type", 3) yield Bits(self, "SAP_delta_time", 28) class SegmentIndexBox(FieldSet): def createFields(self): yield UInt8(self, "version") yield NullBits(self, "flags", 24) yield UInt32(self, "reference_ID") yield UInt32(self, "timescale") if self["version"].value == 0: yield UInt32(self, "earliest_presentation_time") yield UInt32(self, "first_offset") else: yield UInt64(self, "earliest_presentation_time") yield UInt64(self, "first_offset") yield NullBits(self, "reserved", 16) yield UInt16(self, "reference_count") for i in range(self["reference_count"].value): yield SegmentIndexBoxReference(self, "reference[]") # ISO/IEC 23001-7:2016 7.2 class SampleEncryptionItem(FieldSet): def createFields(self): yield RawBytes(self, "IV", 8) # TODO: Per_Sample_IV_Size if self["../flags"].value & 0x2: yield UInt16(self, "subsample_count") for i in range(self["subsample_count"].value): yield UInt16(self, "BytesOfClearData[]") yield UInt32(self, "ByteOfProtectedData[]") class SampleEncryptionBox(FieldSet): def createFields(self): yield UInt8(self, "version") yield UInt24(self, "flags") yield UInt32(self, "sample_count") for i in range(self["sample_count"].value): yield SampleEncryptionItem(self, "sample[]") # ISO/IEC 23001-7:2016 8.1.1 class ProtectionSystemSpecificHeaderBox(FieldSet): def createFields(self): yield UInt8(self, "version") yield NullBits(self, "flags", 24) yield RawBytes(self, "SystemID", 16) if self["version"].value > 0: yield UInt32(self, "KID_Count") for i in range(self["KID_Count"].value): yield RawBytes(self, "KID[]", 16) yield UInt32(self, "DataSize") yield RawBytes(self, "Data", self["DataSize"].value) # ISO/IEC 23001-7:2016 8.2 class TrackEncryptionBox(FieldSet): def createFields(self): yield UInt8(self, "version") yield NullBits(self, "flags", 24) yield NullBits(self, "reserved", 8) if self["version"].value == 0: yield NullBits(self, "reserved2", 8) else: yield Bits(self, "default_crypt_byte_block", 4) yield Bits(self, "default_skip_byte_block", 4) yield UInt8(self, "default_isProtected") yield UInt8(self, "default_Per_sample_IV_Size") yield RawBytes(self, "default_KID", 16) if self["default_isProtected"].value == 1 and self["default_Per_sample_IV_Size"].value == 0: yield UInt8(self, "default_constant_IV_size") yield RawBytes(self, "default_constant_IV", self["default_constant_IV_size"].value) class Atom(FieldSet): tag_info = { "ftyp": (FileType, "file_type", "File type and compatibility"), # pdin: progressive download information # pnot: movie preview (old QT spec) "moov": (AtomList, "movie", "Container for all metadata"), "mvhd": (MovieHeader, "movie_hdr", "Movie header, overall declarations"), # clip: movie clipping (old QT spec) # crgn: movie clipping region (old QT spec) "trak": (AtomList, "track", "Container for an individual track or stream"), "tkhd": (TrackHeader, "track_hdr", "Track header, overall information about the track"), # matt: track matte (old QT spec) # kmat: compressed matte (old QT spec) "tref": (AtomList, "tref", "Track reference container"), "hint": (TrackReferenceType, "hint", "Original media track(s) for this hint track"), "cdsc": (TrackReferenceType, "cdsc", "Reference to track described by this track"), "edts": (AtomList, "edts", "Edit list container"), "elst": (EditList, "elst", "Edit list"), "load": (Load, "load", "Track loading settings (old QT spec)"), # imap: Track input map (old QT spec) "senc": (SampleEncryptionBox, "senc", "Sample encryption information"), "mdia": (AtomList, "media", "Container for the media information in a track"), "mdhd": (MediaHeader, "media_hdr", "Media header, overall information about the media"), "hdlr": (Handler, "hdlr", "Handler, declares the media or metadata (handler) type"), "minf": (AtomList, "minf", "Media information container"), "vmhd": (VideoMediaHeader, "vmhd", "Video media header, overall information (video track only)"), "smhd": (SoundMediaHeader, "smhd", "Sound media header, overall information (sound track only)"), "hmhd": (HintMediaHeader, "hmhd", "Hint media header, overall information (hint track only)"), # nmhd: Null media header, overall information (some tracks only) (unparsed) "dinf": (AtomList, "dinf", "Data information, container"), "dref": (DataReference, "dref", "Data reference, declares source(s) of media data in track"), "url ": (DataEntryUrl, "url", "URL data reference"), "urn ": (DataEntryUrn, "urn", "URN data reference"), "stbl": (AtomList, "stbl", "Sample table, container for the time/space map"), "stsd": (SampleDescription, "stsd", "Sample descriptions (codec types, initialization etc.)"), "esds": (ESDBox, "esds", "Elementary stream descriptor"), "avcC": (AVCConfigurationBox, "avcC", "AVC configuration"), "btrt": (MPEG4BitRateBox, "btrt", "AVC stream bitrate"), "dec3": (EC3SpecificBox, "dec3", "Enhanced AC-3 speicifc information"), "sinf": (ProtectionSchemeInfoBox, "sinf", "Protection scheme information"), "frma": (OriginalFormatBox, "frma", "original format"), "schm": (SchemeTypeBox, "schm", "scheme type"), "schi": (SchemeInformationBox, "schi", "scheme information"), "tenc": (TrackEncryptionBox, "tenc", "track encryption"), "stts": (SampleDecodeTimeTable, "stts", "decoding time-to-sample delta table"), "ctts": (SampleCompositionTimeTable, "ctts", "composition time-to-sample offset table"), "stsc": (SampleToChunkTable, "stsc", "sample-to-chunk, partial data-offset information"), "stsz": (SampleSizeTable, "stsz", "Sample size table (framing)"), "stz2": (CompactSampleSizeTable, "stz2", "Compact sample size table (framing)"), "stco": (ChunkOffsetTable, "stco", "Chunk offset, partial data-offset information"), "co64": (ChunkOffsetTable64, "co64", "64-bit chunk offset"), "stss": (SyncSampleTable, "stss", "Sync sample table (random access points)"), # stsh: shadow sync sample table # padb: sample padding bits # stdp: sample degradation priority # sdtp: independent and disposable samples # sbgp: sample-to-group # sgpd: sample group description # subs: sub-sample information # ctab color table (old QT spec) # mvex: movie extends # mehd: movie extends header # trex: track extends defaults # ipmc: IPMP control "pssh": (ProtectionSystemSpecificHeaderBox, "pssh", "Protection system information"), "moof": (AtomList, "moof", "movie fragment"), "mfhd": (MovieFragmentHeader, "mfhd", "movie fragment header"), "traf": (AtomList, "traf", "track fragment"), "tfhd": (TrackFragmentHeaderBox, "tfgd", "track fragment header"), "trun": (TrackRunBox, "trun", "track fragment run"), # sdtp: independent and disposable samples # sbgp: sample-to-group # subs: sub-sample information "mfra": (AtomList, "mfra", "movie fragment random access"), "tfra": (TrackFragmentRandomAccess, "tfra", "track fragment random access"), "mfro": (MovieFragmentRandomAccessOffset, "mfro", "movie fragment random access offset"), "sidx": (SegmentIndexBox, "sidx", "segment index"), # mdat: media data container # free: free space (unparsed) # skip: free space (unparsed) "udta": (AtomList, "udta", "User data"), "meta": (META, "meta", "File metadata"), "keys": (KeyList, "keys", "Metadata keys"), # hdlr # dinf # dref: data reference, declares source(s) of metadata items # ipmc: IPMP control # iloc: item location # ipro: item protection # sinf: protection scheme information # frma: original format # imif: IPMP information # schm: scheme type # schi: scheme information # iinf: item information # xml : XML container # bxml: binary XML container # pitm: primary item reference # other tags "ilst": (ItemList, "ilst", "Item list"), "trkn": (AtomList, "trkn", "Metadata: Track number"), "disk": (AtomList, "disk", "Metadata: Disk number"), "tmpo": (AtomList, "tempo", "Metadata: Tempo"), "cpil": (AtomList, "cpil", "Metadata: Compilation"), "gnre": (AtomList, "gnre", "Metadata: Genre"), "\xa9cpy": (AtomList, "copyright", "Metadata: Copyright statement"), "\xa9day": (AtomList, "date", "Metadata: Date of content creation"), "\xa9dir": (AtomList, "director", "Metadata: Movie director"), "\xa9ed1": (AtomList, "edit1", "Metadata: Edit date and description (1)"), "\xa9ed2": (AtomList, "edit2", "Metadata: Edit date and description (2)"), "\xa9ed3": (AtomList, "edit3", "Metadata: Edit date and description (3)"), "\xa9ed4": (AtomList, "edit4", "Metadata: Edit date and description (4)"), "\xa9ed5": (AtomList, "edit5", "Metadata: Edit date and description (5)"), "\xa9ed6": (AtomList, "edit6", "Metadata: Edit date and description (6)"), "\xa9ed7": (AtomList, "edit7", "Metadata: Edit date and description (7)"), "\xa9ed8": (AtomList, "edit8", "Metadata: Edit date and description (8)"), "\xa9ed9": (AtomList, "edit9", "Metadata: Edit date and description (9)"), "\xa9fmt": (AtomList, "format", "Metadata: Movie format (CGI, digitized, etc.)"), "\xa9inf": (AtomList, "info", "Metadata: Information about the movie"), "\xa9prd": (AtomList, "producer", "Metadata: Movie producer"), "\xa9prf": (AtomList, "performers", "Metadata: Performer names"), "\xa9req": (AtomList, "requirements", "Metadata: Special hardware and software requirements"), "\xa9src": (AtomList, "source", "Metadata: Credits for those who provided movie source content"), "\xa9nam": (AtomList, "name", "Metadata: Name of song or video"), "\xa9des": (AtomList, "description", "Metadata: File description"), "\xa9cmt": (AtomList, "comment", "Metadata: General comment"), "\xa9alb": (AtomList, "album", "Metadata: Album name"), "\xa9gen": (AtomList, "genre", "Metadata: Custom genre"), "\xa9ART": (AtomList, "artist", "Metadata: Artist name"), "\xa9too": (AtomList, "encoder", "Metadata: Encoder"), "\xa9wrt": (AtomList, "writer", "Metadata: Writer"), "covr": (AtomList, "cover", "Metadata: Cover art"), "----": (AtomList, "misc", "Metadata: Miscellaneous"), "tags": (AtomList, "tags", "File tags"), "tseg": (AtomList, "tseg", "tseg"), "chpl": (NeroChapters, "chpl", "Nero chapter data"), } # noqa tag_handler = [item[0] for item in tag_info] tag_desc = [item[1] for item in tag_info] def createFields(self): yield UInt32(self, "size") yield String(self, "tag", 4, charset="ASCII") size = self["size"].value if size == 1: # 64-bit size yield UInt64(self, "size64") size = self["size64"].value - 16 elif size == 0: # Unbounded atom if self._size is None: size = (self.parent.size - self.parent.current_size) // 8 - 8 else: size = (self.size - self.current_size) // 8 else: size = size - 8 if self['tag'].value == 'uuid': yield GUID(self, "usertag") tag = self["usertag"].value size -= 16 else: tag = self["tag"].value if size > 0: if tag in self.tag_info: handler, name, desc = self.tag_info[tag] yield handler(self, name, desc, size=size * 8) else: yield RawBytes(self, "data", size) def createDescription(self): if self["tag"].value == "uuid": return "Atom: uuid: " + self["usertag"].value return "Atom: %s" % self["tag"].value class MP4File(Parser): PARSER_TAGS = { "id": "mov", "category": "video", "file_ext": ("mov", "qt", "mp4", "m4v", "m4a", "m4p", "m4b"), "mime": ("video/quicktime", 'video/mp4'), "min_size": 8 * 8, "magic": ((b"moov", 4 * 8),), "description": "Apple QuickTime movie" } BRANDS = { # File type brand => MIME type 'mp41': 'video/mp4', 'mp42': 'video/mp4', 'avc1': 'video/mp4', 'isom': 'video/mp4', 'iso2': 'video/mp4', } endian = BIG_ENDIAN def __init__(self, *args, **kw): Parser.__init__(self, *args, **kw) is_mpeg4 = property(lambda self: self.mime_type == 'video/mp4') def validate(self): # TODO: Write better code, erk! size = self.stream.readBits(0, 32, self.endian) if size < 8: return "Invalid first atom size" tag = self.stream.readBytes(4 * 8, 4) if tag not in (b"ftyp", b"moov", b"free"): return "Unknown MOV file type" return True def createFields(self): while not self.eof: yield Atom(self, "atom[]") def createMimeType(self): first = self[0] try: # Read brands in the file type if first['tag'].value != "ftyp": return None file_type = first["file_type"] brand = file_type["brand"].value if brand in self.BRANDS: return self.BRANDS[brand] for field in file_type.array("compat_brand"): brand = field.value if brand in self.BRANDS: return self.BRANDS[brand] except MissingField: pass return 'video/quicktime'