diff --git a/tools/lib/framereader.py b/tools/lib/framereader.py index 695be4ab6a..dd27ba1706 100644 --- a/tools/lib/framereader.py +++ b/tools/lib/framereader.py @@ -1,55 +1,51 @@ # pylint: skip-file - -import os -import sys import json +import os +import pickle +import queue import struct +import subprocess import tempfile import threading -import xml.etree.ElementTree as ET -import numpy as np -import _io -if sys.version_info >= (3,0): - import queue - import pickle - from io import BytesIO as StringIO -else: - import Queue as queue - import cPickle as pickle - from cStringIO import StringIO +from functools import wraps -import subprocess +import numpy as np from aenum import Enum from lru import LRU -from functools import wraps +import _io from tools.lib.cache import cache_path_for_file_path from tools.lib.exceptions import DataUnreadableError +from tools.lib.file_helpers import atomic_write_in_dir + try: from xx.chffr.lib.filereader import FileReader except ImportError: from tools.lib.filereader import FileReader -from tools.lib.file_helpers import atomic_write_in_dir -from tools.lib.mkvparse import mkvindex -from tools.lib.route import Route - -H264_SLICE_P = 0 -H264_SLICE_B = 1 -H264_SLICE_I = 2 HEVC_SLICE_B = 0 HEVC_SLICE_P = 1 HEVC_SLICE_I = 2 -SLICE_I = 2 # hevc and h264 are the same :) + +class GOPReader: + def get_gop(self, num): + # returns (start_frame_num, num_frames, frames_to_skip, gop_data) + raise NotImplementedError + + +class DoNothingContextManager: + def __enter__(self): + return self + + def __exit__(self, *x): + pass + class FrameType(Enum): raw = 1 h265_stream = 2 - h264_mp4 = 3 - h264_pstream = 4 - ffv1_mkv = 5 - ffvhuff_mkv = 6 + def fingerprint_video(fn): with FileReader(fn) as f: @@ -61,23 +57,17 @@ def fingerprint_video(fn): elif header == b"\x00\x00\x00\x01": if 'hevc' in fn: return FrameType.h265_stream - elif os.path.basename(fn) in ("camera", "acamera"): - return FrameType.h264_pstream else: raise NotImplementedError(fn) - elif header == b"\x00\x00\x00\x1c": - return FrameType.h264_mp4 - elif header == b"\x1a\x45\xdf\xa3": - return FrameType.ffv1_mkv else: raise NotImplementedError(fn) def ffprobe(fn, fmt=None): cmd = ["ffprobe", - "-v", "quiet", - "-print_format", "json", - "-show_format", "-show_streams"] + "-v", "quiet", + "-print_format", "json", + "-show_format", "-show_streams"] if fmt: cmd += ["-f", fmt] cmd += [fn] @@ -94,7 +84,7 @@ def vidindex(fn, typ): vidindex_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "vidindex") vidindex = os.path.join(vidindex_dir, "vidindex") - subprocess.check_call(["make"], cwd=vidindex_dir, stdout=open("/dev/null","w")) + subprocess.check_call(["make"], cwd=vidindex_dir, stdout=open("/dev/null", "w")) with tempfile.NamedTemporaryFile() as prefix_f, \ tempfile.NamedTemporaryFile() as index_f: @@ -138,9 +128,10 @@ def cache_fn(func): return cache_inner + @cache_fn def index_stream(fn, typ): - assert typ in ("hevc", "h264") + assert typ in ("hevc", ) with FileReader(fn) as f: assert os.path.exists(f.name), fn @@ -153,22 +144,6 @@ def index_stream(fn, typ): 'probe': probe } -@cache_fn -def index_mp4(fn): - with FileReader(fn) as f: - return vidindex_mp4(f.name) - -@cache_fn -def index_mkv(fn): - with FileReader(fn) as f: - probe = ffprobe(f.name, "matroska") - with open(f.name, "rb") as d_f: - config_record, index = mkvindex.mkvindex(d_f) - return { - 'probe': probe, - 'config_record': config_record, - 'index': index - } def index_videos(camera_paths, cache_prefix=None): """Requires that paths in camera_paths are contiguous and of the same type.""" @@ -176,11 +151,9 @@ def index_videos(camera_paths, cache_prefix=None): raise ValueError("must provide at least one video to index") frame_type = fingerprint_video(camera_paths[0]) - if frame_type == FrameType.h264_pstream: - index_pstream(camera_paths, "h264", cache_prefix) - else: - for fn in camera_paths: - index_video(fn, frame_type, cache_prefix) + for fn in camera_paths: + index_video(fn, frame_type, cache_prefix) + def index_video(fn, frame_type=None, cache_prefix=None): cache_path = cache_path_for_file_path(fn, cache_prefix) @@ -191,20 +164,11 @@ def index_video(fn, frame_type=None, cache_prefix=None): if frame_type is None: frame_type = fingerprint_video(fn[0]) - if frame_type == FrameType.h264_pstream: - #hack: try to index the whole route now - route = Route.from_file_path(fn) - - camera_paths = route.camera_paths() - if fn not in camera_paths: - raise DataUnreadableError("Not a contiguous route camera file: {}".format(fn)) - - print("no pstream cache for %s, indexing route %s now" % (fn, route.name)) - index_pstream(route.camera_paths(), "h264", cache_prefix) - elif frame_type == FrameType.h265_stream: + if frame_type == FrameType.h265_stream: index_stream(fn, "hevc", cache_prefix=cache_prefix) - elif frame_type == FrameType.h264_mp4: - index_mp4(fn, cache_prefix=cache_prefix) + else: + raise NotImplementedError("Only h265 supported") + def get_video_index(fn, frame_type, cache_prefix=None): cache_path = cache_path_for_file_path(fn, cache_prefix) @@ -217,202 +181,6 @@ def get_video_index(fn, frame_type, cache_prefix=None): with open(cache_path, "rb") as cache_file: return pickle.load(cache_file) -def pstream_predecompress(fns, probe, indexes, global_prefix, cache_prefix, multithreaded=False): - assert len(fns) == len(indexes) - out_fns = [cache_path_for_file_path(fn, cache_prefix, extension=".predecom.mkv") for fn in fns] - out_exists = map(os.path.exists, out_fns) - if all(out_exists): - return - - w = probe['streams'][0]['width'] - h = probe['streams'][0]['height'] - - frame_size = w*h*3/2 # yuv420p - - decompress_proc = subprocess.Popen( - ["ffmpeg", - "-threads", "0" if multithreaded else "1", - "-vsync", "0", - "-f", "h264", - "-i", "pipe:0", - "-threads", "0" if multithreaded else "1", - "-f", "rawvideo", - "-pix_fmt", "yuv420p", - "pipe:1"], - stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=open("/dev/null", "wb")) - - def write_thread(): - for fn in fns: - with FileReader(fn) as f: - decompress_proc.stdin.write(f.read()) - decompress_proc.stdin.close() - - def read_frame(): - frame = None - try: - frame = decompress_proc.stdout.read(frame_size) - except (IOError, ValueError): - pass - if frame is None or frame == "" or len(frame) != frame_size: - raise DataUnreadableError("pre-decompression failed for %s" % fn) - return frame - - t = threading.Thread(target=write_thread) - t.daemon = True - t.start() - - try: - for fn, out_fn, out_exist, index in zip(fns, out_fns, out_exists, indexes): - if out_exist: - for fi in range(index.shape[0]-1): - read_frame() - continue - - with atomic_write_in_dir(out_fn, mode="w+b", overwrite=True) as out_tmp: - compress_proc = subprocess.Popen( - ["ffmpeg", - "-threads", "0" if multithreaded else "1", - "-y", - "-vsync", "0", - "-f", "rawvideo", - "-pix_fmt", "yuv420p", - "-s", "%dx%d" % (w, h), - "-i", "pipe:0", - "-threads", "0" if multithreaded else "1", - "-f", "matroska", - "-vcodec", "ffv1", - "-g", "0", - out_tmp.name], - stdin=subprocess.PIPE, stderr=open("/dev/null", "wb")) - try: - for fi in range(index.shape[0]-1): - frame = read_frame() - compress_proc.stdin.write(frame) - compress_proc.stdin.close() - except: - compress_proc.kill() - raise - - assert compress_proc.wait() == 0 - - cache_path = cache_path_for_file_path(fn, cache_prefix) - with atomic_write_in_dir(cache_path, mode="wb", overwrite=True) as cache_file: - pickle.dump({ - 'predecom': os.path.basename(out_fn), - 'index': index, - 'probe': probe, - 'global_prefix': global_prefix, - }, cache_file, -1) - - except: - decompress_proc.kill() - raise - finally: - t.join() - - rc = decompress_proc.wait() - if rc != 0: - raise DataUnreadableError(fns[0]) - - -def index_pstream(fns, typ, cache_prefix=None): - if typ != "h264": - raise NotImplementedError(typ) - - if not fns: - raise DataUnreadableError("chffr h264 requires contiguous files") - - out_fns = [cache_path_for_file_path(fn, cache_prefix) for fn in fns] - out_exists = map(os.path.exists, out_fns) - if all(out_exists): return - - # load existing index files to avoid re-doing work - existing_indexes = [] - for out_fn, exists in zip(out_fns, out_exists): - existing = None - if exists: - with open(out_fn, "rb") as cache_file: - existing = pickle.load(cache_file) - existing_indexes.append(existing) - - # probe the first file - if existing_indexes[0]: - probe = existing_indexes[0]['probe'] - else: - with FileReader(fns[0]) as f: - probe = ffprobe(f.name, typ) - - global_prefix = None - - # get the video index of all the segments in this stream - indexes = [] - for i, fn in enumerate(fns): - if existing_indexes[i]: - index = existing_indexes[i]['index'] - prefix = existing_indexes[i]['global_prefix'] - else: - with FileReader(fn) as f: - index, prefix = vidindex(f.name, typ) - if i == 0: - # assert prefix - if not prefix: - raise DataUnreadableError("vidindex failed for %s" % fn) - global_prefix = prefix - indexes.append(index) - - assert global_prefix - - if np.sum(indexes[0][:, 0] == H264_SLICE_I) <= 1: - print("pstream %s is unseekable. pre-decompressing all the segments..." % (fns[0])) - pstream_predecompress(fns, probe, indexes, global_prefix, cache_prefix) - return - - # generate what's required to make each segment self-contained - # (the partial GOP from the end of each segments are put asside to add - # to the start of the following segment) - prefix_data = ["" for _ in fns] - prefix_index = [[] for _ in fns] - for i in range(len(fns)-1): - if indexes[i+1][0, 0] == H264_SLICE_I and indexes[i+1][0, 1] <= 1: - # next file happens to start with a i-frame, dont need use this file's end - continue - - index = indexes[i] - if i == 0 and np.sum(index[:, 0] == H264_SLICE_I) <= 1: - raise NotImplementedError("No I-frames in pstream.") - - # find the last GOP in the index - frame_b = len(index)-1 - while frame_b > 0 and index[frame_b, 0] != H264_SLICE_I: - frame_b -= 1 - - assert frame_b >= 0 - assert index[frame_b, 0] == H264_SLICE_I - - with FileReader(fns[i]) as vid: - vid.seek(index[frame_b, 1]) - end_data = vid.read() - - prefix_data[i+1] = end_data - prefix_index[i+1] = index[frame_b:-1] - # indexes[i] = index[:frame_b] - - for i, fn in enumerate(fns): - cache_path = out_fns[i] - - if os.path.exists(cache_path): - continue - - segment_index = { - 'index': indexes[i], - 'global_prefix': global_prefix, - 'probe': probe, - 'prefix_frame_data': prefix_data[i], # data to prefix the first GOP with - 'num_prefix_frames': len(prefix_index[i]), # number of frames to skip in the first GOP - } - - with atomic_write_in_dir(cache_path, mode="wb", overwrite=True) as cache_file: - pickle.dump(segment_index, cache_file, -1) def read_file_check_size(f, sz, cookie): buff = bytearray(sz) @@ -421,118 +189,65 @@ def read_file_check_size(f, sz, cookie): return buff -import signal -import ctypes -def _set_pdeathsig(sig=signal.SIGTERM): - def f(): - libc = ctypes.CDLL('libc.so.6') - return libc.prctl(1, sig) - return f - -def vidindex_mp4(fn): - try: - xmls = subprocess.check_output(["MP4Box", fn, "-diso", "-out", "/dev/stdout"]) - except subprocess.CalledProcessError: - raise DataUnreadableError(fn) - - tree = ET.fromstring(xmls) - - def parse_content(s): - assert s.startswith("data:application/octet-string,") - return s[len("data:application/octet-string,"):].decode("hex") - - avc_element = tree.find(".//AVCSampleEntryBox") - width = int(avc_element.attrib['Width']) - height = int(avc_element.attrib['Height']) +def rgb24toyuv420(rgb): + yuv_from_rgb = np.array([[ 0.299 , 0.587 , 0.114 ], + [-0.14714119, -0.28886916, 0.43601035 ], + [ 0.61497538, -0.51496512, -0.10001026 ]]) + img = np.dot(rgb.reshape(-1, 3), yuv_from_rgb.T).reshape(rgb.shape) - sps_element = avc_element.find(".//AVCDecoderConfigurationRecord/SequenceParameterSet") - pps_element = avc_element.find(".//AVCDecoderConfigurationRecord/PictureParameterSet") + y_len = img.shape[0] * img.shape[1] + uv_len = y_len / 4 - sps = parse_content(sps_element.attrib['content']) - pps = parse_content(pps_element.attrib['content']) + ys = img[:, :, 0] + us = (img[::2, ::2, 1] + img[1::2, ::2, 1] + img[::2, 1::2, 1] + img[1::2, 1::2, 1]) / 4 + 128 + vs = (img[::2, ::2, 2] + img[1::2, ::2, 2] + img[::2, 1::2, 2] + img[1::2, 1::2, 2]) / 4 + 128 - media_header = tree.find("MovieBox/TrackBox/MediaBox/MediaHeaderBox") - time_scale = int(media_header.attrib['TimeScale']) + yuv420 = np.empty(y_len + 2 * uv_len, dtype=img.dtype) + yuv420[:y_len] = ys.reshape(-1) + yuv420[y_len:y_len + uv_len] = us.reshape(-1) + yuv420[y_len + uv_len:y_len + 2 * uv_len] = vs.reshape(-1) - sample_sizes = [ - int(entry.attrib['Size']) for entry in tree.findall( - "MovieBox/TrackBox/MediaBox/MediaInformationBox/SampleTableBox/SampleSizeBox/SampleSizeEntry") - ] + return yuv420.clip(0,255).astype('uint8') - sample_dependency = [ - entry.attrib['dependsOnOther'] == "yes" for entry in tree.findall( - "MovieBox/TrackBox/MediaBox/MediaInformationBox/SampleTableBox/SampleDependencyTypeBox/SampleDependencyEntry") - ] - assert len(sample_sizes) == len(sample_dependency) +def decompress_video_data(rawdat, vid_fmt, w, h, pix_fmt, multithreaded=False): + # using a tempfile is much faster than proc.communicate for some reason - chunk_offsets = [ - int(entry.attrib['offset']) for entry in tree.findall( - "MovieBox/TrackBox/MediaBox/MediaInformationBox/SampleTableBox/ChunkOffsetBox/ChunkEntry") - ] + with tempfile.TemporaryFile() as tmpf: + tmpf.write(rawdat) + tmpf.seek(0) - sample_chunk_table = [ - (int(entry.attrib['FirstChunk'])-1, int(entry.attrib['SamplesPerChunk'])) for entry in tree.findall( - "MovieBox/TrackBox/MediaBox/MediaInformationBox/SampleTableBox/SampleToChunkBox/SampleToChunkEntry") - ] + proc = subprocess.Popen( + ["ffmpeg", + "-threads", "0" if multithreaded else "1", + "-vsync", "0", + "-f", vid_fmt, + "-flags2", "showall", + "-i", "pipe:0", + "-threads", "0" if multithreaded else "1", + "-f", "rawvideo", + "-pix_fmt", pix_fmt, + "pipe:1"], + stdin=tmpf, stdout=subprocess.PIPE, stderr=open("/dev/null")) - sample_offsets = [None for _ in sample_sizes] + # dat = proc.communicate()[0] + dat = proc.stdout.read() + if proc.wait() != 0: + raise DataUnreadableError("ffmpeg failed") - sample_i = 0 - for i, (first_chunk, samples_per_chunk) in enumerate(sample_chunk_table): - if i == len(sample_chunk_table)-1: - last_chunk = len(chunk_offsets)-1 - else: - last_chunk = sample_chunk_table[i+1][0]-1 - for k in range(first_chunk, last_chunk+1): - sample_offset = chunk_offsets[k] - for _ in range(samples_per_chunk): - sample_offsets[sample_i] = sample_offset - sample_offset += sample_sizes[sample_i] - sample_i += 1 - - assert sample_i == len(sample_sizes) - - pts_offset_table = [ - ( int(entry.attrib['CompositionOffset']), int(entry.attrib['SampleCount']) ) for entry in tree.findall( - "MovieBox/TrackBox/MediaBox/MediaInformationBox/SampleTableBox/CompositionOffsetBox/CompositionOffsetEntry") - ] - sample_pts_offset = [0 for _ in sample_sizes] - sample_i = 0 - for dt, count in pts_offset_table: - for _ in range(count): - sample_pts_offset[sample_i] = dt - sample_i += 1 - - sample_time_table = [ - ( int(entry.attrib['SampleDelta']), int(entry.attrib['SampleCount']) ) for entry in tree.findall( - "MovieBox/TrackBox/MediaBox/MediaInformationBox/SampleTableBox/TimeToSampleBox/TimeToSampleEntry") - ] - sample_time = [None for _ in sample_sizes] - cur_ts = 0 - sample_i = 0 - for dt, count in sample_time_table: - for _ in range(count): - sample_time[sample_i] = (cur_ts + sample_pts_offset[sample_i]) * 1000 / time_scale - - cur_ts += dt - sample_i += 1 - - sample_time.sort() # because we ony decode GOPs in PTS order + if pix_fmt == "rgb24": + ret = np.frombuffer(dat, dtype=np.uint8).reshape(-1, h, w, 3) + elif pix_fmt == "yuv420p": + ret = np.frombuffer(dat, dtype=np.uint8).reshape(-1, (h*w*3//2)) + elif pix_fmt == "yuv444p": + ret = np.frombuffer(dat, dtype=np.uint8).reshape(-1, 3, h, w) + else: + raise NotImplementedError - return { - 'width': width, - 'height': height, - 'sample_offsets': sample_offsets, - 'sample_sizes': sample_sizes, - 'sample_dependency': sample_dependency, - 'sample_time': sample_time, - 'sps': sps, - 'pps': pps - } + return ret -class BaseFrameReader(object): +class BaseFrameReader: # properties: frame_type, frame_count, w, h def __enter__(self): @@ -547,48 +262,20 @@ class BaseFrameReader(object): def get(self, num, count=1, pix_fmt="yuv420p"): raise NotImplementedError + def FrameReader(fn, cache_prefix=None, readahead=False, readbehind=False, multithreaded=True, index_data=None): frame_type = fingerprint_video(fn) if frame_type == FrameType.raw: return RawFrameReader(fn) - elif frame_type in (FrameType.h265_stream, FrameType.h264_pstream): + elif frame_type in (FrameType.h265_stream,): if not index_data: index_data = get_video_index(fn, frame_type, cache_prefix) - if index_data is not None and "predecom" in index_data: - cache_path = cache_path_for_file_path(fn, cache_prefix) - return MKVFrameReader( - os.path.join(os.path.dirname(cache_path), index_data["predecom"])) - else: - return StreamFrameReader(fn, frame_type, index_data, - readahead=readahead, readbehind=readbehind, multithreaded=multithreaded) - elif frame_type == FrameType.h264_mp4: - return MP4FrameReader(fn, readahead=readahead) - elif frame_type == FrameType.ffv1_mkv: - return MKVFrameReader(fn) + return StreamFrameReader(fn, frame_type, index_data, readahead=readahead, readbehind=readbehind, multithreaded=multithreaded) else: raise NotImplementedError(frame_type) -def rgb24toyuv420(rgb): - yuv_from_rgb = np.array([[ 0.299 , 0.587 , 0.114 ], - [-0.14714119, -0.28886916, 0.43601035 ], - [ 0.61497538, -0.51496512, -0.10001026 ]]) - img = np.dot(rgb.reshape(-1, 3), yuv_from_rgb.T).reshape(rgb.shape) - - y_len = img.shape[0] * img.shape[1] - uv_len = y_len / 4 - - ys = img[:, :, 0] - us = (img[::2, ::2, 1] + img[1::2, ::2, 1] + img[::2, 1::2, 1] + img[1::2, 1::2, 1]) / 4 + 128 - vs = (img[::2, ::2, 2] + img[1::2, ::2, 2] + img[::2, 1::2, 2] + img[1::2, 1::2, 2]) / 4 + 128 - yuv420 = np.empty(y_len + 2 * uv_len, dtype=img.dtype) - yuv420[:y_len] = ys.reshape(-1) - yuv420[y_len:y_len + uv_len] = us.reshape(-1) - yuv420[y_len + uv_len:y_len + 2 * uv_len] = vs.reshape(-1) - - return yuv420.clip(0,255).astype('uint8') - -class RawData(object): +class RawData: def __init__(self, f): self.f = _io.FileIO(f, 'rb') self.lenn = struct.unpack("I", self.f.read(4))[0] @@ -598,6 +285,7 @@ class RawData(object): self.f.seek((self.lenn+4)*i + 4) return self.f.read(self.lenn) + class RawFrameReader(BaseFrameReader): def __init__(self, fn): # raw camera @@ -609,12 +297,9 @@ class RawFrameReader(BaseFrameReader): def load_and_debayer(self, img): img = np.frombuffer(img, dtype='uint8').reshape(960, 1280) - cimg = np.dstack([img[0::2, 1::2], ( - (img[0::2, 0::2].astype("uint16") + img[1::2, 1::2].astype("uint16")) - >> 1).astype("uint8"), img[1::2, 0::2]]) + cimg = np.dstack([img[0::2, 1::2], ((img[0::2, 0::2].astype("uint16") + img[1::2, 1::2].astype("uint16")) >> 1).astype("uint8"), img[1::2, 0::2]]) return cimg - def get(self, num, count=1, pix_fmt="yuv420p"): assert self.frame_count is not None assert num+count <= self.frame_count @@ -635,43 +320,8 @@ class RawFrameReader(BaseFrameReader): return app -def decompress_video_data(rawdat, vid_fmt, w, h, pix_fmt, multithreaded=False): - # using a tempfile is much faster than proc.communicate for some reason - - with tempfile.TemporaryFile() as tmpf: - tmpf.write(rawdat) - tmpf.seek(0) - - proc = subprocess.Popen( - ["ffmpeg", - "-threads", "0" if multithreaded else "1", - "-vsync", "0", - "-f", vid_fmt, - "-flags2", "showall", - "-i", "pipe:0", - "-threads", "0" if multithreaded else "1", - "-f", "rawvideo", - "-pix_fmt", pix_fmt, - "pipe:1"], - stdin=tmpf, stdout=subprocess.PIPE, stderr=open("/dev/null")) - - # dat = proc.communicate()[0] - dat = proc.stdout.read() - if proc.wait() != 0: - raise DataUnreadableError("ffmpeg failed") - - if pix_fmt == "rgb24": - ret = np.frombuffer(dat, dtype=np.uint8).reshape(-1, h, w, 3) - elif pix_fmt == "yuv420p": - ret = np.frombuffer(dat, dtype=np.uint8).reshape(-1, (h*w*3//2)) - elif pix_fmt == "yuv444p": - ret = np.frombuffer(dat, dtype=np.uint8).reshape(-1, 3, h, w) - else: - raise NotImplementedError - - return ret -class VideoStreamDecompressor(object): +class VideoStreamDecompressor: def __init__(self, vid_fmt, w, h, pix_fmt, multithreaded=False): self.vid_fmt = vid_fmt self.w = w @@ -750,56 +400,74 @@ class VideoStreamDecompressor(object): assert self.proc.wait() == 0 -class MKVFrameReader(BaseFrameReader): - def __init__(self, fn): +class StreamGOPReader(GOPReader): + def __init__(self, fn, frame_type, index_data): + assert frame_type == FrameType.h265_stream + self.fn = fn - #print("MKVFrameReader", fn) - index_data = index_mkv(fn) - stream = index_data['probe']['streams'][0] - self.w = stream['width'] - self.h = stream['height'] + self.frame_type = frame_type + self.frame_count = None + self.w, self.h = None, None - if stream['codec_name'] == 'ffv1': - self.frame_type = FrameType.ffv1_mkv - elif stream['codec_name'] == 'ffvhuff': - self.frame_type = FrameType.ffvhuff_mkv - else: - raise NotImplementedError + self.prefix = None + self.index = None - self.config_record = index_data['config_record'] self.index = index_data['index'] + self.prefix = index_data['global_prefix'] + probe = index_data['probe'] - self.frame_count = len(self.index) + self.prefix_frame_data = None + self.num_prefix_frames = 0 + self.vid_fmt = "hevc" - def get(self, num, count=1, pix_fmt="yuv420p"): - assert 0 < num+count <= self.frame_count + i = 0 + while i < self.index.shape[0] and self.index[i, 0] != HEVC_SLICE_I: + i += 1 + self.first_iframe = i - frame_dats = [] - with FileReader(self.fn) as f: - for i in range(num, num+count): - pos, length, _ = self.index[i] - f.seek(pos) - frame_dats.append(f.read(length)) + assert self.first_iframe == 0 + + self.frame_count = len(self.index) - 1 + + self.w = probe['streams'][0]['width'] + self.h = probe['streams'][0]['height'] - of = StringIO() - mkvindex.simple_gen(of, self.config_record, self.w, self.h, frame_dats) + def _lookup_gop(self, num): + frame_b = num + while frame_b > 0 and self.index[frame_b, 0] != HEVC_SLICE_I: + frame_b -= 1 - r = decompress_video_data(of.getvalue(), "matroska", self.w, self.h, pix_fmt) - assert len(r) == count + frame_e = num + 1 + while frame_e < (len(self.index) - 1) and self.index[frame_e, 0] != HEVC_SLICE_I: + frame_e += 1 - return r + offset_b = self.index[frame_b, 1] + offset_e = self.index[frame_e, 1] + return (frame_b, frame_e, offset_b, offset_e) -class GOPReader(object): def get_gop(self, num): - # returns (start_frame_num, num_frames, frames_to_skip, gop_data) - raise NotImplementedError + frame_b, frame_e, offset_b, offset_e = self._lookup_gop(num) + assert frame_b <= num < frame_e + num_frames = frame_e - frame_b -class DoNothingContextManager(object): - def __enter__(self): return self - def __exit__(*x): pass + with FileReader(self.fn) as f: + f.seek(offset_b) + rawdat = f.read(offset_e - offset_b) + + if num < self.first_iframe: + assert self.prefix_frame_data + rawdat = self.prefix_frame_data + rawdat + + rawdat = self.prefix + rawdat + + skip_frames = 0 + if num < self.first_iframe: + skip_frames = self.num_prefix_frames + + return frame_b, num_frames, skip_frames, rawdat class GOPFrameReader(BaseFrameReader): @@ -850,10 +518,10 @@ class GOPFrameReader(BaseFrameReader): num, pix_fmt = self.readahead_last if self.readbehind: - for k in range(num-1, max(0, num-self.readahead_len), -1): + for k in range(num - 1, max(0, num - self.readahead_len), -1): self._get_one(k, pix_fmt) else: - for k in range(num, min(self.frame_count, num+self.readahead_len)): + for k in range(num, min(self.frame_count, num + self.readahead_len)): self._get_one(k, pix_fmt) def _get_one(self, num, pix_fmt): @@ -897,151 +565,6 @@ class GOPFrameReader(BaseFrameReader): return ret -class MP4GOPReader(GOPReader): - def __init__(self, fn): - self.fn = fn - self.frame_type = FrameType.h264_mp4 - - self.index = index_mp4(fn) - - self.w = self.index['width'] - self.h = self.index['height'] - self.sample_sizes = self.index['sample_sizes'] - self.sample_offsets = self.index['sample_offsets'] - self.sample_dependency = self.index['sample_dependency'] - - self.vid_fmt = "h264" - - self.frame_count = len(self.sample_sizes) - - self.prefix = "\x00\x00\x00\x01"+self.index['sps']+"\x00\x00\x00\x01"+self.index['pps'] - - def _lookup_gop(self, num): - frame_b = num - while frame_b > 0 and self.sample_dependency[frame_b]: - frame_b -= 1 - - frame_e = num+1 - while frame_e < (len(self.sample_dependency)-1) and self.sample_dependency[frame_e]: - frame_e += 1 - - return (frame_b, frame_e) - - def get_gop(self, num): - frame_b, frame_e = self._lookup_gop(num) - assert frame_b <= num < frame_e - - num_frames = frame_e-frame_b - - with FileReader(self.fn) as f: - rawdat = [] - - sample_i = frame_b - while sample_i < frame_e: - size = self.sample_sizes[sample_i] - start_offset = self.sample_offsets[sample_i] - - # try to read contiguously because a read could actually be a http request - sample_i += 1 - while sample_i < frame_e and size < 10000000 and start_offset+size == self.sample_offsets[sample_i]: - size += self.sample_sizes[sample_i] - sample_i += 1 - - f.seek(start_offset) - sampledat = f.read(size) - - # read length-prefixed NALUs and output in Annex-B - i = 0 - while i < len(sampledat): - nal_len, = struct.unpack(">I", sampledat[i:i+4]) - rawdat.append("\x00\x00\x00\x01"+sampledat[i+4:i+4+nal_len]) - i = i+4+nal_len - assert i == len(sampledat) - - rawdat = self.prefix+''.join(rawdat) - - return frame_b, num_frames, 0, rawdat - -class MP4FrameReader(MP4GOPReader, GOPFrameReader): - def __init__(self, fn, readahead=False): - MP4GOPReader.__init__(self, fn) - GOPFrameReader.__init__(self, readahead) - -class StreamGOPReader(GOPReader): - def __init__(self, fn, frame_type, index_data): - self.fn = fn - - self.frame_type = frame_type - self.frame_count = None - self.w, self.h = None, None - - self.prefix = None - self.index = None - - self.index = index_data['index'] - self.prefix = index_data['global_prefix'] - probe = index_data['probe'] - - if self.frame_type == FrameType.h265_stream: - self.prefix_frame_data = None - self.num_prefix_frames = 0 - self.vid_fmt = "hevc" - - elif self.frame_type == FrameType.h264_pstream: - self.prefix_frame_data = index_data['prefix_frame_data'] - self.num_prefix_frames = index_data['num_prefix_frames'] - - self.vid_fmt = "h264" - - i = 0 - while i < self.index.shape[0] and self.index[i, 0] != SLICE_I: - i += 1 - self.first_iframe = i - - if self.frame_type == FrameType.h265_stream: - assert self.first_iframe == 0 - - self.frame_count = len(self.index)-1 - - self.w = probe['streams'][0]['width'] - self.h = probe['streams'][0]['height'] - - - def _lookup_gop(self, num): - frame_b = num - while frame_b > 0 and self.index[frame_b, 0] != SLICE_I: - frame_b -= 1 - - frame_e = num+1 - while frame_e < (len(self.index)-1) and self.index[frame_e, 0] != SLICE_I: - frame_e += 1 - - offset_b = self.index[frame_b, 1] - offset_e = self.index[frame_e, 1] - - return (frame_b, frame_e, offset_b, offset_e) - - def get_gop(self, num): - frame_b, frame_e, offset_b, offset_e = self._lookup_gop(num) - assert frame_b <= num < frame_e - - num_frames = frame_e-frame_b - - with FileReader(self.fn) as f: - f.seek(offset_b) - rawdat = f.read(offset_e-offset_b) - - if num < self.first_iframe: - assert self.prefix_frame_data - rawdat = self.prefix_frame_data + rawdat - - rawdat = self.prefix + rawdat - - skip_frames = 0 - if num < self.first_iframe: - skip_frames = self.num_prefix_frames - - return frame_b, num_frames, skip_frames, rawdat class StreamFrameReader(StreamGOPReader, GOPFrameReader): def __init__(self, fn, frame_type, index_data, readahead=False, readbehind=False, multithreaded=False): @@ -1049,16 +572,12 @@ class StreamFrameReader(StreamGOPReader, GOPFrameReader): GOPFrameReader.__init__(self, readahead, readbehind, multithreaded) - - def GOPFrameIterator(gop_reader, pix_fmt, multithreaded=True): # this is really ugly. ill think about how to refactor it when i can think good - IN_FLIGHT_GOPS = 6 # should be enough that the stream decompressor starts returning data - - with VideoStreamDecompressor( - gop_reader.vid_fmt, gop_reader.w, gop_reader.h, pix_fmt, multithreaded) as dec: + IN_FLIGHT_GOPS = 6 # should be enough that the stream decompressor starts returning data + with VideoStreamDecompressor(gop_reader.vid_fmt, gop_reader.w, gop_reader.h, pix_fmt, multithreaded) as dec: read_work = [] def readthing(): @@ -1083,79 +602,21 @@ def GOPFrameIterator(gop_reader, pix_fmt, multithreaded=True): read_work.append([skip_frames, num_frames]) while len(read_work) >= IN_FLIGHT_GOPS: - for v in readthing(): yield v + for v in readthing(): + yield v dec.eos() while read_work: - for v in readthing(): yield v + for v in readthing(): + yield v def FrameIterator(fn, pix_fmt, **kwargs): fr = FrameReader(fn, **kwargs) if isinstance(fr, GOPReader): - for v in GOPFrameIterator(fr, pix_fmt, kwargs.get("multithreaded", True)): yield v + for v in GOPFrameIterator(fr, pix_fmt, kwargs.get("multithreaded", True)): + yield v else: for i in range(fr.frame_count): yield fr.get(i, pix_fmt=pix_fmt)[0] - - -def FrameWriter(ofn, frames, vid_fmt=FrameType.ffvhuff_mkv, pix_fmt="rgb24", framerate=20, multithreaded=False): - if pix_fmt not in ("rgb24", "yuv420p"): - raise NotImplementedError - - if vid_fmt == FrameType.ffv1_mkv: - assert ofn.endswith(".mkv") - vcodec = "ffv1" - elif vid_fmt == FrameType.ffvhuff_mkv: - assert ofn.endswith(".mkv") - vcodec = "ffvhuff" - else: - raise NotImplementedError - - frame_gen = iter(frames) - first_frame = next(frame_gen) - - # assert len(frames) > 1 - if pix_fmt == "rgb24": - h, w = first_frame.shape[:2] - elif pix_fmt == "yuv420p": - w = first_frame.shape[1] - h = 2*first_frame.shape[0]//3 - else: - raise NotImplementedError - - compress_proc = subprocess.Popen( - ["ffmpeg", - "-threads", "0" if multithreaded else "1", - "-y", - "-framerate", str(framerate), - "-vsync", "0", - "-f", "rawvideo", - "-pix_fmt", pix_fmt, - "-s", "%dx%d" % (w, h), - "-i", "pipe:0", - "-threads", "0" if multithreaded else "1", - "-f", "matroska", - "-vcodec", vcodec, - "-g", "0", - ofn], - stdin=subprocess.PIPE, stderr=open("/dev/null", "wb")) - try: - compress_proc.stdin.write(first_frame.tobytes()) - for frame in frame_gen: - compress_proc.stdin.write(frame.tobytes()) - compress_proc.stdin.close() - except: - compress_proc.kill() - raise - - assert compress_proc.wait() == 0 - -if __name__ == "__main__": - fn = "cd:/1c79456b0c90f15a/2017-05-10--08-17-00/2/fcamera.hevc" - f = FrameReader(fn) - # print f.get(0, 1).shape - # print f.get(15, 1).shape - for v in GOPFrameIterator(f, "yuv420p"): - print(v) diff --git a/tools/lib/mkvparse/README.md b/tools/lib/mkvparse/README.md deleted file mode 100644 index 2d82f190b9..0000000000 --- a/tools/lib/mkvparse/README.md +++ /dev/null @@ -1,24 +0,0 @@ -Simple easy-to-use hacky matroska parser - -Define your handler class: - - class MyMatroskaHandler(mkvparse.MatroskaHandler): - def tracks_available(self): - ... - - def segment_info_available(self): - ... - - def frame(self, track_id, timestamp, data, more_laced_blocks, duration, keyframe_flag, invisible_flag, discardable_flag): - ... - -and `mkvparse.mkvparse(file, MyMatroskaHandler())` - - -Supports lacing and setting global timecode scale, subtitles (BlockGroup). Does not support cues, tags, chapters, seeking and so on. Supports resyncing when something bad is encountered in matroska stream. - -Also contains example of generation of Matroska files from python - -Subtitles should remain as text, binary data gets encoded to hex. - -Licence=MIT diff --git a/tools/lib/mkvparse/__init__.py b/tools/lib/mkvparse/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tools/lib/mkvparse/mkvgen.py b/tools/lib/mkvparse/mkvgen.py deleted file mode 100755 index a98fa299b0..0000000000 --- a/tools/lib/mkvparse/mkvgen.py +++ /dev/null @@ -1,187 +0,0 @@ -#!/usr/bin/env python -import sys -import random - -# Simple hacky Matroska generator -# Reads mp3 file "q.mp3" and jpeg images from img/0.jpg, img/1.jpg and so on and -# writes Matroska file with mjpeg and mp3 to stdout - -# License=MIT - -# unsigned -def big_endian_number(number): - if(number<0x100): - return chr(number) - return big_endian_number(number>>8) + chr(number&0xFF) - -ben=big_endian_number - -def ebml_encode_number(number): - def trailing_bits(rest_of_number, number_of_bits): - # like big_endian_number, but can do padding zeroes - if number_of_bits==8: - return chr(rest_of_number&0xFF); - else: - return trailing_bits(rest_of_number>>8, number_of_bits-8) + chr(rest_of_number&0xFF) - - if number == -1: - return chr(0xFF) - if number < 2**7 - 1: - return chr(number|0x80) - if number < 2**14 - 1: - return chr(0x40 | (number>>8)) + trailing_bits(number, 8) - if number < 2**21 - 1: - return chr(0x20 | (number>>16)) + trailing_bits(number, 16) - if number < 2**28 - 1: - return chr(0x10 | (number>>24)) + trailing_bits(number, 24) - if number < 2**35 - 1: - return chr(0x08 | (number>>32)) + trailing_bits(number, 32) - if number < 2**42 - 1: - return chr(0x04 | (number>>40)) + trailing_bits(number, 40) - if number < 2**49 - 1: - return chr(0x02 | (number>>48)) + trailing_bits(number, 48) - if number < 2**56 - 1: - return chr(0x01) + trailing_bits(number, 56) - raise Exception("NUMBER TOO BIG") - -def ebml_element(element_id, data, length=None): - if length==None: - length = len(data) - return big_endian_number(element_id) + ebml_encode_number(length) + data - - -def write_ebml_header(f, content_type, version, read_version): - f.write( - ebml_element(0x1A45DFA3, "" # EBML - + ebml_element(0x4286, ben(1)) # EBMLVersion - + ebml_element(0x42F7, ben(1)) # EBMLReadVersion - + ebml_element(0x42F2, ben(4)) # EBMLMaxIDLength - + ebml_element(0x42F3, ben(8)) # EBMLMaxSizeLength - + ebml_element(0x4282, content_type) # DocType - + ebml_element(0x4287, ben(version)) # DocTypeVersion - + ebml_element(0x4285, ben(read_version)) # DocTypeReadVersion - )) - -def write_infinite_segment_header(f): - # write segment element header - f.write(ebml_element(0x18538067,"",-1)) # Segment (unknown length) - -def random_uid(): - def rint(): - return int(random.random()*(0x100**4)) - return ben(rint()) + ben(rint()) + ben(rint()) + ben(rint()) - - -def example(): - write_ebml_header(sys.stdout, "matroska", 2, 2) - write_infinite_segment_header(sys.stdout) - - - # write segment info (optional) - sys.stdout.write(ebml_element(0x1549A966, "" # SegmentInfo - + ebml_element(0x73A4, random_uid()) # SegmentUID - + ebml_element(0x7BA9, "mkvgen.py test") # Title - + ebml_element(0x4D80, "mkvgen.py") # MuxingApp - + ebml_element(0x5741, "mkvgen.py") # WritingApp - )) - - # write trans data (codecs etc.) - sys.stdout.write(ebml_element(0x1654AE6B, "" # Tracks - + ebml_element(0xAE, "" # TrackEntry - + ebml_element(0xD7, ben(1)) # TrackNumber - + ebml_element(0x73C5, ben(0x77)) # TrackUID - + ebml_element(0x83, ben(0x01)) # TrackType - #0x01 track is a video track - #0x02 track is an audio track - #0x03 track is a complex track, i.e. a combined video and audio track - #0x10 track is a logo track - #0x11 track is a subtitle track - #0x12 track is a button track - #0x20 track is a control track - + ebml_element(0x536E, "mjpeg data") # Name - + ebml_element(0x86, "V_MJPEG") # CodecID - #+ ebml_element(0x23E383, ben(100000000)) # DefaultDuration (opt.), nanoseconds - #+ ebml_element(0x6DE7, ben(100)) # MinCache - + ebml_element(0xE0, "" # Video - + ebml_element(0xB0, ben(640)) # PixelWidth - + ebml_element(0xBA, ben(480)) # PixelHeight - ) - ) - + ebml_element(0xAE, "" # TrackEntry - + ebml_element(0xD7, ben(2)) # TrackNumber - + ebml_element(0x73C5, ben(0x78)) # TrackUID - + ebml_element(0x83, ben(0x02)) # TrackType - #0x01 track is a video track - #0x02 track is an audio track - #0x03 track is a complex track, i.e. a combined video and audio track - #0x10 track is a logo track - #0x11 track is a subtitle track - #0x12 track is a button track - #0x20 track is a control track - + ebml_element(0x536E, "content of mp3 file") # Name - #+ ebml_element(0x6DE7, ben(100)) # MinCache - + ebml_element(0x86, "A_MPEG/L3") # CodecID - #+ ebml_element(0xE1, "") # Audio - ) - )) - - - mp3file = open("q.mp3", "rb") - mp3file.read(500000); - - def mp3framesgenerator(f): - debt="" - while True: - for i in range(0,len(debt)+1): - if i >= len(debt)-1: - debt = debt + f.read(8192) - break - #sys.stderr.write("i="+str(i)+" len="+str(len(debt))+"\n") - if ord(debt[i])==0xFF and (ord(debt[i+1]) & 0xF0)==0XF0 and i>700: - if i>0: - yield debt[0:i] - # sys.stderr.write("len="+str(i)+"\n") - debt = debt[i:] - break - - - mp3 = mp3framesgenerator(mp3file) - next(mp3) - - - for i in range(0,530): - framefile = open("img/"+str(i)+".jpg", "rb") - framedata = framefile.read() - framefile.close() - - # write cluster (actual video data) - - if random.random()<1: - sys.stdout.write(ebml_element(0x1F43B675, "" # Cluster - + ebml_element(0xE7, ben(int(i*26*4))) # TimeCode, uint, milliseconds - # + ebml_element(0xA7, ben(0)) # Position, uint - + ebml_element(0xA3, "" # SimpleBlock - + ebml_encode_number(1) # track number - + chr(0x00) + chr(0x00) # timecode, relative to Cluster timecode, sint16, in milliseconds - + chr(0x00) # flags - + framedata - ))) - - for u in range(0,4): - mp3f=next(mp3) - if random.random()<1: - sys.stdout.write(ebml_element(0x1F43B675, "" # Cluster - + ebml_element(0xE7, ben(i*26*4+u*26)) # TimeCode, uint, milliseconds - + ebml_element(0xA3, "" # SimpleBlock - + ebml_encode_number(2) # track number - + chr(0x00) + chr(0x00) # timecode, relative to Cluster timecode, sint16, in milliseconds - + chr(0x00) # flags - + mp3f - ))) - - - - - -if __name__ == '__main__': - example() diff --git a/tools/lib/mkvparse/mkvindex.py b/tools/lib/mkvparse/mkvindex.py deleted file mode 100644 index 694965fbf6..0000000000 --- a/tools/lib/mkvparse/mkvindex.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python -# flake8: noqa - -import re -import binascii - -from tools.lib.mkvparse import mkvparse -from tools.lib.mkvparse import mkvgen -from tools.lib.mkvparse.mkvgen import ben, ebml_element, ebml_encode_number - -class MatroskaIndex(mkvparse.MatroskaHandler): - # def __init__(self, banlist, nocluster_mode): - # pass - def __init__(self): - self.frameindex = [] - - def tracks_available(self): - _, self.config_record = self.tracks[1]['CodecPrivate'] # pylint: disable=no-member - - def frame(self, track_id, timestamp, pos, length, more_laced_frames, duration, - keyframe, invisible, discardable): - self.frameindex.append((pos, length, keyframe)) - - - -def mkvindex(f): - handler = MatroskaIndex() - mkvparse.mkvparse(f, handler) - return handler.config_record, handler.frameindex - - -def simple_gen(of, config_record, w, h, framedata): - mkvgen.write_ebml_header(of, "matroska", 2, 2) - mkvgen.write_infinite_segment_header(of) - - of.write(ebml_element(0x1654AE6B, "" # Tracks - + ebml_element(0xAE, "" # TrackEntry - + ebml_element(0xD7, ben(1)) # TrackNumber - + ebml_element(0x73C5, ben(1)) # TrackUID - + ebml_element(0x83, ben(1)) # TrackType = video track - + ebml_element(0x86, "V_MS/VFW/FOURCC") # CodecID - + ebml_element(0xE0, "" # Video - + ebml_element(0xB0, ben(w)) # PixelWidth - + ebml_element(0xBA, ben(h)) # PixelHeight - ) - + ebml_element(0x63A2, config_record) # CodecPrivate (ffv1 configuration record) - ) - )) - - blocks = [] - for fd in framedata: - blocks.append( - ebml_element(0xA3, "" # SimpleBlock - + ebml_encode_number(1) # track number - + chr(0x00) + chr(0x00) # timecode, relative to Cluster timecode, sint16, in milliseconds - + chr(0x80) # flags (keyframe) - + fd - ) - ) - - of.write(ebml_element(0x1F43B675, "" # Cluster - + ebml_element(0xE7, ben(0)) # TimeCode, uint, milliseconds - # + ebml_element(0xA7, ben(0)) # Position, uint - + ''.join(blocks))) diff --git a/tools/lib/mkvparse/mkvparse.py b/tools/lib/mkvparse/mkvparse.py deleted file mode 100644 index 8e9816347c..0000000000 --- a/tools/lib/mkvparse/mkvparse.py +++ /dev/null @@ -1,763 +0,0 @@ -# Licence==MIT; Vitaly "_Vi" Shukela 2012 - -# Simple easy-to-use hacky matroska parser - -# Supports SimpleBlock and BlockGroup, lacing, TimecodeScale. -# Does not support seeking, cues, chapters and other features. -# No proper EOF handling unfortunately - -# See "mkvuser.py" for the example -# pylint: skip-file -# flake8: noqa - -import traceback -from struct import unpack - -import sys -import datetime - -if sys.version < '3': - range=xrange # pylint disable=undefined-variable -else: - #identity=lambda x:x - def ord(something): - if type(something)==bytes: - if something == b"": - raise StopIteration - return something[0] - else: - return something - -def get_major_bit_number(n): - ''' - Takes uint8, returns number of the most significant bit plus the number with that bit cleared. - Examples: - 0b10010101 -> (0, 0b00010101) - 0b00010101 -> (3, 0b00000101) - 0b01111111 -> (1, 0b00111111) - ''' - if not n: - raise Exception("Bad number") - i=0x80; - r=0 - while not n&i: - r+=1 - i>>=1 - return (r,n&~i); - -def read_matroska_number(f, unmodified=False, signed=False): - ''' - Read ebml number. Unmodified means don't clear the length bit (as in Element IDs) - Returns the number and it's length as a tuple - - See examples in "parse_matroska_number" function - ''' - if unmodified and signed: - raise Exception("Contradictary arguments") - first_byte=f.read(1) - if(first_byte==""): - raise StopIteration - r = ord(first_byte) - (n,r2) = get_major_bit_number(r) - if not unmodified: - r=r2 - # from now "signed" means "negative" - i=n - while i: - r = r * 0x100 + ord(f.read(1)) - i-=1 - if signed: - r-=(2**(7*n+7)-1) - else: - if r==2**(7*n+7)-1: - return (-1, n+1) - return (r,n+1) - -def parse_matroska_number(data, pos, unmodified=False, signed=False): - ''' - Parse ebml number from buffer[pos:]. Just like read_matroska_number. - Unmodified means don't clear the length bit (as in Element IDs) - Returns the number plus the new position in input buffer - - Examples: - "\x81" -> (1, pos+1) - "\x40\x01" -> (1, pos+2) - "\x20\x00\x01" -> (1, pos+3) - "\x3F\xFF\xFF" -> (0x1FFFFF, pos+3) - "\x20\x00\x01" unmodified -> (0x200001, pos+3) - "\xBF" signed -> (0, pos+1) - "\xBE" signed -> (-1, pos+1) - "\xC0" signed -> (1, pos+1) - "\x5F\xEF" signed -> (-16, pos+2) - ''' - if unmodified and signed: - raise Exception("Contradictary arguments") - r = ord(data[pos]) - pos+=1 - (n,r2) = get_major_bit_number(r) - if not unmodified: - r=r2 - # from now "signed" means "negative" - i=n - while i: - r = r * 0x100 + ord(data[pos]) - pos+=1 - i-=1 - if signed: - r-=(2**(7*n+6)-1) - else: - if r==2**(7*n+7)-1: - return (-1, pos) - return (r,pos) - -def parse_xiph_number(data, pos): - ''' - Parse the Xiph lacing number from data[pos:] - Returns the number plus the new position - - Examples: - "\x01" -> (1, pos+1) - "\x55" -> (0x55, pos+1) - "\xFF\x04" -> (0x103, pos+2) - "\xFF\xFF\x04" -> (0x202, pos+3) - "\xFF\xFF\x00" -> (0x1FE, pos+3) - ''' - v = ord(data[pos]) - pos+=1 - - r=0 - while v==255: - r+=v - v = ord(data[pos]) - pos+=1 - - r+=v - return (r, pos) - - -def parse_fixedlength_number(data, pos, length, signed=False): - ''' - Read the big-endian number from data[pos:pos+length] - Returns the number plus the new position - - Examples: - "\x01" -> (0x1, pos+1) - "\x55" -> (0x55, pos+1) - "\x55" signed -> (0x55, pos+1) - "\xFF\x04" -> (0xFF04, pos+2) - "\xFF\x04" signed -> (-0x00FC, pos+2) - ''' - r=0 - for i in range(length): - r=r*0x100+ord(data[pos+i]) - if signed: - if ord(data[pos]) & 0x80: - r-=2**(8*length) - return (r, pos+length) - -def read_fixedlength_number(f, length, signed=False): - """ Read length bytes and parse (parse_fixedlength_number) it. - Returns only the number""" - buf = f.read(length) - (r, pos) = parse_fixedlength_number(buf, 0, length, signed) - return r - -def read_ebml_element_header(f): - ''' - Read Element ID and size - Returns id, element size and this header size - ''' - (id_, n) = read_matroska_number(f, unmodified=True) - (size, n2) = read_matroska_number(f) - return (id_, size, n+n2) - -class EbmlElementType: - VOID=0 - MASTER=1 # read all subelements and return tree. Don't use this too large things like Segment - UNSIGNED=2 - SIGNED=3 - TEXTA=4 - TEXTU=5 - BINARY=6 - FLOAT=7 - DATE=8 - - JUST_GO_ON=10 # For "Segment". - # Actually MASTER, but don't build the tree for all subelements, - # interpreting all child elements as if they were top-level elements - - -EET=EbmlElementType - -# lynx -width=10000 -dump http://matroska.org/technical/specs/index.html -# | sed 's/not 0/not0/g; s/> 0/>0/g; s/Sampling Frequency/SamplingFrequency/g' -# | awk '{print $1 " " $3 " " $8}' -# | grep '\[..\]' -# | perl -ne '/(\S+) (\S+) (.)/; -# $name=$1; $id=$2; $type=$3; -# $id=~s/\[|\]//g; -# %types = (m=>"EET.MASTER", -# u=>"EET.UNSIGNED", -# i=>"EET.SIGNED", -# 8=>"EET.TEXTU", -# s=>"EET.TEXTA", -# b=>"EET.BINARY", -# f=>"EET.FLOAT", -# d=>"EET.DATE"); -# $t=$types{$type}; -# next unless $t; -# $t="EET.JUST_GO_ON" if $name eq "Segment" or $name eq "Cluster"; -# print "\t0x$id: ($t, \"$name\"),\n";' - -element_types_names = { - 0x1A45DFA3: (EET.MASTER, "EBML"), - 0x4286: (EET.UNSIGNED, "EBMLVersion"), - 0x42F7: (EET.UNSIGNED, "EBMLReadVersion"), - 0x42F2: (EET.UNSIGNED, "EBMLMaxIDLength"), - 0x42F3: (EET.UNSIGNED, "EBMLMaxSizeLength"), - 0x4282: (EET.TEXTA, "DocType"), - 0x4287: (EET.UNSIGNED, "DocTypeVersion"), - 0x4285: (EET.UNSIGNED, "DocTypeReadVersion"), - 0xEC: (EET.BINARY, "Void"), - 0xBF: (EET.BINARY, "CRC-32"), - 0x1B538667: (EET.MASTER, "SignatureSlot"), - 0x7E8A: (EET.UNSIGNED, "SignatureAlgo"), - 0x7E9A: (EET.UNSIGNED, "SignatureHash"), - 0x7EA5: (EET.BINARY, "SignaturePublicKey"), - 0x7EB5: (EET.BINARY, "Signature"), - 0x7E5B: (EET.MASTER, "SignatureElements"), - 0x7E7B: (EET.MASTER, "SignatureElementList"), - 0x6532: (EET.BINARY, "SignedElement"), - 0x18538067: (EET.JUST_GO_ON, "Segment"), - 0x114D9B74: (EET.MASTER, "SeekHead"), - 0x4DBB: (EET.MASTER, "Seek"), - 0x53AB: (EET.BINARY, "SeekID"), - 0x53AC: (EET.UNSIGNED, "SeekPosition"), - 0x1549A966: (EET.MASTER, "Info"), - 0x73A4: (EET.BINARY, "SegmentUID"), - 0x7384: (EET.TEXTU, "SegmentFilename"), - 0x3CB923: (EET.BINARY, "PrevUID"), - 0x3C83AB: (EET.TEXTU, "PrevFilename"), - 0x3EB923: (EET.BINARY, "NextUID"), - 0x3E83BB: (EET.TEXTU, "NextFilename"), - 0x4444: (EET.BINARY, "SegmentFamily"), - 0x6924: (EET.MASTER, "ChapterTranslate"), - 0x69FC: (EET.UNSIGNED, "ChapterTranslateEditionUID"), - 0x69BF: (EET.UNSIGNED, "ChapterTranslateCodec"), - 0x69A5: (EET.BINARY, "ChapterTranslateID"), - 0x2AD7B1: (EET.UNSIGNED, "TimecodeScale"), - 0x4489: (EET.FLOAT, "Duration"), - 0x4461: (EET.DATE, "DateUTC"), - 0x7BA9: (EET.TEXTU, "Title"), - 0x4D80: (EET.TEXTU, "MuxingApp"), - 0x5741: (EET.TEXTU, "WritingApp"), - 0x1F43B675: (EET.JUST_GO_ON, "Cluster"), - 0xE7: (EET.UNSIGNED, "Timecode"), - 0x5854: (EET.MASTER, "SilentTracks"), - 0x58D7: (EET.UNSIGNED, "SilentTrackNumber"), - 0xA7: (EET.UNSIGNED, "Position"), - 0xAB: (EET.UNSIGNED, "PrevSize"), - 0xA3: (EET.BINARY, "SimpleBlock"), - 0xA0: (EET.MASTER, "BlockGroup"), - 0xA1: (EET.BINARY, "Block"), - 0xA2: (EET.BINARY, "BlockVirtual"), - 0x75A1: (EET.MASTER, "BlockAdditions"), - 0xA6: (EET.MASTER, "BlockMore"), - 0xEE: (EET.UNSIGNED, "BlockAddID"), - 0xA5: (EET.BINARY, "BlockAdditional"), - 0x9B: (EET.UNSIGNED, "BlockDuration"), - 0xFA: (EET.UNSIGNED, "ReferencePriority"), - 0xFB: (EET.SIGNED, "ReferenceBlock"), - 0xFD: (EET.SIGNED, "ReferenceVirtual"), - 0xA4: (EET.BINARY, "CodecState"), - 0x8E: (EET.MASTER, "Slices"), - 0xE8: (EET.MASTER, "TimeSlice"), - 0xCC: (EET.UNSIGNED, "LaceNumber"), - 0xCD: (EET.UNSIGNED, "FrameNumber"), - 0xCB: (EET.UNSIGNED, "BlockAdditionID"), - 0xCE: (EET.UNSIGNED, "Delay"), - 0xCF: (EET.UNSIGNED, "SliceDuration"), - 0xC8: (EET.MASTER, "ReferenceFrame"), - 0xC9: (EET.UNSIGNED, "ReferenceOffset"), - 0xCA: (EET.UNSIGNED, "ReferenceTimeCode"), - 0xAF: (EET.BINARY, "EncryptedBlock"), - 0x1654AE6B: (EET.MASTER, "Tracks"), - 0xAE: (EET.MASTER, "TrackEntry"), - 0xD7: (EET.UNSIGNED, "TrackNumber"), - 0x73C5: (EET.UNSIGNED, "TrackUID"), - 0x83: (EET.UNSIGNED, "TrackType"), - 0xB9: (EET.UNSIGNED, "FlagEnabled"), - 0x88: (EET.UNSIGNED, "FlagDefault"), - 0x55AA: (EET.UNSIGNED, "FlagForced"), - 0x9C: (EET.UNSIGNED, "FlagLacing"), - 0x6DE7: (EET.UNSIGNED, "MinCache"), - 0x6DF8: (EET.UNSIGNED, "MaxCache"), - 0x23E383: (EET.UNSIGNED, "DefaultDuration"), - 0x23314F: (EET.FLOAT, "TrackTimecodeScale"), - 0x537F: (EET.SIGNED, "TrackOffset"), - 0x55EE: (EET.UNSIGNED, "MaxBlockAdditionID"), - 0x536E: (EET.TEXTU, "Name"), - 0x22B59C: (EET.TEXTA, "Language"), - 0x86: (EET.TEXTA, "CodecID"), - 0x63A2: (EET.BINARY, "CodecPrivate"), - 0x258688: (EET.TEXTU, "CodecName"), - 0x7446: (EET.UNSIGNED, "AttachmentLink"), - 0x3A9697: (EET.TEXTU, "CodecSettings"), - 0x3B4040: (EET.TEXTA, "CodecInfoURL"), - 0x26B240: (EET.TEXTA, "CodecDownloadURL"), - 0xAA: (EET.UNSIGNED, "CodecDecodeAll"), - 0x6FAB: (EET.UNSIGNED, "TrackOverlay"), - 0x6624: (EET.MASTER, "TrackTranslate"), - 0x66FC: (EET.UNSIGNED, "TrackTranslateEditionUID"), - 0x66BF: (EET.UNSIGNED, "TrackTranslateCodec"), - 0x66A5: (EET.BINARY, "TrackTranslateTrackID"), - 0xE0: (EET.MASTER, "Video"), - 0x9A: (EET.UNSIGNED, "FlagInterlaced"), - 0x53B8: (EET.UNSIGNED, "StereoMode"), - 0x53B9: (EET.UNSIGNED, "OldStereoMode"), - 0xB0: (EET.UNSIGNED, "PixelWidth"), - 0xBA: (EET.UNSIGNED, "PixelHeight"), - 0x54AA: (EET.UNSIGNED, "PixelCropBottom"), - 0x54BB: (EET.UNSIGNED, "PixelCropTop"), - 0x54CC: (EET.UNSIGNED, "PixelCropLeft"), - 0x54DD: (EET.UNSIGNED, "PixelCropRight"), - 0x54B0: (EET.UNSIGNED, "DisplayWidth"), - 0x54BA: (EET.UNSIGNED, "DisplayHeight"), - 0x54B2: (EET.UNSIGNED, "DisplayUnit"), - 0x54B3: (EET.UNSIGNED, "AspectRatioType"), - 0x2EB524: (EET.BINARY, "ColourSpace"), - 0x2FB523: (EET.FLOAT, "GammaValue"), - 0x2383E3: (EET.FLOAT, "FrameRate"), - 0xE1: (EET.MASTER, "Audio"), - 0xB5: (EET.FLOAT, "SamplingFrequency"), - 0x78B5: (EET.FLOAT, "OutputSamplingFrequency"), - 0x9F: (EET.UNSIGNED, "Channels"), - 0x7D7B: (EET.BINARY, "ChannelPositions"), - 0x6264: (EET.UNSIGNED, "BitDepth"), - 0xE2: (EET.MASTER, "TrackOperation"), - 0xE3: (EET.MASTER, "TrackCombinePlanes"), - 0xE4: (EET.MASTER, "TrackPlane"), - 0xE5: (EET.UNSIGNED, "TrackPlaneUID"), - 0xE6: (EET.UNSIGNED, "TrackPlaneType"), - 0xE9: (EET.MASTER, "TrackJoinBlocks"), - 0xED: (EET.UNSIGNED, "TrackJoinUID"), - 0xC0: (EET.UNSIGNED, "TrickTrackUID"), - 0xC1: (EET.BINARY, "TrickTrackSegmentUID"), - 0xC6: (EET.UNSIGNED, "TrickTrackFlag"), - 0xC7: (EET.UNSIGNED, "TrickMasterTrackUID"), - 0xC4: (EET.BINARY, "TrickMasterTrackSegmentUID"), - 0x6D80: (EET.MASTER, "ContentEncodings"), - 0x6240: (EET.MASTER, "ContentEncoding"), - 0x5031: (EET.UNSIGNED, "ContentEncodingOrder"), - 0x5032: (EET.UNSIGNED, "ContentEncodingScope"), - 0x5033: (EET.UNSIGNED, "ContentEncodingType"), - 0x5034: (EET.MASTER, "ContentCompression"), - 0x4254: (EET.UNSIGNED, "ContentCompAlgo"), - 0x4255: (EET.BINARY, "ContentCompSettings"), - 0x5035: (EET.MASTER, "ContentEncryption"), - 0x47E1: (EET.UNSIGNED, "ContentEncAlgo"), - 0x47E2: (EET.BINARY, "ContentEncKeyID"), - 0x47E3: (EET.BINARY, "ContentSignature"), - 0x47E4: (EET.BINARY, "ContentSigKeyID"), - 0x47E5: (EET.UNSIGNED, "ContentSigAlgo"), - 0x47E6: (EET.UNSIGNED, "ContentSigHashAlgo"), - 0x1C53BB6B: (EET.MASTER, "Cues"), - 0xBB: (EET.MASTER, "CuePoint"), - 0xB3: (EET.UNSIGNED, "CueTime"), - 0xB7: (EET.MASTER, "CueTrackPositions"), - 0xF7: (EET.UNSIGNED, "CueTrack"), - 0xF1: (EET.UNSIGNED, "CueClusterPosition"), - 0x5378: (EET.UNSIGNED, "CueBlockNumber"), - 0xEA: (EET.UNSIGNED, "CueCodecState"), - 0xDB: (EET.MASTER, "CueReference"), - 0x96: (EET.UNSIGNED, "CueRefTime"), - 0x97: (EET.UNSIGNED, "CueRefCluster"), - 0x535F: (EET.UNSIGNED, "CueRefNumber"), - 0xEB: (EET.UNSIGNED, "CueRefCodecState"), - 0x1941A469: (EET.MASTER, "Attachments"), - 0x61A7: (EET.MASTER, "AttachedFile"), - 0x467E: (EET.TEXTU, "FileDescription"), - 0x466E: (EET.TEXTU, "FileName"), - 0x4660: (EET.TEXTA, "FileMimeType"), - 0x465C: (EET.BINARY, "FileData"), - 0x46AE: (EET.UNSIGNED, "FileUID"), - 0x4675: (EET.BINARY, "FileReferral"), - 0x4661: (EET.UNSIGNED, "FileUsedStartTime"), - 0x4662: (EET.UNSIGNED, "FileUsedEndTime"), - 0x1043A770: (EET.MASTER, "Chapters"), - 0x45B9: (EET.MASTER, "EditionEntry"), - 0x45BC: (EET.UNSIGNED, "EditionUID"), - 0x45BD: (EET.UNSIGNED, "EditionFlagHidden"), - 0x45DB: (EET.UNSIGNED, "EditionFlagDefault"), - 0x45DD: (EET.UNSIGNED, "EditionFlagOrdered"), - 0xB6: (EET.MASTER, "ChapterAtom"), - 0x73C4: (EET.UNSIGNED, "ChapterUID"), - 0x91: (EET.UNSIGNED, "ChapterTimeStart"), - 0x92: (EET.UNSIGNED, "ChapterTimeEnd"), - 0x98: (EET.UNSIGNED, "ChapterFlagHidden"), - 0x4598: (EET.UNSIGNED, "ChapterFlagEnabled"), - 0x6E67: (EET.BINARY, "ChapterSegmentUID"), - 0x6EBC: (EET.UNSIGNED, "ChapterSegmentEditionUID"), - 0x63C3: (EET.UNSIGNED, "ChapterPhysicalEquiv"), - 0x8F: (EET.MASTER, "ChapterTrack"), - 0x89: (EET.UNSIGNED, "ChapterTrackNumber"), - 0x80: (EET.MASTER, "ChapterDisplay"), - 0x85: (EET.TEXTU, "ChapString"), - 0x437C: (EET.TEXTA, "ChapLanguage"), - 0x437E: (EET.TEXTA, "ChapCountry"), - 0x6944: (EET.MASTER, "ChapProcess"), - 0x6955: (EET.UNSIGNED, "ChapProcessCodecID"), - 0x450D: (EET.BINARY, "ChapProcessPrivate"), - 0x6911: (EET.MASTER, "ChapProcessCommand"), - 0x6922: (EET.UNSIGNED, "ChapProcessTime"), - 0x6933: (EET.BINARY, "ChapProcessData"), - 0x1254C367: (EET.MASTER, "Tags"), - 0x7373: (EET.MASTER, "Tag"), - 0x63C0: (EET.MASTER, "Targets"), - 0x68CA: (EET.UNSIGNED, "TargetTypeValue"), - 0x63CA: (EET.TEXTA, "TargetType"), - 0x63C5: (EET.UNSIGNED, "TagTrackUID"), - 0x63C9: (EET.UNSIGNED, "TagEditionUID"), - 0x63C4: (EET.UNSIGNED, "TagChapterUID"), - 0x63C6: (EET.UNSIGNED, "TagAttachmentUID"), - 0x67C8: (EET.MASTER, "SimpleTag"), - 0x45A3: (EET.TEXTU, "TagName"), - 0x447A: (EET.TEXTA, "TagLanguage"), - 0x4484: (EET.UNSIGNED, "TagDefault"), - 0x4487: (EET.TEXTU, "TagString"), - 0x4485: (EET.BINARY, "TagBinary"), - 0x56AA: (EET.UNSIGNED, "CodecDelay"), - 0x56BB: (EET.UNSIGNED, "SeekPreRoll"), - 0xF0: (EET.UNSIGNED, "CueRelativePosition"), - 0x53C0: (EET.UNSIGNED, "AlphaMode"), - 0x55B2: (EET.UNSIGNED, "BitsPerChannel"), - 0x55B5: (EET.UNSIGNED, "CbSubsamplingHorz"), - 0x55B6: (EET.UNSIGNED, "CbSubsamplingVert"), - 0x5654: (EET.TEXTU, "ChapterStringUID"), - 0x55B7: (EET.UNSIGNED, "ChromaSitingHorz"), - 0x55B8: (EET.UNSIGNED, "ChromaSitingVert"), - 0x55B3: (EET.UNSIGNED, "ChromaSubsamplingHorz"), - 0x55B4: (EET.UNSIGNED, "ChromaSubsamplingVert"), - 0x55B0: (EET.MASTER, "Colour"), - 0x234E7A: (EET.UNSIGNED, "DefaultDecodedFieldDuration"), - 0x75A2: (EET.SIGNED, "DiscardPadding"), - 0x9D: (EET.UNSIGNED, "FieldOrder"), - 0x55D9: (EET.FLOAT, "LuminanceMax"), - 0x55DA: (EET.FLOAT, "LuminanceMin"), - 0x55D0: (EET.MASTER, "MasteringMetadata"), - 0x55B1: (EET.UNSIGNED, "MatrixCoefficients"), - 0x55BC: (EET.UNSIGNED, "MaxCLL"), - 0x55BD: (EET.UNSIGNED, "MaxFALL"), - 0x55BB: (EET.UNSIGNED, "Primaries"), - 0x55D5: (EET.FLOAT, "PrimaryBChromaticityX"), - 0x55D6: (EET.FLOAT, "PrimaryBChromaticityY"), - 0x55D3: (EET.FLOAT, "PrimaryGChromaticityX"), - 0x55D4: (EET.FLOAT, "PrimaryGChromaticityY"), - 0x55D1: (EET.FLOAT, "PrimaryRChromaticityX"), - 0x55D2: (EET.FLOAT, "PrimaryRChromaticityY"), - 0x55B9: (EET.UNSIGNED, "Range"), - 0x55BA: (EET.UNSIGNED, "TransferCharacteristics"), - 0x55D7: (EET.FLOAT, "WhitePointChromaticityX"), - 0x55D8: (EET.FLOAT, "WhitePointChromaticityY"), -} - -def read_simple_element(f, type_, size): - date = None - if size==0: - return "" - - if type_==EET.UNSIGNED: - data=read_fixedlength_number(f, size, False) - elif type_==EET.SIGNED: - data=read_fixedlength_number(f, size, True) - elif type_==EET.TEXTA: - data=f.read(size) - data = data.replace(b"\x00", b"") # filter out \0, for gstreamer - data = data.decode("ascii") - elif type_==EET.TEXTU: - data=f.read(size) - data = data.replace(b"\x00", b"") # filter out \0, for gstreamer - data = data.decode("UTF-8") - elif type_==EET.MASTER: - data=read_ebml_element_tree(f, size) - elif type_==EET.DATE: - data=read_fixedlength_number(f, size, True) - data*= 1e-9 - data+= (datetime.datetime(2001, 1, 1) - datetime.datetime(1970, 1, 1)).total_seconds() - # now should be UNIX date - elif type_==EET.FLOAT: - if size==4: - data = f.read(4) - data = unpack(">f", data)[0] - elif size==8: - data = f.read(8) - data = unpack(">d", data)[0] - else: - data=read_fixedlength_number(f, size, False) - sys.stderr.write("mkvparse: Floating point of size %d is not supported\n" % size) - data = None - else: - data=f.read(size) - return data - -def read_ebml_element_tree(f, total_size): - ''' - Build tree of elements, reading f until total_size reached - Don't use for the whole segment, it's not Haskell - - Returns list of pairs (element_name, element_value). - element_value can also be list of pairs - ''' - childs=[] - while(total_size>0): - (id_, size, hsize) = read_ebml_element_header(f) - if size == -1: - sys.stderr.write("mkvparse: Element %x without size? Damaged data? Skipping %d bytes\n" % (id_, size, total_size)) # pylint disable=too-many-format-args - f.read(total_size); - break; - if size>total_size: - sys.stderr.write("mkvparse: Element %x with size %d? Damaged data? Skipping %d bytes\n" % (id_, size, total_size)) - f.read(total_size); - break - type_ = EET.BINARY - name = "unknown_%x"%id_ - if id_ in element_types_names: - (type_, name) = element_types_names[id_] - data = read_simple_element(f, type_, size) - total_size-=(size+hsize) - childs.append((name, (type_, data))) - return childs - - -class MatroskaHandler: - """ User for mkvparse should override these methods """ - def tracks_available(self): - pass - def segment_info_available(self): - pass - def frame(self, track_id, timestamp, data, more_laced_frames, duration, keyframe, invisible, discardable): - pass - def ebml_top_element(self, id_, name_, type_, data_): - pass - def before_handling_an_element(self): - pass - def begin_handling_ebml_element(self, id_, name, type_, headersize, datasize): - return type_ - def element_data_available(self, id_, name, type_, headersize, data): - pass - -def handle_block(buffer, buffer_pos, handler, cluster_timecode, timecode_scale=1000000, duration=None, header_removal_headers_for_tracks={}): - ''' - Decode a block, handling all lacings, send it to handler with appropriate timestamp, track number - ''' - pos=0 - (tracknum, pos) = parse_matroska_number(buffer, pos, signed=False) - (tcode, pos) = parse_fixedlength_number(buffer, pos, 2, signed=True) - flags = ord(buffer[pos]); pos+=1 - f_keyframe = (flags&0x80 == 0x80) - f_invisible = (flags&0x08 == 0x08) - f_discardable = (flags&0x01 == 0x01) - laceflags=flags&0x06 - - block_timecode = (cluster_timecode + tcode)*(timecode_scale*0.000000001) - - header_removal_prefix = b"" - if tracknum in header_removal_headers_for_tracks: - # header_removal_prefix = header_removal_headers_for_tracks[tracknum] - raise NotImplementedError - - if laceflags == 0x00: # no lacing - # buf = buffer[pos:] - handler.frame(tracknum, block_timecode, buffer_pos+pos, len(buffer)-pos, - 0, duration, f_keyframe, f_invisible, f_discardable) - return - - numframes = ord(buffer[pos]); pos+=1 - numframes+=1 - - lengths=[] - - if laceflags == 0x02: # Xiph lacing - accumlength=0 - for i in range(numframes-1): - (l, pos) = parse_xiph_number(buffer, pos) - lengths.append(l) - accumlength+=l - lengths.append(len(buffer)-pos-accumlength) - elif laceflags == 0x06: # EBML lacing - accumlength=0 - if numframes: - (flength, pos) = parse_matroska_number(buffer, pos, signed=False) - lengths.append(flength) - accumlength+=flength - for i in range(numframes-2): - (l, pos) = parse_matroska_number(buffer, pos, signed=True) - flength+=l - lengths.append(flength) - accumlength+=flength - lengths.append(len(buffer)-pos-accumlength) - elif laceflags==0x04: # Fixed size lacing - fl=int((len(buffer)-pos)/numframes) - for i in range(numframes): - lengths.append(fl) - - more_laced_frames=numframes-1 - for i in lengths: - # buf = buffer[pos:pos+i] - handler.frame(tracknum, block_timecode, buffer_pos+pos, i, more_laced_frames, duration, - f_keyframe, f_invisible, f_discardable) - pos+=i - more_laced_frames-=1 - - -def resync(f): - sys.stderr.write("mvkparse: Resyncing\n") - while True: - b = f.read(1); - if b == b"": return (None, None); - if b == b"\x1F": - b2 = f.read(3); - if b2 == b"\x43\xB6\x75": - (seglen, x) = read_matroska_number(f) - return (0x1F43B675, seglen, x+4) # cluster - if b == b"\x18": - b2 = f.read(3) - if b2 == b"\x53\x80\x67": - (seglen, x) = read_matroska_number(f) - return (0x18538067, seglen, x+4) # segment - if b == b"\x16": - b2 = f.read(3) - if b2 == b"\x54\xAE\x6B": - (seglen ,x )= read_matroska_number(f) - return (0x1654AE6B, seglen, x+4) # tracks - - - - -def mkvparse(f, handler): - ''' - Read mkv file f and call handler methods when track or segment information is ready or when frame is read. - Handles lacing, timecodes (except of per-track scaling) - ''' - timecode_scale = 1000000 - current_cluster_timecode = 0 - resync_element_id = None - resync_element_size = None - resync_element_headersize = None - header_removal_headers_for_tracks = {} - while f: - (id_, size, hsize) = (None, None, None) - tree = None - data = None - (type_, name) = (None, None) - try: - if not resync_element_id: - try: - handler.before_handling_an_element() - (id_, size, hsize) = read_ebml_element_header(f) - except StopIteration: - break; - if not (id_ in element_types_names): - sys.stderr.write("mkvparse: Unknown element with id %x and size %d\n"%(id_, size)) - (resync_element_id, resync_element_size, resync_element_headersize) = resync(f) - if resync_element_id: - continue; - else: - break; - else: - id_ = resync_element_id - size=resync_element_size - hsize=resync_element_headersize - resync_element_id = None - resync_element_size = None - resync_element_headersize = None - - (type_, name) = element_types_names[id_] - (type_, name) = element_types_names[id_] - type_ = handler.begin_handling_ebml_element(id_, name, type_, hsize, size) - - if type_ == EET.MASTER: - tree = read_ebml_element_tree(f, size) - data = tree - - except Exception: - traceback.print_exc() - handler.before_handling_an_element() - (resync_element_id, resync_element_size, resync_element_headersize) = resync(f) - if resync_element_id: - continue; - else: - break; - - if name=="EBML" and type(data) == list: - d = dict(tree) - if 'EBMLReadVersion' in d: - if d['EBMLReadVersion'][1]>1: sys.stderr.write("mkvparse: Warning: EBMLReadVersion too big\n") - if 'DocTypeReadVersion' in d: - if d['DocTypeReadVersion'][1]>2: sys.stderr.write("mkvparse: Warning: DocTypeReadVersion too big\n") - dt = d['DocType'][1] - if dt != "matroska" and dt != "webm": - sys.stderr.write("mkvparse: Warning: EBML DocType is not \"matroska\" or \"webm\"") - elif name=="Info" and type(data) == list: - handler.segment_info = tree - handler.segment_info_available() - - d = dict(tree) - if "TimecodeScale" in d: - timecode_scale = d["TimecodeScale"][1] - elif name=="Tracks" and type(data) == list: - handler.tracks={} - for (ten, (_t, track)) in tree: - if ten != "TrackEntry": continue - d = dict(track) - n = d['TrackNumber'][1] - handler.tracks[n]=d - tt = d['TrackType'][1] - if tt==0x01: d['type']='video' - elif tt==0x02: d['type']='audio' - elif tt==0x03: d['type']='complex' - elif tt==0x10: d['type']='logo' - elif tt==0x11: d['type']='subtitle' - elif tt==0x12: d['type']='button' - elif tt==0x20: d['type']='control' - if 'TrackTimecodeScale' in d: - sys.stderr.write("mkvparse: Warning: TrackTimecodeScale is not supported\n") - if 'ContentEncodings' in d: - try: - compr = dict(d["ContentEncodings"][1][0][1][1][0][1][1]) - if compr["ContentCompAlgo"][1] == 3: - header_removal_headers_for_tracks[n] = compr["ContentCompSettings"][1] - else: - sys.stderr.write("mkvparse: Warning: compression other than " \ - "header removal is not supported\n") - except: - sys.stderr.write("mkvparse: Warning: unsuccessfully tried " \ - "to handle header removal compression\n") - handler.tracks_available() - # cluster contents: - elif name=="Timecode" and type_ == EET.UNSIGNED: - data=read_fixedlength_number(f, size, False) - current_cluster_timecode = data; - elif name=="SimpleBlock" and type_ == EET.BINARY: - pos = f.tell() - data=f.read(size) - handle_block(data, pos, handler, current_cluster_timecode, timecode_scale, None, header_removal_headers_for_tracks) - elif name=="BlockGroup" and type_ == EET.MASTER: - d2 = dict(tree) - duration=None - raise NotImplementedError - # if 'BlockDuration' in d2: - # duration = d2['BlockDuration'][1] - # duration = duration*0.000000001*timecode_scale - # if 'Block' in d2: - # handle_block(d2['Block'][1], None, handler, current_cluster_timecode, timecode_scale, duration, header_removal_headers_for_tracks) - else: - if type_!=EET.JUST_GO_ON and type_!=EET.MASTER: - data = read_simple_element(f, type_, size) - - handler.ebml_top_element(id_, name, type_, data); - - - -if __name__ == '__main__': - print("Run mkvuser.py for the example")