H264 concatenation functions are moved to cutting module.
This commit is contained in:
+1
-1
@@ -12,8 +12,8 @@ from pathlib import Path
|
||||
|
||||
import coloredlogs
|
||||
|
||||
from tscut.models import ProcessingOptions
|
||||
from tscut.exceptions import TSCutError
|
||||
from tscut.models import ProcessingOptions
|
||||
from tscut.pipeline import process_recording
|
||||
from tscut.tools.discovery import check_required_tools
|
||||
from tscut.tools.timeframe import compare_time_interval, parse_time_interval
|
||||
|
||||
@@ -12,16 +12,10 @@ from tscut.tools.mkvtoolnix import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# TODO: finish this procedure
|
||||
def do_coarse_processing(ffmpeg_path:str, ffprobe_path:str, mkvmerge_path:str,
|
||||
input_file: IO[bytes], begin, end, nb_frames, framerate,
|
||||
files_prefix, streams, width, height, temporaries, dump_mem_fd) -> None:
|
||||
# pylint: disable=W0613
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Internal video with all streams (video, audio and subtitles)
|
||||
internal_mkv_name = f'{files_prefix}.mkv'
|
||||
|
||||
|
||||
+66
-2
@@ -4,10 +4,19 @@
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from os import (
|
||||
SEEK_SET,
|
||||
fstat,
|
||||
lseek,
|
||||
read,
|
||||
write,
|
||||
)
|
||||
from shutil import copyfile
|
||||
from typing import IO, Any
|
||||
from typing import IO, Any, BinaryIO, Sequence, TextIO
|
||||
|
||||
import hexdump
|
||||
from tqdm import tqdm
|
||||
from typeguard import typechecked
|
||||
|
||||
from tscut.exceptions import InvalidMediaError
|
||||
from tscut.h264.avc import get_avc_config_from_h264
|
||||
@@ -25,11 +34,66 @@ from tscut.tools.mkvtoolnix import (
|
||||
remove_video_tracks_from_mkv,
|
||||
)
|
||||
from tscut.tools.timeframe import get_ts_frame
|
||||
from tscut.tscut import concatenate_h264_parts, concatenate_h264_ts_parts
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@typechecked
|
||||
def concatenate_h264_parts(h264parts: Sequence[BinaryIO], output: BinaryIO) -> None:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
total_length = 0
|
||||
for h264 in h264parts:
|
||||
fd = h264.fileno()
|
||||
total_length += fstat(fd).st_size
|
||||
|
||||
logger.info('Total length: %d', total_length)
|
||||
|
||||
outfd = output.fileno()
|
||||
lseek(outfd, 0, SEEK_SET)
|
||||
|
||||
pb = tqdm(total=total_length, unit='bytes', desc='Concatenation')
|
||||
for h264 in h264parts:
|
||||
fd = h264.fileno()
|
||||
lseek(fd, 0, SEEK_SET)
|
||||
while True:
|
||||
buf = read(fd, 1000000)
|
||||
if buf is None or len(buf) == 0:
|
||||
break
|
||||
pos = 0
|
||||
while pos < len(buf):
|
||||
nb_bytes = write(outfd, buf[pos:])
|
||||
pb.update(nb_bytes)
|
||||
pos += nb_bytes
|
||||
|
||||
def concatenate_h264_ts_parts(h264_ts_parts: Sequence[TextIO], output: TextIO) -> None:
|
||||
logger = logging.getLogger(__name__)
|
||||
header = '# timestamp format v2\n'
|
||||
output.write(header)
|
||||
|
||||
last = 0.
|
||||
first = True
|
||||
for part in h264_ts_parts:
|
||||
if first:
|
||||
offset = last
|
||||
else:
|
||||
# TODO: take framerate into account
|
||||
offset = last + 40
|
||||
logger.debug('Parsing file: %s. Offset=%d', part, offset)
|
||||
isheader = part.readline()
|
||||
if (not isheader) or (isheader != header):
|
||||
logger.error('Impossible to find a valid header: "%s"', isheader)
|
||||
exit(-1)
|
||||
while True:
|
||||
line = part.readline()
|
||||
if not line:
|
||||
break
|
||||
ts = offset + float(line)
|
||||
last = max(last,ts)
|
||||
output.write(f'{ts:f}\n')
|
||||
if first:
|
||||
first = False
|
||||
|
||||
def cut_recording(media: PreparedMedia, options: ProcessingOptions,
|
||||
temporaries: list[IO[Any]]) -> CutResult:
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
Useful SPS/PPS discussion:
|
||||
1.https://copyprogramming.com/howto/including-sps-and-pps-in-a-raw-h264-track
|
||||
2. https://gitlab.com/mbunkus/mkvtoolnix/-/issues/2390
|
||||
|
||||
Strategy: a possible way of handling multiple SPS/PPS gracefully.
|
||||
Encode each head and trailer with FFMPEG using only I-frame (to be sure the NAL unit will never refer to another image).
|
||||
Encode using an different SPS-ID all of them (using sps-id parameter of libx264 library, e.g 1 instead of 0).
|
||||
For the video track produce only a raw H264 file and a file containing timestamps of the different frames.
|
||||
For the rest of the tracks (audio, subtitles) produce directly a MKV (this is already done).
|
||||
Concatenate all raw H264 in a giant one (like cat), and the same for timestamps of video frames (to keep sound and video synchronized).
|
||||
Then use mkvmerge to remux the H264 track and the rest of tracks.
|
||||
MKVmerge "concatenate" subcommand is able to concatenate different SPS/PPS data into a bigger Private Codec Data.
|
||||
However, this is proved to be not reliable.
|
||||
Sometimes it results in a AVC context containing a single SPS/PPS.
|
||||
So we have to rely on a manual parsing of the H264 AVC context of original movie and the ones produced for headers and trailers, and then merging them into a bigger AVC context.
|
||||
Then finally, change the Private Codec Data in the final MKV.
|
||||
|
||||
|
||||
Extract SPS/PPS:
|
||||
1. https://gitlab.com/mbunkus/mkvtoolnix/-/issues/2390
|
||||
```bash
|
||||
ffmpeg -i <InputFile (before concatenation)> -c:v copy -an -sn -bsf:v trace_headers -t 0.01 -report -loglevel 0 -f null -
|
||||
``
|
||||
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
'''A module to remove parts of video (.e.g advertisements) with single frame precision.'''
|
||||
|
||||
# Standard modules
|
||||
import logging
|
||||
from os import (
|
||||
SEEK_SET,
|
||||
fstat,
|
||||
lseek,
|
||||
read,
|
||||
write,
|
||||
)
|
||||
from sys import exit
|
||||
from typing import IO, BinaryIO, Sequence, TextIO
|
||||
|
||||
# Third party libraries
|
||||
from tqdm import tqdm
|
||||
from typeguard import typechecked
|
||||
|
||||
from tscut.models import SupportedFormat
|
||||
from tscut.tools.mkvtoolnix import (
|
||||
extract_mkv_part,
|
||||
)
|
||||
|
||||
# Useful SPS/PPS discussion.
|
||||
# https://copyprogramming.com/howto/including-sps-and-pps-in-a-raw-h264-track
|
||||
# https://gitlab.com/mbunkus/mkvtoolnix/-/issues/2390
|
||||
|
||||
# New strategy: a possible way of handling multiple SPS/PPS gracefully.
|
||||
# Encode each head and trailer with FFMPEG using only I-frame (to be sure the NAL unit will never
|
||||
# refer to another image).
|
||||
# Encode using an different SPS-ID all of them (using sps-id parameter of libx264 library, e.g
|
||||
# 1 instead of 0).
|
||||
# For the video track produce only a raw H264 file and a file containing timestamps of the
|
||||
# different frames.
|
||||
# For the rest of the tracks (audio, subtitles) produce directly a MKV (this is already done).
|
||||
# Concatenate all raw H264 in a giant one (like cat), and the same for timestamps of video frames
|
||||
# (to keep sound and video synchronized).
|
||||
# Then use mkvmerge to remux the H264 track and the rest of tracks.
|
||||
# MKVmerge "concatenate" subcommand is able to concatenate different SPS/PPS data into a bigger
|
||||
# Private Codec Data.
|
||||
# However, this is proved to be not reliable. Sometimes it results in a AVC context containing
|
||||
# a single SPS/PPS.
|
||||
# So we have to rely on a manual parsing of the H264 AVC context of original movie
|
||||
# and the ones produced for headers and trailers, and then merging them into a bigger AVC context.
|
||||
# Then finally, change the Private Codec Data in the final MKV.
|
||||
|
||||
|
||||
# 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\
|
||||
# -report -loglevel 0 -f null -
|
||||
|
||||
|
||||
|
||||
|
||||
@typechecked
|
||||
def concatenate_h264_parts(h264parts: Sequence[BinaryIO], output: BinaryIO) -> None:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
total_length = 0
|
||||
for h264 in h264parts:
|
||||
fd = h264.fileno()
|
||||
total_length += fstat(fd).st_size
|
||||
|
||||
logger.info('Total length: %d', total_length)
|
||||
|
||||
outfd = output.fileno()
|
||||
lseek(outfd, 0, SEEK_SET)
|
||||
|
||||
pb = tqdm(total=total_length, unit='bytes', desc='Concatenation')
|
||||
for h264 in h264parts:
|
||||
fd = h264.fileno()
|
||||
lseek(fd, 0, SEEK_SET)
|
||||
while True:
|
||||
buf = read(fd, 1000000)
|
||||
if buf is None or len(buf) == 0:
|
||||
break
|
||||
pos = 0
|
||||
while pos < len(buf):
|
||||
nb_bytes = write(outfd, buf[pos:])
|
||||
pb.update(nb_bytes)
|
||||
pos += nb_bytes
|
||||
|
||||
def concatenate_h264_ts_parts(h264_ts_parts: Sequence[TextIO], output: TextIO) -> None:
|
||||
logger = logging.getLogger(__name__)
|
||||
header = '# timestamp format v2\n'
|
||||
output.write(header)
|
||||
|
||||
last = 0.
|
||||
first = True
|
||||
for part in h264_ts_parts:
|
||||
if first:
|
||||
offset = last
|
||||
else:
|
||||
# TODO: take framerate into account
|
||||
offset = last + 40
|
||||
logger.debug('Parsing file: %s. Offset=%d', part, offset)
|
||||
isheader = part.readline()
|
||||
if (not isheader) or (isheader != header):
|
||||
logger.error('Impossible to find a valid header: "%s"', isheader)
|
||||
exit(-1)
|
||||
while True:
|
||||
line = part.readline()
|
||||
if not line:
|
||||
break
|
||||
ts = offset + float(line)
|
||||
last = max(last,ts)
|
||||
output.write(f'{ts:f}\n')
|
||||
if first:
|
||||
first = False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user