223 lines
6.1 KiB
Python
223 lines
6.1 KiB
Python
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}")
|