mirror of
https://github.com/soimort/you-get.git
synced 2025-01-23 21:45:02 +03:00
908 lines
29 KiB
Python
Executable File
908 lines
29 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
# reference: c041828_ISO_IEC_14496-12_2005(E).pdf
|
|
|
|
##################################################
|
|
# reader and writer
|
|
##################################################
|
|
|
|
import struct
|
|
from io import BytesIO
|
|
|
|
def skip(stream, n):
|
|
stream.seek(stream.tell() + n)
|
|
|
|
def skip_zeros(stream, n):
|
|
assert stream.read(n) == b'\x00' * n
|
|
|
|
def read_int(stream):
|
|
return struct.unpack('>i', stream.read(4))[0]
|
|
|
|
def read_uint(stream):
|
|
return struct.unpack('>I', stream.read(4))[0]
|
|
|
|
def write_uint(stream, n):
|
|
stream.write(struct.pack('>I', n))
|
|
|
|
def read_ushort(stream):
|
|
return struct.unpack('>H', stream.read(2))[0]
|
|
|
|
def read_ulong(stream):
|
|
return struct.unpack('>Q', stream.read(8))[0]
|
|
|
|
def read_byte(stream):
|
|
return ord(stream.read(1))
|
|
|
|
def copy_stream(source, target, n):
|
|
buffer_size = 1024 * 1024
|
|
while n > 0:
|
|
to_read = min(buffer_size, n)
|
|
s = source.read(to_read)
|
|
assert len(s) == to_read, 'no enough data'
|
|
target.write(s)
|
|
n -= to_read
|
|
|
|
class Atom:
|
|
def __init__(self, type, size, body):
|
|
assert len(type) == 4
|
|
self.type = type
|
|
self.size = size
|
|
self.body = body
|
|
def __str__(self):
|
|
#return '<Atom(%s):%s>' % (self.type, repr(self.body))
|
|
return '<Atom(%s):%s>' % (self.type, '')
|
|
def __repr__(self):
|
|
return str(self)
|
|
def write1(self, stream):
|
|
write_uint(stream, self.size)
|
|
stream.write(self.type)
|
|
def write(self, stream):
|
|
assert type(self.body) == bytes, '%s: %s' % (self.type, type(self.body))
|
|
assert self.size == 8 + len(self.body)
|
|
self.write1(stream)
|
|
stream.write(self.body)
|
|
def calsize(self):
|
|
return self.size
|
|
|
|
class CompositeAtom(Atom):
|
|
def __init__(self, type, size, body):
|
|
assert isinstance(body, list)
|
|
Atom.__init__(self, type, size, body)
|
|
def write(self, stream):
|
|
assert type(self.body) == list
|
|
self.write1(stream)
|
|
for atom in self.body:
|
|
atom.write(stream)
|
|
def calsize(self):
|
|
self.size = 8 + sum([atom.calsize() for atom in self.body])
|
|
return self.size
|
|
def get1(self, k):
|
|
for a in self.body:
|
|
if a.type == k:
|
|
return a
|
|
else:
|
|
raise Exception('atom not found: ' + k)
|
|
def get(self, *keys):
|
|
atom = self
|
|
for k in keys:
|
|
atom = atom.get1(k)
|
|
return atom
|
|
def get_all(self, k):
|
|
return list(filter(lambda x: x.type == k, self.body))
|
|
|
|
class VariableAtom(Atom):
|
|
def __init__(self, type, size, body, variables):
|
|
assert isinstance(body, bytes)
|
|
Atom.__init__(self, type, size, body)
|
|
self.variables = variables
|
|
def write(self, stream):
|
|
self.write1(stream)
|
|
i = 0
|
|
n = 0
|
|
for name, offset, value in self.variables:
|
|
stream.write(self.body[i:offset])
|
|
write_uint(stream, value)
|
|
n += offset - i + 4
|
|
i = offset + 4
|
|
stream.write(self.body[i:])
|
|
n += len(self.body) - i
|
|
assert n == len(self.body)
|
|
def get(self, k):
|
|
for v in self.variables:
|
|
if v[0] == k:
|
|
return v[2]
|
|
else:
|
|
raise Exception('field not found: ' + k)
|
|
def set(self, k, v):
|
|
for i in range(len(self.variables)):
|
|
variable = self.variables[i]
|
|
if variable[0] == k:
|
|
self.variables[i] = (k, variable[1], v)
|
|
break
|
|
else:
|
|
raise Exception('field not found: '+k)
|
|
|
|
def read_raw(stream, size, left, type):
|
|
assert size == left + 8
|
|
body = stream.read(left)
|
|
return Atom(type, size, body)
|
|
|
|
def read_body_stream(stream, left):
|
|
body = stream.read(left)
|
|
assert len(body) == left
|
|
return body, BytesIO(body)
|
|
|
|
def read_full_atom(stream):
|
|
value = read_uint(stream)
|
|
version = value >> 24
|
|
flags = value & 0xffffff
|
|
assert version == 0
|
|
return value
|
|
|
|
def read_mvhd(stream, size, left, type):
|
|
body, stream = read_body_stream(stream, left)
|
|
value = read_full_atom(stream)
|
|
left -= 4
|
|
|
|
# new Date(movieTime * 1000 - 2082850791998L);
|
|
creation_time = read_uint(stream)
|
|
modification_time = read_uint(stream)
|
|
time_scale = read_uint(stream)
|
|
duration = read_uint(stream)
|
|
left -= 16
|
|
|
|
qt_preferred_fate = read_uint(stream)
|
|
qt_preferred_volume = read_ushort(stream)
|
|
assert stream.read(10) == b'\x00' * 10
|
|
qt_matrixA = read_uint(stream)
|
|
qt_matrixB = read_uint(stream)
|
|
qt_matrixU = read_uint(stream)
|
|
qt_matrixC = read_uint(stream)
|
|
qt_matrixD = read_uint(stream)
|
|
qt_matrixV = read_uint(stream)
|
|
qt_matrixX = read_uint(stream)
|
|
qt_matrixY = read_uint(stream)
|
|
qt_matrixW = read_uint(stream)
|
|
qt_previewTime = read_uint(stream)
|
|
qt_previewDuration = read_uint(stream)
|
|
qt_posterTime = read_uint(stream)
|
|
qt_selectionTime = read_uint(stream)
|
|
qt_selectionDuration = read_uint(stream)
|
|
qt_currentTime = read_uint(stream)
|
|
nextTrackID = read_uint(stream)
|
|
left -= 80
|
|
assert left == 0
|
|
return VariableAtom(b'mvhd', size, body, [('duration', 16, duration)])
|
|
|
|
def read_tkhd(stream, size, left, type):
|
|
body, stream = read_body_stream(stream, left)
|
|
value = read_full_atom(stream)
|
|
left -= 4
|
|
|
|
# new Date(movieTime * 1000 - 2082850791998L);
|
|
creation_time = read_uint(stream)
|
|
modification_time = read_uint(stream)
|
|
track_id = read_uint(stream)
|
|
assert stream.read(4) == b'\x00' * 4
|
|
duration = read_uint(stream)
|
|
left -= 20
|
|
|
|
assert stream.read(8) == b'\x00' * 8
|
|
qt_layer = read_ushort(stream)
|
|
qt_alternate_group = read_ushort(stream)
|
|
qt_volume = read_ushort(stream)
|
|
assert stream.read(2) == b'\x00\x00'
|
|
qt_matrixA = read_uint(stream)
|
|
qt_matrixB = read_uint(stream)
|
|
qt_matrixU = read_uint(stream)
|
|
qt_matrixC = read_uint(stream)
|
|
qt_matrixD = read_uint(stream)
|
|
qt_matrixV = read_uint(stream)
|
|
qt_matrixX = read_uint(stream)
|
|
qt_matrixY = read_uint(stream)
|
|
qt_matrixW = read_uint(stream)
|
|
qt_track_width = read_uint(stream)
|
|
width = qt_track_width >> 16
|
|
qt_track_height = read_uint(stream)
|
|
height = qt_track_height >> 16
|
|
left -= 60
|
|
assert left == 0
|
|
return VariableAtom(b'tkhd', size, body, [('duration', 20, duration)])
|
|
|
|
def read_mdhd(stream, size, left, type):
|
|
body, stream = read_body_stream(stream, left)
|
|
value = read_full_atom(stream)
|
|
left -= 4
|
|
|
|
# new Date(movieTime * 1000 - 2082850791998L);
|
|
creation_time = read_uint(stream)
|
|
modification_time = read_uint(stream)
|
|
time_scale = read_uint(stream)
|
|
duration = read_uint(stream)
|
|
left -= 16
|
|
|
|
packed_language = read_ushort(stream)
|
|
qt_quality = read_ushort(stream)
|
|
left -= 4
|
|
|
|
assert left == 0
|
|
return VariableAtom(b'mdhd', size, body, [('duration', 16, duration)])
|
|
|
|
def read_hdlr(stream, size, left, type):
|
|
body, stream = read_body_stream(stream, left)
|
|
value = read_full_atom(stream)
|
|
left -= 4
|
|
|
|
qt_component_type = read_uint(stream)
|
|
handler_type = read_uint(stream)
|
|
qt_component_manufacturer = read_uint(stream)
|
|
qt_component_flags = read_uint(stream)
|
|
qt_component_flags_mask = read_uint(stream)
|
|
left -= 20
|
|
|
|
track_name = stream.read(left - 1)
|
|
assert stream.read(1) == b'\x00'
|
|
|
|
return Atom(b'hdlr', size, body)
|
|
|
|
def read_vmhd(stream, size, left, type):
|
|
body, stream = read_body_stream(stream, left)
|
|
value = read_full_atom(stream)
|
|
left -= 4
|
|
|
|
assert left == 8
|
|
graphic_mode = read_ushort(stream)
|
|
op_color_read = read_ushort(stream)
|
|
op_color_green = read_ushort(stream)
|
|
op_color_blue = read_ushort(stream)
|
|
|
|
return Atom(b'vmhd', size, body)
|
|
|
|
def read_stsd(stream, size, left, type):
|
|
value = read_full_atom(stream)
|
|
left -= 4
|
|
|
|
entry_count = read_uint(stream)
|
|
left -= 4
|
|
|
|
children = []
|
|
for i in range(entry_count):
|
|
atom = read_atom(stream)
|
|
children.append(atom)
|
|
left -= atom.size
|
|
|
|
assert left == 0
|
|
#return Atom('stsd', size, children)
|
|
class stsd_atom(Atom):
|
|
def __init__(self, type, size, body):
|
|
Atom.__init__(self, type, size, body)
|
|
def write(self, stream):
|
|
self.write1(stream)
|
|
write_uint(stream, self.body[0])
|
|
write_uint(stream, len(self.body[1]))
|
|
for atom in self.body[1]:
|
|
atom.write(stream)
|
|
def calsize(self):
|
|
oldsize = self.size # TODO: remove
|
|
self.size = 8 + 4 + 4 + sum([atom.calsize() for atom in self.body[1]])
|
|
assert oldsize == self.size, '%s: %d, %d' % (self.type, oldsize, self.size) # TODO: remove
|
|
return self.size
|
|
return stsd_atom(b'stsd', size, (value, children))
|
|
|
|
def read_avc1(stream, size, left, type):
|
|
body, stream = read_body_stream(stream, left)
|
|
|
|
skip_zeros(stream, 6)
|
|
data_reference_index = read_ushort(stream)
|
|
skip_zeros(stream, 2)
|
|
skip_zeros(stream, 2)
|
|
skip_zeros(stream, 12)
|
|
width = read_ushort(stream)
|
|
height = read_ushort(stream)
|
|
horizontal_rez = read_uint(stream) >> 16
|
|
vertical_rez = read_uint(stream) >> 16
|
|
assert stream.read(4) == b'\x00' * 4
|
|
frame_count = read_ushort(stream)
|
|
string_len = read_byte(stream)
|
|
compressor_name = stream.read(31)
|
|
depth = read_ushort(stream)
|
|
assert stream.read(2) == b'\xff\xff'
|
|
left -= 78
|
|
|
|
child = read_atom(stream)
|
|
assert child.type in (b'avcC', b'pasp'), 'if the sub atom is not avcC or pasp (actual %s), you should not cache raw body' % child.type
|
|
left -= child.size
|
|
stream.read(left) # XXX
|
|
return Atom(b'avc1', size, body)
|
|
|
|
def read_avcC(stream, size, left, type):
|
|
stream.read(left)
|
|
return Atom(b'avcC', size, None)
|
|
|
|
def read_stts(stream, size, left, type):
|
|
value = read_full_atom(stream)
|
|
left -= 4
|
|
|
|
entry_count = read_uint(stream)
|
|
assert entry_count == 1
|
|
left -= 4
|
|
|
|
samples = []
|
|
for i in range(entry_count):
|
|
sample_count = read_uint(stream)
|
|
sample_duration = read_uint(stream)
|
|
samples.append((sample_count, sample_duration))
|
|
left -= 8
|
|
|
|
assert left == 0
|
|
#return Atom('stts', size, None)
|
|
class stts_atom(Atom):
|
|
def __init__(self, type, size, body):
|
|
Atom.__init__(self, type, size, body)
|
|
def write(self, stream):
|
|
self.write1(stream)
|
|
write_uint(stream, self.body[0])
|
|
write_uint(stream, len(self.body[1]))
|
|
for sample_count, sample_duration in self.body[1]:
|
|
write_uint(stream, sample_count)
|
|
write_uint(stream, sample_duration)
|
|
def calsize(self):
|
|
oldsize = self.size # TODO: remove
|
|
self.size = 8 + 4 + 4 + len(self.body[1]) * 8
|
|
assert oldsize == self.size, '%s: %d, %d' % (self.type, oldsize, self.size) # TODO: remove
|
|
return self.size
|
|
return stts_atom(b'stts', size, (value, samples))
|
|
|
|
def read_stss(stream, size, left, type):
|
|
value = read_full_atom(stream)
|
|
left -= 4
|
|
|
|
entry_count = read_uint(stream)
|
|
left -= 4
|
|
|
|
samples = []
|
|
for i in range(entry_count):
|
|
sample = read_uint(stream)
|
|
samples.append(sample)
|
|
left -= 4
|
|
|
|
assert left == 0
|
|
#return Atom('stss', size, None)
|
|
class stss_atom(Atom):
|
|
def __init__(self, type, size, body):
|
|
Atom.__init__(self, type, size, body)
|
|
def write(self, stream):
|
|
self.write1(stream)
|
|
write_uint(stream, self.body[0])
|
|
write_uint(stream, len(self.body[1]))
|
|
for sample in self.body[1]:
|
|
write_uint(stream, sample)
|
|
def calsize(self):
|
|
self.size = 8 + 4 + 4 + len(self.body[1]) * 4
|
|
return self.size
|
|
return stss_atom(b'stss', size, (value, samples))
|
|
|
|
def read_stsc(stream, size, left, type):
|
|
value = read_full_atom(stream)
|
|
left -= 4
|
|
|
|
entry_count = read_uint(stream)
|
|
left -= 4
|
|
|
|
chunks = []
|
|
for i in range(entry_count):
|
|
first_chunk = read_uint(stream)
|
|
samples_per_chunk = read_uint(stream)
|
|
sample_description_index = read_uint(stream)
|
|
assert sample_description_index == 1 # what is it?
|
|
chunks.append((first_chunk, samples_per_chunk, sample_description_index))
|
|
left -= 12
|
|
#chunks, samples = zip(*chunks)
|
|
#total = 0
|
|
#for c, s in zip(chunks[1:], samples):
|
|
# total += c*s
|
|
#print 'total', total
|
|
|
|
assert left == 0
|
|
#return Atom('stsc', size, None)
|
|
class stsc_atom(Atom):
|
|
def __init__(self, type, size, body):
|
|
Atom.__init__(self, type, size, body)
|
|
def write(self, stream):
|
|
self.write1(stream)
|
|
write_uint(stream, self.body[0])
|
|
write_uint(stream, len(self.body[1]))
|
|
for first_chunk, samples_per_chunk, sample_description_index in self.body[1]:
|
|
write_uint(stream, first_chunk)
|
|
write_uint(stream, samples_per_chunk)
|
|
write_uint(stream, sample_description_index)
|
|
def calsize(self):
|
|
self.size = 8 + 4 + 4 + len(self.body[1]) * 12
|
|
return self.size
|
|
return stsc_atom(b'stsc', size, (value, chunks))
|
|
|
|
def read_stsz(stream, size, left, type):
|
|
value = read_full_atom(stream)
|
|
left -= 4
|
|
|
|
sample_size = read_uint(stream)
|
|
sample_count = read_uint(stream)
|
|
left -= 8
|
|
|
|
assert sample_size == 0
|
|
total = 0
|
|
sizes = []
|
|
if sample_size == 0:
|
|
for i in range(sample_count):
|
|
entry_size = read_uint(stream)
|
|
sizes.append(entry_size)
|
|
total += entry_size
|
|
left -= 4
|
|
|
|
assert left == 0
|
|
#return Atom('stsz', size, None)
|
|
class stsz_atom(Atom):
|
|
def __init__(self, type, size, body):
|
|
Atom.__init__(self, type, size, body)
|
|
def write(self, stream):
|
|
self.write1(stream)
|
|
write_uint(stream, self.body[0])
|
|
write_uint(stream, self.body[1])
|
|
write_uint(stream, self.body[2])
|
|
for entry_size in self.body[3]:
|
|
write_uint(stream, entry_size)
|
|
def calsize(self):
|
|
self.size = 8 + 4 + 8 + len(self.body[3]) * 4
|
|
return self.size
|
|
return stsz_atom(b'stsz', size, (value, sample_size, sample_count, sizes))
|
|
|
|
def read_stco(stream, size, left, type):
|
|
value = read_full_atom(stream)
|
|
left -= 4
|
|
|
|
entry_count = read_uint(stream)
|
|
left -= 4
|
|
|
|
offsets = []
|
|
for i in range(entry_count):
|
|
chunk_offset = read_uint(stream)
|
|
offsets.append(chunk_offset)
|
|
left -= 4
|
|
|
|
assert left == 0
|
|
#return Atom('stco', size, None)
|
|
class stco_atom(Atom):
|
|
def __init__(self, type, size, body):
|
|
Atom.__init__(self, type, size, body)
|
|
def write(self, stream):
|
|
self.write1(stream)
|
|
write_uint(stream, self.body[0])
|
|
write_uint(stream, len(self.body[1]))
|
|
for chunk_offset in self.body[1]:
|
|
write_uint(stream, chunk_offset)
|
|
def calsize(self):
|
|
self.size = 8 + 4 + 4 + len(self.body[1]) * 4
|
|
return self.size
|
|
return stco_atom(b'stco', size, (value, offsets))
|
|
|
|
def read_ctts(stream, size, left, type):
|
|
value = read_full_atom(stream)
|
|
left -= 4
|
|
|
|
entry_count = read_uint(stream)
|
|
left -= 4
|
|
|
|
samples = []
|
|
for i in range(entry_count):
|
|
sample_count = read_uint(stream)
|
|
sample_offset = read_uint(stream)
|
|
samples.append((sample_count, sample_offset))
|
|
left -= 8
|
|
|
|
assert left == 0
|
|
class ctts_atom(Atom):
|
|
def __init__(self, type, size, body):
|
|
Atom.__init__(self, type, size, body)
|
|
def write(self, stream):
|
|
self.write1(stream)
|
|
write_uint(stream, self.body[0])
|
|
write_uint(stream, len(self.body[1]))
|
|
for sample_count, sample_offset in self.body[1]:
|
|
write_uint(stream, sample_count)
|
|
write_uint(stream, sample_offset)
|
|
def calsize(self):
|
|
self.size = 8 + 4 + 4 + len(self.body[1]) * 8
|
|
return self.size
|
|
return ctts_atom(b'ctts', size, (value, samples))
|
|
|
|
def read_smhd(stream, size, left, type):
|
|
body, stream = read_body_stream(stream, left)
|
|
value = read_full_atom(stream)
|
|
left -= 4
|
|
|
|
balance = read_ushort(stream)
|
|
assert stream.read(2) == b'\x00\x00'
|
|
left -= 4
|
|
|
|
assert left == 0
|
|
return Atom(b'smhd', size, body)
|
|
|
|
def read_mp4a(stream, size, left, type):
|
|
body, stream = read_body_stream(stream, left)
|
|
|
|
assert stream.read(6) == b'\x00' * 6
|
|
data_reference_index = read_ushort(stream)
|
|
assert stream.read(8) == b'\x00' * 8
|
|
channel_count = read_ushort(stream)
|
|
sample_size = read_ushort(stream)
|
|
assert stream.read(4) == b'\x00' * 4
|
|
time_scale = read_ushort(stream)
|
|
assert stream.read(2) == b'\x00' * 2
|
|
left -= 28
|
|
|
|
atom = read_atom(stream)
|
|
assert atom.type == b'esds'
|
|
left -= atom.size
|
|
|
|
assert left == 0
|
|
return Atom(b'mp4a', size, body)
|
|
|
|
def read_descriptor(stream):
|
|
tag = read_byte(stream)
|
|
raise NotImplementedError()
|
|
|
|
def read_esds(stream, size, left, type):
|
|
value = read_uint(stream)
|
|
version = value >> 24
|
|
assert version == 0
|
|
flags = value & 0xffffff
|
|
left -= 4
|
|
|
|
body = stream.read(left)
|
|
return Atom(b'esds', size, None)
|
|
|
|
def read_composite_atom(stream, size, left, type):
|
|
children = []
|
|
while left > 0:
|
|
atom = read_atom(stream)
|
|
children.append(atom)
|
|
left -= atom.size
|
|
assert left == 0, left
|
|
return CompositeAtom(type, size, children)
|
|
|
|
def read_mdat(stream, size, left, type):
|
|
source_start = stream.tell()
|
|
source_size = left
|
|
skip(stream, left)
|
|
#return Atom(type, size, None)
|
|
#raise NotImplementedError()
|
|
class mdat_atom(Atom):
|
|
def __init__(self, type, size, body):
|
|
Atom.__init__(self, type, size, body)
|
|
def write(self, stream):
|
|
self.write1(stream)
|
|
self.write2(stream)
|
|
def write2(self, stream):
|
|
source, source_start, source_size = self.body
|
|
original = source.tell()
|
|
source.seek(source_start)
|
|
copy_stream(source, stream, source_size)
|
|
def calsize(self):
|
|
return self.size
|
|
return mdat_atom(b'mdat', size, (stream, source_start, source_size))
|
|
|
|
atom_readers = {
|
|
b'mvhd': read_mvhd, # merge duration
|
|
b'tkhd': read_tkhd, # merge duration
|
|
b'mdhd': read_mdhd, # merge duration
|
|
b'hdlr': read_hdlr, # nothing
|
|
b'vmhd': read_vmhd, # nothing
|
|
b'stsd': read_stsd, # nothing
|
|
b'avc1': read_avc1, # nothing
|
|
b'avcC': read_avcC, # nothing
|
|
b'stts': read_stts, # sample_count, sample_duration
|
|
b'stss': read_stss, # join indexes
|
|
b'stsc': read_stsc, # merge # sample numbers
|
|
b'stsz': read_stsz, # merge # samples
|
|
b'stco': read_stco, # merge # chunk offsets
|
|
b'ctts': read_ctts, # merge
|
|
b'smhd': read_smhd, # nothing
|
|
b'mp4a': read_mp4a, # nothing
|
|
b'esds': read_esds, # noting
|
|
|
|
b'ftyp': read_raw,
|
|
b'yqoo': read_raw,
|
|
b'moov': read_composite_atom,
|
|
b'trak': read_composite_atom,
|
|
b'mdia': read_composite_atom,
|
|
b'minf': read_composite_atom,
|
|
b'dinf': read_composite_atom,
|
|
b'stbl': read_composite_atom,
|
|
b'iods': read_raw,
|
|
b'dref': read_raw,
|
|
b'free': read_raw,
|
|
b'edts': read_raw,
|
|
b'pasp': read_raw,
|
|
|
|
b'mdat': read_mdat,
|
|
}
|
|
#stsd sample descriptions (codec types, initialization etc.)
|
|
#stts (decoding) time-to-sample
|
|
#ctts (composition) time to sample
|
|
#stsc sample-to-chunk, partial data-offset information
|
|
#stsz sample sizes (framing)
|
|
#stz2 compact sample sizes (framing)
|
|
#stco chunk offset, partial data-offset information
|
|
#co64 64-bit chunk offset
|
|
#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
|
|
|
|
|
|
def read_atom(stream):
|
|
header = stream.read(8)
|
|
if not header:
|
|
return
|
|
assert len(header) == 8
|
|
n = 0
|
|
size = struct.unpack('>I', header[:4])[0]
|
|
assert size > 0
|
|
n += 4
|
|
type = header[4:8]
|
|
n += 4
|
|
assert type != b'uuid'
|
|
if size == 1:
|
|
size = read_ulong(stream)
|
|
n += 8
|
|
|
|
left = size - n
|
|
if type in atom_readers:
|
|
return atom_readers[type](stream, size, left, type)
|
|
raise NotImplementedError('%s: %d' % (type, left))
|
|
|
|
def write_atom(stream, atom):
|
|
atom.write(stream)
|
|
|
|
def parse_atoms(stream):
|
|
atoms = []
|
|
while True:
|
|
atom = read_atom(stream)
|
|
if atom:
|
|
atoms.append(atom)
|
|
else:
|
|
break
|
|
return atoms
|
|
|
|
def read_mp4(stream):
|
|
atoms = parse_atoms(stream)
|
|
moov = list(filter(lambda x: x.type == b'moov', atoms))
|
|
mdat = list(filter(lambda x: x.type == b'mdat', atoms))
|
|
assert len(moov) == 1
|
|
assert len(mdat) == 1
|
|
moov = moov[0]
|
|
mdat = mdat[0]
|
|
return atoms, moov, mdat
|
|
|
|
##################################################
|
|
# merge
|
|
##################################################
|
|
|
|
def merge_stts(samples_list):
|
|
sample_list = []
|
|
for samples in samples_list:
|
|
assert len(samples) == 1
|
|
sample_list.append(samples[0])
|
|
counts, durations = zip(*sample_list)
|
|
assert len(set(durations)) == 1, 'not all durations equal'
|
|
return [(sum(counts), durations[0])]
|
|
|
|
def merge_stss(samples, sample_number_list):
|
|
results = []
|
|
start = 0
|
|
for samples, sample_number_list in zip(samples, sample_number_list):
|
|
results.extend(map(lambda x: start + x, samples))
|
|
start += sample_number_list
|
|
return results
|
|
|
|
def merge_stsc(chunks_list, total_chunk_number_list):
|
|
results = []
|
|
chunk_index = 1
|
|
for chunks, total in zip(chunks_list, total_chunk_number_list):
|
|
for i in range(len(chunks)):
|
|
if i < len(chunks) - 1:
|
|
chunk_number = chunks[i + 1][0] - chunks[i][0]
|
|
else:
|
|
chunk_number = total + 1 - chunks[i][0]
|
|
sample_number = chunks[i][1]
|
|
description = chunks[i][2]
|
|
results.append((chunk_index, sample_number, description))
|
|
chunk_index += chunk_number
|
|
return results
|
|
|
|
def merge_stco(offsets_list, mdats):
|
|
offset = 0
|
|
results = []
|
|
for offsets, mdat in zip(offsets_list, mdats):
|
|
results.extend(offset + x - mdat.body[1] for x in offsets)
|
|
offset += mdat.size - 8
|
|
return results
|
|
|
|
def merge_stsz(sizes_list):
|
|
return sum(sizes_list, [])
|
|
|
|
def merge_mdats(mdats):
|
|
total_size = sum(x.size - 8 for x in mdats) + 8
|
|
class multi_mdat_atom(Atom):
|
|
def __init__(self, type, size, body):
|
|
Atom.__init__(self, type, size, body)
|
|
def write(self, stream):
|
|
self.write1(stream)
|
|
self.write2(stream)
|
|
def write2(self, stream):
|
|
for mdat in self.body:
|
|
mdat.write2(stream)
|
|
def calsize(self):
|
|
return self.size
|
|
return multi_mdat_atom(b'mdat', total_size, mdats)
|
|
|
|
def merge_moov(moovs, mdats):
|
|
mvhd_duration = 0
|
|
for x in moovs:
|
|
mvhd_duration += x.get(b'mvhd').get('duration')
|
|
tkhd_durations = [0, 0]
|
|
mdhd_durations = [0, 0]
|
|
for x in moovs:
|
|
traks = x.get_all(b'trak')
|
|
assert len(traks) == 2
|
|
tkhd_durations[0] += traks[0].get(b'tkhd').get('duration')
|
|
tkhd_durations[1] += traks[1].get(b'tkhd').get('duration')
|
|
mdhd_durations[0] += traks[0].get(b'mdia', b'mdhd').get('duration')
|
|
mdhd_durations[1] += traks[1].get(b'mdia', b'mdhd').get('duration')
|
|
#mvhd_duration = min(mvhd_duration, tkhd_durations)
|
|
|
|
trak0s = [x.get_all(b'trak')[0] for x in moovs]
|
|
trak1s = [x.get_all(b'trak')[1] for x in moovs]
|
|
|
|
stts0 = merge_stts(x.get(b'mdia', b'minf', b'stbl', b'stts').body[1] for x in trak0s)
|
|
stts1 = merge_stts(x.get(b'mdia', b'minf', b'stbl', b'stts').body[1] for x in trak1s)
|
|
|
|
stss = merge_stss((x.get(b'mdia', b'minf', b'stbl', b'stss').body[1] for x in trak0s), (len(x.get(b'mdia', b'minf', b'stbl', b'stsz').body[3]) for x in trak0s))
|
|
|
|
stsc0 = merge_stsc((x.get(b'mdia', b'minf', b'stbl', b'stsc').body[1] for x in trak0s), (len(x.get(b'mdia', b'minf', b'stbl', b'stco').body[1]) for x in trak0s))
|
|
stsc1 = merge_stsc((x.get(b'mdia', b'minf', b'stbl', b'stsc').body[1] for x in trak1s), (len(x.get(b'mdia', b'minf', b'stbl', b'stco').body[1]) for x in trak1s))
|
|
|
|
stco0 = merge_stco((x.get(b'mdia', b'minf', b'stbl', b'stco').body[1] for x in trak0s), mdats)
|
|
stco1 = merge_stco((x.get(b'mdia', b'minf', b'stbl', b'stco').body[1] for x in trak1s), mdats)
|
|
|
|
stsz0 = merge_stsz((x.get(b'mdia', b'minf', b'stbl', b'stsz').body[3] for x in trak0s))
|
|
stsz1 = merge_stsz((x.get(b'mdia', b'minf', b'stbl', b'stsz').body[3] for x in trak1s))
|
|
|
|
ctts = sum((x.get(b'mdia', b'minf', b'stbl', b'ctts').body[1] for x in trak0s), [])
|
|
|
|
moov = moovs[0]
|
|
|
|
moov.get(b'mvhd').set('duration', mvhd_duration)
|
|
trak0 = moov.get_all(b'trak')[0]
|
|
trak1 = moov.get_all(b'trak')[1]
|
|
trak0.get(b'tkhd').set('duration', tkhd_durations[0])
|
|
trak1.get(b'tkhd').set('duration', tkhd_durations[1])
|
|
trak0.get(b'mdia', b'mdhd').set('duration', mdhd_durations[0])
|
|
trak1.get(b'mdia', b'mdhd').set('duration', mdhd_durations[1])
|
|
|
|
stts_atom = trak0.get(b'mdia', b'minf', b'stbl', b'stts')
|
|
stts_atom.body = stts_atom.body[0], stts0
|
|
stts_atom = trak1.get(b'mdia', b'minf', b'stbl', b'stts')
|
|
stts_atom.body = stts_atom.body[0], stts1
|
|
|
|
stss_atom = trak0.get(b'mdia', b'minf', b'stbl', b'stss')
|
|
stss_atom.body = stss_atom.body[0], stss
|
|
|
|
stsc_atom = trak0.get(b'mdia', b'minf', b'stbl', b'stsc')
|
|
stsc_atom.body = stsc_atom.body[0], stsc0
|
|
stsc_atom = trak1.get(b'mdia', b'minf', b'stbl', b'stsc')
|
|
stsc_atom.body = stsc_atom.body[0], stsc1
|
|
|
|
stco_atom = trak0.get(b'mdia', b'minf', b'stbl', b'stco')
|
|
stco_atom.body = stss_atom.body[0], stco0
|
|
stco_atom = trak1.get(b'mdia', b'minf', b'stbl', b'stco')
|
|
stco_atom.body = stss_atom.body[0], stco1
|
|
|
|
stsz_atom = trak0.get(b'mdia', b'minf', b'stbl', b'stsz')
|
|
stsz_atom.body = stsz_atom.body[0], stsz_atom.body[1], len(stsz0), stsz0
|
|
stsz_atom = trak1.get(b'mdia', b'minf', b'stbl', b'stsz')
|
|
stsz_atom.body = stsz_atom.body[0], stsz_atom.body[1], len(stsz1), stsz1
|
|
|
|
ctts_atom = trak0.get(b'mdia', b'minf', b'stbl', b'ctts')
|
|
ctts_atom.body = ctts_atom.body[0], ctts
|
|
|
|
old_moov_size = moov.size
|
|
new_moov_size = moov.calsize()
|
|
new_mdat_start = mdats[0].body[1] + new_moov_size - old_moov_size
|
|
stco0 = list(map(lambda x: x + new_mdat_start, stco0))
|
|
stco1 = list(map(lambda x: x + new_mdat_start, stco1))
|
|
stco_atom = trak0.get(b'mdia', b'minf', b'stbl', b'stco')
|
|
stco_atom.body = stss_atom.body[0], stco0
|
|
stco_atom = trak1.get(b'mdia', b'minf', b'stbl', b'stco')
|
|
stco_atom.body = stss_atom.body[0], stco1
|
|
|
|
return moov
|
|
|
|
def merge_mp4s(files, output):
|
|
assert files
|
|
ins = [open(mp4, 'rb') for mp4 in files]
|
|
mp4s = list(map(read_mp4, ins))
|
|
moovs = list(map(lambda x: x[1], mp4s))
|
|
mdats = list(map(lambda x: x[2], mp4s))
|
|
moov = merge_moov(moovs, mdats)
|
|
mdat = merge_mdats(mdats)
|
|
with open(output, 'wb') as output:
|
|
for x in mp4s[0][0]:
|
|
if x.type == b'moov':
|
|
moov.write(output)
|
|
elif x.type == b'mdat':
|
|
mdat.write(output)
|
|
else:
|
|
x.write(output)
|
|
|
|
##################################################
|
|
# main
|
|
##################################################
|
|
|
|
# TODO: FIXME: duplicate of join_flv
|
|
|
|
def guess_output(inputs):
|
|
import os.path
|
|
inputs = map(os.path.basename, inputs)
|
|
n = min(map(len, inputs))
|
|
for i in reversed(range(1, n)):
|
|
if len(set(s[:i] for s in inputs)) == 1:
|
|
return inputs[0][:i] + '.mp4'
|
|
return 'output.mp4'
|
|
|
|
def concat_mp4(mp4s, output = None):
|
|
assert mp4s, 'no mp4 file found'
|
|
import os.path
|
|
if not output:
|
|
output = guess_output(mp4s)
|
|
elif os.path.isdir(output):
|
|
output = os.path.join(output, guess_output(mp4s))
|
|
|
|
print('Merging video parts...')
|
|
merge_mp4s(mp4s, output)
|
|
|
|
return output
|
|
|
|
def usage():
|
|
print('Usage: [python3] join_mp4.py --output TARGET.mp4 mp4...')
|
|
|
|
def main():
|
|
import sys, getopt
|
|
try:
|
|
opts, args = getopt.getopt(sys.argv[1:], "ho:", ["help", "output="])
|
|
except getopt.GetoptError as err:
|
|
usage()
|
|
sys.exit(1)
|
|
output = None
|
|
for o, a in opts:
|
|
if o in ("-h", "--help"):
|
|
usage()
|
|
sys.exit()
|
|
elif o in ("-o", "--output"):
|
|
output = a
|
|
else:
|
|
usage()
|
|
sys.exit(1)
|
|
if not args:
|
|
usage()
|
|
sys.exit(1)
|
|
|
|
concat_mp4(args, output)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|