Skip to main content

oximedia_proxy/
generation.rs

1//! Proxy generation: profiles, task management, and queue.
2//!
3//! Provides lightweight, pure-Rust types for queuing and tracking proxy
4//! generation tasks without requiring an actual encoder.
5
6#![allow(dead_code)]
7#![allow(missing_docs)]
8
9// ---------------------------------------------------------------------------
10// ProxyProfile
11// ---------------------------------------------------------------------------
12
13/// Configuration that describes how a proxy should be encoded.
14#[derive(Debug, Clone, PartialEq)]
15pub struct ProxyProfile {
16    /// Human-readable name for this profile.
17    pub name: String,
18    /// Output width in pixels.
19    pub width: u32,
20    /// Output height in pixels.
21    pub height: u32,
22    /// Target bitrate in kilobits per second.
23    pub bitrate_kbps: u32,
24    /// Codec identifier (e.g. "h264", "vp9").
25    pub codec: String,
26    /// Target frame rate.
27    pub frame_rate: f32,
28}
29
30impl ProxyProfile {
31    /// Create a new proxy profile.
32    #[must_use]
33    pub fn new(
34        name: impl Into<String>,
35        width: u32,
36        height: u32,
37        bitrate_kbps: u32,
38        codec: impl Into<String>,
39        frame_rate: f32,
40    ) -> Self {
41        Self {
42            name: name.into(),
43            width,
44            height,
45            bitrate_kbps,
46            codec: codec.into(),
47            frame_rate,
48        }
49    }
50
51    /// Standard offline-edit proxy: 1920×1080 H.264 @ 8 000 kbps / 25 fps.
52    #[must_use]
53    pub fn offline_edit() -> Self {
54        Self::new("offline_edit", 1920, 1080, 8_000, "h264", 25.0)
55    }
56
57    /// Web preview proxy: 1280×720 H.264 @ 2 000 kbps / 25 fps.
58    #[must_use]
59    pub fn web_preview() -> Self {
60        Self::new("web_preview", 1280, 720, 2_000, "h264", 25.0)
61    }
62
63    /// Mobile proxy: 854×480 H.264 @ 800 kbps / 25 fps.
64    #[must_use]
65    pub fn mobile() -> Self {
66        Self::new("mobile", 854, 480, 800, "h264", 25.0)
67    }
68}
69
70// ---------------------------------------------------------------------------
71// ProxyStatus
72// ---------------------------------------------------------------------------
73
74/// Lifecycle state of a single proxy generation task.
75#[derive(Debug, Clone, PartialEq)]
76pub enum ProxyStatus {
77    /// Waiting to be picked up by a worker.
78    Queued,
79    /// Currently being encoded; inner value is percentage complete (0–100).
80    Processing(u8),
81    /// Successfully finished.
82    Done,
83    /// Failed with an error message.
84    Failed(String),
85}
86
87impl ProxyStatus {
88    /// Returns `true` when the task has reached a terminal state (done or failed).
89    #[must_use]
90    pub fn is_complete(&self) -> bool {
91        matches!(self, Self::Done | Self::Failed(_))
92    }
93
94    /// Returns the progress percentage.
95    ///
96    /// * `Queued`      → 0
97    /// * `Processing(p)` → p
98    /// * `Done`        → 100
99    /// * `Failed`      → 0
100    #[must_use]
101    pub fn progress_pct(&self) -> u8 {
102        match self {
103            Self::Queued => 0,
104            Self::Processing(p) => *p,
105            Self::Done => 100,
106            Self::Failed(_) => 0,
107        }
108    }
109}
110
111// ---------------------------------------------------------------------------
112// ProxyTask
113// ---------------------------------------------------------------------------
114
115/// A single proxy generation task.
116#[derive(Debug, Clone)]
117pub struct ProxyTask {
118    /// Path to the high-resolution source file.
119    pub source_path: String,
120    /// Destination path for the generated proxy.
121    pub output_path: String,
122    /// Encoding profile to use.
123    pub profile: ProxyProfile,
124    /// Current status of this task.
125    pub status: ProxyStatus,
126}
127
128impl ProxyTask {
129    /// Create a new task with [`ProxyStatus::Queued`] status.
130    #[must_use]
131    pub fn new(
132        source_path: impl Into<String>,
133        output_path: impl Into<String>,
134        profile: ProxyProfile,
135    ) -> Self {
136        Self {
137            source_path: source_path.into(),
138            output_path: output_path.into(),
139            profile,
140            status: ProxyStatus::Queued,
141        }
142    }
143}
144
145// ---------------------------------------------------------------------------
146// ProxyGenerator
147// ---------------------------------------------------------------------------
148
149/// Queue-based proxy generator that tracks tasks.
150#[derive(Debug, Default)]
151pub struct ProxyGenerator {
152    /// All registered tasks.
153    pub tasks: Vec<ProxyTask>,
154}
155
156impl ProxyGenerator {
157    /// Create an empty proxy generator.
158    #[must_use]
159    pub fn new() -> Self {
160        Self::default()
161    }
162
163    /// Enqueue a new proxy generation task.
164    pub fn queue(&mut self, source: &str, output: &str, profile: ProxyProfile) {
165        self.tasks.push(ProxyTask::new(source, output, profile));
166    }
167
168    /// Number of tasks that have not yet completed (Queued or Processing).
169    #[must_use]
170    pub fn pending_count(&self) -> usize {
171        self.tasks
172            .iter()
173            .filter(|t| !t.status.is_complete())
174            .count()
175    }
176
177    /// Number of tasks that finished successfully.
178    #[must_use]
179    pub fn complete_count(&self) -> usize {
180        self.tasks
181            .iter()
182            .filter(|t| matches!(t.status, ProxyStatus::Done))
183            .count()
184    }
185
186    /// References to all tasks that have failed.
187    #[must_use]
188    pub fn failed_tasks(&self) -> Vec<&ProxyTask> {
189        self.tasks
190            .iter()
191            .filter(|t| matches!(t.status, ProxyStatus::Failed(_)))
192            .collect()
193    }
194}
195
196// ---------------------------------------------------------------------------
197// Unit tests
198// ---------------------------------------------------------------------------
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_proxy_profile_offline_edit() {
206        let p = ProxyProfile::offline_edit();
207        assert_eq!(p.width, 1920);
208        assert_eq!(p.height, 1080);
209        assert_eq!(p.codec, "h264");
210    }
211
212    #[test]
213    fn test_proxy_profile_web_preview() {
214        let p = ProxyProfile::web_preview();
215        assert_eq!(p.width, 1280);
216        assert_eq!(p.height, 720);
217    }
218
219    #[test]
220    fn test_proxy_profile_mobile() {
221        let p = ProxyProfile::mobile();
222        assert_eq!(p.width, 854);
223        assert_eq!(p.height, 480);
224        assert!(p.bitrate_kbps < 1_000);
225    }
226
227    #[test]
228    fn test_proxy_status_is_complete_queued() {
229        assert!(!ProxyStatus::Queued.is_complete());
230    }
231
232    #[test]
233    fn test_proxy_status_is_complete_processing() {
234        assert!(!ProxyStatus::Processing(50).is_complete());
235    }
236
237    #[test]
238    fn test_proxy_status_is_complete_done() {
239        assert!(ProxyStatus::Done.is_complete());
240    }
241
242    #[test]
243    fn test_proxy_status_is_complete_failed() {
244        assert!(ProxyStatus::Failed("err".to_string()).is_complete());
245    }
246
247    #[test]
248    fn test_proxy_status_progress_queued() {
249        assert_eq!(ProxyStatus::Queued.progress_pct(), 0);
250    }
251
252    #[test]
253    fn test_proxy_status_progress_processing() {
254        assert_eq!(ProxyStatus::Processing(75).progress_pct(), 75);
255    }
256
257    #[test]
258    fn test_proxy_status_progress_done() {
259        assert_eq!(ProxyStatus::Done.progress_pct(), 100);
260    }
261
262    #[test]
263    fn test_proxy_generator_queue_pending() {
264        let mut gen = ProxyGenerator::new();
265        gen.queue("src.mov", "out.mp4", ProxyProfile::offline_edit());
266        gen.queue("src2.mov", "out2.mp4", ProxyProfile::mobile());
267        assert_eq!(gen.pending_count(), 2);
268        assert_eq!(gen.complete_count(), 0);
269    }
270
271    #[test]
272    fn test_proxy_generator_complete_count() {
273        let mut gen = ProxyGenerator::new();
274        gen.queue("src.mov", "out.mp4", ProxyProfile::offline_edit());
275        gen.tasks[0].status = ProxyStatus::Done;
276        assert_eq!(gen.complete_count(), 1);
277        assert_eq!(gen.pending_count(), 0);
278    }
279
280    #[test]
281    fn test_proxy_generator_failed_tasks() {
282        let mut gen = ProxyGenerator::new();
283        gen.queue("a.mov", "a.mp4", ProxyProfile::mobile());
284        gen.queue("b.mov", "b.mp4", ProxyProfile::mobile());
285        gen.tasks[0].status = ProxyStatus::Failed("codec error".to_string());
286        let failed = gen.failed_tasks();
287        assert_eq!(failed.len(), 1);
288        assert_eq!(failed[0].source_path, "a.mov");
289    }
290}