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::SvtAv1 {
97        return preset.to_string();
98    }
99    match preset {
100        "ultrafast" => "12",
101        "superfast" => "11",
102        "veryfast" => "10",
103        "faster" => "9",
104        "fast" => "8",
105        "medium" => "6",
106        "slow" => "4",
107        "slower" => "2",
108        "veryslow" => "0",
109        other => return other.to_string(),
110    }
111    .to_string()
112}
113
114fn hw_preset_for_codec(codec: Codec, preset: &str) -> String {
115    match codec.backend() {
116        EncoderBackend::Nvenc => match preset {
117            "ultrafast" | "superfast" => "p1".into(),
118            "veryfast" => "p2".into(),
119            "faster" => "p3".into(),
120            "fast" => "p4".into(),
121            "medium" => "p5".into(),
122            "slow" => "p6".into(),
123            "slower" | "veryslow" => "p7".into(),
124            other => other.to_string(),
125        },
126        EncoderBackend::Qsv => preset.to_string(),
127        EncoderBackend::Vaapi => match preset {
128            "ultrafast" | "superfast" => "1".into(),
129            "veryfast" | "faster" => "2".into(),
130            "fast" | "medium" => "3".into(),
131            "slow" => "4".into(),
132            "slower" | "veryslow" => "5".into(),
133            other => other.to_string(),
134        },
135        EncoderBackend::Amf => match preset {
136            "ultrafast" | "superfast" => "speed".into(),
137            "veryfast" | "faster" | "fast" => "balanced".into(),
138            "medium" | "slow" | "slower" | "veryslow" => "quality".into(),
139            other => other.to_string(),
140        },
141        EncoderBackend::VideoToolbox => preset.to_string(),
142        EncoderBackend::Software => preset.to_string(),
143    }
144}
145
146fn num_cpus() -> usize {
147    std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4)
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use viser_ffmpeg::{Codec, RateControlMode};
154
155    #[test]
156    fn test_config_default() {
157        let cfg = Config::default();
158        assert_eq!(cfg.resolutions.len(), 3);
159        assert_eq!(cfg.crf_values.len(), 7);
160        assert_eq!(cfg.codecs.len(), 1);
161        assert_eq!(cfg.preset, "veryfast");
162        assert_eq!(cfg.subsample, 5);
163        assert_eq!(cfg.rate_control, RateControlMode::Crf);
164    }
165
166    #[test]
167    fn test_config_validate_ok() {
168        let cfg = Config::default();
169        assert!(cfg.validate().is_ok());
170    }
171
172    #[test]
173    fn test_config_validate_empty_resolutions() {
174        let cfg = Config { resolutions: vec![], ..Config::default() };
175        assert!(cfg.validate().is_err());
176    }
177
178    #[test]
179    fn test_config_validate_empty_crf() {
180        let cfg = Config { crf_values: vec![], ..Config::default() };
181        assert!(cfg.validate().is_err());
182    }
183
184    #[test]
185    fn test_config_validate_empty_codecs() {
186        let cfg = Config { codecs: vec![], ..Config::default() };
187        assert!(cfg.validate().is_err());
188    }
189
190    #[test]
191    fn test_config_validate_negative_subsample() {
192        let cfg = Config { subsample: -1, ..Config::default() };
193        assert!(cfg.validate().is_err());
194    }
195
196    #[test]
197    fn test_config_validate_zero_subsample_ok() {
198        let cfg = Config { subsample: 0, ..Config::default() };
199        assert!(cfg.validate().is_ok());
200    }
201
202    #[test]
203    fn test_effective_parallel_uses_explicit() {
204        let cfg = Config { parallel: 8, ..Config::default() };
205        assert_eq!(cfg.effective_parallel(), 8);
206    }
207
208    #[test]
209    fn test_effective_parallel_auto() {
210        let cfg = Config { parallel: 0, ..Config::default() };
211        let p = cfg.effective_parallel();
212        assert!(p >= 2);
213    }
214
215    #[test]
216    fn test_preset_for_codec_passthrough() {
217        assert_eq!(preset_for_codec(Codec::X264, "veryfast"), "veryfast");
218        assert_eq!(preset_for_codec(Codec::X265, "slow"), "slow");
219    }
220
221    #[test]
222    fn test_preset_for_codec_svtav1_maps() {
223        assert_eq!(preset_for_codec(Codec::SvtAv1, "ultrafast"), "12");
224        assert_eq!(preset_for_codec(Codec::SvtAv1, "superfast"), "11");
225        assert_eq!(preset_for_codec(Codec::SvtAv1, "veryfast"), "10");
226        assert_eq!(preset_for_codec(Codec::SvtAv1, "faster"), "9");
227        assert_eq!(preset_for_codec(Codec::SvtAv1, "fast"), "8");
228        assert_eq!(preset_for_codec(Codec::SvtAv1, "medium"), "6");
229        assert_eq!(preset_for_codec(Codec::SvtAv1, "slow"), "4");
230        assert_eq!(preset_for_codec(Codec::SvtAv1, "slower"), "2");
231        assert_eq!(preset_for_codec(Codec::SvtAv1, "veryslow"), "0");
232    }
233
234    #[test]
235    fn test_preset_for_codec_svtav1_passthrough_unknown() {
236        assert_eq!(preset_for_codec(Codec::SvtAv1, "custom"), "custom");
237    }
238
239    #[test]
240    fn test_preset_for_codec_nvenc_maps() {
241        assert_eq!(preset_for_codec(Codec::NvencH264, "ultrafast"), "p1");
242        assert_eq!(preset_for_codec(Codec::NvencH264, "medium"), "p5");
243        assert_eq!(preset_for_codec(Codec::NvencH264, "veryslow"), "p7");
244    }
245
246    #[test]
247    fn test_preset_for_codec_qsv_passthrough() {
248        assert_eq!(preset_for_codec(Codec::QsvH264, "veryfast"), "veryfast");
249        assert_eq!(preset_for_codec(Codec::QsvH264, "medium"), "medium");
250    }
251
252    #[test]
253    fn test_preset_for_codec_vaapi_maps() {
254        assert_eq!(preset_for_codec(Codec::VaapiH264, "ultrafast"), "1");
255        assert_eq!(preset_for_codec(Codec::VaapiH264, "medium"), "3");
256        assert_eq!(preset_for_codec(Codec::VaapiH264, "veryslow"), "5");
257    }
258
259    #[test]
260    fn test_preset_for_codec_amf_maps() {
261        assert_eq!(preset_for_codec(Codec::AmfH264, "ultrafast"), "speed");
262        assert_eq!(preset_for_codec(Codec::AmfH264, "fast"), "balanced");
263        assert_eq!(preset_for_codec(Codec::AmfH264, "medium"), "quality");
264    }
265
266    #[test]
267    fn test_config_serde_roundtrip() {
268        let cfg = Config::default();
269        let json = serde_json::to_string(&cfg).unwrap();
270        let back: Config = serde_json::from_str(&json).unwrap();
271        assert_eq!(back.resolutions.len(), cfg.resolutions.len());
272        assert_eq!(back.crf_values, cfg.crf_values);
273        assert_eq!(back.preset, cfg.preset);
274    }
275}