Skip to main content

oximedia_proxy/
proxy_format.rs

1//! Format-oriented proxy configuration presets.
2//!
3//! Provides [`QualityPreset`], [`ProxyFormatConfig`], and [`FormatSelector`]
4//! for choosing an encoding format and bitrate configuration that suits a
5//! given production context (editing, review, archive, etc.).
6
7#![allow(dead_code)]
8
9/// High-level quality preset that bundles codec, resolution, and bitrate choices.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum QualityPreset {
12    /// Ultra-low bandwidth for remote / offline mobile editing.
13    Mobile,
14    /// Web-quality proxy suitable for browser-based review tools.
15    Web,
16    /// Editorial proxy — good enough for a Resolve / Premiere timeline.
17    Editorial,
18    /// Near-lossless proxy for critical colour grading preparation.
19    ColorGrade,
20    /// Archival intermediate — visually lossless (e.g. DNxHD / ProRes 4444).
21    Archive,
22}
23
24impl QualityPreset {
25    /// Return a short human-readable name.
26    pub fn name(self) -> &'static str {
27        match self {
28            Self::Mobile => "Mobile",
29            Self::Web => "Web",
30            Self::Editorial => "Editorial",
31            Self::ColorGrade => "Color Grade",
32            Self::Archive => "Archive",
33        }
34    }
35
36    /// Return the next higher preset, or `None` if already at [`Archive`](Self::Archive).
37    pub fn upgrade(self) -> Option<Self> {
38        match self {
39            Self::Mobile => Some(Self::Web),
40            Self::Web => Some(Self::Editorial),
41            Self::Editorial => Some(Self::ColorGrade),
42            Self::ColorGrade => Some(Self::Archive),
43            Self::Archive => None,
44        }
45    }
46
47    /// Return the next lower preset, or `None` if already at [`Mobile`](Self::Mobile).
48    pub fn downgrade(self) -> Option<Self> {
49        match self {
50            Self::Mobile => None,
51            Self::Web => Some(Self::Mobile),
52            Self::Editorial => Some(Self::Web),
53            Self::ColorGrade => Some(Self::Editorial),
54            Self::Archive => Some(Self::ColorGrade),
55        }
56    }
57
58    /// Return all presets in ascending quality order.
59    pub fn all() -> &'static [QualityPreset] {
60        &[
61            Self::Mobile,
62            Self::Web,
63            Self::Editorial,
64            Self::ColorGrade,
65            Self::Archive,
66        ]
67    }
68}
69
70impl std::fmt::Display for QualityPreset {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.write_str(self.name())
73    }
74}
75
76/// Concrete encoding parameters for a particular [`QualityPreset`].
77#[derive(Debug, Clone)]
78pub struct ProxyFormatConfig {
79    /// The preset this config was derived from.
80    pub preset: QualityPreset,
81    /// Target video bitrate in kilobits per second.
82    pub video_kbps: u32,
83    /// Target audio bitrate in kilobits per second.
84    pub audio_kbps: u32,
85    /// Maximum horizontal resolution in pixels.
86    pub max_width: u32,
87    /// Maximum vertical resolution in pixels.
88    pub max_height: u32,
89    /// Container format hint (e.g. `"mp4"`, `"mov"`, `"mxf"`).
90    pub container: &'static str,
91    /// Video codec hint (e.g. `"h264"`, `"prores"`, `"dnxhd"`).
92    pub codec: &'static str,
93}
94
95impl ProxyFormatConfig {
96    /// Build a config with sensible defaults for `preset`.
97    pub fn for_preset(preset: QualityPreset) -> Self {
98        match preset {
99            QualityPreset::Mobile => Self {
100                preset,
101                video_kbps: 400,
102                audio_kbps: 64,
103                max_width: 640,
104                max_height: 360,
105                container: "mp4",
106                codec: "h264",
107            },
108            QualityPreset::Web => Self {
109                preset,
110                video_kbps: 1_500,
111                audio_kbps: 128,
112                max_width: 1_280,
113                max_height: 720,
114                container: "mp4",
115                codec: "h264",
116            },
117            QualityPreset::Editorial => Self {
118                preset,
119                video_kbps: 8_000,
120                audio_kbps: 192,
121                max_width: 1_920,
122                max_height: 1_080,
123                container: "mp4",
124                codec: "h264",
125            },
126            QualityPreset::ColorGrade => Self {
127                preset,
128                video_kbps: 45_000,
129                audio_kbps: 320,
130                max_width: 3_840,
131                max_height: 2_160,
132                container: "mov",
133                codec: "prores",
134            },
135            QualityPreset::Archive => Self {
136                preset,
137                video_kbps: 185_000,
138                audio_kbps: 320,
139                max_width: 3_840,
140                max_height: 2_160,
141                container: "mxf",
142                codec: "dnxhd",
143            },
144        }
145    }
146
147    /// Total bitrate in kbps (video + audio).
148    pub fn total_kbps(&self) -> u32 {
149        self.video_kbps + self.audio_kbps
150    }
151
152    /// Return `true` if this config fits within `budget_kbps`.
153    pub fn fits_budget(&self, budget_kbps: u32) -> bool {
154        self.total_kbps() <= budget_kbps
155    }
156
157    /// Return `true` if this config supports the given resolution.
158    pub fn supports_resolution(&self, width: u32, height: u32) -> bool {
159        width <= self.max_width && height <= self.max_height
160    }
161}
162
163/// Selects the most suitable [`ProxyFormatConfig`] for a given constraint.
164#[derive(Default)]
165pub struct FormatSelector {
166    configs: Vec<ProxyFormatConfig>,
167}
168
169impl FormatSelector {
170    /// Create a selector populated with default configs for every preset.
171    pub fn new() -> Self {
172        let configs = QualityPreset::all()
173            .iter()
174            .map(|&p| ProxyFormatConfig::for_preset(p))
175            .collect();
176        Self { configs }
177    }
178
179    /// Create a selector with custom configs.
180    pub fn with_configs(configs: Vec<ProxyFormatConfig>) -> Self {
181        Self { configs }
182    }
183
184    /// Return the highest-quality preset that fits `budget_kbps`.
185    ///
186    /// Returns `None` if even the lowest-quality preset exceeds the budget.
187    pub fn select_for_budget(&self, budget_kbps: u32) -> Option<&ProxyFormatConfig> {
188        // Try from highest to lowest quality
189        let mut sorted: Vec<&ProxyFormatConfig> = self.configs.iter().collect();
190        sorted.sort_by(|a, b| b.video_kbps.cmp(&a.video_kbps));
191        sorted.into_iter().find(|c| c.fits_budget(budget_kbps))
192    }
193
194    /// Return the config for `preset`, if present.
195    pub fn get(&self, preset: QualityPreset) -> Option<&ProxyFormatConfig> {
196        self.configs.iter().find(|c| c.preset == preset)
197    }
198
199    /// Return all configs sorted from lowest to highest video bitrate.
200    pub fn all_ascending(&self) -> Vec<&ProxyFormatConfig> {
201        let mut v: Vec<&ProxyFormatConfig> = self.configs.iter().collect();
202        v.sort_by_key(|c| c.video_kbps);
203        v
204    }
205
206    /// Number of configs in the selector.
207    pub fn len(&self) -> usize {
208        self.configs.len()
209    }
210
211    /// Return `true` if the selector contains no configs.
212    pub fn is_empty(&self) -> bool {
213        self.configs.is_empty()
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn quality_preset_name() {
223        assert_eq!(QualityPreset::Mobile.name(), "Mobile");
224        assert_eq!(QualityPreset::Archive.name(), "Archive");
225    }
226
227    #[test]
228    fn quality_preset_display() {
229        assert_eq!(format!("{}", QualityPreset::Editorial), "Editorial");
230    }
231
232    #[test]
233    fn quality_preset_upgrade_chain() {
234        assert_eq!(QualityPreset::Mobile.upgrade(), Some(QualityPreset::Web));
235        assert_eq!(QualityPreset::Archive.upgrade(), None);
236    }
237
238    #[test]
239    fn quality_preset_downgrade_chain() {
240        assert_eq!(QualityPreset::Web.downgrade(), Some(QualityPreset::Mobile));
241        assert_eq!(QualityPreset::Mobile.downgrade(), None);
242    }
243
244    #[test]
245    fn quality_preset_all_count() {
246        assert_eq!(QualityPreset::all().len(), 5);
247    }
248
249    #[test]
250    fn proxy_format_config_total_kbps() {
251        let cfg = ProxyFormatConfig::for_preset(QualityPreset::Mobile);
252        assert_eq!(cfg.total_kbps(), 400 + 64);
253    }
254
255    #[test]
256    fn proxy_format_config_fits_budget_true() {
257        let cfg = ProxyFormatConfig::for_preset(QualityPreset::Mobile);
258        assert!(cfg.fits_budget(1_000));
259    }
260
261    #[test]
262    fn proxy_format_config_fits_budget_false() {
263        let cfg = ProxyFormatConfig::for_preset(QualityPreset::Archive);
264        assert!(!cfg.fits_budget(1_000));
265    }
266
267    #[test]
268    fn proxy_format_config_supports_resolution_ok() {
269        let cfg = ProxyFormatConfig::for_preset(QualityPreset::Editorial);
270        assert!(cfg.supports_resolution(1_920, 1_080));
271    }
272
273    #[test]
274    fn proxy_format_config_supports_resolution_fail() {
275        let cfg = ProxyFormatConfig::for_preset(QualityPreset::Mobile);
276        assert!(!cfg.supports_resolution(1_920, 1_080));
277    }
278
279    #[test]
280    fn proxy_format_config_codec_preset_mobile() {
281        let cfg = ProxyFormatConfig::for_preset(QualityPreset::Mobile);
282        assert_eq!(cfg.codec, "h264");
283        assert_eq!(cfg.container, "mp4");
284    }
285
286    #[test]
287    fn proxy_format_config_codec_preset_archive() {
288        let cfg = ProxyFormatConfig::for_preset(QualityPreset::Archive);
289        assert_eq!(cfg.codec, "dnxhd");
290        assert_eq!(cfg.container, "mxf");
291    }
292
293    #[test]
294    fn format_selector_new_has_all_presets() {
295        let sel = FormatSelector::new();
296        assert_eq!(sel.len(), QualityPreset::all().len());
297    }
298
299    #[test]
300    fn format_selector_get_by_preset() {
301        let sel = FormatSelector::new();
302        let cfg = sel.get(QualityPreset::Web);
303        assert!(cfg.is_some());
304        assert_eq!(
305            cfg.expect("should succeed in test").preset,
306            QualityPreset::Web
307        );
308    }
309
310    #[test]
311    fn format_selector_select_for_budget_returns_none_if_too_low() {
312        let sel = FormatSelector::new();
313        // budget of 1 kbps should match nothing
314        assert!(sel.select_for_budget(1).is_none());
315    }
316
317    #[test]
318    fn format_selector_select_for_budget_returns_mobile_for_small_budget() {
319        let sel = FormatSelector::new();
320        let cfg = sel.select_for_budget(500);
321        assert!(cfg.is_some());
322        assert_eq!(
323            cfg.expect("should succeed in test").preset,
324            QualityPreset::Mobile
325        );
326    }
327
328    #[test]
329    fn format_selector_all_ascending_ordered() {
330        let sel = FormatSelector::new();
331        let ascending = sel.all_ascending();
332        for pair in ascending.windows(2) {
333            assert!(pair[0].video_kbps <= pair[1].video_kbps);
334        }
335    }
336
337    #[test]
338    fn format_selector_is_empty_false() {
339        let sel = FormatSelector::new();
340        assert!(!sel.is_empty());
341    }
342}