From ae1a0405908378a7dcdb5f7a977d8e526f9cb49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Tronel?= Date: Sun, 30 Aug 2026 17:19:03 +0200 Subject: [PATCH] Refactoring: extract cutting engine inside a dedicated module. --- src/tscut/cli.py | 2 +- src/tscut/config.py | 23 ---- src/tscut/cutting.py | 311 ++++++++++++++++++++++++++++++++++++++++++ src/tscut/models.py | 60 ++++++++ src/tscut/pipeline.py | 289 +++++---------------------------------- src/tscut/tscut.py | 24 +--- 6 files changed, 406 insertions(+), 303 deletions(-) delete mode 100644 src/tscut/config.py create mode 100644 src/tscut/cutting.py create mode 100644 src/tscut/models.py diff --git a/src/tscut/cli.py b/src/tscut/cli.py index c1ffda4..d67c104 100644 --- a/src/tscut/cli.py +++ b/src/tscut/cli.py @@ -12,7 +12,7 @@ from pathlib import Path import coloredlogs -from tscut.config import ProcessingOptions +from tscut.models import ProcessingOptions from tscut.exceptions import TSCutError from tscut.pipeline import process_recording from tscut.tools.discovery import check_required_tools diff --git a/src/tscut/config.py b/src/tscut/config.py deleted file mode 100644 index 25dd409..0000000 --- a/src/tscut/config.py +++ /dev/null @@ -1,23 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later -# -# Copyright (C) 2026 Frédéric Tronel - -from dataclasses import dataclass -from datetime import timedelta -from pathlib import Path - - -@dataclass -class ProcessingOptions: - input_file: Path - output_file: Path - parts: list[tuple[timedelta, timedelta]] - tools_paths: dict[str, str] - all_optional_tools: bool - framerate: int|None = None - threshold: int = 0 - coarse: bool = False - subtitles_ocr: bool = True - verbose: bool = False - dump_memory: bool = False - keep_temporaries: bool = False diff --git a/src/tscut/cutting.py b/src/tscut/cutting.py new file mode 100644 index 0000000..d6166c9 --- /dev/null +++ b/src/tscut/cutting.py @@ -0,0 +1,311 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright (C) 2026 Frédéric Tronel + +import logging +from typing import IO, Any +from datetime import timedelta +from shutil import copyfile, move + +import hexdump + +from tscut.exceptions import InvalidMediaError +from tscut.models import PreparedMedia, ProcessingOptions, CutResult +from tscut.tools.ffprobe import ( + find_subtitles_tracks, + get_format, + get_frame_rate, + get_movie_duration, + get_nearest_iframe, + get_streams, +) +from tscut.tools.ffmpeg import extract_all_streams, ffmpeg_convert +from tscut.h264.avc import get_avc_config_from_h264, parse_codec_private +from tscut.tools.mkvtoolnix import ( + change_codec_private_data, + extract_mkv_part, + extract_track_from_mkv, + get_codec_private_data_from_mkv, + merge_mkvs, + remove_video_tracks_from_mkv, + remux_srt_subtitles, +) +from tscut.tools.timeframe import get_ts_frame +from tscut.tscut import ( + concatenate_h264_parts, + concatenate_h264_ts_parts +) +from tscut.matroska.codec import dump_codec_private_data + + +logger = logging.getLogger(__name__) + + +def cut_recording(media: PreparedMedia, options: ProcessingOptions, + temporaries: list[IO[Any]]) -> CutResult: + + # Pour chaque portion + partnum = 0 + mkvparts = [] + h264parts = [] + h264_ts = [] + checks = [] + pos = timedelta() + other_avc_configs = [] + mkvfilename = media.basename+'.mkv' + + + for ts1, ts2 in options.parts: + # TODO: translate comment in english + # Trouver l'estampille de la trame 'I' la plus proche (mais postérieure) au début + # de la portion. + # Trouver l'estampille de la trame 'I' la plus proche (mais antérieure) à la fin + # de la portion. + # On a alors + # debut ----- trame --------- trame --------- fin fin+1 + # 'B/P' 'B/P'* 'I' 'I' 'B/P'* 'B/P' 'I/B/P' + # Si la trame de début est déjà 'I', il n'y a rien à faire. + # Sinon on extrait les trames 'B' ou 'P' depuis le début jusqu'à la trame 'I' non incluse. + # Si la trame de fin précède une trame I, on n'a rien à faire. + # Sinon on extrait toutes les trames depuis la dernière trame I jusqu'à la trame de fin. + + partnum = partnum + 1 + + # Get the nearest I-frame whose timestamp is greater or equal to the beginning. + head_frames = get_nearest_iframe(options.tools_paths['ffprobe'], media.movie, ts1, + before=False) + if head_frames is None: + raise InvalidMediaError("Impossible to retrieve first I-frame") + + # Get the nearest I-frame whose timestamp ... + # TODO: wrong here ... + tail_frames = get_nearest_iframe(options.tools_paths['ffprobe'], media.movie, + ts2, before=True) + if tail_frames is None: + raise InvalidMediaError("Impossible to retrieve last I-frame") + + nb_head_frames, head_iframe = head_frames + nb_tail_frames, tail_iframe = tail_frames + + logger.info("Found %d frames between beginning of current part and first I-frame", + nb_head_frames) + logger.info("Found %d frames between last I-frame and end of current part", + nb_tail_frames) + + head_iframe_ts = get_ts_frame(head_iframe) + if head_iframe_ts is None: + raise InvalidMediaError("Impossible to retrieve timestamp of first i-frame.") + tail_iframe_ts = get_ts_frame(tail_iframe) + if tail_iframe_ts is None: + raise InvalidMediaError("Impossible to retrieve timestamp of final i-frame.") + + checks.append(pos+head_iframe_ts-ts1) + + subparts = [] + + # TODO: separate pipeline processing between coarse and not fine grain options. + + # if args.coarse: + # do_coarse_processing(ffmpeg=paths['ffmpeg'], ffprobe=paths['ffprobe'], input_file=mkv, + # begin=ts1, end=head_iframe_ts, nb_frames=nb_head_frames-1, + # frameRate=frameRate, files_prefix='part-%d-head' % (partnum), + # streams=streams, width=width, height=height, + # temporaries=temporaries, dump_mem_fd=args.dump) + # else: + # doFineGrainProcessing(ffmpeg=paths['ffmpeg'], ffprobe=paths['ffprobe'], + # input_file=mkv, begin=ts1, end=head_iframe_ts, + # nb_frames=nb_head_frames-1, frameRate=frameRate, + # files_prefix='part-%d-head' % (partnum), streams=streams, + # width=width, height=height, temporaries=temporaries, + # dump_mem_fd=args.dump) + + if (not options.coarse) and (nb_head_frames > options.threshold): + # We extract all frames between the beginning upto the frame that immediately preceeds + # the I-frame. + h264_head, h264_head_ts, mkv_head = extract_all_streams( + ffmpeg_path=options.tools_paths['ffmpeg'], + ffprobe_path=options.tools_paths['ffprobe'], + input_file=media.movie, begin=ts1, + end=head_iframe_ts, + nb_frames=nb_head_frames-1, + framerate=media.framerate, + files_prefix=f'part-{partnum:d}-head', + streams=media.streams, width=media.width, + height=media.height, + temporaries=temporaries, + dump_mem_fd=options.dump_memory) + + # If we are not at an exact boundary: + if mkv_head is not None: + subparts.append(mkv_head) + if h264_head is not None: + avcconfig = get_avc_config_from_h264(h264_head) + other_avc_configs.append(avcconfig) + h264parts.append(h264_head) + if h264_head_ts is not None: + h264_ts.append(h264_head_ts) + + # Creating MKV file that corresponds to current part between I-frames + # Internal video with all streams (video, audio and subtitles) + internal_mkv_name = f'part-{partnum:d}-internal.mkv' + # Internal video stream as a raw H264 stream + internal_h264_name = f'part-{partnum:d}-internal.h264' + # Internal video timestamps + internal_h264_ts_name = f'part-{partnum:d}-internal-ts.txt' + # Internal video with only audio and subtitles streams + internal_novideo_mkv_name = f'part-{partnum:d}-internal-novideo.mkv' + + try: + internal_mkv = open(internal_mkv_name, 'wb+') + except OSError: + logger.error('Impossible to create file: %s', internal_mkv_name) + exit(-1) + + try: + internal_novideo_mkv = open(internal_novideo_mkv_name, 'wb+') + except OSError: + logger.error('Impossible to create file: %s', internal_novideo_mkv_name) + exit(-1) + + try: + internal_h264 = open(internal_h264_name, 'wb+') + except OSError: + logger.error('Impossible to create file: %s', internal_h264_name) + exit(-1) + + try: + internal_h264_ts = open(internal_h264_ts_name, 'w+', encoding='utf8') + except OSError: + logger.error('Impossible to create file: %s', internal_h264_ts_name) + exit(-1) + + # logger.info('Merge header, middle and trailer subpart into: %s' % internal_mkv_name) + # Extract internal part of MKV + extract_mkv_part(mkvmerge_path=options.tools_paths['mkvmerge'], input_file=media.movie, + output_file=internal_mkv, begin=head_iframe_ts, end=tail_iframe_ts) + + # Extract video stream of internal part as a raw H264 and its timestamps. + logger.info('Extract video track as raw H264 file.') + extract_track_from_mkv(mkvextract_path=options.tools_paths['mkvextract'], + input_file=internal_mkv, index=0, output_file=internal_h264, + timestamps=internal_h264_ts) + + # Remove video track from internal part of MKV + logger.info('Remove video track from %s', internal_mkv_name) + remove_video_tracks_from_mkv(mkvmerge_path=options.tools_paths['mkvmerge'], + input_file=internal_mkv, output_file=internal_novideo_mkv) + + temporaries.append(internal_mkv) + temporaries.append(internal_h264) + temporaries.append(internal_h264_ts) + temporaries.append(internal_novideo_mkv) + + h264parts.append(internal_h264) + h264_ts.append(internal_h264_ts) + subparts.append(internal_novideo_mkv) + + if (not options.coarse) and (nb_tail_frames > options.threshold): + # We extract all frames between the I-frame (including it) upto the end. + h264_tail, h264_tail_ts, mkv_tail = extract_all_streams(ffmpeg_path=options.tools_paths['ffmpeg'], + ffprobe_path=options.tools_paths['ffprobe'], + input_file=media.movie, begin=tail_iframe_ts, + end=ts2, nb_frames=nb_tail_frames, + framerate=media.framerate, + files_prefix=f'part-{partnum:d}-tail', + streams=media.streams, + width=media.width, height=media.height, + temporaries=temporaries, + dump_mem_fd=options.dump_memory) + + if mkv_tail is not None: + subparts.append(mkv_tail) + if h264_tail is not None: + avcconfig = get_avc_config_from_h264(h264_tail) + other_avc_configs.append(avcconfig) + h264parts.append(h264_tail) + if h264_tail_ts is not None: + h264_ts.append(h264_tail_ts) + + logger.info('Merging MKV: %s', subparts) + + part = merge_mkvs(mkvmerge_path=options.tools_paths['mkvmerge'], inputs=subparts, + output_name=f'part-{partnum:d}.mkv', concatenate=True) + mkvparts.append(part) + temporaries.append(part) + + pos = pos+tail_iframe_ts-ts1 + + # We need to check the end also + checks.append(pos) + + # When using coarse option there is a single AVC configuration. + for avc_config in other_avc_configs: + media.avc_config.merge(avc_config) + logger.debug('Merged AVC configuration: %s', media.avc_config) + + nb_mkv_parts = len(mkvparts) + if nb_mkv_parts > 0: + try: + full_h264 = open(f'{media.basename}-full.h264', 'wb+') + except OSError: + logger.error('Impossible to create file full H264 stream.') + exit(-1) + + logger.info('Merging all H264 tracks') + concatenate_h264_parts(h264parts=h264parts, output=full_h264) + temporaries.append(full_h264) + + try: + full_h264_ts = open(f'{media.basename}-ts.txt', 'w+', encoding='utf8') + except OSError: + logger.error('Impossible to create file containing all video timestamps.') + exit(-1) + + logger.info('Merging H264 timestamps') + concatenate_h264_ts_parts(h264_ts_parts=h264_ts, output=full_h264_ts) + temporaries.append(full_h264_ts) + + final_novideo_name = f'{media.basename}-novideo.mkv' + final_with_video_name = f'{media.basename}-video.mkv' + + if nb_mkv_parts > 1: + logger.info('Merging all audio and subtitles parts: %s', mkvparts) + merge_mkvs(mkvmerge_path=options.tools_paths['mkvmerge'], inputs=mkvparts, + output_name=final_novideo_name, concatenate=True) + elif nb_mkv_parts == 1: + copyfile('part-1.mkv', final_novideo_name) + else: + logger.info("Nothing else to do.") + copyfile(mkvfilename, final_with_video_name) + + if nb_mkv_parts >=1 : + try: + final_novideo = open(final_novideo_name, 'rb') + except OSError: + logger.error('Impossible to open file: %s.', final_novideo_name) + exit(-1) + + temporaries.append(final_novideo) + + full_h264_ts.seek(0) + + logger.info('Merging final video track and all other tracks together') + final_with_video = merge_mkvs(mkvmerge_path=options.tools_paths['mkvmerge'], + inputs=[full_h264, final_novideo], + output_name=final_with_video_name, concatenate=False, + timestamps={0: full_h264_ts}) + final_codec_private_data = dump_codec_private_data(media.avc_config) + logger.debug('Final codec private data: %s', hexdump.dump(final_codec_private_data, + sep=':')) + logger.info('Changing codec private data with the new one.') + change_codec_private_data(options.tools_paths['mkvinfo'], final_with_video, + final_codec_private_data) + + return CutResult( + filename = final_with_video_name, + movie = final_with_video, + check_positions = checks + ) + + + diff --git a/src/tscut/models.py b/src/tscut/models.py new file mode 100644 index 0000000..9173593 --- /dev/null +++ b/src/tscut/models.py @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright (C) 2026 Frédéric Tronel + +from dataclasses import dataclass +from datetime import timedelta +from pathlib import Path +from enum import IntEnum, unique +from typing import BinaryIO, IO + +from tscut.h264.avc import AVCDecoderConfiguration + +@unique +class SupportedFormat(IntEnum): + TS = 1 + MP4 = 2 + MATROSKA = 3 + + def __str__(self): + match self: + case SupportedFormat.TS: + return 'mpegts' + case SupportedFormat.MP4: + return 'mov,mp4,m4a,3gp,3g2,mj2' + case SupportedFormat.MATROSKA: + return 'matroska,webm' + case _: + return 'Unsupported format' + +@dataclass +class ProcessingOptions: + input_file: Path + output_file: Path + parts: list[tuple[timedelta, timedelta]] + tools_paths: dict[str, str] + all_optional_tools: bool + framerate: int|None = None + threshold: int = 0 + coarse: bool = False + subtitles_ocr: bool = True + verbose: bool = False + dump_memory: bool = False + keep_temporaries: bool = False + +@dataclass +class PreparedMedia: + basename: str + movie: BinaryIO + duration: timedelta + framerate: float + streams: list + width: int + height: int + avc_config: AVCDecoderConfiguration + +@dataclass +class CutResult: + filename: str + movie: IO[bytes] + check_positions: list[timedelta] diff --git a/src/tscut/pipeline.py b/src/tscut/pipeline.py index cf0a00f..50634db 100644 --- a/src/tscut/pipeline.py +++ b/src/tscut/pipeline.py @@ -12,12 +12,23 @@ from typing import IO, Any, BinaryIO import hexdump -from tscut.config import ProcessingOptions +from tscut.models import ProcessingOptions, PreparedMedia, SupportedFormat +from tscut.cutting import cut_recording from tscut.exceptions import InvalidMediaError -from tscut.h264.avc import get_avc_config_from_h264, parse_codec_private +from tscut.h264.avc import ( + get_avc_config_from_h264, + parse_codec_private +) from tscut.matroska.codec import dump_codec_private_data -from tscut.subtitles.ocr import do_ocr, extract_srt, get_tesseract_supported_lang -from tscut.tools.ffmpeg import extract_all_streams, ffmpeg_convert +from tscut.subtitles.ocr import ( + do_ocr, + extract_srt, + get_tesseract_supported_lang +) +from tscut.tools.ffmpeg import ( + extract_all_streams, + ffmpeg_convert +) from tscut.tools.ffprobe import ( find_subtitles_tracks, get_format, @@ -36,7 +47,10 @@ from tscut.tools.mkvtoolnix import ( remux_srt_subtitles, ) from tscut.tools.timeframe import get_ts_frame -from tscut.tscut import SupportedFormat, concatenate_h264_parts, concatenate_h264_ts_parts +from tscut.tscut import ( + concatenate_h264_parts, + concatenate_h264_ts_parts +) logger = logging.getLogger(__name__) @@ -182,259 +196,22 @@ def process_recording(options: ProcessingOptions) -> None: iso_avc_config) exit(-1) - # Pour chaque portion - partnum = 0 - mkvparts = [] - h264parts = [] - h264_ts = [] - checks = [] - pos = timedelta() + prepared_movie = PreparedMedia( + basename = basename, + movie = mkv, + duration = duration, + framerate = framerate, + streams = streams, + width = int(width), + height = int(height), + avc_config = main_avc_config + ) - other_avc_configs = [] + cut_movie = cut_recording(prepared_movie, options, temporaries) - for ts1, ts2 in options.parts: - # TODO: translate comment in english - # Trouver l'estampille de la trame 'I' la plus proche (mais postérieure) au début - # de la portion. - # Trouver l'estampille de la trame 'I' la plus proche (mais antérieure) à la fin - # de la portion. - # On a alors - # debut ----- trame --------- trame --------- fin fin+1 - # 'B/P' 'B/P'* 'I' 'I' 'B/P'* 'B/P' 'I/B/P' - # Si la trame de début est déjà 'I', il n'y a rien à faire. - # Sinon on extrait les trames 'B' ou 'P' depuis le début jusqu'à la trame 'I' non incluse. - # Si la trame de fin précède une trame I, on n'a rien à faire. - # Sinon on extrait toutes les trames depuis la dernière trame I jusqu'à la trame de fin. - - partnum = partnum + 1 - - # Get the nearest I-frame whose timestamp is greater or equal to the beginning. - head_frames = get_nearest_iframe(options.tools_paths['ffprobe'], mkv, ts1, before=False) - if head_frames is None: - raise InvalidMediaError("Impossible to retrieve first I-frame") - - # Get the nearest I-frame whose timestamp ... - # TODO: wrong here ... - tail_frames = get_nearest_iframe(options.tools_paths['ffprobe'], mkv, ts2, before=True) - if tail_frames is None: - raise InvalidMediaError("Impossible to retrieve last I-frame") - - nb_head_frames, head_iframe = head_frames - nb_tail_frames, tail_iframe = tail_frames - - logger.info("Found %d frames between beginning of current part and first I-frame", - nb_head_frames) - logger.info("Found %d frames between last I-frame and end of current part", - nb_tail_frames) - - head_iframe_ts = get_ts_frame(head_iframe) - if head_iframe_ts is None: - raise InvalidMediaError("Impossible to retrieve timestamp of first i-frame.") - tail_iframe_ts = get_ts_frame(tail_iframe) - if tail_iframe_ts is None: - raise InvalidMediaError("Impossible to retrieve timestamp of final i-frame.") - - checks.append(pos+head_iframe_ts-ts1) - - subparts = [] - - # TODO: separate pipeline processing between coarse and not fine grain options. - - # if args.coarse: - # do_coarse_processing(ffmpeg=paths['ffmpeg'], ffprobe=paths['ffprobe'], input_file=mkv, - # begin=ts1, end=head_iframe_ts, nb_frames=nb_head_frames-1, - # frameRate=frameRate, files_prefix='part-%d-head' % (partnum), - # streams=streams, width=width, height=height, - # temporaries=temporaries, dump_mem_fd=args.dump) - # else: - # doFineGrainProcessing(ffmpeg=paths['ffmpeg'], ffprobe=paths['ffprobe'], - # input_file=mkv, begin=ts1, end=head_iframe_ts, - # nb_frames=nb_head_frames-1, frameRate=frameRate, - # files_prefix='part-%d-head' % (partnum), streams=streams, - # width=width, height=height, temporaries=temporaries, - # dump_mem_fd=args.dump) - - if (not options.coarse) and (nb_head_frames > options.threshold): - # We extract all frames between the beginning upto the frame that immediately preceeds - # the I-frame. - h264_head, h264_head_ts, mkv_head = extract_all_streams( - ffmpeg_path=options.tools_paths['ffmpeg'], - ffprobe_path=options.tools_paths['ffprobe'], - input_file=mkv, begin=ts1, - end=head_iframe_ts, - nb_frames=nb_head_frames-1, - framerate=framerate, - files_prefix=f'part-{partnum:d}-head', - streams=streams, width=width, - height=height, - temporaries=temporaries, - dump_mem_fd=options.dump_memory) - - # If we are not at an exact boundary: - if mkv_head is not None: - subparts.append(mkv_head) - if h264_head is not None: - avcconfig = get_avc_config_from_h264(h264_head) - other_avc_configs.append(avcconfig) - h264parts.append(h264_head) - if h264_head_ts is not None: - h264_ts.append(h264_head_ts) - - # Creating MKV file that corresponds to current part between I-frames - # Internal video with all streams (video, audio and subtitles) - internal_mkv_name = f'part-{partnum:d}-internal.mkv' - # Internal video stream as a raw H264 stream - internal_h264_name = f'part-{partnum:d}-internal.h264' - # Internal video timestamps - internal_h264_ts_name = f'part-{partnum:d}-internal-ts.txt' - # Internal video with only audio and subtitles streams - internal_novideo_mkv_name = f'part-{partnum:d}-internal-novideo.mkv' - - try: - internal_mkv = open(internal_mkv_name, 'wb+') - except OSError: - logger.error('Impossible to create file: %s', internal_mkv_name) - exit(-1) - - try: - internal_novideo_mkv = open(internal_novideo_mkv_name, 'wb+') - except OSError: - logger.error('Impossible to create file: %s', internal_novideo_mkv_name) - exit(-1) - - try: - internal_h264 = open(internal_h264_name, 'wb+') - except OSError: - logger.error('Impossible to create file: %s', internal_h264_name) - exit(-1) - - try: - internal_h264_ts = open(internal_h264_ts_name, 'w+', encoding='utf8') - except OSError: - logger.error('Impossible to create file: %s', internal_h264_ts_name) - exit(-1) - - # logger.info('Merge header, middle and trailer subpart into: %s' % internal_mkv_name) - # Extract internal part of MKV - extract_mkv_part(mkvmerge_path=options.tools_paths['mkvmerge'], input_file=mkv, - output_file=internal_mkv, begin=head_iframe_ts, end=tail_iframe_ts) - - # Extract video stream of internal part as a raw H264 and its timestamps. - logger.info('Extract video track as raw H264 file.') - extract_track_from_mkv(mkvextract_path=options.tools_paths['mkvextract'], - input_file=internal_mkv, index=0, output_file=internal_h264, - timestamps=internal_h264_ts) - - # Remove video track from internal part of MKV - logger.info('Remove video track from %s', internal_mkv_name) - remove_video_tracks_from_mkv(mkvmerge_path=options.tools_paths['mkvmerge'], - input_file=internal_mkv, output_file=internal_novideo_mkv) - - temporaries.append(internal_mkv) - temporaries.append(internal_h264) - temporaries.append(internal_h264_ts) - temporaries.append(internal_novideo_mkv) - - h264parts.append(internal_h264) - h264_ts.append(internal_h264_ts) - subparts.append(internal_novideo_mkv) - - if (not options.coarse) and (nb_tail_frames > options.threshold): - # We extract all frames between the I-frame (including it) upto the end. - h264_tail, h264_tail_ts, mkv_tail = extract_all_streams(ffmpeg_path=options.tools_paths['ffmpeg'], - ffprobe_path=options.tools_paths['ffprobe'], - input_file=mkv, begin=tail_iframe_ts, - end=ts2, nb_frames=nb_tail_frames, - framerate=framerate, - files_prefix=f'part-{partnum:d}-tail', - streams=streams, - width=width, height=height, - temporaries=temporaries, - dump_mem_fd=options.dump_memory) - - if mkv_tail is not None: - subparts.append(mkv_tail) - if h264_tail is not None: - avcconfig = get_avc_config_from_h264(h264_tail) - other_avc_configs.append(avcconfig) - h264parts.append(h264_tail) - if h264_tail_ts is not None: - h264_ts.append(h264_tail_ts) - - logger.info('Merging MKV: %s', subparts) - - part = merge_mkvs(mkvmerge_path=options.tools_paths['mkvmerge'], inputs=subparts, - output_name=f'part-{partnum:d}.mkv', concatenate=True) - mkvparts.append(part) - temporaries.append(part) - - pos = pos+tail_iframe_ts-ts1 - - # We need to check the end also - checks.append(pos) - - # When using coarse option there is a single AVC configuration. - for avc_config in other_avc_configs: - main_avc_config.merge(avc_config) - logger.debug('Merged AVC configuration: %s', main_avc_config) - - nb_mkv_parts = len(mkvparts) - if nb_mkv_parts > 0: - try: - full_h264 = open(f'{basename}-full.h264', 'wb+') - except OSError: - logger.error('Impossible to create file full H264 stream.') - exit(-1) - - logger.info('Merging all H264 tracks') - concatenate_h264_parts(h264parts=h264parts, output=full_h264) - temporaries.append(full_h264) - - try: - full_h264_ts = open(f'{basename}-ts.txt', 'w+', encoding='utf8') - except OSError: - logger.error('Impossible to create file containing all video timestamps.') - exit(-1) - - logger.info('Merging H264 timestamps') - concatenate_h264_ts_parts(h264_ts_parts=h264_ts, output=full_h264_ts) - temporaries.append(full_h264_ts) - - final_novideo_name = f'{basename}-novideo.mkv' - final_with_video_name = f'{basename}-video.mkv' - - if nb_mkv_parts > 1: - logger.info('Merging all audio and subtitles parts: %s', mkvparts) - merge_mkvs(mkvmerge_path=options.tools_paths['mkvmerge'], inputs=mkvparts, - output_name=final_novideo_name, concatenate=True) - elif nb_mkv_parts == 1: - copyfile('part-1.mkv', final_novideo_name) - else: - logger.info("Nothing else to do.") - copyfile(mkvfilename, final_with_video_name) - - if nb_mkv_parts >=1 : - try: - final_novideo = open(final_novideo_name, 'rb') - except OSError: - logger.error('Impossible to open file: %s.', final_novideo_name) - exit(-1) - - temporaries.append(final_novideo) - - full_h264_ts.seek(0) - - logger.info('Merging final video track and all other tracks together') - final_with_video = merge_mkvs(mkvmerge_path=options.tools_paths['mkvmerge'], - inputs=[full_h264, final_novideo], - output_name=final_with_video_name, concatenate=False, - timestamps={0: full_h264_ts}) - final_codec_private_data = dump_codec_private_data(main_avc_config) - logger.debug('Final codec private data: %s', hexdump.dump(final_codec_private_data, - sep=':')) - logger.info('Changing codec private data with the new one.') - change_codec_private_data(options.tools_paths['mkvinfo'], final_with_video, - final_codec_private_data) + final_with_video_name = cut_movie.filename + final_with_video = cut_movie.movie + checks = cut_movie.check_positions if options.subtitles_ocr: if not options.all_optional_tools: diff --git a/src/tscut/tscut.py b/src/tscut/tscut.py index fe51378..928aa0e 100755 --- a/src/tscut/tscut.py +++ b/src/tscut/tscut.py @@ -3,7 +3,6 @@ # Standard modules import logging -from enum import IntEnum, unique from os import ( SEEK_SET, fstat, @@ -18,6 +17,7 @@ from typing import IO, BinaryIO, Sequence, TextIO from tqdm import tqdm from typeguard import typechecked +from tscut.models import SupportedFormat from tscut.tools.mkvtoolnix import ( extract_mkv_part, ) @@ -46,28 +46,6 @@ from tscut.tools.mkvtoolnix import ( # Then finally, change the Private Codec Data in the final MKV. - - - - - -@unique -class SupportedFormat(IntEnum): - TS = 1 - MP4 = 2 - MATROSKA = 3 - - def __str__(self): - match self: - case SupportedFormat.TS: - return 'mpegts' - case SupportedFormat.MP4: - return 'mov,mp4,m4a,3gp,3g2,mj2' - case SupportedFormat.MATROSKA: - return 'matroska,webm' - case _: - return 'Unsupported format' - # Extract SPS/PPS # https://gitlab.com/mbunkus/mkvtoolnix/-/issues/2390 # ffmpeg -i -c:v copy -an -sn -bsf:v trace_headers -t 0.01\