mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-05 02:43:37 +00:00
881 lines
38 KiB
Python
881 lines
38 KiB
Python
"""
|
|
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<<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[]")
|
|
|
|
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',
|
|
28: 'Lettish',
|
|
29: 'Lappish',
|
|
29: 'Saamisk',
|
|
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[]")
|
|
|
|
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 xrange(self['count'].value):
|
|
yield Atom(self, "atom[]")
|
|
|
|
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 xrange(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")
|
|
yield UInt32(self, "flags") # PreloadAlways = 1 or TrackEnabledPreload = 2
|
|
yield UInt32(self, "hints") # KeepInBuffer = 0x00000004; HighQuality = 0x00000100; SingleFieldVideo = 0x00100000
|
|
|
|
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")
|
|
|
|
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")
|
|
|
|
class MovieFragmentHeader(FieldSet):
|
|
def createFields(self):
|
|
yield UInt8(self, "version", "Version")
|
|
yield NullBits(self, "flags", 24)
|
|
yield UInt32(self, "sequence_number")
|
|
|
|
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 xrange(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)
|
|
|
|
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 xrange(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 xrange(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 xrange(self['count'].value):
|
|
yield UInt64(self, "chapter_start[]")
|
|
yield PascalString8(self, "chapter_name[]", charset='UTF-8')
|
|
|
|
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 xrange(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 xrange(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 xrange(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 xrange(self['count'].value):
|
|
yield UInt64(self, "chunk_offset[]")
|
|
|
|
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':
|
|
# Audio sample entry
|
|
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")
|
|
elif handler['subtype'].value == 'vide':
|
|
# Video sample entry
|
|
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")
|
|
elif handler['subtype'].value == 'hint':
|
|
# Hint sample entry
|
|
pass
|
|
|
|
size = self['size'].value - self.current_size//8
|
|
if size > 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": (AtomList, "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'
|
|
|