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::path::Path;
30use std::process::Command;
31use std::time::Duration;
32
33use error::FfProbeError;
34#[cfg(feature = "streams")]
35mod attachment_stream;
36#[cfg(feature = "streams")]
37mod audio_stream;
38#[cfg(feature = "chapters")]
39mod chapter;
40mod config;
41#[cfg(feature = "streams")]
42mod data_stream;
43#[cfg(feature = "streams")]
44mod disposition;
45pub mod error;
46mod ffprobe;
47#[cfg(feature = "format")]
48mod format;
49mod ratio;
50#[cfg(feature = "streams")]
51mod streams;
52#[cfg(feature = "streams")]
53mod subtitle_stream;
54#[cfg(feature = "streams")]
55mod video_stream;
56
57#[cfg(feature = "streams")]
58pub use attachment_stream::AttachmentStream;
59#[cfg(feature = "streams")]
60pub use attachment_stream::AttachmentTags;
61#[cfg(feature = "streams")]
62pub use audio_stream::AudioStream;
63#[cfg(feature = "streams")]
64pub use audio_stream::AudioTags;
65#[cfg(feature = "chapters")]
66pub use chapter::Chapter;
67#[cfg(feature = "chapters")]
68pub use chapter::ChapterTags;
69pub use config::Config;
70#[cfg(feature = "streams")]
71pub use data_stream::DataStream;
72#[cfg(feature = "streams")]
73pub use data_stream::DataTags;
74#[cfg(feature = "streams")]
75pub use disposition::Disposition;
76pub use ffprobe::FfProbe;
77#[cfg(feature = "format")]
78pub use format::Format;
79#[cfg(feature = "format")]
80pub use format::FormatTags;
81pub use ratio::Ratio;
82use serde::Deserialize;
83use serde::Deserializer;
84#[cfg(feature = "streams")]
85pub use streams::SideData;
86#[cfg(feature = "streams")]
87pub use streams::Stream;
88#[cfg(feature = "streams")]
89pub use streams::StreamKinds;
90#[cfg(feature = "streams")]
91pub use streams::StreamTags;
92#[cfg(feature = "streams")]
93pub use subtitle_stream::SubtititleTags;
94#[cfg(feature = "streams")]
95pub use subtitle_stream::SubtitleStream;
96#[cfg(feature = "streams")]
97pub use video_stream::VideoStream;
98#[cfg(feature = "streams")]
99pub use video_stream::VideoTags;
100/// Execute ffprobe with default settings and return the extracted data.
101///
102/// See [`ffprobe_config`] if you need to customize settings.
103pub fn ffprobe(path: impl AsRef<Path>) -> Result<FfProbe, FfProbeError> {
104    ffprobe_config(Config::new(), path)
105}
106
107/// Run ffprobe with a custom config.
108/// See [`ConfigBuilder`] for more details.
109pub fn ffprobe_config(config: Config, path: impl AsRef<Path>) -> Result<FfProbe, FfProbeError> {
110    let path = path.as_ref();
111    let mut cmd = Command::new(config.ffprobe_bin);
112    // Default args.
113    cmd.args(["-v", "quiet", "-print_format", "json"]);
114    #[cfg(feature = "chapters")]
115    cmd.arg("-show_chapters");
116    #[cfg(feature = "format")]
117    cmd.arg("-show_format");
118    #[cfg(feature = "streams")]
119    cmd.arg("-show_streams");
120
121    if config.count_frames {
122        cmd.arg("-count_frames");
123    }
124
125    cmd.arg(path);
126
127    let out = cmd.output().map_err(FfProbeError::Io)?;
128
129    if !out.status.success() {
130        return Err(FfProbeError::Status(out));
131    }
132
133    serde_json::from_slice::<FfProbe>(&out.stdout).map_err(FfProbeError::Deserialize)
134}
135
136#[cfg(feature = "async")]
137pub async fn ffprobe_async(path: impl AsRef<std::path::Path>) -> Result<FfProbe, FfProbeError> {
138    ffprobe_async_config(Config::new(), path).await
139}
140
141#[cfg(feature = "async")]
142pub async fn ffprobe_async_config(
143    config: Config,
144    path: impl AsRef<Path>,
145) -> Result<FfProbe, FfProbeError> {
146    let mut cmd = tokio::process::Command::new("ffprobe");
147    let path = path.as_ref();
148    cmd.args(["-v", "quiet", "-print_format", "json"]);
149    #[cfg(feature = "chapters")]
150    cmd.arg("-show_chapters");
151    #[cfg(feature = "format")]
152    cmd.arg("-show_format");
153    #[cfg(feature = "streams")]
154    cmd.arg("-show_streams");
155
156    if config.count_frames {
157        cmd.arg("-count_frames");
158    }
159
160    cmd.arg(path);
161
162    let out = cmd.output().await.map_err(FfProbeError::Io)?;
163
164    if !out.status.success() {
165        return Err(FfProbeError::Status(out));
166    }
167
168    serde_json::from_slice::<FfProbe>(&out.stdout).map_err(FfProbeError::Deserialize)
169}
170
171pub fn option_string_to_duration<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
172where
173    D: Deserializer<'de>,
174{
175    let s: Option<String> = Option::deserialize(deserializer)?;
176    match s {
177        Some(s) => s
178            .parse::<f64>()
179            .map(|v| v.max(0.0))
180            .map(Duration::from_secs_f64)
181            .map(Some)
182            .map_err(serde::de::Error::custom),
183        None => Ok(None),
184    }
185}