mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-05 02:43:37 +00:00
247 lines
8.9 KiB
Python
247 lines
8.9 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/
|
||
|
|
||
|
Author: Victor Stinner
|
||
|
Creation: 2 august 2006
|
||
|
"""
|
||
|
|
||
|
from lib.hachoir_parser import Parser
|
||
|
from lib.hachoir_core.field import (ParserError, FieldSet, MissingField,
|
||
|
UInt8, Int16, UInt16, UInt32, TimestampMac32,
|
||
|
String, PascalString8, CString,
|
||
|
RawBytes, PaddingBytes)
|
||
|
from lib.hachoir_core.endian import BIG_ENDIAN
|
||
|
from lib.hachoir_core.text_handler import textHandler, hexadecimal
|
||
|
|
||
|
class QTFloat32(FieldSet):
|
||
|
static_size = 32
|
||
|
def createFields(self):
|
||
|
yield Int16(self, "int_part")
|
||
|
yield UInt16(self, "float_part")
|
||
|
def createValue(self):
|
||
|
return self["int_part"].value + float(self["float_part"].value) / 65535
|
||
|
def createDescription(self):
|
||
|
return str(self.value)
|
||
|
|
||
|
class AtomList(FieldSet):
|
||
|
def createFields(self):
|
||
|
while not self.eof:
|
||
|
yield Atom(self, "atom[]")
|
||
|
|
||
|
class TrackHeader(FieldSet):
|
||
|
def createFields(self):
|
||
|
yield textHandler(UInt8(self, "version"), hexadecimal)
|
||
|
|
||
|
# TODO: sum of :
|
||
|
# TrackEnabled = 1;
|
||
|
# TrackInMovie = 2;
|
||
|
# TrackInPreview = 4;
|
||
|
# TrackInPoster = 8
|
||
|
yield RawBytes(self, "flags", 3)
|
||
|
|
||
|
yield TimestampMac32(self, "creation_date")
|
||
|
yield TimestampMac32(self, "lastmod_date")
|
||
|
yield UInt32(self, "track_id")
|
||
|
yield PaddingBytes(self, "reserved[]", 8)
|
||
|
yield UInt32(self, "duration")
|
||
|
yield PaddingBytes(self, "reserved[]", 8)
|
||
|
yield Int16(self, "video_layer", "Middle is 0, negative in front")
|
||
|
yield PaddingBytes(self, "other", 2)
|
||
|
yield QTFloat32(self, "geom_a", "Width scale")
|
||
|
yield QTFloat32(self, "geom_b", "Width rotate")
|
||
|
yield QTFloat32(self, "geom_u", "Width angle")
|
||
|
yield QTFloat32(self, "geom_c", "Height rotate")
|
||
|
yield QTFloat32(self, "geom_d", "Height scale")
|
||
|
yield QTFloat32(self, "geom_v", "Height angle")
|
||
|
yield QTFloat32(self, "geom_x", "Position X")
|
||
|
yield QTFloat32(self, "geom_y", "Position Y")
|
||
|
yield QTFloat32(self, "geom_w", "Divider scale")
|
||
|
yield QTFloat32(self, "frame_size_width")
|
||
|
yield QTFloat32(self, "frame_size_height")
|
||
|
|
||
|
class HDLR(FieldSet):
|
||
|
def createFields(self):
|
||
|
yield textHandler(UInt8(self, "version"), hexadecimal)
|
||
|
yield RawBytes(self, "flags", 3)
|
||
|
yield String(self, "subtype", 8)
|
||
|
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")
|
||
|
else:
|
||
|
yield PascalString8(self, "name")
|
||
|
|
||
|
class MediaHeader(FieldSet):
|
||
|
def createFields(self):
|
||
|
yield textHandler(UInt8(self, "version"), hexadecimal)
|
||
|
yield RawBytes(self, "flags", 3)
|
||
|
yield TimestampMac32(self, "creation_date")
|
||
|
yield TimestampMac32(self, "lastmod_date")
|
||
|
yield UInt32(self, "time_scale")
|
||
|
yield UInt32(self, "duration")
|
||
|
yield UInt16(self, "mac_lang")
|
||
|
yield Int16(self, "quality")
|
||
|
|
||
|
class ELST(FieldSet):
|
||
|
def createFields(self):
|
||
|
yield textHandler(UInt8(self, "version"), hexadecimal)
|
||
|
yield RawBytes(self, "flags", 3)
|
||
|
yield UInt32(self, "nb_edits")
|
||
|
yield UInt32(self, "length")
|
||
|
yield UInt32(self, "start")
|
||
|
yield QTFloat32(self, "playback_speed")
|
||
|
|
||
|
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 textHandler(UInt8(self, "version"), hexadecimal)
|
||
|
yield RawBytes(self, "flags", 3)
|
||
|
yield TimestampMac32(self, "creation_date")
|
||
|
yield TimestampMac32(self, "lastmod_date")
|
||
|
yield UInt32(self, "time_scale")
|
||
|
yield UInt32(self, "duration")
|
||
|
yield QTFloat32(self, "play_speed")
|
||
|
yield UInt16(self, "volume")
|
||
|
yield PaddingBytes(self, "reserved[]", 10)
|
||
|
yield QTFloat32(self, "geom_a", "Width scale")
|
||
|
yield QTFloat32(self, "geom_b", "Width rotate")
|
||
|
yield QTFloat32(self, "geom_u", "Width angle")
|
||
|
yield QTFloat32(self, "geom_c", "Height rotate")
|
||
|
yield QTFloat32(self, "geom_d", "Height scale")
|
||
|
yield QTFloat32(self, "geom_v", "Height angle")
|
||
|
yield QTFloat32(self, "geom_x", "Position X")
|
||
|
yield QTFloat32(self, "geom_y", "Position Y")
|
||
|
yield QTFloat32(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")
|
||
|
|
||
|
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 Atom(FieldSet):
|
||
|
tag_info = {
|
||
|
# TODO: Use dictionnary of dictionnary, like Matroska parser does
|
||
|
# "elst" is a child of "edts", but not of "moov" for example
|
||
|
"moov": (AtomList, "movie", "Movie"),
|
||
|
"trak": (AtomList, "track", "Track"),
|
||
|
"mdia": (AtomList, "media", "Media"),
|
||
|
"edts": (AtomList, "edts", ""),
|
||
|
"minf": (AtomList, "minf", ""),
|
||
|
"stbl": (AtomList, "stbl", ""),
|
||
|
"dinf": (AtomList, "dinf", ""),
|
||
|
"elst": (ELST, "edts", ""),
|
||
|
"tkhd": (TrackHeader, "track_hdr", "Track header"),
|
||
|
"hdlr": (HDLR, "hdlr", ""),
|
||
|
"mdhd": (MediaHeader, "media_hdr", "Media header"),
|
||
|
"load": (Load, "load", ""),
|
||
|
"mvhd": (MovieHeader, "movie_hdr", "Movie header"),
|
||
|
"ftyp": (FileType, "file_type", "File type"),
|
||
|
}
|
||
|
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)
|
||
|
size = self["size"].value
|
||
|
if size == 1:
|
||
|
raise ParserError("Extended size is not supported!")
|
||
|
#yield UInt64(self, "size64")
|
||
|
size = self["size64"].value
|
||
|
elif size == 0:
|
||
|
#size = (self.root.size - self.root.current_size - self.current_size) / 8
|
||
|
if self._size is None:
|
||
|
size = (self.parent.size - self.current_size) / 8 - 8
|
||
|
else:
|
||
|
size = (self.size - self.current_size) / 8
|
||
|
else:
|
||
|
size = size - 8
|
||
|
if 0 < size:
|
||
|
tag = self["tag"].value
|
||
|
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):
|
||
|
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',
|
||
|
}
|
||
|
endian = BIG_ENDIAN
|
||
|
|
||
|
def __init__(self, *args, **kw):
|
||
|
Parser.__init__(self, *args, **kw)
|
||
|
self.is_mpeg4 = False
|
||
|
|
||
|
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 None
|
||
|
|