Skip to main content

deepshrink_ffmpeg/
run.rs

1//! Run a single ffmpeg pass, streaming progress and surfacing failures.
2
3use std::ffi::OsStr;
4use std::io::{BufRead, BufReader};
5use std::path::Path;
6use std::process::{Command, Stdio};
7
8use crate::progress::{self, Progress};
9use crate::FfmpegError;
10
11/// Run `ffmpeg` with `args`, invoking `on_progress` with a 0.0..=1.0 fraction as
12/// the pass proceeds. `total_secs` is the source duration (for the fraction).
13///
14/// The caller is expected to have included `-progress pipe:1 -nostats` in `args`
15/// so progress is emitted on stdout.
16pub fn run_pass<S: AsRef<OsStr>>(
17    ffmpeg: &Path,
18    args: &[S],
19    total_secs: f64,
20    on_progress: &mut dyn FnMut(f64),
21) -> Result<(), FfmpegError> {
22    let mut child = Command::new(ffmpeg)
23        .args(args)
24        .stdin(Stdio::null())
25        .stdout(Stdio::piped())
26        .stderr(Stdio::piped())
27        .spawn()
28        .map_err(|source| FfmpegError::Spawn {
29            tool: "ffmpeg",
30            source,
31        })?;
32
33    // Drain stderr on a separate thread so a chatty encoder can't deadlock us
34    // while we read stdout for progress.
35    let stderr = child.stderr.take();
36    let stderr_handle = std::thread::spawn(move || {
37        let mut buf = String::new();
38        if let Some(stderr) = stderr {
39            use std::io::Read;
40            let _ = BufReader::new(stderr).read_to_string(&mut buf);
41        }
42        buf
43    });
44
45    if let Some(stdout) = child.stdout.take() {
46        for line in BufReader::new(stdout).lines().map_while(Result::ok) {
47            match progress::parse_line(&line) {
48                Some(Progress::OutTimeUs(us)) => on_progress(progress::fraction(us, total_secs)),
49                Some(Progress::End) => on_progress(1.0),
50                _ => {}
51            }
52        }
53    }
54
55    let status = child.wait().map_err(|source| FfmpegError::Spawn {
56        tool: "ffmpeg",
57        source,
58    })?;
59    let stderr = stderr_handle.join().unwrap_or_default();
60
61    if !status.success() {
62        return Err(FfmpegError::CommandFailed {
63            tool: "ffmpeg",
64            status: status.to_string(),
65            stderr: stderr.trim().to_string(),
66        });
67    }
68    Ok(())
69}