1mod cleanup;
8mod progress;
9
10pub use cleanup::*;
11pub use progress::*;
12
13use serde::{Deserialize, Serialize};
14use viser_ffmpeg::{
15 Codec, EncoderBackend, RES_480P, RES_720P, RES_1080P, RateControlMode, Resolution,
16};
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Config {
21 pub resolutions: Vec<Resolution>,
23 pub crf_values: Vec<i32>,
25 pub codecs: Vec<Codec>,
27 pub preset: String,
29 pub subsample: i32,
31 pub parallel: i32,
33 pub rate_control: RateControlMode,
35}
36
37impl Default for Config {
38 fn default() -> Self {
39 Self {
40 resolutions: vec![RES_480P, RES_720P, RES_1080P],
41 crf_values: vec![18, 22, 26, 30, 34, 38, 42],
42 codecs: vec![Codec::X264],
43 preset: "veryfast".into(),
44 subsample: 5,
45 parallel: 0,
46 rate_control: RateControlMode::Crf,
47 }
48 }
49}
50
51impl Config {
52 pub fn validate(&self) -> anyhow::Result<()> {
54 if self.resolutions.is_empty() {
55 anyhow::bail!("must specify at least one resolution");
56 }
57 if self.crf_values.is_empty() {
58 anyhow::bail!("must specify at least one CRF value");
59 }
60 if self.codecs.is_empty() {
61 anyhow::bail!("must specify at least one codec");
62 }
63 for codec in &self.codecs {
64 let _ = codec;
65 }
66 if self.subsample < 0 {
67 anyhow::bail!("subsample must be >= 0, got {}", self.subsample);
68 }
69 Ok(())
70 }
71
72 pub fn effective_parallel(&self) -> usize {
75 if self.parallel > 0 {
76 return self.parallel as usize;
77 }
78 let p = num_cpus() / 2;
79 p.max(2)
80 }
81}
82
83pub fn preset_for_codec(codec: Codec, preset: &str) -> String {
93 if codec.is_hardware() {
94 return hw_preset_for_codec(codec, preset);
95 }
96 if codec == Codec::Vp9 {
97 return match preset {
98 "ultrafast" | "superfast" => "8",
99 "veryfast" => "6",
100 "faster" => "5",
101 "fast" => "4",
102 "medium" => "2",
103 "slow" => "1",
104 "slower" | "veryslow" => "0",
105 other => return other.to_string(),
106 }
107 .to_string();
108 }
109 if codec != Codec::SvtAv1 {
110 return preset.to_string();
111 }
112 match preset {
113 "ultrafast" => "12",
114 "superfast" => "11",
115 "veryfast" => "10",
116 "faster" => "9",
117 "fast" => "8",
118 "medium" => "6",
119 "slow" => "4",
120 "slower" => "2",
121 "veryslow" => "0",
122 other => return other.to_string(),
123 }
124 .to_string()
125}
126
127fn hw_preset_for_codec(codec: Codec, preset: &str) -> String {
128 match codec.backend() {
129 EncoderBackend::Nvenc => match preset {
130 "ultrafast" | "superfast" => "p1".into(),
131 "veryfast" => "p2".into(),
132 "faster" => "p3".into(),
133 "fast" => "p4".into(),
134 "medium" => "p5".into(),
135 "slow" => "p6".into(),
136 "slower" | "veryslow" => "p7".into(),
137 other => other.to_string(),
138 },
139 EncoderBackend::Qsv => preset.to_string(),
140 EncoderBackend::Vaapi => match preset {
141 "ultrafast" | "superfast" => "1".into(),
142 "veryfast" | "faster" => "2".into(),
143 "fast" | "medium" => "3".into(),
144 "slow" => "4".into(),
145 "slower" | "veryslow" => "5".into(),
146 other => other.to_string(),
147 },
148 EncoderBackend::Amf => match preset {
149 "ultrafast" | "superfast" => "speed".into(),
150 "veryfast" | "faster" | "fast" => "balanced".into(),
151 "medium" | "slow" | "slower" | "veryslow" => "quality".into(),
152 other => other.to_string(),
153 },
154 EncoderBackend::VideoToolbox => preset.to_string(),
155 EncoderBackend::Software => preset.to_string(),
156 }
157}
158
159fn num_cpus() -> usize {
160 std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4)
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use viser_ffmpeg::{Codec, RateControlMode};
167
168 #[test]
169 fn test_config_default() {
170 let cfg = Config::default();
171 assert_eq!(cfg.resolutions.len(), 3);
172 assert_eq!(cfg.crf_values.len(), 7);
173 assert_eq!(cfg.codecs.len(), 1);
174 assert_eq!(cfg.preset, "veryfast");
175 assert_eq!(cfg.subsample, 5);
176 assert_eq!(cfg.rate_control, RateControlMode::Crf);
177 }
178
179 #[test]
180 fn test_config_validate_ok() {
181 let cfg = Config::default();
182 assert!(cfg.validate().is_ok());
183 }
184
185 #[test]
186 fn test_config_validate_empty_resolutions() {
187 let cfg = Config { resolutions: vec![], ..Config::default() };
188 assert!(cfg.validate().is_err());
189 }
190
191 #[test]
192 fn test_config_validate_empty_crf() {
193 let cfg = Config { crf_values: vec![], ..Config::default() };
194 assert!(cfg.validate().is_err());
195 }
196
197 #[test]
198 fn test_config_validate_empty_codecs() {
199 let cfg = Config { codecs: vec![], ..Config::default() };
200 assert!(cfg.validate().is_err());
201 }
202
203 #[test]
204 fn test_config_validate_negative_subsample() {
205 let cfg = Config { subsample: -1, ..Config::default() };
206 assert!(cfg.validate().is_err());
207 }
208
209 #[test]
210 fn test_config_validate_zero_subsample_ok() {
211 let cfg = Config { subsample: 0, ..Config::default() };
212 assert!(cfg.validate().is_ok());
213 }
214
215 #[test]
216 fn test_effective_parallel_uses_explicit() {
217 let cfg = Config { parallel: 8, ..Config::default() };
218 assert_eq!(cfg.effective_parallel(), 8);
219 }
220
221 #[test]
222 fn test_effective_parallel_auto() {
223 let cfg = Config { parallel: 0, ..Config::default() };
224 let p = cfg.effective_parallel();
225 assert!(p >= 2);
226 }
227
228 #[test]
229 fn test_preset_for_codec_passthrough() {
230 assert_eq!(preset_for_codec(Codec::X264, "veryfast"), "veryfast");
231 assert_eq!(preset_for_codec(Codec::X265, "slow"), "slow");
232 }
233
234 #[test]
235 fn test_preset_for_codec_vp9_maps() {
236 assert_eq!(preset_for_codec(Codec::Vp9, "veryfast"), "6");
237 assert_eq!(preset_for_codec(Codec::Vp9, "medium"), "2");
238 assert_eq!(preset_for_codec(Codec::Vp9, "veryslow"), "0");
239 }
240
241 #[test]
242 fn test_preset_for_codec_svtav1_maps() {
243 assert_eq!(preset_for_codec(Codec::SvtAv1, "ultrafast"), "12");
244 assert_eq!(preset_for_codec(Codec::SvtAv1, "superfast"), "11");
245 assert_eq!(preset_for_codec(Codec::SvtAv1, "veryfast"), "10");
246 assert_eq!(preset_for_codec(Codec::SvtAv1, "faster"), "9");
247 assert_eq!(preset_for_codec(Codec::SvtAv1, "fast"), "8");
248 assert_eq!(preset_for_codec(Codec::SvtAv1, "medium"), "6");
249 assert_eq!(preset_for_codec(Codec::SvtAv1, "slow"), "4");
250 assert_eq!(preset_for_codec(Codec::SvtAv1, "slower"), "2");
251 assert_eq!(preset_for_codec(Codec::SvtAv1, "veryslow"), "0");
252 }
253
254 #[test]
255 fn test_preset_for_codec_svtav1_passthrough_unknown() {
256 assert_eq!(preset_for_codec(Codec::SvtAv1, "custom"), "custom");
257 }
258
259 #[test]
260 fn test_preset_for_codec_nvenc_maps() {
261 assert_eq!(preset_for_codec(Codec::NvencH264, "ultrafast"), "p1");
262 assert_eq!(preset_for_codec(Codec::NvencH264, "medium"), "p5");
263 assert_eq!(preset_for_codec(Codec::NvencH264, "veryslow"), "p7");
264 }
265
266 #[test]
267 fn test_preset_for_codec_qsv_passthrough() {
268 assert_eq!(preset_for_codec(Codec::QsvH264, "veryfast"), "veryfast");
269 assert_eq!(preset_for_codec(Codec::QsvH264, "medium"), "medium");
270 }
271
272 #[test]
273 fn test_preset_for_codec_vaapi_maps() {
274 assert_eq!(preset_for_codec(Codec::VaapiH264, "ultrafast"), "1");
275 assert_eq!(preset_for_codec(Codec::VaapiH264, "medium"), "3");
276 assert_eq!(preset_for_codec(Codec::VaapiH264, "veryslow"), "5");
277 }
278
279 #[test]
280 fn test_preset_for_codec_amf_maps() {
281 assert_eq!(preset_for_codec(Codec::AmfH264, "ultrafast"), "speed");
282 assert_eq!(preset_for_codec(Codec::AmfH264, "fast"), "balanced");
283 assert_eq!(preset_for_codec(Codec::AmfH264, "medium"), "quality");
284 }
285
286 #[test]
287 fn test_config_serde_roundtrip() {
288 let cfg = Config::default();
289 let json = serde_json::to_string(&cfg).unwrap();
290 let back: Config = serde_json::from_str(&json).unwrap();
291 assert_eq!(back.resolutions.len(), cfg.resolutions.len());
292 assert_eq!(back.crf_values, cfg.crf_values);
293 assert_eq!(back.preset, cfg.preset);
294 }
295}