Skip to main content

deepshrink_core/
options.rs

1//! Encoding option enums shared between the CLI and the engine.
2//!
3//! These are the decoded, engine-facing forms of the user's flags (the CLI maps
4//! its clap types onto these). Keeping them here lets `plan` stay a pure
5//! function over well-typed inputs.
6
7/// Video codec choice.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum VideoCodec {
10    H264,
11    H265,
12    Av1,
13}
14
15impl VideoCodec {
16    /// The libav encoder name passed to `ffmpeg -c:v`.
17    pub fn encoder(self) -> &'static str {
18        match self {
19            VideoCodec::H264 => "libx264",
20            VideoCodec::H265 => "libx265",
21            // SVT-AV1 is the practical default: far faster than libaom at
22            // comparable quality, and present in mainstream ffmpeg builds.
23            VideoCodec::Av1 => "libsvtav1",
24        }
25    }
26
27    /// A second encoder to try when [`encoder`](Self::encoder) is missing from
28    /// the local ffmpeg build. Only AV1 has one — x264/x265 are universal.
29    /// Apple's hardware encoder (VideoToolbox) for this codec, if it has one.
30    pub fn hardware_encoder(self) -> Option<&'static str> {
31        match self {
32            VideoCodec::H264 => Some("h264_videotoolbox"),
33            VideoCodec::H265 => Some("hevc_videotoolbox"),
34            VideoCodec::Av1 => None,
35        }
36    }
37
38    pub fn fallback_encoder(self) -> Option<&'static str> {
39        match self {
40            VideoCodec::Av1 => Some("libaom-av1"),
41            _ => None,
42        }
43    }
44
45    /// The `-tag:v` value needed for MP4 compatibility, if any.
46    pub fn mp4_tag(self) -> Option<&'static str> {
47        match self {
48            VideoCodec::H264 => None,
49            // Without hvc1, HEVC in MP4 won't play in QuickTime/Safari.
50            VideoCodec::H265 => Some("hvc1"),
51            VideoCodec::Av1 => Some("av01"),
52        }
53    }
54
55    /// Human-readable label for output.
56    pub fn label(self) -> &'static str {
57        match self {
58            VideoCodec::H264 => "H.264",
59            VideoCodec::H265 => "H.265",
60            VideoCodec::Av1 => "AV1",
61        }
62    }
63
64    /// Inclusive CRF range to search when targeting a VMAF score, best quality
65    /// (lowest CRF) first. x265's CRF scale is shifted ~+6 vs x264 for the same
66    /// perceptual quality, and AV1's runs 0..63 — so the bounds differ per codec.
67    pub fn crf_search_bounds(self) -> (u8, u8) {
68        match self {
69            VideoCodec::H264 => (18, 32),
70            VideoCodec::H265 => (22, 36),
71            VideoCodec::Av1 => (25, 50),
72        }
73    }
74}
75
76/// Audio codec choice (also used for pure-audio in session 003).
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum AudioCodec {
79    Aac,
80    Opus,
81    Mp3,
82}
83
84impl AudioCodec {
85    pub fn encoder(self) -> &'static str {
86        match self {
87            AudioCodec::Aac => "aac",
88            AudioCodec::Opus => "libopus",
89            AudioCodec::Mp3 => "libmp3lame",
90        }
91    }
92
93    /// Output file extension for a pure-audio result.
94    pub fn extension(self) -> &'static str {
95        match self {
96            AudioCodec::Aac => "m4a",
97            AudioCodec::Opus => "opus",
98            AudioCodec::Mp3 => "mp3",
99        }
100    }
101
102    /// Human-readable label for output.
103    pub fn label(self) -> &'static str {
104        match self {
105            AudioCodec::Aac => "AAC",
106            AudioCodec::Opus => "Opus",
107            AudioCodec::Mp3 => "MP3",
108        }
109    }
110}
111
112/// What to do with the audio track inside a video.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum AudioChoice {
115    /// Keep the track, re-encoding at a sensible (budget-aware) bitrate.
116    Keep,
117    /// Keep the track at an explicit bitrate in bits/s.
118    Bitrate(u64),
119    /// Drop the audio entirely.
120    Drop,
121}
122
123/// Target resolution.
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum ResolutionOpt {
126    /// Let the engine pick (downscale only if bitrate is too low).
127    Auto,
128    /// Force a target height (width follows aspect ratio).
129    Height(u32),
130}
131
132/// Frame-rate cap.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum FpsOpt {
135    Auto,
136    Cap(u32),
137}
138
139/// Speed/quality trade-off, mapped to the encoder preset.
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum QualityPreset {
142    Fast,
143    Balanced,
144    Max,
145}
146
147impl QualityPreset {
148    /// x264/x265 `-preset` value.
149    pub fn encoder_preset(self) -> &'static str {
150        match self {
151            QualityPreset::Fast => "veryfast",
152            QualityPreset::Balanced => "medium",
153            QualityPreset::Max => "slow",
154        }
155    }
156
157    /// The speed/quality knob for a specific *encoder*, as `(flag, value)`.
158    ///
159    /// Encoders disagree on both the flag and its scale: x264/x265 take named
160    /// `-preset`s, SVT-AV1 takes a numeric `-preset` (0 slowest … 13 fastest),
161    /// and libaom takes `-cpu-used`. Passing an x264 preset name to SVT-AV1 is
162    /// a hard ffmpeg error, so the argv builder must ask per encoder.
163    pub fn speed_flags(self, encoder: &str) -> (&'static str, &'static str) {
164        match encoder {
165            "libsvtav1" => (
166                "-preset",
167                match self {
168                    QualityPreset::Fast => "9",
169                    QualityPreset::Balanced => "7",
170                    QualityPreset::Max => "5",
171                },
172            ),
173            "libaom-av1" => (
174                "-cpu-used",
175                match self {
176                    QualityPreset::Fast => "8",
177                    QualityPreset::Balanced => "5",
178                    QualityPreset::Max => "3",
179                },
180            ),
181            _ => ("-preset", self.encoder_preset()),
182        }
183    }
184
185    /// Default quality-mode CRF for a codec. x265 needs a higher CRF than x264
186    /// for comparable quality, and AV1 higher still, so the numbers are
187    /// codec-specific. These defaults aim for roughly VMAF ~93 (visually
188    /// near-transparent) on typical content.
189    /// VideoToolbox constant quality (`-q:v`, 1–100) giving about the same
190    /// VMAF as [`Self::default_crf`] with the software encoder — calibrated on
191    /// a 4K iPhone clip. Apple H.264 lands near x264's size; Apple HEVC is ~50 %
192    /// larger than x265 at that quality. `None`: no hardware encoder (AV1).
193    pub fn default_hw_quality(self, codec: VideoCodec) -> Option<u8> {
194        match codec {
195            VideoCodec::H264 => Some(match self {
196                QualityPreset::Fast => 55,
197                QualityPreset::Balanced => 66,
198                QualityPreset::Max => 72,
199            }),
200            VideoCodec::H265 => Some(match self {
201                QualityPreset::Fast => 52,
202                QualityPreset::Balanced => 57,
203                QualityPreset::Max => 69,
204            }),
205            VideoCodec::Av1 => None,
206        }
207    }
208
209    pub fn default_crf(self, codec: VideoCodec) -> u8 {
210        match codec {
211            VideoCodec::H264 => match self {
212                QualityPreset::Fast => 25,
213                QualityPreset::Balanced => 23,
214                QualityPreset::Max => 20,
215            },
216            VideoCodec::H265 => match self {
217                QualityPreset::Fast => 30,
218                QualityPreset::Balanced => 28,
219                QualityPreset::Max => 24,
220            },
221            VideoCodec::Av1 => match self {
222                QualityPreset::Fast => 38,
223                QualityPreset::Balanced => 35,
224                QualityPreset::Max => 30,
225            },
226        }
227    }
228}