""" Apple Quicktime Movie (file extension ".mov") 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_parser import Parser from hachoir_parser.common.win32 import GUID from hachoir_core.field import (ParserError, FieldSet, MissingField, Enum, Bit, NullBits, Bits, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, TimestampMac32, String, PascalString8, PascalString16, CString, RawBytes, NullBytes, PaddingBytes) from hachoir_core.endian import BIG_ENDIAN from hachoir_core.text_handler import textHandler, hexadecimal from hachoir_core.tools import MAC_TIMESTAMP_T0, timedelta def timestampMac64(value): if not isinstance(value, (float, int, long)): raise TypeError("an integer or float is required") return MAC_TIMESTAMP_T0 + timedelta(seconds=value) from hachoir_core.field.timestamp import timestampFactory 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< 0: yield RawBytes(self, "extra_data", size) 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 xrange(self['count'].value): yield SampleEntry(self, "sample_entry[]") 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 xrange(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 xrange(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 xrange(self['count'].value): yield Bits(self, "sample_size[]", bitsize) if self.current_size % 8 != 0: yield NullBits(self, "padding[]", 8 - (self.current_size % 8)) 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 xrange(self['count'].value): yield UInt32(self, "first_chunk[]") yield UInt32(self, "samples_per_chunk[]") yield UInt32(self, "sample_description_index[]") 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) "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.)"), "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 "moof": (AtomList, "moof", "movie fragment"), "mfhd": (MovieFragmentHeader, "mfhd", "movie fragment header"), # traf: track fragment # tfhd: track fragment header # 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"), # 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"), } 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 RawBytes(self, "tag", 4) 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 MovFile(Parser): PARSER_TAGS = { "id": "mov", "category": "video", "file_ext": ("mov", "qt", "mp4", "m4v", "m4a", "m4p", "m4b"), "mime": (u"video/quicktime", u'video/mp4'), "min_size": 8*8, "magic": (("moov", 4*8),), "description": "Apple QuickTime movie" } BRANDS = { # File type brand => MIME type 'mp41': u'video/mp4', 'mp42': u'video/mp4', 'avc1': u'video/mp4', 'isom': u'video/mp4', 'iso2': u'video/mp4', } endian = BIG_ENDIAN def __init__(self, *args, **kw): Parser.__init__(self, *args, **kw) is_mpeg4 = property(lambda self:self.mime_type==u'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) return tag in ("ftyp", "moov", "free") 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 u'video/quicktime'