Skip to main content

pito_codecs/media/front/
probe.rs

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}