mirror of
https://github.com/SickGear/SickGear.git
synced 2024-11-26 22:53:38 +00:00
648 lines
20 KiB
Python
648 lines
20 KiB
Python
|
#
|
||
|
# Matroska parser
|
||
|
# Author Julien Muchembled <jm AT jm10.no-ip.com>
|
||
|
# Created: 8 june 2006
|
||
|
#
|
||
|
|
||
|
from hachoir.parser import Parser
|
||
|
from hachoir.field import (FieldSet, Link,
|
||
|
MissingField, ParserError,
|
||
|
Enum as _Enum, String as _String,
|
||
|
Float32, Float64,
|
||
|
NullBits, Bits, Bit, RawBytes, Bytes,
|
||
|
Int16, GenericInteger)
|
||
|
from hachoir.core.endian import BIG_ENDIAN
|
||
|
from hachoir.core.iso639 import ISO639_2
|
||
|
from hachoir.core.tools import humanDatetime
|
||
|
from hachoir.core.text_handler import textHandler, hexadecimal
|
||
|
from hachoir.parser.container.ogg import XiphInt
|
||
|
from datetime import datetime, timedelta
|
||
|
|
||
|
|
||
|
class RawInt(GenericInteger):
|
||
|
"""
|
||
|
Raw integer: have to be used in BIG_ENDIAN!
|
||
|
"""
|
||
|
|
||
|
def __init__(self, parent, name, description=None):
|
||
|
GenericInteger.__init__(self, parent, name, False, 8, description)
|
||
|
i = GenericInteger.createValue(self)
|
||
|
if i == 0:
|
||
|
raise ParserError('Invalid integer length!')
|
||
|
while i < 0x80:
|
||
|
self._size += 8
|
||
|
i <<= 1
|
||
|
|
||
|
|
||
|
class Unsigned(RawInt):
|
||
|
|
||
|
def __init__(self, parent, name, description=None):
|
||
|
RawInt.__init__(self, parent, name, description)
|
||
|
|
||
|
def hasValue(self):
|
||
|
return True
|
||
|
|
||
|
def createValue(self):
|
||
|
header = 1 << self._size // 8 * 7
|
||
|
value = RawInt.createValue(self) - header
|
||
|
if value + 1 == header:
|
||
|
return None
|
||
|
return value
|
||
|
|
||
|
|
||
|
class Signed(Unsigned):
|
||
|
|
||
|
def createValue(self):
|
||
|
header = 1 << self._size // 8 * 7 - 1
|
||
|
value = RawInt.createValue(self) - 3 * header + 1
|
||
|
if value == header:
|
||
|
return None
|
||
|
return value
|
||
|
|
||
|
|
||
|
def Enum(parent, enum):
|
||
|
return _Enum(GenericInteger(parent, 'enum', False, parent['size'].value * 8), enum)
|
||
|
|
||
|
|
||
|
def Bool(parent):
|
||
|
return textHandler(GenericInteger(parent, 'bool', False, parent['size'].value * 8),
|
||
|
lambda chunk: str(chunk.value != 0))
|
||
|
|
||
|
|
||
|
def UInt(parent):
|
||
|
return GenericInteger(parent, 'unsigned', False, parent['size'].value * 8)
|
||
|
|
||
|
|
||
|
def SInt(parent):
|
||
|
return GenericInteger(parent, 'signed', True, parent['size'].value * 8)
|
||
|
|
||
|
|
||
|
def String(parent):
|
||
|
return _String(parent, 'string', parent['size'].value, charset="ASCII", strip="\0")
|
||
|
|
||
|
|
||
|
def EnumString(parent, enum):
|
||
|
return _Enum(String(parent), enum)
|
||
|
|
||
|
|
||
|
def Binary(parent):
|
||
|
return RawBytes(parent, 'binary', parent['size'].value)
|
||
|
|
||
|
|
||
|
class AttachedFile(Bytes):
|
||
|
|
||
|
def __init__(self, parent):
|
||
|
Bytes.__init__(self, parent, 'file', parent['size'].value, None)
|
||
|
|
||
|
def _getFilename(self):
|
||
|
if not hasattr(self, "_filename"):
|
||
|
try:
|
||
|
self._filename = self["../../FileName/unicode"].value
|
||
|
except MissingField:
|
||
|
self._filename = None
|
||
|
return self._filename
|
||
|
|
||
|
def createDescription(self):
|
||
|
filename = self._getFilename()
|
||
|
if filename:
|
||
|
return 'File "%s"' % filename
|
||
|
return "('Filename' entry not found)"
|
||
|
|
||
|
def _createInputStream(self, **args):
|
||
|
tags = args.setdefault("tags", [])
|
||
|
try:
|
||
|
tags.append(("mime", self["../../FileMimeType/string"].value))
|
||
|
except MissingField:
|
||
|
pass
|
||
|
filename = self._getFilename()
|
||
|
if filename:
|
||
|
tags.append(("filename", filename))
|
||
|
return Bytes._createInputStream(self, **args)
|
||
|
|
||
|
|
||
|
def UTF8(parent):
|
||
|
return _String(parent, 'unicode', parent['size'].value, charset='UTF-8')
|
||
|
|
||
|
|
||
|
def Float(parent):
|
||
|
size = parent['size'].value
|
||
|
if size == 4:
|
||
|
return Float32(parent, 'float')
|
||
|
elif size == 8:
|
||
|
return Float64(parent, 'double')
|
||
|
else:
|
||
|
return RawBytes(parent, 'INVALID_FLOAT', size)
|
||
|
|
||
|
|
||
|
TIMESTAMP_T0 = datetime(2001, 1, 1)
|
||
|
|
||
|
|
||
|
def dateToDatetime(value):
|
||
|
return TIMESTAMP_T0 + timedelta(microseconds=value // 1000)
|
||
|
|
||
|
|
||
|
def dateToString(field):
|
||
|
return humanDatetime(dateToDatetime(field.value))
|
||
|
|
||
|
|
||
|
def Date(parent):
|
||
|
return textHandler(GenericInteger(parent, 'date', True, parent['size'].value * 8),
|
||
|
dateToString)
|
||
|
|
||
|
|
||
|
def SeekID(parent):
|
||
|
return textHandler(GenericInteger(parent, 'binary', False, parent['size'].value * 8),
|
||
|
lambda chunk: segment.get(chunk.value, (hexadecimal(chunk),))[0])
|
||
|
|
||
|
|
||
|
def CueClusterPosition(parent):
|
||
|
class Cluster(Link):
|
||
|
|
||
|
def createValue(self):
|
||
|
parent = self.parent
|
||
|
segment = parent['.....']
|
||
|
pos = parent['unsigned'].value * 8 + segment[2].address
|
||
|
return segment.getFieldByAddress(pos, feed=False)
|
||
|
return Cluster(parent, 'cluster')
|
||
|
|
||
|
|
||
|
def CueTrackPositions(parent):
|
||
|
class Block(Link):
|
||
|
|
||
|
def createValue(self):
|
||
|
parent = self.parent
|
||
|
time = parent['../CueTime/unsigned'].value
|
||
|
track = parent['CueTrack/unsigned'].value
|
||
|
cluster = parent['CueClusterPosition/cluster'].value
|
||
|
time -= cluster['Timecode/unsigned'].value
|
||
|
for field in cluster:
|
||
|
if field.name.startswith('BlockGroup['):
|
||
|
for path in ('Block/block', 'SimpleBlock'):
|
||
|
try:
|
||
|
block = field[path]
|
||
|
if block['track'].value == track and \
|
||
|
block['timecode'].value == time:
|
||
|
return field
|
||
|
except MissingField:
|
||
|
pass
|
||
|
parent.error('Cue point not found')
|
||
|
return self
|
||
|
return Block(parent, 'block')
|
||
|
|
||
|
|
||
|
class Lace(FieldSet):
|
||
|
|
||
|
def __init__(self, parent, lacing, size):
|
||
|
self.n_frames = parent['n_frames'].value
|
||
|
self.createFields = (
|
||
|
self.parseXiph, self.parseFixed, self.parseEBML)[lacing]
|
||
|
FieldSet.__init__(self, parent, 'Lace', size=size * 8)
|
||
|
|
||
|
def parseXiph(self):
|
||
|
for i in range(self.n_frames):
|
||
|
yield XiphInt(self, 'size[]')
|
||
|
for i in range(self.n_frames):
|
||
|
yield RawBytes(self, 'frame[]', self['size[' + str(i) + ']'].value)
|
||
|
yield RawBytes(self, 'frame[]', (self._size - self.current_size) // 8)
|
||
|
|
||
|
def parseEBML(self):
|
||
|
yield Unsigned(self, 'size')
|
||
|
for i in range(1, self.n_frames):
|
||
|
yield Signed(self, 'dsize[]')
|
||
|
size = self['size'].value
|
||
|
yield RawBytes(self, 'frame[]', size)
|
||
|
for i in range(self.n_frames - 1):
|
||
|
size += self['dsize[' + str(i) + ']'].value
|
||
|
yield RawBytes(self, 'frame[]', size)
|
||
|
yield RawBytes(self, 'frame[]', (self._size - self.current_size) // 8)
|
||
|
|
||
|
def parseFixed(self):
|
||
|
n = self.n_frames + 1
|
||
|
size = self._size // 8 // n
|
||
|
for i in range(n):
|
||
|
yield RawBytes(self, 'frame[]', size)
|
||
|
|
||
|
|
||
|
class Block(FieldSet):
|
||
|
|
||
|
def __init__(self, parent):
|
||
|
FieldSet.__init__(self, parent, 'block')
|
||
|
self._size = 8 * parent['size'].value
|
||
|
|
||
|
def lacing(self):
|
||
|
return _Enum(Bits(self, 'lacing', 2), ['none', 'Xiph', 'fixed', 'EBML'])
|
||
|
|
||
|
def createFields(self):
|
||
|
yield Unsigned(self, 'track')
|
||
|
yield Int16(self, 'timecode')
|
||
|
|
||
|
if self.parent._name == 'Block':
|
||
|
yield NullBits(self, 'reserved[]', 4)
|
||
|
yield Bit(self, 'invisible')
|
||
|
yield self.lacing()
|
||
|
yield NullBits(self, 'reserved[]', 1)
|
||
|
elif self.parent._name.startswith('SimpleBlock'):
|
||
|
yield Bit(self, 'keyframe')
|
||
|
yield NullBits(self, 'reserved', 3)
|
||
|
yield Bit(self, 'invisible')
|
||
|
yield self.lacing()
|
||
|
yield Bit(self, 'discardable')
|
||
|
else:
|
||
|
yield NullBits(self, 'reserved', 8)
|
||
|
return
|
||
|
|
||
|
size = (self._size - self.current_size) // 8
|
||
|
lacing = self['lacing'].value
|
||
|
if lacing:
|
||
|
yield textHandler(GenericInteger(self, 'n_frames', False, 8),
|
||
|
lambda chunk: str(chunk.value + 1))
|
||
|
yield Lace(self, lacing - 1, size - 1)
|
||
|
else:
|
||
|
yield RawBytes(self, 'frame', size)
|
||
|
|
||
|
|
||
|
ebml = {
|
||
|
0x1A45DFA3: ('EBML[]', {
|
||
|
0x4286: ('EBMLVersion', UInt),
|
||
|
0x42F7: ('EBMLReadVersion', UInt),
|
||
|
0x42F2: ('EBMLMaxIDLength', UInt),
|
||
|
0x42F3: ('EBMLMaxSizeLength', UInt),
|
||
|
0x4282: ('DocType', String),
|
||
|
0x4287: ('DocTypeVersion', UInt),
|
||
|
0x4285: ('DocTypeReadVersion', UInt)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
signature = {
|
||
|
0x7E8A: ('SignatureAlgo', UInt),
|
||
|
0x7E9A: ('SignatureHash', UInt),
|
||
|
0x7EA5: ('SignaturePublicKey', Binary),
|
||
|
0x7EB5: ('Signature', Binary),
|
||
|
0x7E5B: ('SignatureElements', {
|
||
|
0x7E7B: ('SignatureElementList[]', {
|
||
|
0x6532: ('SignedElement[]', Binary)
|
||
|
})
|
||
|
})
|
||
|
}
|
||
|
|
||
|
chapter_atom = {
|
||
|
0x73C4: ('ChapterUID', UInt),
|
||
|
0x91: ('ChapterTimeStart', UInt),
|
||
|
0x92: ('ChapterTimeEnd', UInt),
|
||
|
0x98: ('ChapterFlagHidden', Bool),
|
||
|
0x4598: ('ChapterFlagEnabled', Bool),
|
||
|
0x6E67: ('ChapterSegmentUID', Binary),
|
||
|
0x6EBC: ('ChapterSegmentEditionUID', Binary),
|
||
|
0x63C3: ('ChapterPhysicalEquiv', UInt),
|
||
|
0x8F: ('ChapterTrack', {
|
||
|
0x89: ('ChapterTrackNumber[]', UInt)
|
||
|
}),
|
||
|
0x80: ('ChapterDisplay[]', {
|
||
|
0x85: ('ChapString', UTF8),
|
||
|
0x437C: ('ChapLanguage[]', String),
|
||
|
0x437E: ('ChapCountry[]', String)
|
||
|
}),
|
||
|
0x6944: ('ChapProcess[]', {
|
||
|
0x6955: ('ChapProcessCodecID', UInt),
|
||
|
0x450D: ('ChapProcessPrivate', Binary),
|
||
|
0x6911: ('ChapProcessCommand[]', {
|
||
|
0x6922: ('ChapProcessTime', UInt),
|
||
|
0x6933: ('ChapProcessData', Binary)
|
||
|
})
|
||
|
})
|
||
|
}
|
||
|
|
||
|
simple_tag = {
|
||
|
0x45A3: ('TagName', UTF8),
|
||
|
0x447A: ('TagLanguage', String),
|
||
|
0x44B4: ('TagDefault', Bool), # 0x4484
|
||
|
0x4487: ('TagString', UTF8),
|
||
|
0x4485: ('TagBinary', Binary)
|
||
|
}
|
||
|
|
||
|
segment_seek = {
|
||
|
0x4DBB: ('Seek[]', {
|
||
|
0x53AB: ('SeekID', SeekID),
|
||
|
0x53AC: ('SeekPosition', UInt)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
segment_info = {
|
||
|
0x73A4: ('SegmentUID', Binary),
|
||
|
0x7384: ('SegmentFilename', UTF8),
|
||
|
0x3CB923: ('PrevUID', Binary),
|
||
|
0x3C83AB: ('PrevFilename', UTF8),
|
||
|
0x3EB923: ('NextUID', Binary),
|
||
|
0x3E83BB: ('NextFilename', UTF8),
|
||
|
0x4444: ('SegmentFamily[]', Binary),
|
||
|
0x6924: ('ChapterTranslate[]', {
|
||
|
0x69FC: ('ChapterTranslateEditionUID[]', UInt),
|
||
|
0x69BF: ('ChapterTranslateCodec', UInt),
|
||
|
0x69A5: ('ChapterTranslateID', Binary)
|
||
|
}),
|
||
|
0x2AD7B1: ('TimecodeScale', UInt),
|
||
|
0x4489: ('Duration', Float),
|
||
|
0x4461: ('DateUTC', Date),
|
||
|
0x7BA9: ('Title', UTF8),
|
||
|
0x4D80: ('MuxingApp', UTF8),
|
||
|
0x5741: ('WritingApp', UTF8)
|
||
|
}
|
||
|
|
||
|
segment_clusters = {
|
||
|
0xE7: ('Timecode', UInt),
|
||
|
0x5854: ('SilentTracks', {
|
||
|
0x58D7: ('SilentTrackNumber[]', UInt)
|
||
|
}),
|
||
|
0xA7: ('Position', UInt),
|
||
|
0xAB: ('PrevSize', UInt),
|
||
|
0xA0: ('BlockGroup[]', {
|
||
|
0xA1: ('Block', Block),
|
||
|
0xA2: ('BlockVirtual[]', Block),
|
||
|
0x75A1: ('BlockAdditions', {
|
||
|
0xA6: ('BlockMore[]', {
|
||
|
0xEE: ('BlockAddID', UInt),
|
||
|
0xA5: ('BlockAdditional', Binary)
|
||
|
})
|
||
|
}),
|
||
|
0x9B: ('BlockDuration', UInt),
|
||
|
0xFA: ('ReferencePriority', UInt),
|
||
|
0xFB: ('ReferenceBlock[]', SInt),
|
||
|
0xFD: ('ReferenceVirtual', SInt),
|
||
|
0xA4: ('CodecState', Binary),
|
||
|
0x8E: ('Slices[]', {
|
||
|
0xE8: ('TimeSlice[]', {
|
||
|
0xCC: ('LaceNumber', UInt),
|
||
|
0xCD: ('FrameNumber', UInt),
|
||
|
0xCB: ('BlockAdditionID', UInt),
|
||
|
0xCE: ('Delay', UInt),
|
||
|
0xCF: ('Duration', UInt)
|
||
|
})
|
||
|
})
|
||
|
}),
|
||
|
0xA3: ('SimpleBlock[]', Block)
|
||
|
}
|
||
|
|
||
|
|
||
|
def stereo_mode(parent):
|
||
|
return Enum(parent, ['mono', 'right eye', 'left eye', 'both eyes'])
|
||
|
|
||
|
|
||
|
def display_unit(parent):
|
||
|
return Enum(parent, ['pixels', 'centimeters', 'inches'])
|
||
|
|
||
|
|
||
|
def aspect_ratio_type(parent):
|
||
|
return Enum(parent, ['free resizing', 'keep aspect ratio', 'fixed'])
|
||
|
|
||
|
|
||
|
tracks_video = {
|
||
|
0x9A: ('FlagInterlaced', Bool),
|
||
|
0x53B8: ('StereoMode', stereo_mode),
|
||
|
0xB0: ('PixelWidth', UInt),
|
||
|
0xBA: ('PixelHeight', UInt),
|
||
|
0x54AA: ('PixelCropBottom', UInt),
|
||
|
0x54BB: ('PixelCropTop', UInt),
|
||
|
0x54CC: ('PixelCropLeft', UInt),
|
||
|
0x54DD: ('PixelCropRight', UInt),
|
||
|
0x54B0: ('DisplayWidth', UInt),
|
||
|
0x54BA: ('DisplayHeight', UInt),
|
||
|
0x54B2: ('DisplayUnit', display_unit),
|
||
|
0x54B3: ('AspectRatioType', aspect_ratio_type),
|
||
|
0x2EB524: ('ColourSpace', Binary),
|
||
|
0x2FB523: ('GammaValue', Float)
|
||
|
}
|
||
|
|
||
|
tracks_audio = {
|
||
|
0xB5: ('SamplingFrequency', Float),
|
||
|
0x78B5: ('OutputSamplingFrequency', Float),
|
||
|
0x9F: ('Channels', UInt),
|
||
|
0x7D7B: ('ChannelPositions', Binary),
|
||
|
0x6264: ('BitDepth', UInt)
|
||
|
}
|
||
|
|
||
|
tracks_content_encodings = {
|
||
|
0x6240: ('ContentEncoding[]', {
|
||
|
0x5031: ('ContentEncodingOrder', UInt),
|
||
|
0x5032: ('ContentEncodingScope', UInt),
|
||
|
0x5033: ('ContentEncodingType', UInt),
|
||
|
0x5034: ('ContentCompression', {
|
||
|
0x4254: ('ContentCompAlgo', UInt),
|
||
|
0x4255: ('ContentCompSettings', Binary)
|
||
|
}),
|
||
|
0x5035: ('ContentEncryption', {
|
||
|
0x47e1: ('ContentEncAlgo', UInt),
|
||
|
0x47e2: ('ContentEncKeyID', Binary),
|
||
|
0x47e3: ('ContentSignature', Binary),
|
||
|
0x47e4: ('ContentSigKeyID', Binary),
|
||
|
0x47e5: ('ContentSigAlgo', UInt),
|
||
|
0x47e6: ('ContentSigHashAlgo', UInt),
|
||
|
})
|
||
|
})
|
||
|
}
|
||
|
|
||
|
segment_tracks = {
|
||
|
0xAE: ('TrackEntry[]', {
|
||
|
0xD7: ('TrackNumber', UInt),
|
||
|
0x73C5: ('TrackUID', UInt),
|
||
|
0x83: ('TrackType', lambda parent: Enum(parent, {
|
||
|
0x01: 'video',
|
||
|
0x02: 'audio',
|
||
|
0x03: 'complex',
|
||
|
0x10: 'logo',
|
||
|
0x11: 'subtitle',
|
||
|
0x12: 'buttons',
|
||
|
0x20: 'control'
|
||
|
})),
|
||
|
0xB9: ('FlagEnabled', Bool),
|
||
|
0x88: ('FlagDefault', Bool),
|
||
|
0x55AA: ('FlagForced[]', Bool),
|
||
|
0x9C: ('FlagLacing', Bool),
|
||
|
0x6DE7: ('MinCache', UInt),
|
||
|
0x6DF8: ('MaxCache', UInt),
|
||
|
0x23E383: ('DefaultDuration', UInt),
|
||
|
0x23314F: ('TrackTimecodeScale', Float),
|
||
|
0x537F: ('TrackOffset', SInt),
|
||
|
0x55EE: ('MaxBlockAdditionID', UInt),
|
||
|
0x536E: ('Name', UTF8),
|
||
|
0x22B59C: ('Language', lambda parent: EnumString(parent, ISO639_2)),
|
||
|
0x86: ('CodecID', String),
|
||
|
0x63A2: ('CodecPrivate', Binary),
|
||
|
0x258688: ('CodecName', UTF8),
|
||
|
0x7446: ('AttachmentLink', UInt),
|
||
|
0x3A9697: ('CodecSettings', UTF8),
|
||
|
0x3B4040: ('CodecInfoURL[]', String),
|
||
|
0x26B240: ('CodecDownloadURL[]', String),
|
||
|
0xAA: ('CodecDecodeAll', Bool),
|
||
|
0x6FAB: ('TrackOverlay[]', UInt),
|
||
|
0x6624: ('TrackTranslate[]', {
|
||
|
0x66FC: ('TrackTranslateEditionUID[]', UInt),
|
||
|
0x66BF: ('TrackTranslateCodec', UInt),
|
||
|
0x66A5: ('TrackTranslateTrackID', Binary)
|
||
|
}),
|
||
|
0xE0: ('Video', tracks_video),
|
||
|
0xE1: ('Audio', tracks_audio),
|
||
|
0x6d80: ('ContentEncodings', tracks_content_encodings)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
segment_cues = {
|
||
|
0xBB: ('CuePoint[]', {
|
||
|
0xB3: ('CueTime', UInt),
|
||
|
0xB7: ('CueTrackPositions[]', CueTrackPositions, {
|
||
|
0xF7: ('CueTrack', UInt),
|
||
|
0xF1: ('CueClusterPosition', CueClusterPosition, UInt),
|
||
|
0x5378: ('CueBlockNumber', UInt),
|
||
|
0xEA: ('CueCodecState', UInt),
|
||
|
0xDB: ('CueReference[]', {
|
||
|
0x96: ('CueRefTime', UInt),
|
||
|
0x97: ('CueRefCluster', UInt),
|
||
|
0x535F: ('CueRefNumber', UInt),
|
||
|
0xEB: ('CueRefCodecState', UInt)
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
}
|
||
|
|
||
|
segment_attachments = {
|
||
|
0x61A7: ('AttachedFile[]', {
|
||
|
0x467E: ('FileDescription', UTF8),
|
||
|
0x466E: ('FileName', UTF8),
|
||
|
0x4660: ('FileMimeType', String),
|
||
|
0x465C: ('FileData', AttachedFile),
|
||
|
0x46AE: ('FileUID', UInt),
|
||
|
0x4675: ('FileReferral', Binary)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
segment_chapters = {
|
||
|
0x45B9: ('EditionEntry[]', {
|
||
|
0x45BC: ('EditionUID', UInt),
|
||
|
0x45BD: ('EditionFlagHidden', Bool),
|
||
|
0x45DB: ('EditionFlagDefault', Bool),
|
||
|
0x45DD: ('EditionFlagOrdered', Bool),
|
||
|
0xB6: ('ChapterAtom[]', chapter_atom)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
segment_tags = {
|
||
|
0x7373: ('Tag[]', {
|
||
|
0x63C0: ('Targets', {
|
||
|
0x68CA: ('TargetTypeValue', UInt),
|
||
|
0x63CA: ('TargetType', String),
|
||
|
0x63C5: ('TrackUID[]', UInt),
|
||
|
0x63C9: ('EditionUID[]', UInt),
|
||
|
0x63C4: ('ChapterUID[]', UInt),
|
||
|
0x63C6: ('AttachmentUID[]', UInt)
|
||
|
}),
|
||
|
0x67C8: ('SimpleTag[]', simple_tag)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
segment = {
|
||
|
0x114D9B74: ('SeekHead[]', segment_seek),
|
||
|
0x1549A966: ('Info[]', segment_info),
|
||
|
0x1F43B675: ('Cluster[]', segment_clusters),
|
||
|
0x1654AE6B: ('Tracks[]', segment_tracks),
|
||
|
0x1C53BB6B: ('Cues', segment_cues),
|
||
|
0x1941A469: ('Attachments', segment_attachments),
|
||
|
0x1043A770: ('Chapters', segment_chapters),
|
||
|
0x1254C367: ('Tags[]', segment_tags)
|
||
|
}
|
||
|
|
||
|
|
||
|
class EBML(FieldSet):
|
||
|
|
||
|
def __init__(self, parent, ids):
|
||
|
FieldSet.__init__(self, parent, "?[]")
|
||
|
|
||
|
# Set name
|
||
|
id = self['id'].value
|
||
|
self.val = ids.get(id)
|
||
|
if not self.val:
|
||
|
if id == 0xBF:
|
||
|
self.val = 'CRC-32[]', Binary
|
||
|
elif id == 0xEC:
|
||
|
self.val = 'Void[]', Binary
|
||
|
elif id == 0x1B538667:
|
||
|
self.val = 'SignatureSlot[]', signature
|
||
|
else:
|
||
|
self.val = 'Unknown[]', Binary
|
||
|
self._name = self.val[0]
|
||
|
|
||
|
# Compute size
|
||
|
size = self['size']
|
||
|
if size.value is not None:
|
||
|
self._size = size.address + size.size + size.value * 8
|
||
|
elif self._parent._parent:
|
||
|
raise ParserError(
|
||
|
"Unknown length (only allowed for the last Level 0 element)")
|
||
|
elif self._parent._size is not None:
|
||
|
self._size = self._parent._size - self.address
|
||
|
|
||
|
def createFields(self):
|
||
|
yield RawInt(self, 'id')
|
||
|
yield Unsigned(self, 'size')
|
||
|
for val in self.val[1:]:
|
||
|
if callable(val):
|
||
|
yield val(self)
|
||
|
else:
|
||
|
while not self.eof:
|
||
|
yield EBML(self, val)
|
||
|
|
||
|
|
||
|
class MkvFile(Parser):
|
||
|
EBML_SIGNATURE = 0x1A45DFA3
|
||
|
PARSER_TAGS = {
|
||
|
"id": "matroska",
|
||
|
"category": "container",
|
||
|
"file_ext": ("mka", "mkv", "webm"),
|
||
|
"mime": (
|
||
|
"video/x-matroska",
|
||
|
"audio/x-matroska",
|
||
|
"video/webm",
|
||
|
"audio/webm"),
|
||
|
"min_size": 5 * 8,
|
||
|
"magic": ((b"\x1A\x45\xDF\xA3", 0),),
|
||
|
"description": "Matroska multimedia container"
|
||
|
}
|
||
|
endian = BIG_ENDIAN
|
||
|
|
||
|
def _getDoctype(self):
|
||
|
return self[0]['DocType/string'].value
|
||
|
|
||
|
def validate(self):
|
||
|
if self.stream.readBits(0, 32, self.endian) != self.EBML_SIGNATURE:
|
||
|
return False
|
||
|
try:
|
||
|
first = self[0]
|
||
|
except ParserError:
|
||
|
return False
|
||
|
if 0 < self._size < first._size:
|
||
|
return "First chunk size is invalid"
|
||
|
if self._getDoctype() not in ('matroska', 'webm'):
|
||
|
return "Stream isn't a matroska document."
|
||
|
return True
|
||
|
|
||
|
def createFields(self):
|
||
|
hdr = EBML(self, ebml)
|
||
|
yield hdr
|
||
|
|
||
|
while not self.eof:
|
||
|
yield EBML(self, {0x18538067: ('Segment[]', segment)})
|
||
|
|
||
|
def createContentSize(self):
|
||
|
field = self["Segment[0]/size"]
|
||
|
return field.absolute_address + field.value * 8 + field.size
|
||
|
|
||
|
def createDescription(self):
|
||
|
if self._getDoctype() == 'webm':
|
||
|
return 'WebM video'
|
||
|
else:
|
||
|
return 'Matroska video'
|
||
|
|
||
|
def createMimeType(self):
|
||
|
if self._getDoctype() == 'webm':
|
||
|
return "video/webm"
|
||
|
else:
|
||
|
return "video/x-matroska"
|