1use std::collections::{BTreeMap, HashMap};
2use std::path::Path;
3use std::sync::{LazyLock, Mutex, PoisonError};
4
5use super::ffmpeg::{Ffmpeg, codec_of};
6use super::native::{Key, key, sorted};
7use super::route::Prefs;
8use crate::media::{Codec, Error, Result};
9
10#[derive(Debug, Clone, PartialEq)]
11pub struct Probe {
12 pub duration: f64,
13 pub start: f64,
14 pub video_start: Option<f64>,
15 pub fps: (u32, u32),
16 pub width: u32,
17 pub height: u32,
18 pub rotation: u16,
19 pub video_codec: Option<Codec>,
20 pub video_codec_name: String,
21 pub profile: String,
22 pub bit_depth: u8,
23 pub frames: Option<u64>,
24 pub audio: Vec<AudioTrack>,
25}
26
27impl Probe {
28 pub fn fps(&self) -> f64 {
29 self.fps.0 as f64 / self.fps.1.max(1) as f64
30 }
31
32 pub fn shown_size(&self) -> (u32, u32) {
33 if self.rotation % 180 == 90 {
34 (self.height, self.width)
35 } else {
36 (self.width, self.height)
37 }
38 }
39
40 pub fn video_native(&self) -> bool {
41 matches!(
42 self.video_codec,
43 Some(Codec::H264 | Codec::Hevc | Codec::Vp9)
44 )
45 }
46
47 pub fn unsupported(&self) -> Vec<String> {
48 let mut names: Vec<String> = Vec::new();
49 let video = (!self.video_native()).then_some(self.video_codec_name.as_str());
50 let audio = self
51 .audio
52 .iter()
53 .filter(|a| !a.native())
54 .map(|a| a.codec.as_str());
55 for name in video.into_iter().chain(audio) {
56 let name = if name.is_empty() { "unknown" } else { name };
57 if !names.iter().any(|n| n == name) {
58 names.push(name.to_string());
59 }
60 }
61 names
62 }
63}
64
65#[derive(Debug, Clone, PartialEq)]
66pub struct AudioTrack {
67 pub index: usize,
68 pub stream: usize,
69 pub title: Option<String>,
70 pub language: Option<String>,
71 pub codec: String,
72 pub channels: u16,
73 pub rate: u32,
74 pub bitrate: Option<u32>,
75 pub default: bool,
76 pub start: Option<f64>,
77}
78
79impl AudioTrack {
80 pub fn native(&self) -> bool {
81 let native: &[&str] = if cfg!(feature = "opus") {
82 &["aac", "opus", "pcm"]
83 } else {
84 &["aac", "pcm"]
85 };
86 let ac3: &[&str] = if cfg!(feature = "ac3") {
87 &["ac3", "eac3"]
88 } else {
89 &[]
90 };
91 let mp3: &[&str] = if cfg!(feature = "mp3") {
92 &["mp3", "mp2", "mp1"]
93 } else {
94 &[]
95 };
96 native
97 .iter()
98 .chain(ac3)
99 .chain(mp3)
100 .any(|c| self.codec.eq_ignore_ascii_case(c))
101 }
102}
103
104#[derive(Default)]
105struct Known {
106 probe: Option<Probe>,
107 keys: Option<Vec<f64>>,
108}
109
110fn known<T: Clone>(
111 path: &Path,
112 field: fn(&mut Known) -> &mut Option<T>,
113 make: impl FnOnce() -> Result<T>,
114) -> Result<T> {
115 static KNOWN: LazyLock<Mutex<HashMap<Key, Known>>> = LazyLock::new(Default::default);
116 let k = key(path)?;
117 if let Some(v) = KNOWN
118 .lock()
119 .unwrap_or_else(PoisonError::into_inner)
120 .get_mut(&k)
121 .and_then(|e| field(e).clone())
122 {
123 return Ok(v);
124 }
125 let v = make()?;
126 let mut map = KNOWN.lock().unwrap_or_else(PoisonError::into_inner);
127 if map.len() > 4096 {
128 map.clear();
129 }
130 *field(map.entry(k).or_default()) = Some(v.clone());
131 Ok(v)
132}
133
134pub fn probe(path: &Path, prefs: &Prefs) -> Result<Probe> {
135 known(
136 path,
137 |k| &mut k.probe,
138 || match super::native::probe(path) {
139 Ok(p) => Ok(p),
140 Err(native) => match container_ffmpeg(prefs) {
141 Ok(f) => from_flat(&f.probe_flat(&["-show_format", "-show_streams"], path)?),
142 Err(_) => Err(native),
143 },
144 },
145 )
146}
147
148pub fn keyframes(path: &Path, prefs: &Prefs) -> Result<Vec<f64>> {
149 known(
150 path,
151 |k| &mut k.keys,
152 || match super::native::keyframes(path) {
153 Ok(k) => Ok(k),
154 Err(native) => keyframes_of(path, prefs).map_err(|_| native),
155 },
156 )
157}
158
159pub fn frame_count(path: &Path, prefs: &Prefs) -> Result<u64> {
160 match super::demux::frame_count(path) {
161 Ok(n) => Ok(n),
162 Err(native) => {
163 let Ok(f) = container_ffmpeg(prefs) else {
164 return Err(native);
165 };
166 f.probe_csv(
167 &[
168 "-count_packets",
169 "-select_streams",
170 "v:0",
171 "-show_entries",
172 "stream=nb_read_packets",
173 ],
174 path,
175 )?
176 .trim()
177 .parse::<u64>()
178 .map_err(|_| native)
179 }
180 }
181}
182
183fn keyframes_of(path: &Path, prefs: &Prefs) -> Result<Vec<f64>> {
184 let start = probe(path, prefs).map_or(0.0, |p| p.start);
185 let f = container_ffmpeg(prefs)?;
186 let listing = f.probe_csv(
187 &[
188 "-select_streams",
189 "v:0",
190 "-show_entries",
191 "packet=pts_time,flags",
192 ],
193 path,
194 )?;
195 Ok(sorted(
196 listing
197 .lines()
198 .filter_map(|l| {
199 let (t, flags) = l.trim().split_once(',')?;
200 flags.contains('K').then_some(())?;
201 t.parse::<f64>()
202 .ok()
203 .map(|t| t - start)
204 .filter(|t| *t >= 0.0)
205 })
206 .collect(),
207 ))
208}
209
210pub(crate) fn container_ffmpeg(prefs: &Prefs) -> Result<Ffmpeg> {
211 match prefs.user_ffmpeg() {
212 Some(u) => Ffmpeg::find(u.path.as_deref()),
213 None => Err(Error::Unsupported(
214 "reading containers needs the MP4 module, which is not in this build, or ffmpeg picked in the settings"
215 .to_string(),
216 )),
217 }
218}
219
220fn rate(s: Option<&String>) -> Option<(u32, u32)> {
221 let (n, d) = s?.split_once('/')?;
222 let (n, d) = (n.parse().ok()?, d.parse().ok()?);
223 (n > 0 && d > 0).then_some((n, d))
224}
225
226fn from_flat(m: &BTreeMap<String, String>) -> Result<Probe> {
227 let get = |s: usize, k: &str| m.get(&format!("streams.stream.{s}.{k}"));
228 let count = (0..)
229 .take_while(|s| get(*s, "codec_type").is_some())
230 .count();
231 let video = (0..count)
232 .find(|&s| {
233 get(s, "codec_type").map(String::as_str) == Some("video")
234 && get(s, "disposition.attached_pic").map(String::as_str) != Some("1")
235 })
236 .ok_or_else(|| Error::Invalid("no video stream".to_string()))?;
237 let num = |s: usize, k: &str| get(s, k).and_then(|v| v.parse::<f64>().ok());
238 let fps = rate(get(video, "avg_frame_rate"))
239 .or_else(|| rate(get(video, "r_frame_rate")))
240 .ok_or_else(|| Error::Invalid("no frame rate".to_string()))?;
241 let duration = num(video, "duration")
242 .or_else(|| m.get("format.duration").and_then(|v| v.parse().ok()))
243 .filter(|d: &f64| d.is_finite() && *d >= 0.0)
244 .ok_or_else(|| Error::Invalid("no duration".to_string()))?;
245 let side = (0..8)
246 .find_map(|i| num(video, &format!("side_data_list.side_data.{i}.rotation")))
247 .map(|r| -r);
248 let tag = num(video, "tags.rotate");
249 let rotation = side.or(tag).unwrap_or(0.0).round() as i64;
250 let rotation = ((rotation.rem_euclid(360) + 45) / 90 * 90 % 360) as u16;
251 let codec_name = get(video, "codec_name").cloned().unwrap_or_default();
252 let pix = get(video, "pix_fmt").cloned().unwrap_or_default();
253 let audio = (0..count)
254 .filter(|&s| get(s, "codec_type").map(String::as_str) == Some("audio"))
255 .enumerate()
256 .map(|(index, s)| AudioTrack {
257 index,
258 stream: get(s, "index").and_then(|v| v.parse().ok()).unwrap_or(s),
259 title: ["tags.title", "tags.name", "tags.handler_name"]
260 .iter()
261 .filter_map(|k| get(s, k))
262 .map(|t| t.trim())
263 .find(|t| {
264 let l = t.to_lowercase();
265 !t.is_empty()
266 && ![
267 "soundhandler",
268 "sound handler",
269 "gpac",
270 "l-smash",
271 "media handler",
272 ]
273 .iter()
274 .any(|g| l.contains(g))
275 })
276 .map(str::to_string),
277 language: get(s, "tags.language")
278 .filter(|l| !l.is_empty() && *l != "und")
279 .cloned(),
280 codec: get(s, "codec_name").cloned().unwrap_or_default(),
281 channels: num(s, "channels").unwrap_or(0.0) as u16,
282 rate: num(s, "sample_rate").unwrap_or(0.0) as u32,
283 bitrate: num(s, "bit_rate").map(|b| b as u32),
284 default: get(s, "disposition.default").map(String::as_str) == Some("1"),
285 start: num(s, "start_time").filter(|t| t.is_finite()),
286 })
287 .collect();
288 let start = m
289 .get("format.start_time")
290 .and_then(|v| v.parse::<f64>().ok())
291 .filter(|t| t.is_finite())
292 .unwrap_or(0.0)
293 .max(0.0);
294 Ok(Probe {
295 duration,
296 start,
297 video_start: num(video, "start_time").filter(|t| t.is_finite()),
298 fps,
299 width: num(video, "width").unwrap_or(0.0) as u32,
300 height: num(video, "height").unwrap_or(0.0) as u32,
301 rotation,
302 video_codec: codec_of(&codec_name),
303 video_codec_name: codec_name,
304 profile: get(video, "profile").cloned().unwrap_or_default(),
305 bit_depth: if pix.contains("10") { 10 } else { 8 },
306 frames: num(video, "nb_frames").map(|n| n as u64),
307 audio,
308 })
309}