mirror of
https://github.com/AnimeThemes/animethemes-batch-encoder.git
synced 2026-07-11 01:24:29 +02:00
331 lines
13 KiB
Python
331 lines
13 KiB
Python
from ._bitrate_mode import BitrateMode
|
|
from ._colorspace import Colorspace
|
|
from ._loudnorm_filter import LoudnormFilter
|
|
from ._utils import string_to_seconds
|
|
|
|
import logging
|
|
|
|
|
|
# The class that generates FFmpeg commands for the specific cut in the source file
|
|
# We generate common argument values that can be determined programmatically and then use our config to produce commands
|
|
class EncodeWebM:
|
|
def __init__(self, source_file, seek):
|
|
self.source_file = source_file
|
|
self.seek = seek
|
|
self.loudnorm_filter = LoudnormFilter.from_seek(self.seek)
|
|
self.g = self.get_keyframe_interval()
|
|
self.audio_bitrate = self.get_audio_bitrate()
|
|
self.cbr_bitrate = self.get_cbr_bitrate()
|
|
self.cbr_max_bitrate = self.get_cbr_max_bitrate()
|
|
self.colorspace = Colorspace.value_of(self.source_file)
|
|
|
|
# We want at least 10 keyframes in our encode and consistency in our interval
|
|
def get_keyframe_interval(self) -> int:
|
|
source_file_duration = float(self.source_file.file_format["format"]["duration"])
|
|
|
|
start_time = string_to_seconds(self.seek.ss) if self.seek.ss else 0
|
|
end_time = (
|
|
string_to_seconds(self.seek.to) if self.seek.to else source_file_duration
|
|
)
|
|
duration = end_time - start_time
|
|
|
|
logging.debug(
|
|
f"[EncodeWebm.get_keyframe_interval] "
|
|
f"duration: '{duration}', "
|
|
f"end_time: '{end_time}', "
|
|
f"start_time: '{start_time}'"
|
|
)
|
|
|
|
if duration < 60:
|
|
return 96
|
|
elif duration < 120:
|
|
return 120
|
|
else:
|
|
return 240
|
|
|
|
# Audio must use a default bitrate of 192 kbps
|
|
# Audio must use a bitrate of 320 kbps if the source bitrate is > 320 kbps
|
|
def get_audio_bitrate(self) -> str:
|
|
audio_bitrate = int(self.source_file.audio_format["format"]["bit_rate"])
|
|
|
|
logging.debug(
|
|
f"[EncodeWebm.get_audio_bitrate] audio_bitrate: '{audio_bitrate}'"
|
|
)
|
|
|
|
if audio_bitrate > 320000:
|
|
return "320k"
|
|
else:
|
|
return "192k"
|
|
|
|
# Approximation of target average bitrate near file size limit
|
|
def get_cbr_bitrate(self) -> str:
|
|
resolution = int(self.source_file.video_format["streams"][0]["height"])
|
|
|
|
logging.debug(f"[EncodeWebm.get_cbr_bitrate] resolution: '{resolution}'")
|
|
|
|
if resolution >= 1080:
|
|
return "5600k"
|
|
elif resolution >= 720:
|
|
return "3700k"
|
|
elif resolution >= 576:
|
|
return "3200k"
|
|
else:
|
|
return "2400k"
|
|
|
|
# Approximation of max overall bitrate near file size limit
|
|
def get_cbr_max_bitrate(self) -> str:
|
|
resolution = int(self.source_file.video_format["streams"][0]["height"])
|
|
|
|
logging.debug(f"[EncodeWebm.get_cbr_max_bitrate] resolution: '{resolution}'")
|
|
|
|
if resolution >= 1080:
|
|
return "6400k"
|
|
elif resolution >= 720:
|
|
return "4200k"
|
|
elif resolution >= 576:
|
|
return "3700k"
|
|
else:
|
|
return "3200k"
|
|
|
|
# Limiting file size based on resolution and duration
|
|
def get_limit_file_size(self, video_filters="") -> str:
|
|
source_file_duration = float(self.source_file.file_format["format"]["duration"])
|
|
|
|
start_time = string_to_seconds(self.seek.ss) if self.seek.ss else 0
|
|
end_time = (
|
|
string_to_seconds(self.seek.to) if self.seek.to else source_file_duration
|
|
)
|
|
duration = end_time - start_time
|
|
|
|
for filter in video_filters.split(","):
|
|
if "scale=-1:" in filter:
|
|
resolution = int(filter.split(":")[1])
|
|
break
|
|
else:
|
|
resolution = int(self.source_file.video_format["streams"][0]["height"])
|
|
|
|
logging.debug(f"[EncodeWebm.get_limit_file_size] resolution: '{resolution}'")
|
|
|
|
max_allowed_bitrate = resolution * 6100 + 475000
|
|
max_allowed_size = max_allowed_bitrate * duration
|
|
limit_size = max_allowed_size / 8
|
|
|
|
return str(round(limit_size))
|
|
|
|
# Command to preview a seek
|
|
def preview_seek(self, webm_filename="") -> str:
|
|
return (
|
|
f"ffmpeg {self.seek.get_seek_string()} "
|
|
f"{self.get_audio_filters()} "
|
|
f"-vcodec copy -c:a aac -b:a 128k -sn -f mp4 {webm_filename}.mp4"
|
|
)
|
|
|
|
# First-pass encode
|
|
def get_first_pass(
|
|
self, encoding_mode, crf=None, cbr_bitrate=None, cbr_max_bitrate=None, threads=4
|
|
) -> str:
|
|
return (
|
|
f"ffmpeg {self.colorspace.get_args()} {self.seek.get_seek_string()} "
|
|
f"-pass 1 -passlogfile {self.seek.output_name} "
|
|
f"-map 0:v:{self.source_file.selected_video_stream} "
|
|
f"-map 0:a:{self.source_file.selected_audio_stream} "
|
|
f"-c:v libvpx-vp9 "
|
|
f"{encoding_mode.first_pass_rate_control(cbr_bitrate, cbr_max_bitrate, crf)} "
|
|
f"-cpu-used 4 -g {self.g} -threads {threads} -tile-columns 6 -frame-parallel 0 -auto-alt-ref 1 "
|
|
f"-lag-in-frames 25 -row-mt 1 -pix_fmt yuv420p -an -sn -f webm -y NUL"
|
|
)
|
|
|
|
# Second-pass encode
|
|
def get_second_pass(
|
|
self,
|
|
encoding_mode,
|
|
crf=None,
|
|
cbr_bitrate=None,
|
|
cbr_max_bitrate=None,
|
|
threads=4,
|
|
video_filters="",
|
|
limit_size_enable=True,
|
|
webm_filename="",
|
|
) -> str:
|
|
limit_size = (
|
|
"-fs "
|
|
+ EncodeWebM.get_limit_file_size(self, video_filters=video_filters)
|
|
+ " "
|
|
if limit_size_enable
|
|
else ""
|
|
)
|
|
return (
|
|
f"ffmpeg {self.colorspace.get_args()} {self.seek.get_seek_string()} "
|
|
f"-pass 2 -passlogfile {self.seek.output_name} "
|
|
f"-map 0:v:{self.source_file.selected_video_stream} "
|
|
f"-map 0:a:{self.source_file.selected_audio_stream} "
|
|
f"-c:v libvpx-vp9 "
|
|
f"{encoding_mode.second_pass_rate_control(cbr_bitrate, cbr_max_bitrate, crf)} "
|
|
f"-cpu-used 0 -g {self.g} -threads {threads} {self.get_audio_filters()}{video_filters} -tile-columns 6 "
|
|
f"-frame-parallel 0 -auto-alt-ref 1 -lag-in-frames 25 -row-mt 1 -pix_fmt yuv420p "
|
|
f"-c:a libopus -b:a {self.audio_bitrate} -ar 48k "
|
|
f"{limit_size}"
|
|
f"-map_metadata:g -1 -map_metadata:s:v -1 -map_metadata:s:a -1 -map_chapters -1 -sn -f webm -y {webm_filename}.webm"
|
|
)
|
|
|
|
# Build audio filtergraph for encodes
|
|
def get_audio_filters(self) -> str:
|
|
audio_filters = []
|
|
self.source_file.apply_audio_resampling(audio_filters)
|
|
audio_filters.append(self.loudnorm_filter.get_normalization_filter())
|
|
audio_filters.append(
|
|
self.seek.new_audio_filter
|
|
) if self.seek.new_audio_filter.strip() != "" else None
|
|
return "-af " + ",".join(audio_filters)
|
|
|
|
# Build video filtergraph for encodes
|
|
@staticmethod
|
|
def get_video_filters(config_filter=None) -> str:
|
|
video_filters = []
|
|
|
|
if config_filter is not None:
|
|
video_filters.append(config_filter)
|
|
|
|
if (
|
|
not video_filters
|
|
or len(video_filters[0].strip()) == 0
|
|
or "No Filters" in video_filters
|
|
):
|
|
return ""
|
|
|
|
return " -vf " + ",".join(video_filters)
|
|
|
|
# Build unique WebM filename for encodes
|
|
def get_webm_filename(
|
|
self, crf=None, cbr_bitrate=None, cbr_max_bitrate=None, filter_name=None
|
|
) -> str:
|
|
webm_filename = self.seek.output_name
|
|
|
|
if crf is not None:
|
|
webm_filename += f"-{crf}"
|
|
|
|
if cbr_bitrate is not None:
|
|
webm_filename += f"-{cbr_bitrate}"
|
|
|
|
if cbr_max_bitrate is not None:
|
|
webm_filename += f"-{cbr_max_bitrate}"
|
|
|
|
if filter_name is not None:
|
|
webm_filename += f"-{filter_name}"
|
|
|
|
return webm_filename
|
|
|
|
# Get list of commands in sequence specified by configuration file
|
|
# Sequencing - 1. Encoding Mode, 2: CRF, 3: Filter mapping
|
|
def get_commands(self, encoding_config) -> list[str]:
|
|
file_commands = []
|
|
|
|
if len(encoding_config.video_filters) == 0:
|
|
encoding_config.video_filters.append((None, "No Filters"))
|
|
|
|
logging.debug(
|
|
f"[EncodeWebm.get_commands] encoding_modes: '{encoding_config.encoding_modes}', "
|
|
f"crfs: '{encoding_config.crfs}', "
|
|
f"include_unfiltered: '{encoding_config.include_unfiltered}', "
|
|
f"video_filters: '{encoding_config.video_filters}'"
|
|
)
|
|
|
|
if encoding_config.create_preview:
|
|
file_commands.append(
|
|
self.preview_seek(webm_filename=self.get_webm_filename())
|
|
)
|
|
|
|
for encoding_mode in encoding_config.encoding_modes:
|
|
if BitrateMode.CBR.name == encoding_mode.upper():
|
|
cbr_bitrates = (
|
|
encoding_config.cbr_bitrates
|
|
if encoding_config.cbr_bitrates is not None
|
|
else [self.cbr_bitrate]
|
|
)
|
|
cbr_max_bitrates = (
|
|
encoding_config.cbr_max_bitrates
|
|
if encoding_config.cbr_max_bitrates is not None
|
|
else [self.cbr_max_bitrate]
|
|
)
|
|
|
|
for cbr_bitrate in cbr_bitrates:
|
|
for cbr_max_bitrate in cbr_max_bitrates:
|
|
file_commands.append(
|
|
self.get_first_pass(
|
|
BitrateMode.CBR,
|
|
cbr_bitrate=cbr_bitrate,
|
|
cbr_max_bitrate=cbr_max_bitrate,
|
|
threads=encoding_config.threads,
|
|
)
|
|
)
|
|
for filter_name, filter_value in encoding_config.video_filters:
|
|
file_commands.append(
|
|
self.get_second_pass(
|
|
BitrateMode.CBR,
|
|
cbr_bitrate=cbr_bitrate,
|
|
cbr_max_bitrate=cbr_max_bitrate,
|
|
threads=encoding_config.threads,
|
|
video_filters=EncodeWebM.get_video_filters(
|
|
config_filter=filter_value
|
|
),
|
|
limit_size_enable=encoding_config.limit_size_enable,
|
|
webm_filename=self.get_webm_filename(
|
|
cbr_bitrate=cbr_bitrate,
|
|
cbr_max_bitrate=cbr_max_bitrate,
|
|
filter_name=filter_name,
|
|
),
|
|
)
|
|
)
|
|
elif BitrateMode.VBR.name == encoding_mode.upper():
|
|
for crf in encoding_config.crfs:
|
|
file_commands.append(
|
|
self.get_first_pass(
|
|
BitrateMode.VBR, crf=crf, threads=encoding_config.threads
|
|
)
|
|
)
|
|
for filter_name, filter_value in encoding_config.video_filters:
|
|
file_commands.append(
|
|
self.get_second_pass(
|
|
BitrateMode.VBR,
|
|
crf=crf,
|
|
threads=encoding_config.threads,
|
|
video_filters=EncodeWebM.get_video_filters(
|
|
config_filter=filter_value
|
|
),
|
|
limit_size_enable=encoding_config.limit_size_enable,
|
|
webm_filename=self.get_webm_filename(
|
|
crf=crf, filter_name=filter_name
|
|
),
|
|
)
|
|
)
|
|
elif BitrateMode.CQ.name == encoding_mode.upper():
|
|
for crf in encoding_config.crfs:
|
|
file_commands.append(
|
|
self.get_first_pass(
|
|
BitrateMode.CQ, crf=crf, threads=encoding_config.threads
|
|
)
|
|
)
|
|
for filter_name, filter_value in encoding_config.video_filters:
|
|
file_commands.append(
|
|
self.get_second_pass(
|
|
BitrateMode.CQ,
|
|
crf=crf,
|
|
threads=encoding_config.threads,
|
|
video_filters=EncodeWebM.get_video_filters(
|
|
config_filter=filter_value
|
|
),
|
|
limit_size_enable=encoding_config.limit_size_enable,
|
|
webm_filename=self.get_webm_filename(
|
|
crf=crf,
|
|
cbr_bitrate=self.cbr_bitrate,
|
|
filter_name=filter_name,
|
|
),
|
|
)
|
|
)
|
|
|
|
logging.debug(
|
|
f"[EncodeWebm.get_commands] # of file_commands: '{len(file_commands)}'"
|
|
)
|
|
|
|
return file_commands
|