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::SvtAv1 {
97 return preset.to_string();
98 }
99 match preset {
100 "ultrafast" => "12",
101 "superfast" => "11",
102 "veryfast" => "10",
103 "faster" => "9",
104 "fast" => "8",
105 "medium" => "6",
106 "slow" => "4",
107 "slower" => "2",
108 "veryslow" => "0",
109 other => return other.to_string(),
110 }
111 .to_string()
112}
113
114fn hw_preset_for_codec(codec: Codec, preset: &str) -> String {
115 match codec.backend() {
116 EncoderBackend::Nvenc => match preset {
117 "ultrafast" | "superfast" => "p1".into(),
118 "veryfast" => "p2".into(),
119 "faster" => "p3".into(),
120 "fast" => "p4".into(),
121 "medium" => "p5".into(),
122 "slow" => "p6".into(),
123 "slower" | "veryslow" => "p7".into(),
124 other => other.to_string(),
125 },
126 EncoderBackend::Qsv => preset.to_string(),
127 EncoderBackend::Vaapi => match preset {
128 "ultrafast" | "superfast" => "1".into(),
129 "veryfast" | "faster" => "2".into(),
130 "fast" | "medium" => "3".into(),
131 "slow" => "4".into(),
132 "slower" | "veryslow" => "5".into(),
133 other => other.to_string(),
134 },
135 EncoderBackend::Amf => match preset {
136 "ultrafast" | "superfast" => "speed".into(),
137 "veryfast" | "faster" | "fast" => "balanced".into(),
138 "medium" | "slow" | "slower" | "veryslow" => "quality".into(),
139 other => other.to_string(),
140 },
141 EncoderBackend::VideoToolbox => preset.to_string(),
142 EncoderBackend::Software => preset.to_string(),
143 }
144}
145
146fn num_cpus() -> usize {
147 std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4)
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use viser_ffmpeg::{Codec, RateControlMode};
154
155 #[test]
156 fn test_config_default() {
157 let cfg = Config::default();
158 assert_eq!(cfg.resolutions.len(), 3);
159 assert_eq!(cfg.crf_values.len(), 7);
160 assert_eq!(cfg.codecs.len(), 1);
161 assert_eq!(cfg.preset, "veryfast");
162 assert_eq!(cfg.subsample, 5);
163 assert_eq!(cfg.rate_control, RateControlMode::Crf);
164 }
165
166 #[test]
167 fn test_config_validate_ok() {
168 let cfg = Config::default();
169 assert!(cfg.validate().is_ok());
170 }
171
172 #[test]
173 fn test_config_validate_empty_resolutions() {
174 let cfg = Config { resolutions: vec![], ..Config::default() };
175 assert!(cfg.validate().is_err());
176 }
177
178 #[test]
179 fn test_config_validate_empty_crf() {
180 let cfg = Config { crf_values: vec![], ..Config::default() };
181 assert!(cfg.validate().is_err());
182 }
183
184 #[test]
185 fn test_config_validate_empty_codecs() {
186 let cfg = Config { codecs: vec![], ..Config::default() };
187 assert!(cfg.validate().is_err());
188 }
189
190 #[test]
191 fn test_config_validate_negative_subsample() {
192 let cfg = Config { subsample: -1, ..Config::default() };
193 assert!(cfg.validate().is_err());
194 }
195
196 #[test]
197 fn test_config_validate_zero_subsample_ok() {
198 let cfg = Config { subsample: 0, ..Config::default() };
199 assert!(cfg.validate().is_ok());
200 }
201
202 #[test]
203 fn test_effective_parallel_uses_explicit() {
204 let cfg = Config { parallel: 8, ..Config::default() };
205 assert_eq!(cfg.effective_parallel(), 8);
206 }
207
208 #[test]
209 fn test_effective_parallel_auto() {
210 let cfg = Config { parallel: 0, ..Config::default() };
211 let p = cfg.effective_parallel();
212 assert!(p >= 2);
213 }
214
215 #[test]
216 fn test_preset_for_codec_passthrough() {
217 assert_eq!(preset_for_codec(Codec::X264, "veryfast"), "veryfast");
218 assert_eq!(preset_for_codec(Codec::X265, "slow"), "slow");
219 }
220
221 #[test]
222 fn test_preset_for_codec_svtav1_maps() {
223 assert_eq!(preset_for_codec(Codec::SvtAv1, "ultrafast"), "12");
224 assert_eq!(preset_for_codec(Codec::SvtAv1, "superfast"), "11");
225 assert_eq!(preset_for_codec(Codec::SvtAv1, "veryfast"), "10");
226 assert_eq!(preset_for_codec(Codec::SvtAv1, "faster"), "9");
227 assert_eq!(preset_for_codec(Codec::SvtAv1, "fast"), "8");
228 assert_eq!(preset_for_codec(Codec::SvtAv1, "medium"), "6");
229 assert_eq!(preset_for_codec(Codec::SvtAv1, "slow"), "4");
230 assert_eq!(preset_for_codec(Codec::SvtAv1, "slower"), "2");
231 assert_eq!(preset_for_codec(Codec::SvtAv1, "veryslow"), "0");
232 }
233
234 #[test]
235 fn test_preset_for_codec_svtav1_passthrough_unknown() {
236 assert_eq!(preset_for_codec(Codec::SvtAv1, "custom"), "custom");
237 }
238
239 #[test]
240 fn test_preset_for_codec_nvenc_maps() {
241 assert_eq!(preset_for_codec(Codec::NvencH264, "ultrafast"), "p1");
242 assert_eq!(preset_for_codec(Codec::NvencH264, "medium"), "p5");
243 assert_eq!(preset_for_codec(Codec::NvencH264, "veryslow"), "p7");
244 }
245
246 #[test]
247 fn test_preset_for_codec_qsv_passthrough() {
248 assert_eq!(preset_for_codec(Codec::QsvH264, "veryfast"), "veryfast");
249 assert_eq!(preset_for_codec(Codec::QsvH264, "medium"), "medium");
250 }
251
252 #[test]
253 fn test_preset_for_codec_vaapi_maps() {
254 assert_eq!(preset_for_codec(Codec::VaapiH264, "ultrafast"), "1");
255 assert_eq!(preset_for_codec(Codec::VaapiH264, "medium"), "3");
256 assert_eq!(preset_for_codec(Codec::VaapiH264, "veryslow"), "5");
257 }
258
259 #[test]
260 fn test_preset_for_codec_amf_maps() {
261 assert_eq!(preset_for_codec(Codec::AmfH264, "ultrafast"), "speed");
262 assert_eq!(preset_for_codec(Codec::AmfH264, "fast"), "balanced");
263 assert_eq!(preset_for_codec(Codec::AmfH264, "medium"), "quality");
264 }
265
266 #[test]
267 fn test_config_serde_roundtrip() {
268 let cfg = Config::default();
269 let json = serde_json::to_string(&cfg).unwrap();
270 let back: Config = serde_json::from_str(&json).unwrap();
271 assert_eq!(back.resolutions.len(), cfg.resolutions.len());
272 assert_eq!(back.crf_values, cfg.crf_values);
273 assert_eq!(back.preset, cfg.preset);
274 }
275}