Files
animethemes-batch-encoder/batch_encoder/_encode_webm.py
T

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