1use std::sync::{Arc, Mutex};
4use std::time::{Duration, Instant};
5
6#[derive(Debug, Clone)]
8pub struct ProgressInfo {
9 pub current_frame: u64,
11 pub total_frames: u64,
13 pub percent: f64,
15 pub eta: Option<Duration>,
17 pub fps: f64,
19 pub bitrate: u64,
21 pub elapsed: Duration,
23 pub pass: u32,
25 pub total_passes: u32,
27}
28
29pub type ProgressCallback = Arc<dyn Fn(&ProgressInfo) + Send + Sync>;
31
32pub struct ProgressTracker {
34 start_time: Instant,
35 total_frames: u64,
36 current_frame: Arc<Mutex<u64>>,
37 total_passes: u32,
38 current_pass: Arc<Mutex<u32>>,
39 callback: Option<ProgressCallback>,
40 update_interval: Duration,
41 last_update: Arc<Mutex<Instant>>,
42 frame_times: Arc<Mutex<Vec<Instant>>>,
43}
44
45impl ProgressTracker {
46 #[must_use]
53 pub fn new(total_frames: u64, total_passes: u32) -> Self {
54 Self {
55 start_time: Instant::now(),
56 total_frames,
57 current_frame: Arc::new(Mutex::new(0)),
58 total_passes,
59 current_pass: Arc::new(Mutex::new(1)),
60 callback: None,
61 update_interval: Duration::from_millis(500),
62 last_update: Arc::new(Mutex::new(Instant::now())),
63 frame_times: Arc::new(Mutex::new(Vec::new())),
64 }
65 }
66
67 pub fn set_callback(&mut self, callback: ProgressCallback) {
69 self.callback = Some(callback);
70 }
71
72 pub fn set_update_interval(&mut self, interval: Duration) {
74 self.update_interval = interval;
75 }
76
77 pub fn update_frame(&self, frame: u64) {
79 if let Ok(mut current) = self.current_frame.lock() {
80 *current = frame;
81
82 if let Ok(mut times) = self.frame_times.lock() {
84 times.push(Instant::now());
85 if times.len() > 30 {
87 times.remove(0);
88 }
89 }
90 }
91
92 self.maybe_trigger_callback();
93 }
94
95 pub fn increment_frame(&self) {
97 if let Ok(mut current) = self.current_frame.lock() {
98 *current += 1;
99 let frame = *current;
100 drop(current);
101
102 if let Ok(mut times) = self.frame_times.lock() {
104 times.push(Instant::now());
105 if times.len() > 30 {
106 times.remove(0);
107 }
108 }
109
110 if frame % 10 == 0 {
112 self.maybe_trigger_callback();
113 }
114 }
115 }
116
117 pub fn set_pass(&self, pass: u32) {
119 if let Ok(mut current_pass) = self.current_pass.lock() {
120 *current_pass = pass;
121 }
122 if let Ok(mut current_frame) = self.current_frame.lock() {
124 *current_frame = 0;
125 }
126 self.maybe_trigger_callback();
127 }
128
129 #[must_use]
131 pub fn get_info(&self) -> ProgressInfo {
132 let current_frame = self.current_frame.lock().map_or(0, |f| *f);
133 let current_pass = self.current_pass.lock().map_or(1, |p| *p);
134 let elapsed = self.start_time.elapsed();
135
136 let frames_per_pass = self.total_frames;
138 let total_work = frames_per_pass * u64::from(self.total_passes);
139 let completed_work = frames_per_pass * u64::from(current_pass - 1) + current_frame;
140 let percent = if total_work > 0 {
141 (completed_work as f64 / total_work as f64) * 100.0
142 } else {
143 0.0
144 };
145
146 let fps = self.calculate_fps();
148
149 let eta = if fps > 0.0 && total_work > completed_work {
151 let remaining_frames = total_work - completed_work;
152 let remaining_seconds = remaining_frames as f64 / fps;
153 Some(Duration::from_secs_f64(remaining_seconds))
154 } else {
155 None
156 };
157
158 ProgressInfo {
159 current_frame,
160 total_frames: self.total_frames,
161 percent,
162 eta,
163 fps,
164 bitrate: 0, elapsed,
166 pass: current_pass,
167 total_passes: self.total_passes,
168 }
169 }
170
171 pub fn reset_for_pass(&self, pass: u32) {
173 if let Ok(mut current_frame) = self.current_frame.lock() {
174 *current_frame = 0;
175 }
176 if let Ok(mut current_pass) = self.current_pass.lock() {
177 *current_pass = pass;
178 }
179 if let Ok(mut times) = self.frame_times.lock() {
180 times.clear();
181 }
182 }
183
184 fn calculate_fps(&self) -> f64 {
185 if let Ok(times) = self.frame_times.lock() {
186 if times.len() < 2 {
187 return 0.0;
188 }
189
190 let first = times[0];
191 let last = *times.last().expect("invariant: len >= 2 checked above");
192 let duration = last.duration_since(first);
193
194 if duration.as_secs_f64() > 0.0 {
195 (times.len() - 1) as f64 / duration.as_secs_f64()
196 } else {
197 0.0
198 }
199 } else {
200 0.0
201 }
202 }
203
204 fn maybe_trigger_callback(&self) {
205 if let Some(callback) = &self.callback {
206 if let Ok(mut last_update) = self.last_update.lock() {
207 if last_update.elapsed() >= self.update_interval {
208 *last_update = Instant::now();
209 let info = self.get_info();
210 callback(&info);
211 }
212 }
213 }
214 }
215}
216
217pub struct ProgressTrackerBuilder {
219 #[allow(dead_code)]
220 total_frames: u64,
221 #[allow(dead_code)]
222 total_passes: u32,
223 #[allow(dead_code)]
224 callback: Option<ProgressCallback>,
225 #[allow(dead_code)]
226 update_interval: Duration,
227}
228impl ProgressTrackerBuilder {
229 #[allow(dead_code)]
230 #[must_use]
232 pub fn new(total_frames: u64) -> Self {
233 Self {
234 total_frames,
235 total_passes: 1,
236 callback: None,
237 update_interval: Duration::from_millis(500),
238 }
239 }
240
241 #[must_use]
243 #[allow(dead_code)]
244 pub fn passes(mut self, passes: u32) -> Self {
245 self.total_passes = passes;
246 self
247 }
248
249 #[must_use]
251 #[allow(dead_code)]
252 pub fn callback(mut self, callback: ProgressCallback) -> Self {
253 self.callback = Some(callback);
254 self
255 }
256
257 #[must_use]
259 #[allow(dead_code)]
260 pub fn update_interval(mut self, interval: Duration) -> Self {
261 self.update_interval = interval;
262 self
263 }
264
265 #[must_use]
267 #[allow(dead_code)]
268 pub fn build(self) -> ProgressTracker {
269 let mut tracker = ProgressTracker::new(self.total_frames, self.total_passes);
270 if let Some(callback) = self.callback {
271 tracker.set_callback(callback);
272 }
273 tracker.set_update_interval(self.update_interval);
274 tracker
275 }
276}
277
278impl ProgressInfo {
279 #[must_use]
281 pub fn format_eta(&self) -> String {
282 if let Some(eta) = self.eta {
283 let total_secs = eta.as_secs();
284 let hours = total_secs / 3600;
285 let minutes = (total_secs % 3600) / 60;
286 let seconds = total_secs % 60;
287
288 if hours > 0 {
289 format!("{hours}h {minutes}m {seconds}s")
290 } else if minutes > 0 {
291 format!("{minutes}m {seconds}s")
292 } else {
293 format!("{seconds}s")
294 }
295 } else {
296 "Unknown".to_string()
297 }
298 }
299
300 #[must_use]
302 pub fn format_elapsed(&self) -> String {
303 let total_secs = self.elapsed.as_secs();
304 let hours = total_secs / 3600;
305 let minutes = (total_secs % 3600) / 60;
306 let seconds = total_secs % 60;
307
308 if hours > 0 {
309 format!("{hours}h {minutes}m {seconds}s")
310 } else if minutes > 0 {
311 format!("{minutes}m {seconds}s")
312 } else {
313 format!("{seconds}s")
314 }
315 }
316
317 #[must_use]
319 pub fn format_bitrate(&self) -> String {
320 let kbps = self.bitrate / 1000;
321 if kbps > 1000 {
322 format!("{:.2} Mbps", kbps as f64 / 1000.0)
323 } else {
324 format!("{kbps} kbps")
325 }
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test_progress_tracker_creation() {
335 let tracker = ProgressTracker::new(1000, 1);
336 let info = tracker.get_info();
337
338 assert_eq!(info.current_frame, 0);
339 assert_eq!(info.total_frames, 1000);
340 assert_eq!(info.percent, 0.0);
341 assert_eq!(info.pass, 1);
342 assert_eq!(info.total_passes, 1);
343 }
344
345 #[test]
346 fn test_progress_update() {
347 let tracker = ProgressTracker::new(1000, 1);
348 tracker.update_frame(500);
349
350 let info = tracker.get_info();
351 assert_eq!(info.current_frame, 500);
352 assert!((info.percent - 50.0).abs() < 0.1);
353 }
354
355 #[test]
356 fn test_progress_increment() {
357 let tracker = ProgressTracker::new(1000, 1);
358
359 for _ in 0..100 {
360 tracker.increment_frame();
361 }
362
363 let info = tracker.get_info();
364 assert_eq!(info.current_frame, 100);
365 assert!((info.percent - 10.0).abs() < 0.1);
366 }
367
368 #[test]
369 fn test_multipass_progress() {
370 let tracker = ProgressTracker::new(1000, 2);
371 tracker.update_frame(1000);
372 tracker.set_pass(2);
373
374 let info = tracker.get_info();
375 assert_eq!(info.pass, 2);
376 assert!((info.percent - 50.0).abs() < 0.1);
378 }
379
380 #[test]
381 fn test_progress_reset() {
382 let tracker = ProgressTracker::new(1000, 2);
383 tracker.update_frame(500);
384 tracker.reset_for_pass(2);
385
386 let info = tracker.get_info();
387 assert_eq!(info.current_frame, 0);
388 assert_eq!(info.pass, 2);
389 }
390
391 #[test]
392 fn test_progress_builder() {
393 let tracker = ProgressTrackerBuilder::new(1000)
394 .passes(2)
395 .update_interval(Duration::from_secs(1))
396 .build();
397
398 let info = tracker.get_info();
399 assert_eq!(info.total_frames, 1000);
400 assert_eq!(info.total_passes, 2);
401 }
402
403 #[test]
404 fn test_format_eta() {
405 let info = ProgressInfo {
406 current_frame: 500,
407 total_frames: 1000,
408 percent: 50.0,
409 eta: Some(Duration::from_secs(3725)), fps: 30.0,
411 bitrate: 5_000_000,
412 elapsed: Duration::from_secs(60),
413 pass: 1,
414 total_passes: 1,
415 };
416
417 assert_eq!(info.format_eta(), "1h 2m 5s");
418 }
419
420 #[test]
421 fn test_format_elapsed() {
422 let info = ProgressInfo {
423 current_frame: 500,
424 total_frames: 1000,
425 percent: 50.0,
426 eta: None,
427 fps: 30.0,
428 bitrate: 5_000_000,
429 elapsed: Duration::from_secs(125), pass: 1,
431 total_passes: 1,
432 };
433
434 assert_eq!(info.format_elapsed(), "2m 5s");
435 }
436
437 #[test]
438 fn test_format_bitrate() {
439 let info = ProgressInfo {
440 current_frame: 500,
441 total_frames: 1000,
442 percent: 50.0,
443 eta: None,
444 fps: 30.0,
445 bitrate: 5_500_000,
446 elapsed: Duration::from_secs(60),
447 pass: 1,
448 total_passes: 1,
449 };
450
451 assert_eq!(info.format_bitrate(), "5.50 Mbps");
452 }
453
454 #[test]
455 fn test_format_bitrate_kbps() {
456 let info = ProgressInfo {
457 current_frame: 500,
458 total_frames: 1000,
459 percent: 50.0,
460 eta: None,
461 fps: 30.0,
462 bitrate: 500_000,
463 elapsed: Duration::from_secs(60),
464 pass: 1,
465 total_passes: 1,
466 };
467
468 assert_eq!(info.format_bitrate(), "500 kbps");
469 }
470}