1pub fn ffprobe(path: impl AsRef<std::path::Path>) -> Result<FfProbe, FfProbeError> {
25 ffprobe_config(
26 Config {
27 count_frames: false,
28 ffprobe_bin: "ffprobe".into(),
29 },
30 path,
31 )
32}
33
34pub fn ffprobe_config(
37 config: Config,
38 path: impl AsRef<std::path::Path>,
39) -> Result<FfProbe, FfProbeError> {
40 let path = path.as_ref();
41
42 let mut cmd = std::process::Command::new(config.ffprobe_bin);
43
44 cmd.args([
46 "-v",
47 "quiet",
48 "-show_format",
49 "-show_streams",
50 "-print_format",
51 "json",
52 ]);
53
54 if config.count_frames {
55 cmd.arg("-count_frames");
56 }
57
58 cmd.arg(path);
59
60 let out = cmd.output().map_err(FfProbeError::Io)?;
61
62 if !out.status.success() {
63 return Err(FfProbeError::Status(out));
64 }
65
66 serde_json::from_slice::<FfProbe>(&out.stdout).map_err(FfProbeError::Deserialize)
67}
68
69#[derive(Clone, Debug)]
73pub struct Config {
74 count_frames: bool,
75 ffprobe_bin: std::path::PathBuf,
76}
77
78impl Config {
79 pub fn builder() -> ConfigBuilder {
81 ConfigBuilder::new()
82 }
83}
84
85pub struct ConfigBuilder {
87 config: Config,
88}
89
90impl ConfigBuilder {
91 pub fn new() -> Self {
92 Self {
93 config: Config {
94 count_frames: false,
95 ffprobe_bin: "ffprobe".into(),
96 },
97 }
98 }
99
100 pub fn count_frames(mut self, count_frames: bool) -> Self {
104 self.config.count_frames = count_frames;
105 self
106 }
107
108 pub fn ffprobe_bin(mut self, ffprobe_bin: impl AsRef<std::path::Path>) -> Self {
111 self.config.ffprobe_bin = ffprobe_bin.as_ref().to_path_buf();
112 self
113 }
114
115 pub fn build(self) -> Config {
117 self.config
118 }
119
120 pub fn run(self, path: impl AsRef<std::path::Path>) -> Result<FfProbe, FfProbeError> {
122 ffprobe_config(self.config, path)
123 }
124}
125
126impl Default for ConfigBuilder {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132#[derive(Debug)]
133#[non_exhaustive]
134pub enum FfProbeError {
135 Io(std::io::Error),
136 Status(std::process::Output),
137 Deserialize(serde_json::Error),
138}
139
140impl std::fmt::Display for FfProbeError {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 match self {
143 FfProbeError::Io(e) => e.fmt(f),
144 FfProbeError::Status(o) => {
145 write!(
146 f,
147 "ffprobe exited with status code {}: {}",
148 o.status,
149 String::from_utf8_lossy(&o.stderr)
150 )
151 }
152 FfProbeError::Deserialize(e) => e.fmt(f),
153 }
154 }
155}
156
157impl std::error::Error for FfProbeError {}
158
159#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
160#[cfg_attr(feature = "__internal_deny_unknown_fields", serde(deny_unknown_fields))]
161pub struct FfProbe {
162 pub streams: Vec<Stream>,
163 pub format: Format,
164}
165
166#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
167#[cfg_attr(feature = "__internal_deny_unknown_fields", serde(deny_unknown_fields))]
168pub struct Stream {
169 pub index: i64,
170 pub codec_name: Option<String>,
171 pub sample_aspect_ratio: Option<String>,
172 pub display_aspect_ratio: Option<String>,
173 pub color_range: Option<String>,
174 pub color_space: Option<String>,
175 pub bits_per_raw_sample: Option<String>,
176 pub channel_layout: Option<String>,
177 pub max_bit_rate: Option<String>,
178 pub nb_frames: Option<String>,
179 pub nb_read_frames: Option<String>,
183 pub codec_long_name: Option<String>,
184 pub codec_type: Option<String>,
185 pub codec_time_base: Option<String>,
186 pub codec_tag_string: String,
187 pub codec_tag: String,
188 pub sample_fmt: Option<String>,
189 pub sample_rate: Option<String>,
190 pub channels: Option<i64>,
191 pub bits_per_sample: Option<i64>,
192 pub r_frame_rate: String,
193 pub avg_frame_rate: String,
194 pub time_base: String,
195 pub start_pts: Option<i64>,
196 pub start_time: Option<String>,
197 pub duration_ts: Option<i64>,
198 pub duration: Option<String>,
199 pub bit_rate: Option<String>,
200 pub disposition: Disposition,
201 pub tags: Option<StreamTags>,
202 pub profile: Option<String>,
203 pub width: Option<i64>,
204 pub height: Option<i64>,
205 pub coded_width: Option<i64>,
206 pub coded_height: Option<i64>,
207 pub closed_captions: Option<i64>,
208 pub has_b_frames: Option<i64>,
209 pub pix_fmt: Option<String>,
210 pub level: Option<i64>,
211 pub chroma_location: Option<String>,
212 pub refs: Option<i64>,
213 pub is_avc: Option<String>,
214 pub nal_length: Option<String>,
215 pub nal_length_size: Option<String>,
216 pub field_order: Option<String>,
217 pub id: Option<String>,
218 #[serde(default)]
219 pub side_data_list: Vec<SideData>,
220}
221
222#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
223#[cfg_attr(feature = "__internal_deny_unknown_fields", serde(deny_unknown_fields))]
224#[allow(clippy::derive_partial_eq_without_eq)]
226pub struct SideData {
227 pub side_data_type: String,
228}
229
230#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
231#[cfg_attr(feature = "__internal_deny_unknown_fields", serde(deny_unknown_fields))]
232#[allow(clippy::derive_partial_eq_without_eq)]
234pub struct Disposition {
235 pub default: i64,
236 pub dub: i64,
237 pub original: i64,
238 pub comment: i64,
239 pub lyrics: i64,
240 pub karaoke: i64,
241 pub forced: i64,
242 pub hearing_impaired: i64,
243 pub visual_impaired: i64,
244 pub clean_effects: i64,
245 pub attached_pic: i64,
246 pub timed_thumbnails: i64,
247}
248
249#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
250#[cfg_attr(feature = "__internal_deny_unknown_fields", serde(deny_unknown_fields))]
251#[allow(clippy::derive_partial_eq_without_eq)]
253pub struct StreamTags {
254 pub language: Option<String>,
255 pub creation_time: Option<String>,
256 pub handler_name: Option<String>,
257 pub encoder: Option<String>,
258 pub timecode: Option<String>,
259 pub reel_name: Option<String>,
260}
261
262#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
263#[cfg_attr(feature = "__internal_deny_unknown_fields", serde(deny_unknown_fields))]
264pub struct Format {
265 pub filename: String,
266 pub nb_streams: i64,
267 pub nb_programs: i64,
268 pub format_name: String,
269 pub format_long_name: String,
270 pub start_time: Option<String>,
271 pub duration: Option<String>,
272 #[serde(default)]
274 pub size: String,
275 pub bit_rate: Option<String>,
276 pub probe_score: i64,
277 pub tags: Option<FormatTags>,
278}
279
280impl Format {
281 pub fn try_get_duration(
283 &self,
284 ) -> Option<Result<std::time::Duration, std::num::ParseFloatError>> {
285 self.duration
286 .as_ref()
287 .map(|duration| match duration.parse::<f64>() {
288 Ok(num) => Ok(std::time::Duration::from_secs_f64(num)),
289 Err(error) => Err(error),
290 })
291 }
292
293 pub fn get_duration(&self) -> Option<std::time::Duration> {
298 self.try_get_duration()?.ok()
299 }
300}
301
302#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
303#[cfg_attr(feature = "__internal_deny_unknown_fields", serde(deny_unknown_fields))]
304#[allow(clippy::derive_partial_eq_without_eq)]
305pub struct FormatTags {
306 #[serde(rename = "WMFSDKNeeded")]
307 pub wmfsdkneeded: Option<String>,
308 #[serde(rename = "DeviceConformanceTemplate")]
309 pub device_conformance_template: Option<String>,
310 #[serde(rename = "WMFSDKVersion")]
311 pub wmfsdkversion: Option<String>,
312 #[serde(rename = "IsVBR")]
313 pub is_vbr: Option<String>,
314 pub major_brand: Option<String>,
315 pub minor_version: Option<String>,
316 pub compatible_brands: Option<String>,
317 pub creation_time: Option<String>,
318 pub encoder: Option<String>,
319
320 #[serde(flatten)]
321 pub extra: std::collections::HashMap<String, serde_json::Value>,
322}