Skip to main content

oximedia_proxy/
proxy_registry_ext.rs

1//! Extended proxy registry with best-match selection.
2//!
3//! [`ProxyRegistryExt`] maintains an in-memory list of [`ProxyRecord`]s —
4//! lightweight descriptors that associate a source asset with one or more
5//! proxy variants — and provides [`ProxyRegistryExt::find_best_proxy`] to
6//! return the variant most appropriate for a given resolution target.
7
8#![allow(dead_code)]
9
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12
13/// Resolution expressed as `(width, height)` in pixels.
14pub type Resolution = (u32, u32);
15
16/// Describes a single proxy variant stored in the registry.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ProxyVariantRecord {
19    /// Path to the proxy file on disk.
20    pub path: PathBuf,
21    /// Pixel dimensions of the proxy.
22    pub resolution: Resolution,
23    /// Codec label (e.g. `"h264"`, `"prores"`).
24    pub codec: String,
25    /// Video bitrate in kbps.
26    pub video_kbps: u32,
27}
28
29impl ProxyVariantRecord {
30    /// Create a new variant record.
31    pub fn new(
32        path: impl Into<PathBuf>,
33        resolution: Resolution,
34        codec: impl Into<String>,
35        video_kbps: u32,
36    ) -> Self {
37        Self {
38            path: path.into(),
39            resolution,
40            codec: codec.into(),
41            video_kbps,
42        }
43    }
44
45    /// Number of pixels in the frame.
46    pub fn pixel_count(&self) -> u64 {
47        self.resolution.0 as u64 * self.resolution.1 as u64
48    }
49}
50
51/// Associates a source asset with its registered proxy variants.
52#[derive(Debug, Clone)]
53pub struct ProxyRecord {
54    /// Canonical path to the original (high-resolution) source file.
55    pub source_path: PathBuf,
56    /// All known proxy variants for this source.
57    pub variants: Vec<ProxyVariantRecord>,
58}
59
60impl ProxyRecord {
61    /// Create an empty record for `source_path`.
62    pub fn new(source_path: impl Into<PathBuf>) -> Self {
63        Self {
64            source_path: source_path.into(),
65            variants: Vec::new(),
66        }
67    }
68
69    /// Add a proxy variant to this record.
70    pub fn add_variant(&mut self, variant: ProxyVariantRecord) {
71        self.variants.push(variant);
72    }
73
74    /// Return `true` if at least one variant has been registered.
75    pub fn has_proxies(&self) -> bool {
76        !self.variants.is_empty()
77    }
78
79    /// Find the variant whose pixel count is closest to `target_resolution`.
80    ///
81    /// Among variants with equal pixel distance, the one with the higher bitrate
82    /// is preferred.
83    pub fn best_variant_for(&self, target: Resolution) -> Option<&ProxyVariantRecord> {
84        if self.variants.is_empty() {
85            return None;
86        }
87        let target_px = target.0 as i64 * target.1 as i64;
88        self.variants.iter().min_by(|a, b| {
89            let da = (a.pixel_count() as i64 - target_px).unsigned_abs();
90            let db = (b.pixel_count() as i64 - target_px).unsigned_abs();
91            da.cmp(&db).then_with(|| b.video_kbps.cmp(&a.video_kbps))
92        })
93    }
94}
95
96/// In-memory registry of [`ProxyRecord`]s indexed by source path.
97#[derive(Debug, Default)]
98pub struct ProxyRegistryExt {
99    records: HashMap<PathBuf, ProxyRecord>,
100}
101
102impl ProxyRegistryExt {
103    /// Create an empty registry.
104    pub fn new() -> Self {
105        Self::default()
106    }
107
108    /// Register or replace the record for `source_path`.
109    pub fn insert(&mut self, record: ProxyRecord) {
110        self.records.insert(record.source_path.clone(), record);
111    }
112
113    /// Add a single proxy variant for `source_path`.
114    ///
115    /// Creates a new [`ProxyRecord`] if one does not already exist.
116    pub fn add_variant(&mut self, source_path: impl Into<PathBuf>, variant: ProxyVariantRecord) {
117        let path = source_path.into();
118        self.records
119            .entry(path.clone())
120            .or_insert_with(|| ProxyRecord::new(path))
121            .add_variant(variant);
122    }
123
124    /// Return the record for `source_path`, if present.
125    pub fn get(&self, source_path: &Path) -> Option<&ProxyRecord> {
126        self.records.get(source_path)
127    }
128
129    /// Remove the record for `source_path`, returning it if it existed.
130    pub fn remove(&mut self, source_path: &Path) -> Option<ProxyRecord> {
131        self.records.remove(source_path)
132    }
133
134    /// Return the number of source assets registered.
135    pub fn len(&self) -> usize {
136        self.records.len()
137    }
138
139    /// Return `true` if no records have been registered.
140    pub fn is_empty(&self) -> bool {
141        self.records.is_empty()
142    }
143
144    /// Find the best proxy variant for `source_path` at `target_resolution`.
145    ///
146    /// Returns `None` if the source is not registered or has no variants.
147    pub fn find_best_proxy(
148        &self,
149        source_path: &Path,
150        target_resolution: Resolution,
151    ) -> Option<&ProxyVariantRecord> {
152        self.records
153            .get(source_path)?
154            .best_variant_for(target_resolution)
155    }
156
157    /// Return all source paths that have at least one proxy registered.
158    pub fn sources_with_proxies(&self) -> Vec<&Path> {
159        self.records
160            .values()
161            .filter(|r| r.has_proxies())
162            .map(|r| r.source_path.as_path())
163            .collect()
164    }
165
166    /// Total number of proxy variants across all records.
167    pub fn total_variants(&self) -> usize {
168        self.records.values().map(|r| r.variants.len()).sum()
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    fn make_variant(path: &str, w: u32, h: u32, codec: &str, kbps: u32) -> ProxyVariantRecord {
177        ProxyVariantRecord::new(path, (w, h), codec, kbps)
178    }
179
180    #[test]
181    fn variant_record_pixel_count() {
182        let v = make_variant("p.mp4", 1920, 1080, "h264", 8000);
183        assert_eq!(v.pixel_count(), 1920 * 1080);
184    }
185
186    #[test]
187    fn proxy_record_add_variant() {
188        let mut rec = ProxyRecord::new("/src/clip.mov");
189        assert!(!rec.has_proxies());
190        rec.add_variant(make_variant("/proxy/clip_360.mp4", 640, 360, "h264", 500));
191        assert!(rec.has_proxies());
192        assert_eq!(rec.variants.len(), 1);
193    }
194
195    #[test]
196    fn proxy_record_best_variant_exact_match() {
197        let mut rec = ProxyRecord::new("/src/clip.mov");
198        rec.add_variant(make_variant("/proxy/360p.mp4", 640, 360, "h264", 500));
199        rec.add_variant(make_variant("/proxy/720p.mp4", 1280, 720, "h264", 2000));
200        let best = rec
201            .best_variant_for((1280, 720))
202            .expect("should succeed in test");
203        assert_eq!(best.resolution, (1280, 720));
204    }
205
206    #[test]
207    fn proxy_record_best_variant_closest_pixel_count() {
208        let mut rec = ProxyRecord::new("/src/clip.mov");
209        rec.add_variant(make_variant("/proxy/360p.mp4", 640, 360, "h264", 500));
210        rec.add_variant(make_variant("/proxy/1080p.mp4", 1920, 1080, "h264", 8000));
211        // Target is 720p — 1280×720 = 921 600. 360p is 230 400, 1080p is 2 073 600.
212        // Delta 360p = |230400 - 921600| = 691200
213        // Delta 1080p = |2073600 - 921600| = 1152000
214        // 360p is closer
215        let best = rec
216            .best_variant_for((1280, 720))
217            .expect("should succeed in test");
218        assert_eq!(best.resolution, (640, 360));
219    }
220
221    #[test]
222    fn proxy_record_best_variant_empty_returns_none() {
223        let rec = ProxyRecord::new("/src/clip.mov");
224        assert!(rec.best_variant_for((1280, 720)).is_none());
225    }
226
227    #[test]
228    fn registry_ext_insert_and_get() {
229        let mut reg = ProxyRegistryExt::new();
230        let rec = ProxyRecord::new("/src/a.mov");
231        reg.insert(rec);
232        assert!(reg.get(Path::new("/src/a.mov")).is_some());
233    }
234
235    #[test]
236    fn registry_ext_add_variant_creates_record() {
237        let mut reg = ProxyRegistryExt::new();
238        reg.add_variant(
239            "/src/b.mov",
240            make_variant("/p/b_360.mp4", 640, 360, "h264", 500),
241        );
242        assert!(reg.get(Path::new("/src/b.mov")).is_some());
243    }
244
245    #[test]
246    fn registry_ext_remove() {
247        let mut reg = ProxyRegistryExt::new();
248        reg.insert(ProxyRecord::new("/src/c.mov"));
249        let removed = reg.remove(Path::new("/src/c.mov"));
250        assert!(removed.is_some());
251        assert!(reg.get(Path::new("/src/c.mov")).is_none());
252    }
253
254    #[test]
255    fn registry_ext_len_and_empty() {
256        let mut reg = ProxyRegistryExt::new();
257        assert!(reg.is_empty());
258        reg.insert(ProxyRecord::new("/src/d.mov"));
259        assert_eq!(reg.len(), 1);
260    }
261
262    #[test]
263    fn registry_ext_find_best_proxy() {
264        let mut reg = ProxyRegistryExt::new();
265        reg.add_variant(
266            "/src/e.mov",
267            make_variant("/p/e_360.mp4", 640, 360, "h264", 500),
268        );
269        reg.add_variant(
270            "/src/e.mov",
271            make_variant("/p/e_1080.mp4", 1920, 1080, "h264", 8000),
272        );
273        let best = reg.find_best_proxy(Path::new("/src/e.mov"), (1920, 1080));
274        assert!(best.is_some());
275        assert_eq!(
276            best.expect("should succeed in test").resolution,
277            (1920, 1080)
278        );
279    }
280
281    #[test]
282    fn registry_ext_find_best_proxy_not_found() {
283        let reg = ProxyRegistryExt::new();
284        assert!(reg
285            .find_best_proxy(Path::new("/nonexistent.mov"), (1280, 720))
286            .is_none());
287    }
288
289    #[test]
290    fn registry_ext_sources_with_proxies() {
291        let mut reg = ProxyRegistryExt::new();
292        reg.add_variant(
293            "/src/f.mov",
294            make_variant("/p/f.mp4", 640, 360, "h264", 500),
295        );
296        reg.insert(ProxyRecord::new("/src/g.mov")); // no variants
297        let sources = reg.sources_with_proxies();
298        assert_eq!(sources.len(), 1);
299    }
300
301    #[test]
302    fn registry_ext_total_variants() {
303        let mut reg = ProxyRegistryExt::new();
304        reg.add_variant(
305            "/src/h.mov",
306            make_variant("/p/h1.mp4", 640, 360, "h264", 500),
307        );
308        reg.add_variant(
309            "/src/h.mov",
310            make_variant("/p/h2.mp4", 1280, 720, "h264", 2000),
311        );
312        reg.add_variant(
313            "/src/i.mov",
314            make_variant("/p/i1.mp4", 640, 360, "h264", 500),
315        );
316        assert_eq!(reg.total_variants(), 3);
317    }
318
319    #[test]
320    fn registry_ext_prefer_higher_bitrate_on_tie() {
321        let mut rec = ProxyRecord::new("/src/j.mov");
322        // Two variants at exactly the same resolution
323        rec.add_variant(make_variant("/p/j_low.mp4", 1280, 720, "h264", 1000));
324        rec.add_variant(make_variant("/p/j_high.mp4", 1280, 720, "h264", 5000));
325        let best = rec
326            .best_variant_for((1280, 720))
327            .expect("should succeed in test");
328        assert_eq!(best.video_kbps, 5000);
329    }
330}