ffmpeg_light/
probe.rs

1//! Media probing utilities built on top of `ffprobe`.
2
3use std::collections::HashMap;
4use std::path::Path;
5use std::time::Duration;
6
7use serde::Deserialize;
8
9#[cfg(feature = "tokio")]
10use crate::command::ffprobe_json_async;
11use crate::command::{ffprobe_json, FfmpegBinaryPaths};
12use crate::config::FfmpegLocator;
13use crate::error::Result;
14use crate::types::{
15    AudioStreamInfo, CodecType, DataStreamInfo, FormatInfo, ProbeResult, StreamInfo,
16    SubtitleStreamInfo, VideoStreamInfo,
17};
18
19/// Probe a file using binaries discovered on the current PATH.
20pub fn probe(path: impl AsRef<Path>) -> Result<ProbeResult> {
21    let locator = FfmpegLocator::system()?;
22    probe_with_locator(&locator, path)
23}
24
25/// Async variant of [`probe`] (requires the `tokio` feature).
26#[cfg(feature = "tokio")]
27pub async fn probe_async(path: impl AsRef<Path>) -> Result<ProbeResult> {
28    let locator = FfmpegLocator::system()?;
29    probe_with_locator_async(&locator, path).await
30}
31
32/// Probe a file with a pre-configured locator (useful for custom binary paths).
33pub fn probe_with_locator(locator: &FfmpegLocator, path: impl AsRef<Path>) -> Result<ProbeResult> {
34    probe_with_binaries(locator.binaries(), path)
35}
36
37/// Async variant of [`probe_with_locator`] (requires the `tokio` feature).
38#[cfg(feature = "tokio")]
39pub async fn probe_with_locator_async(
40    locator: &FfmpegLocator,
41    path: impl AsRef<Path>,
42) -> Result<ProbeResult> {
43    probe_with_binaries_async(locator.binaries(), path).await
44}
45
46/// Probe a file using already-resolved binaries.
47pub fn probe_with_binaries(
48    paths: &FfmpegBinaryPaths,
49    path: impl AsRef<Path>,
50) -> Result<ProbeResult> {
51    let json = ffprobe_json(paths, path.as_ref())?;
52    parse_probe_output(&json)
53}
54
55/// Async variant of [`probe_with_binaries`] (requires the `tokio` feature).
56#[cfg(feature = "tokio")]
57pub async fn probe_with_binaries_async(
58    paths: &FfmpegBinaryPaths,
59    path: impl AsRef<Path>,
60) -> Result<ProbeResult> {
61    let json = ffprobe_json_async(paths, path.as_ref()).await?;
62    parse_probe_output(&json)
63}
64
65fn parse_probe_output(json: &str) -> Result<ProbeResult> {
66    let data: FfprobeOutput = serde_json::from_str(json)?;
67    let format = data
68        .format
69        .map(format_info_from_ffprobe)
70        .unwrap_or_else(|| FormatInfo::new(None, None, None, None, None));
71    let streams = data
72        .streams
73        .into_iter()
74        .filter_map(stream_info_from_ffprobe)
75        .collect();
76    Ok(ProbeResult::new(format, streams))
77}
78
79#[derive(Debug, Deserialize)]
80struct FfprobeOutput {
81    format: Option<FfprobeFormat>,
82    #[serde(default)]
83    streams: Vec<FfprobeStream>,
84}
85
86#[derive(Debug, Deserialize)]
87struct FfprobeFormat {
88    format_name: Option<String>,
89    format_long_name: Option<String>,
90    duration: Option<String>,
91    bit_rate: Option<String>,
92    size: Option<String>,
93}
94
95#[derive(Debug, Deserialize)]
96struct FfprobeStream {
97    codec_type: Option<String>,
98    codec_name: Option<String>,
99    width: Option<u32>,
100    height: Option<u32>,
101    bit_rate: Option<String>,
102    avg_frame_rate: Option<String>,
103    channels: Option<u32>,
104    sample_rate: Option<String>,
105    tags: Option<HashMap<String, String>>,
106}
107
108fn format_info_from_ffprobe(format: FfprobeFormat) -> FormatInfo {
109    FormatInfo::new(
110        format.format_name,
111        format.format_long_name,
112        parse_duration(format.duration.as_deref()),
113        parse_u64(format.bit_rate.as_deref()),
114        parse_u64(format.size.as_deref()),
115    )
116}
117
118fn stream_info_from_ffprobe(stream: FfprobeStream) -> Option<StreamInfo> {
119    let codec = stream
120        .codec_name
121        .as_deref()
122        .map(CodecType::from_name)
123        .unwrap_or_else(|| CodecType::Other("unknown".into()));
124    match stream.codec_type.as_deref() {
125        Some("video") => Some(StreamInfo::Video(VideoStreamInfo {
126            codec,
127            width: stream.width,
128            height: stream.height,
129            bit_rate: parse_u64(stream.bit_rate.as_deref()),
130            frame_rate: parse_ratio(stream.avg_frame_rate.as_deref()),
131        })),
132        Some("audio") => Some(StreamInfo::Audio(AudioStreamInfo {
133            codec,
134            channels: stream.channels,
135            sample_rate: parse_u32(stream.sample_rate.as_deref()),
136            bit_rate: parse_u64(stream.bit_rate.as_deref()),
137        })),
138        Some("subtitle") => {
139            let language = stream
140                .tags
141                .as_ref()
142                .and_then(|tags| tags.get("language").cloned());
143            Some(StreamInfo::Subtitle(SubtitleStreamInfo { codec, language }))
144        }
145        Some("data") => Some(StreamInfo::Data(DataStreamInfo {
146            codec,
147            description: stream
148                .tags
149                .as_ref()
150                .and_then(|tags| tags.get("title").cloned()),
151        })),
152        _ => None,
153    }
154}
155
156fn parse_duration(raw: Option<&str>) -> Option<Duration> {
157    raw.and_then(|value| value.parse::<f64>().ok())
158        .map(Duration::from_secs_f64)
159}
160
161fn parse_u64(raw: Option<&str>) -> Option<u64> {
162    raw.and_then(|value| value.parse().ok())
163}
164
165fn parse_u32(raw: Option<&str>) -> Option<u32> {
166    raw.and_then(|value| value.parse().ok())
167}
168
169fn parse_ratio(raw: Option<&str>) -> Option<f64> {
170    let raw = raw?;
171    if raw == "0/0" || raw == "0" {
172        return None;
173    }
174    if let Some((num, den)) = raw.split_once('/') {
175        let num: f64 = num.parse().ok()?;
176        let den: f64 = den.parse().ok()?;
177        if den.abs() < f64::EPSILON {
178            return None;
179        }
180        Some(num / den)
181    } else {
182        raw.parse().ok()
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::parse_ratio;
189
190    #[test]
191    fn ratio_parsing() {
192        assert_eq!(parse_ratio(Some("30000/1001")), Some(30_000.0 / 1_001.0));
193        assert_eq!(parse_ratio(Some("0/0")), None);
194        assert_eq!(parse_ratio(Some("59.94")), Some(59.94));
195        assert_eq!(parse_ratio(None), None);
196    }
197}