Skip to main content

viser_encoding/
lib.rs

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/// Common encoding parameters shared across all optimization modes.
11#[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    /// Returns the actual parallelism to use.
59    /// If parallel is 0, uses num_cpus/2 with a floor of 2.
60    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
69/// Maps a generic preset name to codec-specific presets.
70pub 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}