Skip to main content

viser_encoding/
lib.rs

1//! Shared encoding configuration for the `viser` video-encoding-optimizer workspace.
2//!
3//! Provides the common `Config` of encoding parameters used across all optimization modes,
4//! codec-specific preset mapping (`preset_for_codec`), non-blocking progress reporting, and
5//! cleanup of orphaned temp directories left behind by crashes.
6
7mod 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/// Common encoding parameters shared across all optimization modes.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Config {
19    /// Output resolutions to encode and evaluate.
20    pub resolutions: Vec<Resolution>,
21    /// CRF (constant rate factor) quality values to sweep.
22    pub crf_values: Vec<i32>,
23    /// Codecs to encode with.
24    pub codecs: Vec<Codec>,
25    /// Generic preset name (e.g. `"veryfast"`); mapped per codec via `preset_for_codec`.
26    pub preset: String,
27    /// Frame subsample interval for VMAF scoring; 0 evaluates every frame.
28    pub subsample: i32,
29    /// Number of concurrent encodes; 0 means auto (see `effective_parallel`).
30    pub parallel: i32,
31    /// Rate control mode (e.g. CRF) used for encoding.
32    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    /// Validates the configuration, returning an error if any field is empty or out of range.
51    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    /// Returns the actual parallelism to use.
73    /// If parallel is 0, uses num_cpus/2 with a floor of 2.
74    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
83/// Maps a generic preset name to codec-specific presets.
84pub 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}