Add Avisynth video encoding workflow
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from math import sqrt
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VideoConstraintSpec:
|
||||
width: int
|
||||
height: int
|
||||
bit_depth: int
|
||||
chroma: str
|
||||
fps: float
|
||||
|
||||
|
||||
_CHROMA_RANK = {"420": 0, "422": 1, "444": 2}
|
||||
|
||||
_X264_PROFILES = (
|
||||
("baseline", 8, "420"),
|
||||
("main", 8, "420"),
|
||||
("high", 8, "420"),
|
||||
("high10", 10, "420"),
|
||||
("high422", 10, "422"),
|
||||
("high444", 10, "444"),
|
||||
)
|
||||
|
||||
_X265_PROFILES = (
|
||||
("main", 8, "420"),
|
||||
("main10", 10, "420"),
|
||||
("main12", 12, "420"),
|
||||
("main422-10", 10, "422"),
|
||||
("main422-12", 12, "422"),
|
||||
("main444-8", 8, "444"),
|
||||
("main444-10", 10, "444"),
|
||||
("main444-12", 12, "444"),
|
||||
)
|
||||
|
||||
# (name, maximum luma samples per picture, maximum luma samples per second)
|
||||
_X265_LEVELS = (
|
||||
("1.0", 36_864, 552_960),
|
||||
("2.0", 122_880, 3_686_400),
|
||||
("2.1", 245_760, 7_372_800),
|
||||
("3.0", 552_960, 16_588_800),
|
||||
("3.1", 983_040, 33_177_600),
|
||||
("4.0", 2_228_224, 66_846_720),
|
||||
("4.1", 2_228_224, 133_693_440),
|
||||
("5.0", 8_912_896, 267_386_880),
|
||||
("5.1", 8_912_896, 534_773_760),
|
||||
("5.2", 8_912_896, 1_069_547_520),
|
||||
("6.0", 35_651_584, 1_069_547_520),
|
||||
("6.1", 35_651_584, 2_139_095_040),
|
||||
("6.2", 35_651_584, 4_278_190_080),
|
||||
)
|
||||
|
||||
# (name, maximum macroblocks per frame, maximum macroblocks per second)
|
||||
_X264_LEVELS = (
|
||||
("1.0", 99, 1_485),
|
||||
("1.1", 396, 3_000),
|
||||
("1.2", 396, 6_000),
|
||||
("1.3", 396, 11_880),
|
||||
("2.0", 396, 11_880),
|
||||
("2.1", 792, 19_800),
|
||||
("2.2", 1_620, 20_250),
|
||||
("3.0", 1_620, 40_500),
|
||||
("3.1", 3_600, 108_000),
|
||||
("3.2", 5_120, 216_000),
|
||||
("4.0", 8_192, 245_760),
|
||||
("4.1", 8_192, 245_760),
|
||||
("4.2", 8_704, 522_240),
|
||||
("5.0", 22_080, 589_824),
|
||||
("5.1", 36_864, 983_040),
|
||||
("5.2", 36_864, 2_073_600),
|
||||
("6.0", 139_264, 4_177_920),
|
||||
("6.1", 139_264, 8_355_840),
|
||||
("6.2", 139_264, 16_711_680),
|
||||
)
|
||||
|
||||
|
||||
def profile_names(codec: str) -> list[str]:
|
||||
return [name for name, _, _ in _profiles(codec)]
|
||||
|
||||
|
||||
def profile_supports(codec: str, profile: str, spec: VideoConstraintSpec) -> bool:
|
||||
_, depth, chroma = _profile(codec, profile)
|
||||
return (
|
||||
depth >= spec.bit_depth
|
||||
and _CHROMA_RANK[chroma] >= _CHROMA_RANK[spec.chroma]
|
||||
)
|
||||
|
||||
|
||||
def minimum_profile(codec: str, spec: VideoConstraintSpec) -> str:
|
||||
for name in profile_names(codec):
|
||||
if profile_supports(codec, name, spec):
|
||||
return name
|
||||
raise ValueError(
|
||||
f"{codec} has no profile for "
|
||||
f"{spec.bit_depth}-bit 4:{spec.chroma[1]}:{spec.chroma[2]}"
|
||||
)
|
||||
|
||||
|
||||
def resolve_profile(codec: str, choice: str | None, spec: VideoConstraintSpec) -> str | None:
|
||||
if choice is None or choice == "none":
|
||||
return None
|
||||
if choice == "minimum":
|
||||
return minimum_profile(codec, spec)
|
||||
|
||||
if codec == "libx264":
|
||||
names = profile_names(codec)
|
||||
start = names.index(choice)
|
||||
for name in names[start:]:
|
||||
if profile_supports(codec, name, spec):
|
||||
return name
|
||||
return minimum_profile(codec, spec)
|
||||
|
||||
_, requested_depth, requested_chroma = _profile(codec, choice)
|
||||
required_depth = max(requested_depth, spec.bit_depth)
|
||||
required_chroma = max(
|
||||
_CHROMA_RANK[requested_chroma],
|
||||
_CHROMA_RANK[spec.chroma],
|
||||
)
|
||||
candidates = [
|
||||
(name, depth, chroma)
|
||||
for name, depth, chroma in _profiles(codec)
|
||||
if depth >= required_depth and _CHROMA_RANK[chroma] >= required_chroma
|
||||
]
|
||||
if not candidates:
|
||||
return minimum_profile(codec, spec)
|
||||
return min(
|
||||
candidates,
|
||||
key=lambda item: (item[1], _CHROMA_RANK[item[2]]),
|
||||
)[0]
|
||||
|
||||
|
||||
def available_profiles(codec: str, specs: list[VideoConstraintSpec]) -> list[str]:
|
||||
native_capabilities = {
|
||||
_profile(codec, minimum_profile(codec, spec))[1:]
|
||||
for spec in specs
|
||||
}
|
||||
return [
|
||||
name
|
||||
for name, depth, chroma in _profiles(codec)
|
||||
if (depth, chroma) in native_capabilities
|
||||
]
|
||||
|
||||
|
||||
def level_names(codec: str) -> list[str]:
|
||||
return [name for name, _, _ in _levels(codec)]
|
||||
|
||||
|
||||
def level_supports(codec: str, level: str, spec: VideoConstraintSpec) -> bool:
|
||||
_, max_frame, max_rate = _level(codec, level)
|
||||
if codec == "libx264":
|
||||
width = (spec.width + 15) // 16
|
||||
height = (spec.height + 15) // 16
|
||||
else:
|
||||
width = spec.width
|
||||
height = spec.height
|
||||
frame_size = width * height
|
||||
max_dimension = sqrt(max_frame * 8)
|
||||
return (
|
||||
frame_size <= max_frame
|
||||
and width <= max_dimension
|
||||
and height <= max_dimension
|
||||
and frame_size * spec.fps <= max_rate
|
||||
)
|
||||
|
||||
|
||||
def minimum_level(codec: str, spec: VideoConstraintSpec) -> str:
|
||||
for name in level_names(codec):
|
||||
if level_supports(codec, name, spec):
|
||||
return name
|
||||
raise ValueError(
|
||||
f"{codec} has no supported level for {spec.width}x{spec.height} at {spec.fps:.3f} fps"
|
||||
)
|
||||
|
||||
|
||||
def resolve_level(codec: str, choice: str | None, spec: VideoConstraintSpec) -> str | None:
|
||||
if choice is None or choice == "none":
|
||||
return None
|
||||
minimum = minimum_level(codec, spec)
|
||||
if choice == "minimum":
|
||||
return minimum
|
||||
names = level_names(codec)
|
||||
return names[max(names.index(choice), names.index(minimum))]
|
||||
|
||||
|
||||
def available_levels(codec: str, specs: list[VideoConstraintSpec]) -> list[str]:
|
||||
return [
|
||||
name
|
||||
for name in level_names(codec)
|
||||
if any(level_supports(codec, name, spec) for spec in specs)
|
||||
]
|
||||
|
||||
|
||||
def _profiles(codec: str):
|
||||
if codec == "libx264":
|
||||
return _X264_PROFILES
|
||||
if codec == "libx265":
|
||||
return _X265_PROFILES
|
||||
raise ValueError(f"Unsupported codec: {codec}")
|
||||
|
||||
|
||||
def _profile(codec: str, name: str):
|
||||
for profile in _profiles(codec):
|
||||
if profile[0] == name:
|
||||
return profile
|
||||
raise ValueError(f"Unknown {codec} profile: {name}")
|
||||
|
||||
|
||||
def _levels(codec: str):
|
||||
if codec == "libx264":
|
||||
return _X264_LEVELS
|
||||
if codec == "libx265":
|
||||
return _X265_LEVELS
|
||||
raise ValueError(f"Unsupported codec: {codec}")
|
||||
|
||||
|
||||
def _level(codec: str, name: str):
|
||||
for level in _levels(codec):
|
||||
if level[0] == name:
|
||||
return level
|
||||
raise ValueError(f"Unknown {codec} level: {name}")
|
||||
Reference in New Issue
Block a user