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::{
15    Codec, EncoderBackend, RES_480P, RES_720P, RES_1080P, RateControlMode, Resolution,
16};
17
18/// Common encoding parameters shared across all optimization modes.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Config {
21    /// Output resolutions to encode and evaluate.
22    pub resolutions: Vec<Resolution>,
23    /// CRF (constant rate factor) quality values to sweep.
24    pub crf_values: Vec<i32>,
25    /// Codecs to encode with.
26    pub codecs: Vec<Codec>,
27    /// Generic preset name (e.g. `"veryfast"`); mapped per codec via `preset_for_codec`.
28    pub preset: String,
29    /// Frame subsample interval for VMAF scoring; 0 evaluates every frame.
30    pub subsample: i32,
31    /// Number of concurrent encodes; 0 means auto (see `effective_parallel`).
32    pub parallel: i32,
33    /// Rate control mode (e.g. CRF) used for encoding.
34    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    /// Validates the configuration, returning an error if any field is empty or out of range.
53    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    /// 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.
84///
85/// Software encoders get passthrough (except SVT-AV1 which maps to numeric presets).
86/// Hardware encoders get per-backend preset mapping:
87/// - NVENC: p1..p7
88/// - QSV: passthrough (veryfast..veryslow)
89/// - VideoToolbox: realtime flag for fast presets
90/// - VAAPI: compression_level 1..5
91/// - AMF: speed/balanced/quality
92pub 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::Vp9 {
97        return match preset {
98            "ultrafast" | "superfast" => "8",
99            "veryfast" => "6",
100            "faster" => "5",
101            "fast" => "4",
102            "medium" => "2",
103            "slow" => "1",
104            "slower" | "veryslow" => "0",
105            other => return other.to_string(),
106        }
107        .to_string();
108    }
109    if codec != Codec::SvtAv1 {
110        return preset.to_string();
111    }
112    match preset {
113        "ultrafast" => "12",
114        "superfast" => "11",
115        "veryfast" => "10",
116        "faster" => "9",
117        "fast" => "8",
118        "medium" => "6",
119        "slow" => "4",
120        "slower" => "2",
121        "veryslow" => "0",
122        other => return other.to_string(),
123    }
124    .to_string()
125}
126
127fn hw_preset_for_codec(codec: Codec, preset: &str) -> String {
128    match codec.backend() {
129        EncoderBackend::Nvenc => match preset {
130            "ultrafast" | "superfast" => "p1".into(),
131            "veryfast" => "p2".into(),
132            "faster" => "p3".into(),
133            "fast" => "p4".into(),
134            "medium" => "p5".into(),
135            "slow" => "p6".into(),
136            "slower" | "veryslow" => "p7".into(),
137            other => other.to_string(),
138        },
139        EncoderBackend::Qsv => preset.to_string(),
140        EncoderBackend::Vaapi => match preset {
141            "ultrafast" | "superfast" => "1".into(),
142            "veryfast" | "faster" => "2".into(),
143            "fast" | "medium" => "3".into(),
144            "slow" => "4".into(),
145            "slower" | "veryslow" => "5".into(),
146            other => other.to_string(),
147        },
148        EncoderBackend::Amf => match preset {
149            "ultrafast" | "superfast" => "speed".into(),
150            "veryfast" | "faster" | "fast" => "balanced".into(),
151            "medium" | "slow" | "slower" | "veryslow" => "quality".into(),
152            other => other.to_string(),
153        },
154        EncoderBackend::VideoToolbox => preset.to_string(),
155        EncoderBackend::Software => preset.to_string(),
156    }
157}
158
159fn num_cpus() -> usize {
160    std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4)
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use viser_ffmpeg::{Codec, RateControlMode};
167
168    #[test]
169    fn test_config_default() {
170        let cfg = Config::default();
171        assert_eq!(cfg.resolutions.len(), 3);
172        assert_eq!(cfg.crf_values.len(), 7);
173        assert_eq!(cfg.codecs.len(), 1);
174        assert_eq!(cfg.preset, "veryfast");
175        assert_eq!(cfg.subsample, 5);
176        assert_eq!(cfg.rate_control, RateControlMode::Crf);
177    }
178
179    #[test]
180    fn test_config_validate_ok() {
181        let cfg = Config::default();
182        assert!(cfg.validate().is_ok());
183    }
184
185    #[test]
186    fn test_config_validate_empty_resolutions() {
187        let cfg = Config { resolutions: vec![], ..Config::default() };
188        assert!(cfg.validate().is_err());
189    }
190
191    #[test]
192    fn test_config_validate_empty_crf() {
193        let cfg = Config { crf_values: vec![], ..Config::default() };
194        assert!(cfg.validate().is_err());
195    }
196
197    #[test]
198    fn test_config_validate_empty_codecs() {
199        let cfg = Config { codecs: vec![], ..Config::default() };
200        assert!(cfg.validate().is_err());
201    }
202
203    #[test]
204    fn test_config_validate_negative_subsample() {
205        let cfg = Config { subsample: -1, ..Config::default() };
206        assert!(cfg.validate().is_err());
207    }
208
209    #[test]
210    fn test_config_validate_zero_subsample_ok() {
211        let cfg = Config { subsample: 0, ..Config::default() };
212        assert!(cfg.validate().is_ok());
213    }
214
215    #[test]
216    fn test_effective_parallel_uses_explicit() {
217        let cfg = Config { parallel: 8, ..Config::default() };
218        assert_eq!(cfg.effective_parallel(), 8);
219    }
220
221    #[test]
222    fn test_effective_parallel_auto() {
223        let cfg = Config { parallel: 0, ..Config::default() };
224        let p = cfg.effective_parallel();
225        assert!(p >= 2);
226    }
227
228    #[test]
229    fn test_preset_for_codec_passthrough() {
230        assert_eq!(preset_for_codec(Codec::X264, "veryfast"), "veryfast");
231        assert_eq!(preset_for_codec(Codec::X265, "slow"), "slow");
232    }
233
234    #[test]
235    fn test_preset_for_codec_vp9_maps() {
236        assert_eq!(preset_for_codec(Codec::Vp9, "veryfast"), "6");
237        assert_eq!(preset_for_codec(Codec::Vp9, "medium"), "2");
238        assert_eq!(preset_for_codec(Codec::Vp9, "veryslow"), "0");
239    }
240
241    #[test]
242    fn test_preset_for_codec_svtav1_maps() {
243        assert_eq!(preset_for_codec(Codec::SvtAv1, "ultrafast"), "12");
244        assert_eq!(preset_for_codec(Codec::SvtAv1, "superfast"), "11");
245        assert_eq!(preset_for_codec(Codec::SvtAv1, "veryfast"), "10");
246        assert_eq!(preset_for_codec(Codec::SvtAv1, "faster"), "9");
247        assert_eq!(preset_for_codec(Codec::SvtAv1, "fast"), "8");
248        assert_eq!(preset_for_codec(Codec::SvtAv1, "medium"), "6");
249        assert_eq!(preset_for_codec(Codec::SvtAv1, "slow"), "4");
250        assert_eq!(preset_for_codec(Codec::SvtAv1, "slower"), "2");
251        assert_eq!(preset_for_codec(Codec::SvtAv1, "veryslow"), "0");
252    }
253
254    #[test]
255    fn test_preset_for_codec_svtav1_passthrough_unknown() {
256        assert_eq!(preset_for_codec(Codec::SvtAv1, "custom"), "custom");
257    }
258
259    #[test]
260    fn test_preset_for_codec_nvenc_maps() {
261        assert_eq!(preset_for_codec(Codec::NvencH264, "ultrafast"), "p1");
262        assert_eq!(preset_for_codec(Codec::NvencH264, "medium"), "p5");
263        assert_eq!(preset_for_codec(Codec::NvencH264, "veryslow"), "p7");
264    }
265
266    #[test]
267    fn test_preset_for_codec_qsv_passthrough() {
268        assert_eq!(preset_for_codec(Codec::QsvH264, "veryfast"), "veryfast");
269        assert_eq!(preset_for_codec(Codec::QsvH264, "medium"), "medium");
270    }
271
272    #[test]
273    fn test_preset_for_codec_vaapi_maps() {
274        assert_eq!(preset_for_codec(Codec::VaapiH264, "ultrafast"), "1");
275        assert_eq!(preset_for_codec(Codec::VaapiH264, "medium"), "3");
276        assert_eq!(preset_for_codec(Codec::VaapiH264, "veryslow"), "5");
277    }
278
279    #[test]
280    fn test_preset_for_codec_amf_maps() {
281        assert_eq!(preset_for_codec(Codec::AmfH264, "ultrafast"), "speed");
282        assert_eq!(preset_for_codec(Codec::AmfH264, "fast"), "balanced");
283        assert_eq!(preset_for_codec(Codec::AmfH264, "medium"), "quality");
284    }
285
286    #[test]
287    fn test_config_serde_roundtrip() {
288        let cfg = Config::default();
289        let json = serde_json::to_string(&cfg).unwrap();
290        let back: Config = serde_json::from_str(&json).unwrap();
291        assert_eq!(back.resolutions.len(), cfg.resolutions.len());
292        assert_eq!(back.crf_values, cfg.crf_values);
293        assert_eq!(back.preset, cfg.preset);
294    }
295}