Refactoring: extract cutting engine inside a dedicated module.
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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]
|
||||
+33
-256
@@ -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:
|
||||
|
||||
+1
-23
@@ -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 <InputFile (before concatenation)> -c:v copy -an -sn -bsf:v trace_headers -t 0.01\
|
||||
|
||||
Reference in New Issue
Block a user