1mod cleanup;
8mod progress;
9
10pub use cleanup::*;
11pub use progress::*;
12
13use serde::{Deserialize, Serialize};
14use viser_ffmpeg::{Codec, RES_480P, RES_720P, RES_1080P, RateControlMode, Resolution};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Config {
19 pub resolutions: Vec<Resolution>,
21 pub crf_values: Vec<i32>,
23 pub codecs: Vec<Codec>,
25 pub preset: String,
27 pub subsample: i32,
29 pub parallel: i32,
31 pub rate_control: RateControlMode,
33}
34
35impl Default for Config {
36 fn default() -> Self {
37 Self {
38 resolutions: vec![RES_480P, RES_720P, RES_1080P],
39 crf_values: vec![18, 22, 26, 30, 34, 38, 42],
40 codecs: vec![Codec::X264],
41 preset: "veryfast".into(),
42 subsample: 5,
43 parallel: 0,
44 rate_control: RateControlMode::Crf,
45 }
46 }
47}
48
49impl Config {
50 pub fn validate(&self) -> anyhow::Result<()> {
52 if self.resolutions.is_empty() {
53 anyhow::bail!("must specify at least one resolution");
54 }
55 if self.crf_values.is_empty() {
56 anyhow::bail!("must specify at least one CRF value");
57 }
58 if self.codecs.is_empty() {
59 anyhow::bail!("must specify at least one codec");
60 }
61 for codec in &self.codecs {
62 match codec {
63 Codec::X264 | Codec::X265 | Codec::SvtAv1 => {}
64 }
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 {
85 if codec != Codec::SvtAv1 {
86 return preset.to_string();
87 }
88 match preset {
89 "ultrafast" => "12",
90 "superfast" => "11",
91 "veryfast" => "10",
92 "faster" => "9",
93 "fast" => "8",
94 "medium" => "6",
95 "slow" => "4",
96 "slower" => "2",
97 "veryslow" => "0",
98 other => return other.to_string(),
99 }
100 .to_string()
101}
102
103fn num_cpus() -> usize {
104 std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4)
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use viser_ffmpeg::{Codec, RateControlMode};
111
112 #[test]
113 fn test_config_default() {
114 let cfg = Config::default();
115 assert_eq!(cfg.resolutions.len(), 3);
116 assert_eq!(cfg.crf_values.len(), 7);
117 assert_eq!(cfg.codecs.len(), 1);
118 assert_eq!(cfg.preset, "veryfast");
119 assert_eq!(cfg.subsample, 5);
120 assert_eq!(cfg.rate_control, RateControlMode::Crf);
121 }
122
123 #[test]
124 fn test_config_validate_ok() {
125 let cfg = Config::default();
126 assert!(cfg.validate().is_ok());
127 }
128
129 #[test]
130 fn test_config_validate_empty_resolutions() {
131 let cfg = Config { resolutions: vec![], ..Config::default() };
132 assert!(cfg.validate().is_err());
133 }
134
135 #[test]
136 fn test_config_validate_empty_crf() {
137 let cfg = Config { crf_values: vec![], ..Config::default() };
138 assert!(cfg.validate().is_err());
139 }
140
141 #[test]
142 fn test_config_validate_empty_codecs() {
143 let cfg = Config { codecs: vec![], ..Config::default() };
144 assert!(cfg.validate().is_err());
145 }
146
147 #[test]
148 fn test_config_validate_negative_subsample() {
149 let cfg = Config { subsample: -1, ..Config::default() };
150 assert!(cfg.validate().is_err());
151 }
152
153 #[test]
154 fn test_config_validate_zero_subsample_ok() {
155 let cfg = Config { subsample: 0, ..Config::default() };
156 assert!(cfg.validate().is_ok());
157 }
158
159 #[test]
160 fn test_effective_parallel_uses_explicit() {
161 let cfg = Config { parallel: 8, ..Config::default() };
162 assert_eq!(cfg.effective_parallel(), 8);
163 }
164
165 #[test]
166 fn test_effective_parallel_auto() {
167 let cfg = Config { parallel: 0, ..Config::default() };
168 let p = cfg.effective_parallel();
169 assert!(p >= 2);
170 }
171
172 #[test]
173 fn test_preset_for_codec_passthrough() {
174 assert_eq!(preset_for_codec(Codec::X264, "veryfast"), "veryfast");
175 assert_eq!(preset_for_codec(Codec::X265, "slow"), "slow");
176 }
177
178 #[test]
179 fn test_preset_for_codec_svtav1_maps() {
180 assert_eq!(preset_for_codec(Codec::SvtAv1, "ultrafast"), "12");
181 assert_eq!(preset_for_codec(Codec::SvtAv1, "superfast"), "11");
182 assert_eq!(preset_for_codec(Codec::SvtAv1, "veryfast"), "10");
183 assert_eq!(preset_for_codec(Codec::SvtAv1, "faster"), "9");
184 assert_eq!(preset_for_codec(Codec::SvtAv1, "fast"), "8");
185 assert_eq!(preset_for_codec(Codec::SvtAv1, "medium"), "6");
186 assert_eq!(preset_for_codec(Codec::SvtAv1, "slow"), "4");
187 assert_eq!(preset_for_codec(Codec::SvtAv1, "slower"), "2");
188 assert_eq!(preset_for_codec(Codec::SvtAv1, "veryslow"), "0");
189 }
190
191 #[test]
192 fn test_preset_for_codec_svtav1_passthrough_unknown() {
193 assert_eq!(preset_for_codec(Codec::SvtAv1, "custom"), "custom");
194 }
195
196 #[test]
197 fn test_config_serde_roundtrip() {
198 let cfg = Config::default();
199 let json = serde_json::to_string(&cfg).unwrap();
200 let back: Config = serde_json::from_str(&json).unwrap();
201 assert_eq!(back.resolutions.len(), cfg.resolutions.len());
202 assert_eq!(back.crf_values, cfg.crf_values);
203 assert_eq!(back.preset, cfg.preset);
204 }
205}