1use colored::Colorize;
7use indicatif::{ProgressBar, ProgressStyle};
8use std::time::{Duration, Instant};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
12pub enum ProgressFormat {
13 #[default]
15 Plain,
16 Json,
18}
19
20pub struct TranscodeProgress {
29 bar: ProgressBar,
30 start_time: Instant,
31 frames_total: u64,
32 frames_done: u64,
33 bytes_written: u64,
34 last_update: Instant,
35 update_interval: Duration,
36 pub format: ProgressFormat,
38}
39
40impl TranscodeProgress {
41 pub fn new(total_frames: u64) -> Self {
47 let bar = ProgressBar::new(total_frames);
48
49 let style = ProgressStyle::default_bar()
50 .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} frames ({percent}%) {msg}")
51 .unwrap_or_else(|_| ProgressStyle::default_bar())
52 .progress_chars("=>-");
53
54 bar.set_style(style);
55
56 Self {
57 bar,
58 start_time: Instant::now(),
59 frames_total: total_frames,
60 frames_done: 0,
61 bytes_written: 0,
62 last_update: Instant::now(),
63 update_interval: Duration::from_millis(100),
64 format: ProgressFormat::Plain,
65 }
66 }
67
68 pub fn new_spinner() -> Self {
72 let bar = ProgressBar::new_spinner();
73
74 let style = ProgressStyle::default_spinner()
75 .template("{spinner:.green} [{elapsed_precise}] {pos} frames {msg}")
76 .unwrap_or_else(|_| ProgressStyle::default_spinner());
77
78 bar.set_style(style);
79
80 Self {
81 bar,
82 start_time: Instant::now(),
83 frames_total: 0,
84 frames_done: 0,
85 bytes_written: 0,
86 last_update: Instant::now(),
87 update_interval: Duration::from_millis(100),
88 format: ProgressFormat::Plain,
89 }
90 }
91
92 pub fn new_with_format(total_frames: u64, fmt: ProgressFormat) -> Self {
98 let mut this = Self::new(total_frames);
99 this.set_format(fmt);
100 this
101 }
102
103 pub fn set_format(&mut self, fmt: ProgressFormat) {
105 self.format = fmt;
106 if fmt == ProgressFormat::Json {
107 self.bar
109 .set_draw_target(indicatif::ProgressDrawTarget::hidden());
110 }
111 }
112
113 pub fn update(&mut self, frames: u64) {
119 self.frames_done = frames;
120
121 let now = Instant::now();
123 if now.duration_since(self.last_update) < self.update_interval {
124 return;
125 }
126 self.last_update = now;
127
128 let fps = self.fps();
129 let eta = self.eta();
130 let bitrate = self.bitrate();
131
132 match self.format {
133 ProgressFormat::Plain => {
134 self.bar.set_position(frames);
135 let msg = format!(
136 "{:.1} fps | {} | {}",
137 fps,
138 format_eta(eta),
139 format_bitrate(bitrate)
140 );
141 self.bar.set_message(msg);
142 }
143 ProgressFormat::Json => {
144 let elapsed = self.start_time.elapsed().as_secs_f64();
145 let eta_secs = eta.as_secs_f64();
146 let record = serde_json::json!({
147 "kind": "progress",
148 "frames_done": frames,
149 "frames_total": self.frames_total,
150 "fps": fps,
151 "bitrate_bps": bitrate,
152 "eta_seconds": eta_secs,
153 "elapsed_seconds": elapsed
154 });
155 eprintln!("{record}");
156 }
157 }
158 }
159
160 pub fn set_bytes_written(&mut self, bytes: u64) {
166 self.bytes_written = bytes;
167 }
168
169 #[allow(dead_code)]
175 pub fn set_status(&self, status: &str) {
176 self.bar.set_message(status.to_string());
177 }
178
179 pub fn finish(&self) {
181 let elapsed = self.start_time.elapsed();
182 let avg_fps = if elapsed.as_secs_f64() > 0.0 {
183 self.frames_done as f64 / elapsed.as_secs_f64()
184 } else {
185 0.0
186 };
187
188 match self.format {
189 ProgressFormat::Plain => {
190 let final_msg = format!(
191 "{} | Avg {:.1} fps | {}",
192 "Complete".green().bold(),
193 avg_fps,
194 format_size(self.bytes_written)
195 );
196 self.bar.finish_with_message(final_msg);
197 }
198 ProgressFormat::Json => {
199 let record = serde_json::json!({
200 "kind": "done",
201 "frames_done": self.frames_done,
202 "frames_total": self.frames_total,
203 "avg_fps": avg_fps,
204 "bytes_written": self.bytes_written,
205 "elapsed_seconds": elapsed.as_secs_f64()
206 });
207 eprintln!("{record}");
208 }
209 }
210 }
211
212 #[allow(dead_code)]
218 pub fn finish_with_error(&self, error: &str) {
219 let msg = format!("{} {}", "Failed:".red().bold(), error);
220 self.bar.finish_with_message(msg);
221 }
222
223 pub fn fps(&self) -> f64 {
225 let elapsed = self.start_time.elapsed();
226 if elapsed.as_secs_f64() > 0.0 {
227 self.frames_done as f64 / elapsed.as_secs_f64()
228 } else {
229 0.0
230 }
231 }
232
233 pub fn eta(&self) -> Duration {
235 if self.frames_total == 0 || self.frames_done == 0 {
236 return Duration::from_secs(0);
237 }
238
239 let elapsed = self.start_time.elapsed();
240 let frames_remaining = self.frames_total.saturating_sub(self.frames_done);
241
242 if self.frames_done > 0 {
243 let time_per_frame = elapsed.as_secs_f64() / self.frames_done as f64;
244 let eta_secs = time_per_frame * frames_remaining as f64;
245 Duration::from_secs_f64(eta_secs)
246 } else {
247 Duration::from_secs(0)
248 }
249 }
250
251 pub fn bitrate(&self) -> f64 {
253 let elapsed = self.start_time.elapsed();
254 if elapsed.as_secs_f64() > 0.0 {
255 (self.bytes_written as f64 * 8.0) / elapsed.as_secs_f64()
256 } else {
257 0.0
258 }
259 }
260
261 #[allow(dead_code)]
263 pub fn total_frames(&self) -> u64 {
264 self.frames_total
265 }
266
267 #[allow(dead_code)]
269 pub fn frames_completed(&self) -> u64 {
270 self.frames_done
271 }
272
273 #[allow(dead_code)]
275 pub fn elapsed(&self) -> Duration {
276 self.start_time.elapsed()
277 }
278}
279
280pub struct BatchProgress {
282 bar: ProgressBar,
283 start_time: Instant,
284 #[allow(dead_code)]
285 total_files: usize,
286 completed: usize,
287 failed: usize,
288 pub format: ProgressFormat,
290}
291
292impl BatchProgress {
293 pub fn new(total_files: usize) -> Self {
299 let bar = ProgressBar::new(total_files as u64);
300
301 let style = ProgressStyle::default_bar()
302 .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} files ({percent}%) {msg}")
303 .unwrap_or_else(|_| ProgressStyle::default_bar())
304 .progress_chars("=>-");
305
306 bar.set_style(style);
307
308 Self {
309 bar,
310 start_time: Instant::now(),
311 total_files,
312 completed: 0,
313 failed: 0,
314 format: ProgressFormat::Plain,
315 }
316 }
317
318 pub fn set_format(&mut self, fmt: ProgressFormat) {
320 self.format = fmt;
321 if fmt == ProgressFormat::Json {
322 self.bar
323 .set_draw_target(indicatif::ProgressDrawTarget::hidden());
324 }
325 }
326
327 pub fn inc_success(&mut self) {
329 self.completed += 1;
330 self.bar.inc(1);
331 self.emit_tick();
332 }
333
334 pub fn inc_failed(&mut self) {
336 self.failed += 1;
337 self.bar.inc(1);
338 self.emit_tick();
339 }
340
341 fn emit_tick(&self) {
343 match self.format {
344 ProgressFormat::Plain => {
345 let msg = if self.failed > 0 {
346 format!(
347 "{} succeeded, {} failed",
348 self.completed.to_string().green(),
349 self.failed.to_string().red()
350 )
351 } else {
352 format!("{} succeeded", self.completed.to_string().green())
353 };
354 self.bar.set_message(msg);
355 }
356 ProgressFormat::Json => {
357 let elapsed = self.start_time.elapsed().as_secs_f64();
358 let record = serde_json::json!({
359 "kind": "batch_progress",
360 "completed": self.completed,
361 "failed": self.failed,
362 "total": self.total_files,
363 "elapsed_seconds": elapsed
364 });
365 eprintln!("{record}");
366 }
367 }
368 }
369
370 pub fn finish(&self) {
372 let elapsed = self.start_time.elapsed();
373 match self.format {
374 ProgressFormat::Plain => {
375 let msg = format!(
376 "{} | {} succeeded, {} failed | Took {}",
377 "Complete".green().bold(),
378 self.completed,
379 self.failed,
380 format_duration(elapsed)
381 );
382 self.bar.finish_with_message(msg);
383 }
384 ProgressFormat::Json => {
385 let record = serde_json::json!({
386 "kind": "batch_done",
387 "completed": self.completed,
388 "failed": self.failed,
389 "total": self.total_files,
390 "elapsed_seconds": elapsed.as_secs_f64()
391 });
392 eprintln!("{record}");
393 }
394 }
395 }
396}
397
398fn format_duration(duration: Duration) -> String {
400 let total_secs = duration.as_secs();
401 let hours = total_secs / 3600;
402 let minutes = (total_secs % 3600) / 60;
403 let seconds = total_secs % 60;
404
405 if hours > 0 {
406 format!("{}h {}m {}s", hours, minutes, seconds)
407 } else if minutes > 0 {
408 format!("{}m {}s", minutes, seconds)
409 } else {
410 format!("{}s", seconds)
411 }
412}
413
414fn format_eta(eta: Duration) -> String {
416 let eta_str = format!("ETA {}", format_duration(eta));
417
418 if eta.as_secs() > 3600 {
419 eta_str.red().to_string()
420 } else if eta.as_secs() > 600 {
421 eta_str.yellow().to_string()
422 } else {
423 eta_str.green().to_string()
424 }
425}
426
427fn format_bitrate(bitrate: f64) -> String {
429 if bitrate >= 1_000_000.0 {
430 format!("{:.2} Mbps", bitrate / 1_000_000.0)
431 } else if bitrate >= 1_000.0 {
432 format!("{:.1} kbps", bitrate / 1_000.0)
433 } else {
434 format!("{:.0} bps", bitrate)
435 }
436}
437
438fn format_size(bytes: u64) -> String {
440 const KB: u64 = 1024;
441 const MB: u64 = KB * 1024;
442 const GB: u64 = MB * 1024;
443
444 if bytes >= GB {
445 format!("{:.2} GB", bytes as f64 / GB as f64)
446 } else if bytes >= MB {
447 format!("{:.2} MB", bytes as f64 / MB as f64)
448 } else if bytes >= KB {
449 format!("{:.2} KB", bytes as f64 / KB as f64)
450 } else {
451 format!("{} B", bytes)
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458
459 #[test]
460 fn test_format_duration() {
461 assert_eq!(format_duration(Duration::from_secs(30)), "30s");
462 assert_eq!(format_duration(Duration::from_secs(90)), "1m 30s");
463 assert_eq!(format_duration(Duration::from_secs(3661)), "1h 1m 1s");
464 }
465
466 #[test]
467 fn test_format_bitrate() {
468 assert_eq!(format_bitrate(500.0), "500 bps");
469 assert_eq!(format_bitrate(1500.0), "1.5 kbps");
470 assert_eq!(format_bitrate(2_500_000.0), "2.50 Mbps");
471 }
472
473 #[test]
474 fn test_format_size() {
475 assert_eq!(format_size(500), "500 B");
476 assert_eq!(format_size(1536), "1.50 KB");
477 assert_eq!(format_size(2_097_152), "2.00 MB");
478 assert_eq!(format_size(1_610_612_736), "1.50 GB");
479 }
480
481 #[test]
482 fn test_progress_fps() {
483 let mut progress = TranscodeProgress::new(100);
484 std::thread::sleep(Duration::from_millis(100));
485 progress.update(10);
486
487 let fps = progress.fps();
488 assert!(fps > 0.0);
489 }
490
491 #[test]
492 fn test_progress_eta() {
493 let mut progress = TranscodeProgress::new(100);
494 std::thread::sleep(Duration::from_millis(100));
495 progress.update(10);
496
497 let eta = progress.eta();
498 let _ = eta.as_secs(); }
500
501 #[test]
502 fn test_set_format_json_does_not_panic() {
503 let mut progress = TranscodeProgress::new(100);
504 progress.set_format(ProgressFormat::Json);
505 assert_eq!(progress.format, ProgressFormat::Json);
506 progress.update(5);
508 }
509
510 #[test]
511 fn test_set_format_plain_roundtrip() {
512 let mut progress = TranscodeProgress::new_spinner();
513 progress.set_format(ProgressFormat::Plain);
514 assert_eq!(progress.format, ProgressFormat::Plain);
515 }
516
517 #[test]
518 fn test_batch_progress_json_emit() {
519 let mut bp = BatchProgress::new(3);
520 bp.set_format(ProgressFormat::Json);
521 bp.inc_success();
522 bp.inc_failed();
523 bp.finish();
525 }
526}