#![warn(missing_docs)]
#![warn(clippy::all)]
#![warn(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::must_use_candidate)]
pub mod error;
pub mod process;
pub mod types;
pub mod utils;
pub use error::{Error, Result, ResultExt};
pub use process::{CommandBuilder, Process, ProcessConfig, ProcessOutput, Progress};
pub use types::{
Codec, Duration, LogLevel, MediaPath, PixelFormat, SampleFormat, Size, StreamSpecifier,
StreamType,
};
#[derive(Debug, Clone)]
pub struct Version {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub version_string: String,
pub configuration: Vec<String>,
}
impl Version {
pub fn parse(output: &str) -> Result<Self> {
let lines: Vec<&str> = output.lines().collect();
if lines.is_empty() {
return Err(Error::ParseError("Empty version output".to_string()));
}
let version_line = lines[0];
let version_string = if let Some(start) = version_line.find("version ") {
let version_part = &version_line[start + 8..];
version_part.split_whitespace().next().unwrap_or("").to_string()
} else {
return Err(Error::ParseError("Version line not found".to_string()));
};
let parts: Vec<&str> = version_string.split(&['.', '-'][..]).collect();
let major = parts.get(0)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let minor = parts.get(1)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let patch = parts.get(2)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let configuration = lines.iter()
.find(|line| line.starts_with("configuration:"))
.map(|line| {
line[14..]
.split_whitespace()
.map(String::from)
.collect()
})
.unwrap_or_default();
Ok(Self {
major,
minor,
patch,
version_string,
configuration,
})
}
pub fn is_at_least(&self, major: u32, minor: u32, patch: u32) -> bool {
if self.major > major {
return true;
}
if self.major < major {
return false;
}
if self.minor > minor {
return true;
}
if self.minor < minor {
return false;
}
self.patch >= patch
}
}
pub async fn get_version(executable: &str) -> Result<Version> {
let path = process::find_executable(executable)?;
let config = ProcessConfig::new(path)
.capture_stdout(true)
.capture_stderr(false);
let output = Process::spawn(config, vec!["-version".to_string()])
.await?
.wait()
.await?
.into_result()?;
let version_output = output.stdout_str()
.ok_or_else(|| Error::ParseError("No version output".to_string()))?;
Version::parse(&version_output)
}
#[derive(Debug, Clone, Default)]
pub struct Capabilities {
pub codecs: Vec<String>,
pub formats: Vec<String>,
pub filters: Vec<String>,
pub protocols: Vec<String>,
pub pixel_formats: Vec<String>,
pub sample_formats: Vec<String>,
}
impl Capabilities {
pub async fn detect(executable: &str) -> Result<Self> {
let caps = Self::default();
Ok(caps)
}
pub fn has_codec(&self, codec: &str) -> bool {
self.codecs.iter().any(|c| c == codec)
}
pub fn has_format(&self, format: &str) -> bool {
self.formats.iter().any(|f| f == format)
}
pub fn has_filter(&self, filter: &str) -> bool {
self.filters.iter().any(|f| f == filter)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_parsing() {
let output = r#"ffmpeg version 4.4.2-0ubuntu0.22.04.1 Copyright (c) 2000-2021 the FFmpeg developers
built with gcc 11 (Ubuntu 11.2.0-19ubuntu1)
configuration: --prefix=/usr --extra-version=0ubuntu0.22.04.1 --toolchain=hardened"#;
let version = Version::parse(output).unwrap();
assert_eq!(version.major, 4);
assert_eq!(version.minor, 4);
assert_eq!(version.patch, 2);
assert!(version.version_string.starts_with("4.4.2"));
assert!(!version.configuration.is_empty());
}
#[test]
fn test_version_comparison() {
let version = Version {
major: 4,
minor: 4,
patch: 2,
version_string: "4.4.2".to_string(),
configuration: vec![],
};
assert!(version.is_at_least(4, 4, 2));
assert!(version.is_at_least(4, 4, 1));
assert!(version.is_at_least(4, 3, 5));
assert!(version.is_at_least(3, 9, 9));
assert!(!version.is_at_least(4, 4, 3));
assert!(!version.is_at_least(4, 5, 0));
assert!(!version.is_at_least(5, 0, 0));
}
}