Enhance Avisynth video workflow and metadata tools
This commit is contained in:
+70
-122
@@ -4,12 +4,14 @@ import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
|
||||
from tools.console import PipelineProgressView, ProgressView
|
||||
from tools.video_color import ColorMetadata
|
||||
from tools.video_encode_output import (
|
||||
AudioEncodeOptions,
|
||||
VideoCodecOptions,
|
||||
VideoEncodeRequest,
|
||||
VideoEncodeResult,
|
||||
add_audio_input_options,
|
||||
add_audio_options,
|
||||
@@ -17,78 +19,46 @@ from tools.video_encode_output import (
|
||||
pixel_format_bit_depth,
|
||||
read_ffmpeg_progress,
|
||||
run_y4m_encoder,
|
||||
x264_static_hdr_options,
|
||||
)
|
||||
|
||||
|
||||
def encode_video_with_timestamps(
|
||||
*,
|
||||
y4m_command: list[str],
|
||||
ffmpeg: Path,
|
||||
request: VideoEncodeRequest,
|
||||
mkvmerge: Path,
|
||||
x264: Path | None,
|
||||
script: Path,
|
||||
frame_durations: list[float],
|
||||
audio_source: Path,
|
||||
output: Path,
|
||||
options: VideoCodecOptions,
|
||||
audio_options: AudioEncodeOptions,
|
||||
audio_start: float,
|
||||
audio_duration: float | None,
|
||||
audio_segments: list[tuple[float, float]] | None,
|
||||
audio_tempo: float,
|
||||
audio_sample_rate: int | None,
|
||||
source_has_audio: bool,
|
||||
allow_audio_copy: bool,
|
||||
pixel_format: str,
|
||||
colorspace: str | None,
|
||||
frame_count: int | None = None,
|
||||
progress_label: str | None = None,
|
||||
) -> VideoEncodeResult:
|
||||
if not frame_durations:
|
||||
raise RuntimeError("Cannot encode VFR output with no frame durations")
|
||||
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
request.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Network shares may allow the final output but deny creating temporary folders.
|
||||
with tempfile.TemporaryDirectory(prefix="mbt-vfr-") as temp_dir:
|
||||
temp_root = Path(temp_dir)
|
||||
timecodes = temp_root / "frames.timecodes.txt"
|
||||
write_timecode_v2(timecodes, frame_durations)
|
||||
intermediate = temp_root / (
|
||||
"encoded.h264" if options.codec == "libx264" else "encoded.mkv"
|
||||
"encoded.h264" if request.options.codec == "libx264" else "encoded.mkv"
|
||||
)
|
||||
if options.codec == "libx264":
|
||||
if request.options.codec == "libx264":
|
||||
if x264 is None:
|
||||
raise RuntimeError("x264 CLI is required for timestamp-aware x264 encoding")
|
||||
renderer_returncode, encoder_log = encode_x264_y4m(
|
||||
y4m_command=y4m_command,
|
||||
request=request,
|
||||
x264=x264,
|
||||
script=script,
|
||||
timecodes=timecodes,
|
||||
output=intermediate,
|
||||
options=options,
|
||||
pixel_format=pixel_format,
|
||||
colorspace=colorspace,
|
||||
frame_count=len(frame_durations),
|
||||
progress_label=progress_label,
|
||||
)
|
||||
elif options.codec == "libx265":
|
||||
elif request.options.codec == "libx265":
|
||||
result = encode_video_only_y4m(
|
||||
y4m_command=y4m_command,
|
||||
ffmpeg=ffmpeg,
|
||||
script=script,
|
||||
audio_source=None,
|
||||
output=intermediate,
|
||||
options=options,
|
||||
audio_options=AudioEncodeOptions(mode="none"),
|
||||
pixel_format=pixel_format,
|
||||
colorspace=colorspace,
|
||||
frame_count=len(frame_durations),
|
||||
progress_label=progress_label,
|
||||
replace(request, output=intermediate, audio_options=AudioEncodeOptions(mode="none"))
|
||||
)
|
||||
renderer_returncode = result.renderer_returncode
|
||||
encoder_log = result.encoder_log
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported timestamp-aware codec: {options.codec}")
|
||||
raise RuntimeError(f"Unsupported timestamp-aware codec: {request.options.codec}")
|
||||
|
||||
timed_video = temp_root / "timed.mkv"
|
||||
apply_video_timecodes(
|
||||
@@ -96,27 +66,16 @@ def encode_video_with_timestamps(
|
||||
source=intermediate,
|
||||
timecodes=timecodes,
|
||||
output=timed_video,
|
||||
color_metadata=request.color_metadata,
|
||||
)
|
||||
ffmpeg_returncode = mux_timed_video_with_audio(
|
||||
ffmpeg=ffmpeg,
|
||||
request=request,
|
||||
timed_video=timed_video,
|
||||
audio_source=audio_source,
|
||||
output=output,
|
||||
audio_options=audio_options,
|
||||
audio_start=audio_start,
|
||||
audio_duration=audio_duration,
|
||||
audio_segments=audio_segments,
|
||||
audio_tempo=audio_tempo,
|
||||
audio_sample_rate=audio_sample_rate,
|
||||
source_has_audio=source_has_audio,
|
||||
allow_audio_copy=allow_audio_copy,
|
||||
frame_count=len(frame_durations),
|
||||
progress_label=progress_label,
|
||||
)
|
||||
|
||||
return VideoEncodeResult(
|
||||
script=script,
|
||||
output=output,
|
||||
script=request.script,
|
||||
output=request.output,
|
||||
renderer_returncode=renderer_returncode,
|
||||
ffmpeg_returncode=ffmpeg_returncode,
|
||||
encoder_log=encoder_log,
|
||||
@@ -136,30 +95,22 @@ def write_timecode_v2(path: Path, frame_durations: list[float]) -> None:
|
||||
|
||||
def encode_x264_y4m(
|
||||
*,
|
||||
y4m_command: list[str],
|
||||
request: VideoEncodeRequest,
|
||||
x264: Path,
|
||||
script: Path,
|
||||
timecodes: Path,
|
||||
output: Path,
|
||||
options: VideoCodecOptions,
|
||||
pixel_format: str,
|
||||
colorspace: str | None,
|
||||
frame_count: int | None,
|
||||
progress_label: str | None,
|
||||
) -> tuple[int, str]:
|
||||
run = run_y4m_encoder(
|
||||
y4m_command=y4m_command,
|
||||
script=script,
|
||||
y4m_command=request.y4m_command,
|
||||
script=request.script,
|
||||
encoder_command=x264_command(
|
||||
request=request,
|
||||
x264=x264,
|
||||
timecodes=timecodes,
|
||||
output=output,
|
||||
options=options,
|
||||
pixel_format=pixel_format,
|
||||
colorspace=colorspace,
|
||||
),
|
||||
frame_count=frame_count,
|
||||
progress_label=progress_label or script.name,
|
||||
frame_count=request.frame_count,
|
||||
progress_label=request.progress_label,
|
||||
read_progress=read_x264_progress,
|
||||
)
|
||||
if run.encoder_returncode != 0:
|
||||
@@ -167,12 +118,12 @@ def encode_x264_y4m(
|
||||
if run.renderer_returncode != 0:
|
||||
details += "\nAvisynth runner:\n" + run.renderer_log
|
||||
raise RuntimeError(
|
||||
f"x264 failed for {script} with exit {run.encoder_returncode}:\n"
|
||||
f"x264 failed for {request.script} with exit {run.encoder_returncode}:\n"
|
||||
+ details.strip()
|
||||
)
|
||||
if run.renderer_returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"Avisynth runner failed for {script} with exit {run.renderer_returncode}:\n"
|
||||
f"Avisynth runner failed for {request.script} with exit {run.renderer_returncode}:\n"
|
||||
+ run.renderer_log
|
||||
)
|
||||
return run.renderer_returncode, run.encoder_log
|
||||
@@ -180,12 +131,10 @@ def encode_x264_y4m(
|
||||
|
||||
def x264_command(
|
||||
*,
|
||||
request: VideoEncodeRequest,
|
||||
x264: Path,
|
||||
timecodes: Path,
|
||||
output: Path,
|
||||
options: VideoCodecOptions,
|
||||
pixel_format: str,
|
||||
colorspace: str | None,
|
||||
) -> list[str]:
|
||||
command = [
|
||||
str(x264),
|
||||
@@ -194,24 +143,27 @@ def x264_command(
|
||||
"--tcfile-in",
|
||||
str(timecodes),
|
||||
"--crf",
|
||||
str(options.crf),
|
||||
str(request.options.crf),
|
||||
"--preset",
|
||||
options.preset,
|
||||
request.options.preset,
|
||||
"--output-depth",
|
||||
str(pixel_format_bit_depth(pixel_format)),
|
||||
str(pixel_format_bit_depth(request.pixel_format)),
|
||||
"--output-csp",
|
||||
_x264_output_csp(pixel_format),
|
||||
_x264_output_csp(request.pixel_format),
|
||||
"--verbose",
|
||||
"--no-progress",
|
||||
]
|
||||
if options.profile:
|
||||
command.extend(["--profile", options.profile])
|
||||
if options.level:
|
||||
command.extend(["--level", options.level])
|
||||
if options.threads is not None:
|
||||
command.extend(["--threads", str(options.threads)])
|
||||
if colorspace:
|
||||
command.extend(["--colormatrix", colorspace])
|
||||
if request.options.profile:
|
||||
command.extend(["--profile", request.options.profile])
|
||||
if request.options.level:
|
||||
command.extend(["--level", request.options.level])
|
||||
if request.options.threads is not None:
|
||||
command.extend(["--threads", str(request.options.threads)])
|
||||
if request.color_metadata is not None:
|
||||
command.extend(request.color_metadata.x264_args())
|
||||
elif request.colorspace:
|
||||
command.extend(["--colormatrix", request.colorspace])
|
||||
command.extend(x264_static_hdr_options(request.static_hdr))
|
||||
command.extend(["--output", str(output), "-"])
|
||||
return command
|
||||
|
||||
@@ -230,6 +182,7 @@ def apply_video_timecodes(
|
||||
source: Path,
|
||||
timecodes: Path,
|
||||
output: Path,
|
||||
color_metadata: ColorMetadata | None = None,
|
||||
) -> None:
|
||||
completed = subprocess.run(
|
||||
[
|
||||
@@ -241,6 +194,7 @@ def apply_video_timecodes(
|
||||
str(output),
|
||||
"--timestamps",
|
||||
f"0:{timecodes}",
|
||||
*(color_metadata.mkvmerge_args() if color_metadata else []),
|
||||
str(source),
|
||||
],
|
||||
capture_output=True,
|
||||
@@ -256,23 +210,11 @@ def apply_video_timecodes(
|
||||
|
||||
def mux_timed_video_with_audio(
|
||||
*,
|
||||
ffmpeg: Path,
|
||||
request: VideoEncodeRequest,
|
||||
timed_video: Path,
|
||||
audio_source: Path,
|
||||
output: Path,
|
||||
audio_options: AudioEncodeOptions,
|
||||
audio_start: float,
|
||||
audio_duration: float | None,
|
||||
audio_segments: list[tuple[float, float]] | None,
|
||||
audio_tempo: float,
|
||||
audio_sample_rate: int | None,
|
||||
source_has_audio: bool,
|
||||
allow_audio_copy: bool,
|
||||
frame_count: int | None,
|
||||
progress_label: str | None,
|
||||
) -> int:
|
||||
command = [
|
||||
str(ffmpeg),
|
||||
str(request.ffmpeg),
|
||||
"-y",
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
@@ -283,31 +225,37 @@ def mux_timed_video_with_audio(
|
||||
"-i",
|
||||
str(timed_video),
|
||||
]
|
||||
if audio_options.mode != "none" and source_has_audio:
|
||||
if request.audio_options.mode != "none" and request.source_has_audio:
|
||||
add_audio_input_options(
|
||||
command,
|
||||
audio_options=audio_options,
|
||||
audio_start=audio_start,
|
||||
audio_duration=audio_duration,
|
||||
allow_audio_copy=allow_audio_copy,
|
||||
audio_options=request.audio_options,
|
||||
audio_start=request.audio_start,
|
||||
audio_duration=request.audio_duration,
|
||||
allow_audio_copy=request.allow_audio_copy,
|
||||
)
|
||||
command.extend(["-i", str(audio_source)])
|
||||
command.extend(["-i", str(request.audio_source)])
|
||||
|
||||
command.extend(["-map", "0:v:0", "-c:v", "copy"])
|
||||
if output.suffix.lower() == ".mp4":
|
||||
command.extend(["-video_track_timescale", "90000"])
|
||||
if request.color_metadata:
|
||||
command.extend(request.color_metadata.ffmpeg_args())
|
||||
if request.options.codec == "libx265":
|
||||
bitstream_filter = request.color_metadata.hevc_bitstream_filter() if request.color_metadata else None
|
||||
if bitstream_filter:
|
||||
command.extend(["-bsf:v", bitstream_filter])
|
||||
if request.output.suffix.lower() == ".mp4":
|
||||
command.extend(["-video_track_timescale", "90000", "-movflags", "+write_colr"])
|
||||
add_audio_options(
|
||||
command,
|
||||
audio_options=audio_options,
|
||||
audio_start=audio_start,
|
||||
audio_duration=audio_duration,
|
||||
audio_segments=audio_segments,
|
||||
audio_tempo=audio_tempo,
|
||||
audio_sample_rate=audio_sample_rate,
|
||||
source_has_audio=source_has_audio,
|
||||
allow_audio_copy=allow_audio_copy,
|
||||
audio_options=request.audio_options,
|
||||
audio_start=request.audio_start,
|
||||
audio_duration=request.audio_duration,
|
||||
audio_segments=request.audio_segments,
|
||||
audio_tempo=request.audio_tempo,
|
||||
audio_sample_rate=request.audio_sample_rate,
|
||||
source_has_audio=request.source_has_audio,
|
||||
allow_audio_copy=request.allow_audio_copy,
|
||||
)
|
||||
command.append(str(output))
|
||||
command.append(str(request.output))
|
||||
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
@@ -317,12 +265,12 @@ def mux_timed_video_with_audio(
|
||||
)
|
||||
ffmpeg_stderr = read_ffmpeg_progress(
|
||||
process,
|
||||
total_frames=frame_count,
|
||||
label=f"Muxing {progress_label or output.name}",
|
||||
total_frames=request.frame_count,
|
||||
label=f"Muxing {request.progress_label}",
|
||||
)
|
||||
if process.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"FFmpeg failed while muxing timestamped output {output} with exit "
|
||||
f"FFmpeg failed while muxing timestamped output {request.output} with exit "
|
||||
f"{process.returncode}:\n{ffmpeg_stderr}"
|
||||
)
|
||||
return process.returncode
|
||||
|
||||
Reference in New Issue
Block a user