"""Encoding settings: the immutable description of *what* to do."""
from dataclasses import dataclass, field
#: Extensions scanned by default (always compared lowercase).
DEFAULT_EXTENSIONS = frozenset(
{
".avi", ".mp4", ".flv", ".wmv", ".mkv", ".ts", ".webm", ".m4v",
".mov", ".mpg", ".mpeg", ".m2ts", ".mts", ".3gp",
}
)
#: Encoder name (ffmpeg ``-c:v`` value) -> codec name (as reported by ffprobe).
ENCODER_TO_CODEC = {
"libx265": "hevc",
"libx264": "h264",
"libsvtav1": "av1",
"libaom-av1": "av1",
"librav1e": "av1",
"libvpx-vp9": "vp9",
"libvpx": "vp8",
"mpeg4": "mpeg4",
}
#: Hardware/vendor encoder suffixes stripped by the heuristic of :func:`derive_codec_name`.
_HW_SUFFIXES = ("_nvenc", "_qsv", "_amf", "_videotoolbox", "_vaapi", "_v4l2m2m", "_mf")
#: Subtitle codecs that matroska accepts as-is (``-c:s copy``).
MKV_COPY_SUBS = frozenset(
{
"subrip",
"srt",
"ass",
"ssa",
"webvtt",
"hdmv_pgs_subtitle",
"dvd_subtitle",
"dvb_subtitle",
}
)
#: Text subtitle codecs that must be converted for matroska (``-c:s srt``).
TEXT_CONVERT_SUBS = frozenset({"mov_text", "text"})
#: ffprobe ``field_order`` values meaning the source is interlaced.
INTERLACED_FIELD_ORDERS = frozenset({"tt", "bb", "tb", "bt"})
[docs]
def derive_codec_name(encoder):
"""
Derive the ffprobe codec name matching an encoder name.
Parameters
----------
encoder: :class:`str`
ffmpeg encoder (``-c:v`` value), e.g. ``libx265``, ``hevc_nvenc``.
Returns
-------
:class:`str` or None
Codec name as reported by ffprobe (e.g. ``hevc``), or None if it cannot
be derived. Callers must then require an explicit ``codec_name``.
Examples
--------
>>> derive_codec_name('libx265')
'hevc'
>>> derive_codec_name('hevc_nvenc')
'hevc'
>>> derive_codec_name('libsvtav1')
'av1'
>>> derive_codec_name('mystery_encoder') is None
True
"""
if encoder in ENCODER_TO_CODEC:
return ENCODER_TO_CODEC[encoder]
name = encoder
for suffix in _HW_SUFFIXES:
name = name.removesuffix(suffix)
if name != encoder:
return {"x265": "hevc", "x264": "h264"}.get(name, name)
stripped = name.removeprefix("lib")
if stripped in {"x265", "hevc"}:
return "hevc"
if stripped in {"x264", "h264"}:
return "h264"
if stripped != name and stripped in {"av1", "vp9", "vp8", "aom-av1"}:
return stripped.removeprefix("aom-")
return None
[docs]
@dataclass(frozen=True)
class Settings:
"""
Resolved encoding settings (immutable).
Attributes
----------
codec: :class:`str`
ffmpeg encoder used for transcoding (free choice: ``libx264``,
``libsvtav1``, ``hevc_nvenc``...).
codec_name: :class:`str`, optional
ffprobe codec name considered *compliant* (no transcode needed).
Derived from `codec` when None; an explicit value is required when the
derivation fails.
crf: :class:`int`, optional
Constant rate factor. None keeps the encoder default.
ffmpeg_preset: :class:`str`, optional
ffmpeg ``-preset`` (``medium``, ``slow``...). None keeps the encoder default.
height: :class:`int`, optional
Maximal definition, understood as the short side of the *display*
frame (rotation-aware): a 1920x1080 landscape and a 1080x1920 portrait
are both "1080p" and both get downscaled to 720p when this is 720.
None disables resizing (the CLI/TOML sentinel for None is ``0``).
container: :class:`str`
Output container (extension without dot). Everything is muxed to mkv by
default: it accepts about any codec/subtitle combination.
map_streams: :class:`bool`
Explicitly map video/audio/subtitle streams (``-map 0:V -map 0:a? -map 0:s?``).
When False, keep ffmpeg default stream selection.
replace: :class:`bool`
When True, the original file is deleted after a successful compression.
When False, it is kept next to the output, renamed ``ori_<name>``.
hostname: :class:`str`
SSH alias of the compression server (resolved through ``~/.ssh/config``),
or ``local`` to compress on this machine.
remote_ffmpeg: :class:`str`, optional
Explicit path of the ffmpeg binary on the remote server, for hosts
whose non-interactive PATH misses it (typical on Synology). Must be a
space-free ASCII path; forward slashes work on Windows servers too.
remote_workdir: :class:`str`, optional
Workspace directory on the server as seen by the *shell* running
ffmpeg (default: ``.rcomp``, relative to the login home). Space-free
ASCII.
remote_workdir_sftp: :class:`str`, optional
The same workspace as seen by the *SFTP* channel, when the two views
differ. Synology chroots SFTP into a virtual share tree: its ``/home``
is the login home, so ``remote_workdir_sftp = "/home/.rcomp"``
(with the default `remote_workdir`) points both channels at the same
physical directory. Defaults to `remote_workdir`.
extensions: :class:`frozenset` of :class:`str`
File extensions (lowercase, with dot) considered as videos when scanning.
auto_local: :class:`bool`
When True, a hostname that resolves to this very machine switches to
local mode automatically.
auto_purge_days: :class:`float`, optional
Age threshold for the opportunistic purge of the remote workspace at
connection time. None disables it (the TOML sentinel for None is ``0``).
"""
codec: str = "libx265"
codec_name: str | None = None
crf: int | None = None
ffmpeg_preset: str | None = None
height: int | None = 720
container: str = "mkv"
map_streams: bool = True
replace: bool = False
hostname: str = "remote_host"
remote_ffmpeg: str | None = None
remote_workdir: str | None = None
remote_workdir_sftp: str | None = None
extensions: frozenset = field(default=DEFAULT_EXTENSIONS)
auto_local: bool = True
auto_purge_days: float | None = 7.0
@property
def effective_codec_name(self):
"""
:class:`str`: ffprobe codec name considered compliant.
Raises
------
ValueError
If `codec_name` is not set and cannot be derived from `codec`.
"""
if self.codec_name is not None:
return self.codec_name
derived = derive_codec_name(self.codec)
if derived is None:
msg = (
f"Cannot derive the probe codec name from encoder '{self.codec}'. "
"Set codec_name explicitly (--codec-name on the command line, "
"or codec_name in the configuration file)."
)
raise ValueError(msg)
return derived