File size: 5,099 Bytes
7344bef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
SDR_VIDEO_CODEC_CHOICES = [
    ("x265 CRF 28 (Balanced)", "libx265_28"),
    ("x264 Level 8 (Balanced)", "libx264_8"),
    ("x265 CRF 8 (High Quality)", "libx265_8"),
    ("x264 Level 10 (High Quality)", "libx264_10"),
    ("x264 Lossless", "libx264_lossless"),
    ("ProRes 422 (editing)", "prores_422"),
    ("DNxHR HQ (editing)", "dnxhr_hq"),
]

VIDEO_CONTAINER_CHOICES = [
    ("MP4", "mp4"),
    ("MOV / QuickTime", "mov"),
    ("MKV / Matroska", "mkv"),
]

SUPPORTED_VIDEO_CONTAINERS = {"mkv", "mov", "mp4"}
CONFIG_VIDEO_CONTAINERS = {value for _, value in VIDEO_CONTAINER_CHOICES}
PROFESSIONAL_VIDEO_CODECS = {"prores_422", "dnxhr_hq"}
QUICKTIME_AUDIO_CODEC_KEYS = {"aac_128", "aac_192", "aac_256", "aac_320", "alac"}


def normalize_video_container(container: str | None) -> str:
    return str(container or "mp4").strip().lower() or "mp4"


def normalize_video_codec(codec_key: str | None) -> str:
    return str(codec_key or "libx264_8").strip().lower() or "libx264_8"


def normalize_video_audio_codec(codec_key: str | None) -> str:
    return str(codec_key or "aac_128").strip().lower() or "aac_128"


def get_video_container_extension(container: str | None) -> str:
    container = normalize_video_container(container)
    return f".{container}" if container in SUPPORTED_VIDEO_CONTAINERS else ".mp4"


def get_video_encode_args(codec_key: str | None, container: str | None) -> list[str]:
    codec_key = normalize_video_codec(codec_key)
    container = normalize_video_container(container)
    if codec_key == "libx264_8":
        return ["-c:v", "libx264", "-crf", "23", "-pix_fmt", "yuv420p"]
    if codec_key == "libx264_10":
        return ["-c:v", "libx264", "-crf", "21", "-pix_fmt", "yuv420p"]
    if codec_key == "libx265_28":
        return ["-c:v", "libx265", "-crf", "28", "-pix_fmt", "yuv420p", "-x265-params", "log-level=none"]
    if codec_key == "libx265_8":
        return ["-c:v", "libx265", "-crf", "8", "-pix_fmt", "yuv420p", "-x265-params", "log-level=none"]
    if codec_key == "libx264_lossless":
        if container == "mkv":
            return ["-c:v", "ffv1", "-pix_fmt", "rgb24"]
        return ["-c:v", "libx264", "-crf", "0", "-pix_fmt", "yuv444p"]
    if codec_key == "prores_422":
        return ["-c:v", "prores_ks", "-profile:v", "2", "-pix_fmt", "yuv422p10le"]
    if codec_key == "dnxhr_hq":
        return ["-c:v", "dnxhd", "-profile:v", "dnxhr_hq", "-pix_fmt", "yuv422p"]
    return ["-c:v", "libx264", "-crf", "23", "-pix_fmt", "yuv420p"]


def get_imageio_codec_params(codec_key: str | None, container: str | None) -> dict:
    codec_key = normalize_video_codec(codec_key)
    container = normalize_video_container(container)
    if codec_key == "libx264_8":
        return {"codec": "libx264", "quality": 8, "pixelformat": "yuv420p"}
    if codec_key == "libx264_10":
        return {"codec": "libx264", "quality": 10, "pixelformat": "yuv420p"}
    if codec_key == "libx265_28":
        return {"codec": "libx265", "pixelformat": "yuv420p", "output_params": ["-crf", "28", "-x265-params", "log-level=none", "-hide_banner", "-nostats"]}
    if codec_key == "libx265_8":
        return {"codec": "libx265", "pixelformat": "yuv420p", "output_params": ["-crf", "8", "-x265-params", "log-level=none", "-hide_banner", "-nostats"]}
    if codec_key == "libx264_lossless":
        if container == "mkv":
            return {"codec": "ffv1", "pixelformat": "rgb24"}
        return {"codec": "libx264", "output_params": ["-crf", "0"], "pixelformat": "yuv444p"}
    if codec_key == "prores_422":
        return {"codec": "prores_ks", "pixelformat": "yuv422p10le", "output_params": ["-profile:v", "2", "-hide_banner", "-nostats"]}
    if codec_key == "dnxhr_hq":
        return {"codec": "dnxhd", "pixelformat": "yuv422p", "output_params": ["-profile:v", "dnxhr_hq", "-hide_banner", "-nostats"]}
    return {"codec": "libx264", "pixelformat": "yuv420p"}


def validate_video_output_settings(video_codec: str | None, video_container: str | None, audio_codec: str | None = None, width: int | None = None, height: int | None = None, *, allowed_containers: set[str] | None = None) -> str | None:
    video_codec = normalize_video_codec(video_codec)
    video_container = normalize_video_container(video_container)
    audio_codec = normalize_video_audio_codec(audio_codec)
    allowed = CONFIG_VIDEO_CONTAINERS if allowed_containers is None else allowed_containers
    if video_container not in allowed:
        return f"Unsupported video container: {video_container}."
    if video_codec in PROFESSIONAL_VIDEO_CODECS and video_container not in {"mkv", "mov"}:
        return "ProRes 422 and DNxHR HQ require the MOV / QuickTime or MKV container."
    if video_container in {"mp4", "mov"} and audio_codec not in QUICKTIME_AUDIO_CODEC_KEYS:
        return f"{video_container.upper()} output does not support audio codec setting '{audio_codec}'."
    if video_codec == "dnxhr_hq" and width is not None and height is not None and (int(width) < 256 or int(height) < 120):
        return "DNxHR HQ output requires a resolution of at least 256x120."
    return None