Skip to main content

video_subtitle/ffmpeg/
util.rs

1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4use crate::error::{AppError, Result};
5
6/// 已解析的 FFmpeg 可执行文件句柄。
7///
8/// 通过 [`Ffmpeg::resolve`] 或 [`check_ffmpeg`] 获得,后续 [`extract_audio`](super::extract_audio)
9/// 与 [`burn_subtitles`](super::burn_subtitles) 均复用同一实例。
10#[derive(Clone, Debug)]
11pub struct Ffmpeg {
12    executable: PathBuf,
13}
14
15impl Ffmpeg {
16    /// 解析 FFmpeg 路径。
17    ///
18    /// - `explicit` 为 `Some` 时使用给定路径(须存在)
19    /// - 否则在 PATH 中探测 `ffmpeg` / `ffmpeg.exe`
20    pub fn resolve(explicit: Option<&Path>) -> Result<Self> {
21        if let Some(path) = explicit {
22            let path = path.to_path_buf();
23            if !path.exists() {
24                return Err(AppError::InvalidPath(path));
25            }
26            return Ok(Self { executable: path });
27        }
28
29        which_ffmpeg().map(|executable| Self { executable })
30    }
31
32    /// 已解析的可执行文件路径。
33    pub fn path(&self) -> &Path {
34        &self.executable
35    }
36
37    /// 同步执行 FFmpeg 命令;非零退出码时返回 [`AppError::FfmpegFailed`]。
38    ///
39    /// `args` 为传给 FFmpeg 的参数列表(不含程序名本身)。
40    pub fn run(&self, args: &[&str]) -> Result<std::process::Output> {
41        self.run_in(None, args)
42    }
43
44    /// 在可选工作目录下执行 FFmpeg(用于 `subtitles=` 使用相对路径,规避 Windows 盘符转义问题)。
45    pub fn run_in(&self, cwd: Option<&Path>, args: &[&str]) -> Result<std::process::Output> {
46        let mut cmd = Command::new(self.path());
47        if let Some(cwd) = cwd {
48            cmd.current_dir(cwd);
49        }
50        let output = cmd.args(args).output()?;
51
52        if output.status.success() {
53            Ok(output)
54        } else {
55            let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
56            Err(AppError::FfmpegFailed {
57                code: output.status.code(),
58                stderr,
59            })
60        }
61    }
62}
63
64/// 探测 FFmpeg 是否可用,等价于 [`Ffmpeg::resolve`] 后丢弃实例。
65pub fn check_ffmpeg(explicit: Option<&Path>) -> Result<()> {
66    Ffmpeg::resolve(explicit).map(|_| ())
67}
68
69/// 在 PATH 中查找可执行的 `ffmpeg` / `ffmpeg.exe`。
70fn which_ffmpeg() -> Result<PathBuf> {
71    let candidates = ["ffmpeg", "ffmpeg.exe"];
72    for name in candidates {
73        if let Ok(output) = Command::new(name).arg("-version").output() {
74            if output.status.success() {
75                return Ok(PathBuf::from(name));
76            }
77        }
78    }
79    Err(AppError::FfmpegNotFound)
80}