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