1use 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
19pub fn probe(path: impl AsRef<Path>) -> Result<ProbeResult> {
21 let locator = FfmpegLocator::system()?;
22 probe_with_locator(&locator, path)
23}
24
25#[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
32pub fn probe_with_locator(locator: &FfmpegLocator, path: impl AsRef<Path>) -> Result<ProbeResult> {
34 probe_with_binaries(locator.binaries(), path)
35}
36
37#[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
46pub 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#[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}