ffmpeg_common/
lib.rs

1//! Common functionality for FFmpeg suite Rust wrappers
2//!
3//! This crate provides shared types, utilities, and error handling for the
4//! FFmpeg, FFprobe, and FFplay Rust wrappers.
5
6#![warn(missing_docs)]
7#![warn(clippy::all)]
8#![warn(clippy::pedantic)]
9#![allow(clippy::module_name_repetitions)]
10#![allow(clippy::must_use_candidate)]
11
12pub mod error;
13pub mod process;
14pub mod types;
15pub mod utils;
16
17// Re-export commonly used items
18pub use error::{Error, Result, ResultExt};
19pub use process::{CommandBuilder, Process, ProcessConfig, ProcessOutput, Progress};
20pub use types::{
21    Codec, Duration, LogLevel, MediaPath, PixelFormat, SampleFormat, Size, StreamSpecifier,
22    StreamType,
23};
24
25/// Version information for the FFmpeg suite
26#[derive(Debug, Clone)]
27pub struct Version {
28    /// Major version number
29    pub major: u32,
30    /// Minor version number
31    pub minor: u32,
32    /// Patch version number
33    pub patch: u32,
34    /// Version string
35    pub version_string: String,
36    /// Configuration flags
37    pub configuration: Vec<String>,
38}
39
40impl Version {
41    /// Parse version information from FFmpeg output
42    pub fn parse(output: &str) -> Result<Self> {
43        // FFmpeg version output format:
44        // ffmpeg version 4.4.2-0ubuntu0.22.04.1 Copyright (c) 2000-2021 the FFmpeg developers
45        // built with gcc 11 (Ubuntu 11.2.0-19ubuntu1)
46        // configuration: --prefix=/usr --extra-version=0ubuntu0.22.04.1 ...
47
48        let lines: Vec<&str> = output.lines().collect();
49        if lines.is_empty() {
50            return Err(Error::ParseError("Empty version output".to_string()));
51        }
52
53        // Parse version line
54        let version_line = lines[0];
55        let version_string = if let Some(start) = version_line.find("version ") {
56            let version_part = &version_line[start + 8..];
57            version_part.split_whitespace().next().unwrap_or("").to_string()
58        } else {
59            return Err(Error::ParseError("Version line not found".to_string()));
60        };
61
62        // Extract major.minor.patch
63        let parts: Vec<&str> = version_string.split(&['.', '-'][..]).collect();
64        let major = parts.get(0)
65            .and_then(|s| s.parse().ok())
66            .unwrap_or(0);
67        let minor = parts.get(1)
68            .and_then(|s| s.parse().ok())
69            .unwrap_or(0);
70        let patch = parts.get(2)
71            .and_then(|s| s.parse().ok())
72            .unwrap_or(0);
73
74        // Parse configuration
75        let configuration = lines.iter()
76            .find(|line| line.starts_with("configuration:"))
77            .map(|line| {
78                line[14..]
79                    .split_whitespace()
80                    .map(String::from)
81                    .collect()
82            })
83            .unwrap_or_default();
84
85        Ok(Self {
86            major,
87            minor,
88            patch,
89            version_string,
90            configuration,
91        })
92    }
93
94    /// Check if this version is at least the specified version
95    pub fn is_at_least(&self, major: u32, minor: u32, patch: u32) -> bool {
96        if self.major > major {
97            return true;
98        }
99        if self.major < major {
100            return false;
101        }
102        if self.minor > minor {
103            return true;
104        }
105        if self.minor < minor {
106            return false;
107        }
108        self.patch >= patch
109    }
110}
111
112/// Get version information for an FFmpeg executable
113pub async fn get_version(executable: &str) -> Result<Version> {
114    let path = process::find_executable(executable)?;
115    let config = ProcessConfig::new(path)
116        .capture_stdout(true)
117        .capture_stderr(false);
118
119    let output = Process::spawn(config, vec!["-version".to_string()])
120        .await?
121        .wait()
122        .await?
123        .into_result()?;
124
125    let version_output = output.stdout_str()
126        .ok_or_else(|| Error::ParseError("No version output".to_string()))?;
127
128    Version::parse(&version_output)
129}
130
131/// Capabilities detection for FFmpeg tools
132#[derive(Debug, Clone, Default)]
133pub struct Capabilities {
134    /// Available codecs
135    pub codecs: Vec<String>,
136    /// Available formats
137    pub formats: Vec<String>,
138    /// Available filters
139    pub filters: Vec<String>,
140    /// Available protocols
141    pub protocols: Vec<String>,
142    /// Available pixel formats
143    pub pixel_formats: Vec<String>,
144    /// Available sample formats
145    pub sample_formats: Vec<String>,
146}
147
148impl Capabilities {
149    /// Detect capabilities by running FFmpeg with various list options
150    pub async fn detect(executable: &str) -> Result<Self> {
151        let caps = Self::default();
152
153        // This is a simplified version - in a real implementation,
154        // we would parse the output of ffmpeg -codecs, -formats, etc.
155
156        Ok(caps)
157    }
158
159    /// Check if a codec is available
160    pub fn has_codec(&self, codec: &str) -> bool {
161        self.codecs.iter().any(|c| c == codec)
162    }
163
164    /// Check if a format is available
165    pub fn has_format(&self, format: &str) -> bool {
166        self.formats.iter().any(|f| f == format)
167    }
168
169    /// Check if a filter is available
170    pub fn has_filter(&self, filter: &str) -> bool {
171        self.filters.iter().any(|f| f == filter)
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_version_parsing() {
181        let output = r#"ffmpeg version 4.4.2-0ubuntu0.22.04.1 Copyright (c) 2000-2021 the FFmpeg developers
182built with gcc 11 (Ubuntu 11.2.0-19ubuntu1)
183configuration: --prefix=/usr --extra-version=0ubuntu0.22.04.1 --toolchain=hardened"#;
184
185        let version = Version::parse(output).unwrap();
186        assert_eq!(version.major, 4);
187        assert_eq!(version.minor, 4);
188        assert_eq!(version.patch, 2);
189        assert!(version.version_string.starts_with("4.4.2"));
190        assert!(!version.configuration.is_empty());
191    }
192
193    #[test]
194    fn test_version_comparison() {
195        let version = Version {
196            major: 4,
197            minor: 4,
198            patch: 2,
199            version_string: "4.4.2".to_string(),
200            configuration: vec![],
201        };
202
203        assert!(version.is_at_least(4, 4, 2));
204        assert!(version.is_at_least(4, 4, 1));
205        assert!(version.is_at_least(4, 3, 5));
206        assert!(version.is_at_least(3, 9, 9));
207        assert!(!version.is_at_least(4, 4, 3));
208        assert!(!version.is_at_least(4, 5, 0));
209        assert!(!version.is_at_least(5, 0, 0));
210    }
211}