use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
#[cfg(feature = "tokio")]
use tokio::process::Command as TokioCommand;
use which::which;
use crate::error::{Error, Result};
#[derive(Debug, Clone)]
pub struct FfmpegBinaryPaths {
ffmpeg: PathBuf,
ffprobe: PathBuf,
}
impl FfmpegBinaryPaths {
pub fn auto() -> Result<Self> {
let ffmpeg = which("ffmpeg").map_err(|_| Error::FFmpegNotFound {
suggestion: Some("install ffmpeg with 'brew install ffmpeg' (macOS), 'apt install ffmpeg' (Linux), or download from ffmpeg.org".to_string()),
})?;
let ffprobe = which("ffprobe").map_err(|_| Error::FFmpegNotFound {
suggestion: Some("ffprobe comes with ffmpeg installation".to_string()),
})?;
Ok(Self { ffmpeg, ffprobe })
}
pub fn with_paths<P, Q>(ffmpeg: P, ffprobe: Q) -> Self
where
P: Into<PathBuf>,
Q: Into<PathBuf>,
{
Self {
ffmpeg: ffmpeg.into(),
ffprobe: ffprobe.into(),
}
}
pub fn ffmpeg(&self) -> &Path {
&self.ffmpeg
}
pub fn ffprobe(&self) -> &Path {
&self.ffprobe
}
}
#[derive(Debug)]
pub struct FfmpegCommand {
binary: PathBuf,
args: Vec<OsString>,
}
impl FfmpegCommand {
pub fn new(binary: impl Into<PathBuf>) -> Self {
Self {
binary: binary.into(),
args: Vec::new(),
}
}
pub fn arg<T: AsRef<OsStr>>(&mut self, arg: T) -> &mut Self {
self.args.push(arg.as_ref().into());
self
}
pub fn args<T: AsRef<OsStr>>(&mut self, args: &[T]) -> &mut Self {
self.args.extend(args.iter().map(|arg| arg.as_ref().into()));
self
}
fn spawn_command(&self) -> Command {
let mut cmd = Command::new(&self.binary);
cmd.args(&self.args)
.stdout(Stdio::inherit())
.stderr(Stdio::piped());
cmd
}
#[cfg(feature = "tokio")]
fn spawn_async_command(&self) -> TokioCommand {
let mut cmd = TokioCommand::new(&self.binary);
cmd.args(&self.args)
.stdout(Stdio::inherit())
.stderr(Stdio::piped());
cmd
}
pub fn run(&self) -> Result<()> {
let output = self.run_with_output()?;
if !output.status.success() {
return Err(Error::command_failed(
display_path(&self.binary),
output.status.code(),
&output.stderr,
));
}
Ok(())
}
pub fn run_with_output(&self) -> Result<Output> {
let mut cmd = self.spawn_command();
let output = cmd.output()?;
Ok(output)
}
#[cfg(feature = "tokio")]
pub async fn run_async(&self) -> Result<()> {
let output = self.run_with_output_async().await?;
if !output.status.success() {
return Err(Error::command_failed(
display_path(&self.binary),
output.status.code(),
&output.stderr,
));
}
Ok(())
}
#[cfg(feature = "tokio")]
pub async fn run_with_output_async(&self) -> Result<Output> {
let mut cmd = self.spawn_async_command();
let output = cmd.output().await?;
Ok(output)
}
}
pub struct FfprobeCommand {
binary: PathBuf,
input: PathBuf,
extra_args: Vec<OsString>,
}
impl FfprobeCommand {
pub fn new(binary: impl Into<PathBuf>, input: impl Into<PathBuf>) -> Self {
Self {
binary: binary.into(),
input: input.into(),
extra_args: Vec::new(),
}
}
pub fn arg<T: AsRef<OsStr>>(&mut self, arg: T) -> &mut Self {
self.extra_args.push(arg.as_ref().into());
self
}
fn build_command(&self) -> Command {
let mut cmd = Command::new(&self.binary);
cmd.arg("-v")
.arg("quiet")
.arg("-print_format")
.arg("json")
.arg("-show_format")
.arg("-show_streams");
for arg in &self.extra_args {
cmd.arg(arg);
}
cmd.arg(&self.input)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
cmd
}
#[cfg(feature = "tokio")]
fn build_async_command(&self) -> TokioCommand {
let mut cmd = TokioCommand::new(&self.binary);
cmd.arg("-v")
.arg("quiet")
.arg("-print_format")
.arg("json")
.arg("-show_format")
.arg("-show_streams");
for arg in &self.extra_args {
cmd.arg(arg);
}
cmd.arg(&self.input)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
cmd
}
pub fn run(&self) -> Result<Output> {
let output = self.build_command().output()?;
if !output.status.success() {
return Err(Error::command_failed(
display_path(&self.binary),
output.status.code(),
&output.stderr,
));
}
Ok(output)
}
#[cfg(feature = "tokio")]
pub async fn run_async(&self) -> Result<Output> {
let output = self.build_async_command().output().await?;
if !output.status.success() {
return Err(Error::command_failed(
display_path(&self.binary),
output.status.code(),
&output.stderr,
));
}
Ok(output)
}
}
pub fn ffprobe_json(paths: &FfmpegBinaryPaths, input: impl AsRef<Path>) -> Result<String> {
let cmd = FfprobeCommand::new(paths.ffprobe(), input.as_ref());
let output = cmd.run()?;
let json = String::from_utf8(output.stdout).map_err(|err| Error::Parse(err.to_string()))?;
Ok(json)
}
#[cfg(feature = "tokio")]
pub async fn ffprobe_json_async(
paths: &FfmpegBinaryPaths,
input: impl AsRef<Path>,
) -> Result<String> {
let mut cmd = FfprobeCommand::new(paths.ffprobe(), input.as_ref());
let output = cmd.run_async().await?;
let json = String::from_utf8(output.stdout).map_err(|err| Error::Parse(err.to_string()))?;
Ok(json)
}
fn display_path(path: &Path) -> &str {
path.to_str().unwrap_or("<invalid utf8 path>")
}
#[cfg(test)]
impl FfmpegCommand {
pub(crate) fn test_binary(&self) -> &Path {
&self.binary
}
pub(crate) fn test_args(&self) -> &[OsString] {
&self.args
}
}
#[cfg(test)]
mod tests {
use super::*;
fn stringify_args(cmd: &FfmpegCommand) -> Vec<String> {
cmd.test_args()
.iter()
.map(|arg| arg.to_string_lossy().into_owned())
.collect()
}
#[test]
fn ffmpeg_command_collects_args_in_order() {
let mut cmd = FfmpegCommand::new("/usr/bin/ffmpeg");
cmd.arg("-y")
.arg("-i")
.arg("input.mp4")
.args(&[OsStr::new("-c:v"), OsStr::new("libx264")]);
assert_eq!(cmd.test_binary(), Path::new("/usr/bin/ffmpeg"));
assert_eq!(
stringify_args(&cmd),
vec!["-y", "-i", "input.mp4", "-c:v", "libx264"]
);
}
#[test]
fn ffprobe_command_includes_json_flags() {
let cmd = FfprobeCommand::new("/usr/bin/ffprobe", "video.mkv");
let process = cmd.build_command();
let args = process
.get_args()
.map(|arg| arg.to_string_lossy().into_owned())
.collect::<Vec<_>>();
assert_eq!(
args,
vec![
"-v",
"quiet",
"-print_format",
"json",
"-show_format",
"-show_streams",
"video.mkv"
]
);
}
}