Skip to main content

oximedia_transcode/
transcode_session.rs

1//! Transcode session tracking with lifecycle management.
2//!
3//! Provides `SessionState`, `TranscodeSession`, and `TranscodeSessionManager`
4//! for monitoring active and completed transcode operations.
5
6#![allow(dead_code)]
7
8use std::collections::HashMap;
9
10/// Lifecycle state of a transcode session.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum SessionState {
13    /// Session has been created but not yet started.
14    Pending,
15    /// Transcoding is currently in progress.
16    Running,
17    /// Transcoding finished successfully.
18    Completed,
19    /// Transcoding failed with an error.
20    Failed,
21    /// Session was cancelled by the user.
22    Cancelled,
23}
24
25impl SessionState {
26    /// Returns `true` if the session is currently active (Running).
27    #[must_use]
28    pub fn is_active(self) -> bool {
29        self == SessionState::Running
30    }
31
32    /// Returns `true` if the session has reached a terminal state.
33    #[must_use]
34    pub fn is_terminal(self) -> bool {
35        matches!(
36            self,
37            SessionState::Completed | SessionState::Failed | SessionState::Cancelled
38        )
39    }
40
41    /// Returns a short label for display purposes.
42    #[must_use]
43    pub fn label(self) -> &'static str {
44        match self {
45            SessionState::Pending => "pending",
46            SessionState::Running => "running",
47            SessionState::Completed => "completed",
48            SessionState::Failed => "failed",
49            SessionState::Cancelled => "cancelled",
50        }
51    }
52}
53
54/// A transcode session representing one input-to-output operation.
55#[derive(Debug, Clone)]
56pub struct TranscodeSession {
57    /// Unique session identifier.
58    pub id: u64,
59    /// Input file path.
60    pub input_path: String,
61    /// Output file path.
62    pub output_path: String,
63    /// Current state of the session.
64    pub state: SessionState,
65    /// Start time in milliseconds since epoch (0 if not started).
66    pub start_ms: u64,
67    /// End time in milliseconds since epoch (0 if not finished).
68    pub end_ms: u64,
69    /// Progress as a value in `[0.0, 1.0]`.
70    progress: f64,
71    /// Total duration of the input in milliseconds.
72    pub total_duration_ms: u64,
73}
74
75impl TranscodeSession {
76    /// Creates a new session in the `Pending` state.
77    pub fn new(
78        id: u64,
79        input_path: impl Into<String>,
80        output_path: impl Into<String>,
81        total_duration_ms: u64,
82    ) -> Self {
83        Self {
84            id,
85            input_path: input_path.into(),
86            output_path: output_path.into(),
87            state: SessionState::Pending,
88            start_ms: 0,
89            end_ms: 0,
90            progress: 0.0,
91            total_duration_ms,
92        }
93    }
94
95    /// Marks the session as started at the given timestamp.
96    pub fn start(&mut self, now_ms: u64) {
97        self.state = SessionState::Running;
98        self.start_ms = now_ms;
99    }
100
101    /// Updates the progress (clamped to [0.0, 1.0]).
102    pub fn set_progress(&mut self, pct: f64) {
103        self.progress = pct.clamp(0.0, 1.0);
104    }
105
106    /// Marks the session as completed at the given timestamp.
107    pub fn complete(&mut self, now_ms: u64) {
108        self.state = SessionState::Completed;
109        self.end_ms = now_ms;
110        self.progress = 1.0;
111    }
112
113    /// Marks the session as failed at the given timestamp.
114    pub fn fail(&mut self, now_ms: u64) {
115        self.state = SessionState::Failed;
116        self.end_ms = now_ms;
117    }
118
119    /// Marks the session as cancelled at the given timestamp.
120    pub fn cancel(&mut self, now_ms: u64) {
121        self.state = SessionState::Cancelled;
122        self.end_ms = now_ms;
123    }
124
125    /// Returns elapsed time in milliseconds (0 if not started, uses `end_ms` if terminal).
126    #[must_use]
127    pub fn elapsed_ms(&self, now_ms: u64) -> u64 {
128        if self.start_ms == 0 {
129            return 0;
130        }
131        let end = if self.state.is_terminal() {
132            self.end_ms
133        } else {
134            now_ms
135        };
136        end.saturating_sub(self.start_ms)
137    }
138
139    /// Returns progress as a percentage (0–100).
140    #[allow(clippy::cast_precision_loss)]
141    #[must_use]
142    pub fn progress_pct(&self) -> f64 {
143        self.progress * 100.0
144    }
145
146    /// Returns `true` if the session is currently running.
147    #[must_use]
148    pub fn is_active(&self) -> bool {
149        self.state.is_active()
150    }
151}
152
153/// Manages a collection of transcode sessions.
154#[derive(Debug, Default)]
155pub struct TranscodeSessionManager {
156    sessions: HashMap<u64, TranscodeSession>,
157    next_id: u64,
158}
159
160impl TranscodeSessionManager {
161    /// Creates a new, empty manager.
162    #[must_use]
163    pub fn new() -> Self {
164        Self::default()
165    }
166
167    /// Creates a new session and registers it. Returns the session ID.
168    pub fn create(
169        &mut self,
170        input_path: impl Into<String>,
171        output_path: impl Into<String>,
172        total_duration_ms: u64,
173    ) -> u64 {
174        let id = self.next_id;
175        self.next_id += 1;
176        let session = TranscodeSession::new(id, input_path, output_path, total_duration_ms);
177        self.sessions.insert(id, session);
178        id
179    }
180
181    /// Returns a reference to the session with the given ID, if it exists.
182    #[must_use]
183    pub fn get(&self, id: u64) -> Option<&TranscodeSession> {
184        self.sessions.get(&id)
185    }
186
187    /// Returns a mutable reference to the session with the given ID.
188    pub fn get_mut(&mut self, id: u64) -> Option<&mut TranscodeSession> {
189        self.sessions.get_mut(&id)
190    }
191
192    /// Returns the number of currently active (Running) sessions.
193    #[must_use]
194    pub fn active_count(&self) -> usize {
195        self.sessions.values().filter(|s| s.is_active()).count()
196    }
197
198    /// Returns the total number of sessions tracked.
199    #[must_use]
200    pub fn total_count(&self) -> usize {
201        self.sessions.len()
202    }
203
204    /// Removes a session by ID. Returns `true` if it existed.
205    pub fn remove(&mut self, id: u64) -> bool {
206        self.sessions.remove(&id).is_some()
207    }
208
209    /// Returns IDs of all sessions in a given state.
210    #[must_use]
211    pub fn sessions_in_state(&self, state: SessionState) -> Vec<u64> {
212        self.sessions
213            .values()
214            .filter(|s| s.state == state)
215            .map(|s| s.id)
216            .collect()
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_state_is_active_only_running() {
226        assert!(SessionState::Running.is_active());
227        assert!(!SessionState::Pending.is_active());
228        assert!(!SessionState::Completed.is_active());
229        assert!(!SessionState::Failed.is_active());
230        assert!(!SessionState::Cancelled.is_active());
231    }
232
233    #[test]
234    fn test_state_is_terminal() {
235        assert!(SessionState::Completed.is_terminal());
236        assert!(SessionState::Failed.is_terminal());
237        assert!(SessionState::Cancelled.is_terminal());
238        assert!(!SessionState::Pending.is_terminal());
239        assert!(!SessionState::Running.is_terminal());
240    }
241
242    #[test]
243    fn test_state_labels() {
244        assert_eq!(SessionState::Running.label(), "running");
245        assert_eq!(SessionState::Pending.label(), "pending");
246        assert_eq!(SessionState::Completed.label(), "completed");
247    }
248
249    #[test]
250    fn test_session_initial_state_pending() {
251        let s = TranscodeSession::new(0, "in.mp4", "out.mp4", 60_000);
252        assert_eq!(s.state, SessionState::Pending);
253        assert_eq!(s.elapsed_ms(1000), 0);
254    }
255
256    #[test]
257    fn test_session_start_sets_running() {
258        let mut s = TranscodeSession::new(0, "in", "out", 60_000);
259        s.start(1000);
260        assert!(s.is_active());
261        assert_eq!(s.state, SessionState::Running);
262    }
263
264    #[test]
265    fn test_session_elapsed_ms_while_running() {
266        let mut s = TranscodeSession::new(0, "in", "out", 60_000);
267        s.start(1000);
268        assert_eq!(s.elapsed_ms(4000), 3000);
269    }
270
271    #[test]
272    fn test_session_elapsed_ms_after_complete() {
273        let mut s = TranscodeSession::new(0, "in", "out", 60_000);
274        s.start(1000);
275        s.complete(5000);
276        // Should use end_ms regardless of now_ms
277        assert_eq!(s.elapsed_ms(9999), 4000);
278    }
279
280    #[test]
281    fn test_session_progress_pct_clamped() {
282        let mut s = TranscodeSession::new(0, "in", "out", 60_000);
283        s.set_progress(1.5);
284        assert!((s.progress_pct() - 100.0).abs() < f64::EPSILON);
285        s.set_progress(-0.5);
286        assert!((s.progress_pct()).abs() < f64::EPSILON);
287    }
288
289    #[test]
290    fn test_session_complete_sets_progress_full() {
291        let mut s = TranscodeSession::new(0, "in", "out", 60_000);
292        s.start(0);
293        s.complete(1000);
294        assert!((s.progress_pct() - 100.0).abs() < f64::EPSILON);
295    }
296
297    #[test]
298    fn test_session_fail() {
299        let mut s = TranscodeSession::new(0, "in", "out", 60_000);
300        s.start(0);
301        s.fail(500);
302        assert_eq!(s.state, SessionState::Failed);
303        assert!(s.state.is_terminal());
304    }
305
306    #[test]
307    fn test_manager_create_and_get() {
308        let mut mgr = TranscodeSessionManager::new();
309        let id = mgr.create("in.mp4", "out.mp4", 60_000);
310        let s = mgr.get(id).expect("should succeed in test");
311        assert_eq!(s.id, id);
312        assert_eq!(s.state, SessionState::Pending);
313    }
314
315    #[test]
316    fn test_manager_active_count() {
317        let mut mgr = TranscodeSessionManager::new();
318        let id1 = mgr.create("a", "b", 1000);
319        let id2 = mgr.create("c", "d", 1000);
320        mgr.get_mut(id1).expect("should succeed in test").start(0);
321        assert_eq!(mgr.active_count(), 1);
322        mgr.get_mut(id2).expect("should succeed in test").start(0);
323        assert_eq!(mgr.active_count(), 2);
324    }
325
326    #[test]
327    fn test_manager_remove() {
328        let mut mgr = TranscodeSessionManager::new();
329        let id = mgr.create("in", "out", 1000);
330        assert!(mgr.remove(id));
331        assert!(!mgr.remove(id));
332        assert!(mgr.get(id).is_none());
333    }
334
335    #[test]
336    fn test_manager_sessions_in_state() {
337        let mut mgr = TranscodeSessionManager::new();
338        let id = mgr.create("in", "out", 1000);
339        mgr.get_mut(id).expect("should succeed in test").start(0);
340        mgr.get_mut(id)
341            .expect("should succeed in test")
342            .complete(100);
343        let completed = mgr.sessions_in_state(SessionState::Completed);
344        assert!(completed.contains(&id));
345    }
346}