Skip to main content

easy_ffprobe/
lib.rs

1//! Simple wrapper for the [ffprobe](https://ffmpeg.org/ffprobe.html) CLI utility,
2//! which is part of the ffmpeg tool suite.
3//!
4//! This crate allows retrieving typed information about media files (images and videos)
5//! by invoking `ffprobe` with JSON output options and deserializing the data
6//! into convenient Rust types.
7//!
8//!
9//!
10//! ```rust
11//! match ffprobe::ffprobe("path/to/video.mp4") {
12//!    Ok(info) => {
13//!        dbg!(info);
14//!    },
15//!    Err(err) => {
16//!        eprintln!("Could not analyze file with ffprobe: {:?}", err);
17//!     },
18//! }
19//!
20//! ```
21
22//! ## Features
23//! - streams
24//! - format
25//! - chapters
26//! - async
27//!
28
29use std::borrow::Cow;
30use std::path::Path;
31use std::path::PathBuf;
32use std::process::Command;
33use std::time::Duration;
34
35use error::FfProbeError;
36#[cfg(feature = "streams")]
37mod attachment_stream;
38#[cfg(feature = "streams")]
39mod audio_stream;
40#[cfg(feature = "chapters")]
41mod chapter;
42mod config;
43#[cfg(feature = "streams")]
44mod data_stream;
45#[cfg(feature = "streams")]
46mod disposition;
47pub mod error;
48mod ffprobe;
49#[cfg(feature = "format")]
50mod format;
51mod ratio;
52#[cfg(feature = "streams")]
53mod streams;
54#[cfg(feature = "streams")]
55mod subtitle_stream;
56#[cfg(feature = "streams")]
57mod video_stream;
58
59#[cfg(feature = "streams")]
60pub use attachment_stream::AttachmentStream;
61#[cfg(feature = "streams")]
62pub use attachment_stream::AttachmentTags;
63#[cfg(feature = "streams")]
64pub use audio_stream::AudioStream;
65#[cfg(feature = "streams")]
66pub use audio_stream::AudioTags;
67#[cfg(feature = "chapters")]
68pub use chapter::Chapter;
69#[cfg(feature = "chapters")]
70pub use chapter::ChapterTags;
71pub use config::Config;
72#[cfg(feature = "streams")]
73pub use data_stream::DataStream;
74#[cfg(feature = "streams")]
75pub use data_stream::DataTags;
76#[cfg(feature = "streams")]
77pub use disposition::Disposition;
78pub use ffprobe::FfProbe;
79#[cfg(feature = "format")]
80pub use format::Format;
81#[cfg(feature = "format")]
82pub use format::FormatTags;
83pub use ratio::Ratio;
84use serde::Deserialize;
85use serde::Deserializer;
86#[cfg(feature = "streams")]
87pub use streams::SideData;
88#[cfg(feature = "streams")]
89pub use streams::Stream;
90#[cfg(feature = "streams")]
91pub use streams::StreamKinds;
92#[cfg(feature = "streams")]
93pub use streams::StreamTags;
94#[cfg(feature = "streams")]
95pub use subtitle_stream::SubtititleTags;
96#[cfg(feature = "streams")]
97pub use subtitle_stream::SubtitleStream;
98use url::Url;
99#[cfg(feature = "streams")]
100pub use video_stream::VideoStream;
101#[cfg(feature = "streams")]
102pub use video_stream::VideoTags;
103
104pub trait IntoFfprobeArg<'a> {
105    fn into_ffprobe_arg(self) -> Cow<'a, str>;
106}
107
108impl<'a> IntoFfprobeArg<'a> for &'a Path {
109    fn into_ffprobe_arg(self) -> Cow<'a, str> {
110        self.to_string_lossy()
111    }
112}
113
114impl<'a> IntoFfprobeArg<'a> for PathBuf {
115    fn into_ffprobe_arg(self) -> Cow<'a, str> {
116        Cow::Owned(self.to_string_lossy().into_owned())
117    }
118}
119
120impl<'a> IntoFfprobeArg<'a> for Url {
121    fn into_ffprobe_arg(self) -> Cow<'a, str> {
122        Cow::Owned(self.to_string())
123    }
124}
125
126impl<'a> IntoFfprobeArg<'a> for &'a str {
127    fn into_ffprobe_arg(self) -> Cow<'a, str> {
128        self.into()
129    }
130}
131
132impl<'a> IntoFfprobeArg<'a> for String {
133    fn into_ffprobe_arg(self) -> Cow<'a, str> {
134        self.into()
135    }
136}
137
138#[cfg(target_os = "windows")]
139use std::os::windows::process::CommandExt;
140
141/// Execute ffprobe with default settings and return the extracted data.
142///
143/// See [`ffprobe_config`] if you need to customize settings.
144pub fn ffprobe<'a, T: IntoFfprobeArg<'a>>(path: T) -> Result<FfProbe, FfProbeError> {
145    ffprobe_config(Config::new(), path)
146}
147
148/// Run ffprobe with a custom config.
149/// See [`ConfigBuilder`] for more details.
150pub fn ffprobe_config<'a, T: IntoFfprobeArg<'a>>(
151    config: Config,
152    path: T,
153) -> Result<FfProbe, FfProbeError> {
154    let path = path.into_ffprobe_arg();
155    let mut cmd = Command::new(config.ffprobe_bin);
156    // Default args.
157    cmd.args(["-v", "error", "-print_format", "json"]);
158    #[cfg(feature = "chapters")]
159    cmd.arg("-show_chapters");
160    #[cfg(feature = "format")]
161    cmd.arg("-show_format");
162    #[cfg(feature = "streams")]
163    cmd.arg("-show_streams");
164
165    if config.count_frames {
166        cmd.arg("-count_frames");
167    }
168
169    cmd.arg(path.as_ref());
170
171    // Prevent CMD popup on Windows.
172    #[cfg(target_os = "windows")]
173    cmd.creation_flags(0x08000000);
174
175    let out = cmd.output().map_err(FfProbeError::Io)?;
176
177    if !out.status.success() {
178        return Err(FfProbeError::Status(out));
179    }
180
181    serde_json::from_slice::<FfProbe>(&out.stdout).map_err(FfProbeError::Deserialize)
182}
183
184#[cfg(feature = "async")]
185pub async fn ffprobe_async<'a, T: IntoFfprobeArg<'a>>(path: T) -> Result<FfProbe, FfProbeError> {
186    ffprobe_async_config(Config::new(), path).await
187}
188
189#[cfg(feature = "async")]
190pub async fn ffprobe_async_config<'a, T: IntoFfprobeArg<'a>>(
191    config: Config,
192    path: T,
193) -> Result<FfProbe, FfProbeError> {
194    let path = path.into_ffprobe_arg();
195    let mut cmd = tokio::process::Command::new("ffprobe");
196    let path = path.as_ref();
197    cmd.args(["-v", "quiet", "-print_format", "json"]);
198    #[cfg(feature = "chapters")]
199    cmd.arg("-show_chapters");
200    #[cfg(feature = "format")]
201    cmd.arg("-show_format");
202    #[cfg(feature = "streams")]
203    cmd.arg("-show_streams");
204
205    if config.count_frames {
206        cmd.arg("-count_frames");
207    }
208
209    cmd.arg(path.as_ref());
210
211    let out = cmd.output().await.map_err(FfProbeError::Io)?;
212
213    if !out.status.success() {
214        return Err(FfProbeError::Status(out));
215    }
216
217    serde_json::from_slice::<FfProbe>(&out.stdout).map_err(FfProbeError::Deserialize)
218}
219
220pub fn option_string_to_duration<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
221where
222    D: Deserializer<'de>,
223{
224    let s: Option<String> = Option::deserialize(deserializer)?;
225    match s {
226        Some(s) => s
227            .parse::<f64>()
228            .map(|v| v.max(0.0))
229            .map(Duration::from_secs_f64)
230            .map(Some)
231            .map_err(serde::de::Error::custom),
232        None => Ok(None),
233    }
234}