Skip to main content

falcon_library/
media.rs

1//! Durations of audio and video files, stored at import as `duration`
2//! (seconds, with a fraction, as a browser's media element reports it).
3
4use std::fs::File;
5use std::io::{Read, Seek, SeekFrom};
6use std::path::Path;
7
8
9/// Eagle's audio formats.
10pub const AUDIO_EXTENSIONS: [&str; 6] = ["mp3", "wav", "flac", "ogg", "aac", "m4a"];
11/// Eagle's video formats.
12pub const VIDEO_EXTENSIONS: [&str; 18] =
13    ["ts", "3gp", "360", "afx", "vap", "eva", "mp4", "mov", "m4v", "webm", "mkv", "avi", "wmv", "mpg", "mts", "flv", "m2ts", "f4v"];
14
15pub fn is_audio(ext: &str) -> bool {
16    AUDIO_EXTENSIONS.contains(&ext)
17}
18
19pub fn is_video(ext: &str) -> bool {
20    VIDEO_EXTENSIONS.contains(&ext)
21}
22
23/// The duration in seconds of an audio or video file falcon can read:
24/// the audio formats through [`crate::audio`], ISO-media video (mp4, mov,
25/// m4v, 3gp, f4v) from its movie header, and other video through FFmpeg
26/// when it is installed.
27pub fn duration(path: &Path, ext: &str) -> Option<f64> {
28    if is_audio(ext) {
29        return audio_duration(path, ext);
30    }
31    if matches!(ext, "mp4" | "mov" | "m4v" | "3gp" | "f4v")
32        && let Some(d) = File::open(path).ok().and_then(|mut f| mp4_duration(&mut f))
33    {
34        return Some(d);
35    }
36    if is_video(ext) && falcon_ffmpeg::available() {
37        return falcon_ffmpeg::probe(path).ok().map(|i| i.duration).filter(|d| *d > 0.0);
38    }
39    None
40}
41
42/// An audio file's length: from its header, or by adding up packet
43/// durations when the header doesn't say (an MP3 without a Xing header).
44/// `None` when the file doesn't decode (see [`crate::audio`]).
45pub fn audio_duration(path: &Path, ext: &str) -> Option<f64> {
46    let source = crate::audio::open(path, ext).ok()?;
47    if let Some(d) = source.duration() {
48        return Some(d);
49    }
50    drop(source);
51    let mut format = crate::audio::open_format(path, ext).ok()?;
52    let track = format.default_track()?;
53    let id = track.id;
54    let params = track.codec_params.clone();
55    let mut total = 0u64;
56    while let Ok(p) = format.next_packet() {
57        if p.track_id() == id {
58            total += p.dur;
59        }
60    }
61    let secs = if let Some(tb) = params.time_base {
62        let t = tb.calc_time(total);
63        t.seconds as f64 + t.frac
64    } else {
65        total as f64 / f64::from(params.sample_rate?)
66    };
67    (secs > 0.0).then_some(secs)
68}
69
70/// Reads `moov/mvhd` and returns duration รท timescale.
71pub fn mp4_duration<R: Read + Seek>(r: &mut R) -> Option<f64> {
72    let end = r.seek(SeekFrom::End(0)).ok()?;
73    let (start, size) = find_box(r, 0, end, b"moov")?;
74    let (mvhd, _) = find_box(r, start, start + size, b"mvhd")?;
75    r.seek(SeekFrom::Start(mvhd)).ok()?;
76    let mut head = [0u8; 4];
77    r.read_exact(&mut head).ok()?;
78    let (timescale, duration) = if head[0] == 1 {
79        let mut b = [0u8; 28];
80        r.read_exact(&mut b).ok()?;
81        (u32::from_be_bytes(b[16..20].try_into().ok()?), u64::from_be_bytes(b[20..28].try_into().ok()?))
82    } else {
83        let mut b = [0u8; 16];
84        r.read_exact(&mut b).ok()?;
85        (u32::from_be_bytes(b[8..12].try_into().ok()?), u64::from(u32::from_be_bytes(b[12..16].try_into().ok()?)))
86    };
87    (timescale > 0 && duration > 0 && duration != u64::from(u32::MAX)).then(|| duration as f64 / f64::from(timescale))
88}
89
90/// Finds a box by type among the boxes in `[from, to)`; returns the offset
91/// of its payload and the payload's size.
92fn find_box<R: Read + Seek>(r: &mut R, mut at: u64, to: u64, kind: &[u8; 4]) -> Option<(u64, u64)> {
93    while at + 8 <= to {
94        r.seek(SeekFrom::Start(at)).ok()?;
95        let mut h = [0u8; 8];
96        r.read_exact(&mut h).ok()?;
97        let mut size = u64::from(u32::from_be_bytes(h[..4].try_into().ok()?));
98        let mut header = 8;
99        if size == 1 {
100            let mut l = [0u8; 8];
101            r.read_exact(&mut l).ok()?;
102            size = u64::from_be_bytes(l);
103            header = 16;
104        } else if size == 0 {
105            size = to - at;
106        }
107        if size < header {
108            return None;
109        }
110        if &h[4..8] == kind {
111            return Some((at + header, size - header));
112        }
113        at += size;
114    }
115    None
116}
117
118#[cfg(test)]
119pub(crate) mod tests {
120    use super::*;
121    use std::io::Cursor;
122
123    /// A mono 16-bit WAV of `secs` seconds at `rate` Hz, from `f(t)`.
124    pub(crate) fn wav(rate: u32, secs: f64, f: impl Fn(f64) -> f64) -> Vec<u8> {
125        let n = (f64::from(rate) * secs) as u32;
126        let mut v = Vec::new();
127        v.extend_from_slice(b"RIFF");
128        v.extend_from_slice(&(36 + n * 2).to_le_bytes());
129        v.extend_from_slice(b"WAVEfmt ");
130        v.extend_from_slice(&16u32.to_le_bytes());
131        v.extend_from_slice(&1u16.to_le_bytes());
132        v.extend_from_slice(&1u16.to_le_bytes());
133        v.extend_from_slice(&rate.to_le_bytes());
134        v.extend_from_slice(&(rate * 2).to_le_bytes());
135        v.extend_from_slice(&2u16.to_le_bytes());
136        v.extend_from_slice(&16u16.to_le_bytes());
137        v.extend_from_slice(b"data");
138        v.extend_from_slice(&(n * 2).to_le_bytes());
139        for i in 0..n {
140            let s = (f(f64::from(i) / f64::from(rate)).clamp(-1.0, 1.0) * 32767.0) as i16;
141            v.extend_from_slice(&s.to_le_bytes());
142        }
143        v
144    }
145
146    #[test]
147    fn wav_duration() {
148        let dir = tempfile::tempdir().unwrap();
149        let p = dir.path().join("tone.wav");
150        std::fs::write(&p, wav(8000, 2.5, |t| (t * 440.0 * std::f64::consts::TAU).sin())).unwrap();
151        let d = duration(&p, "wav").unwrap();
152        assert!((d - 2.5).abs() < 1e-6, "{d}");
153    }
154
155    #[test]
156    fn mp4_header() {
157        let mvhd = |version: u8| {
158            let mut b = vec![version, 0, 0, 0];
159            if version == 1 {
160                b.extend_from_slice(&[0; 16]);
161                b.extend_from_slice(&600u32.to_be_bytes());
162                b.extend_from_slice(&9000u64.to_be_bytes());
163            } else {
164                b.extend_from_slice(&[0; 8]);
165                b.extend_from_slice(&600u32.to_be_bytes());
166                b.extend_from_slice(&9000u32.to_be_bytes());
167            }
168            b.extend_from_slice(&[0; 80]);
169            boxed(b"mvhd", &b)
170        };
171        for v in [0, 1] {
172            let mut file = boxed(b"ftyp", b"isom\0\0\0\0");
173            file.extend(boxed(b"free", &[0; 3]));
174            file.extend(boxed(b"moov", &mvhd(v)));
175            assert_eq!(mp4_duration(&mut Cursor::new(file)), Some(15.0));
176        }
177        assert_eq!(mp4_duration(&mut Cursor::new(boxed(b"ftyp", b"isom"))), None);
178    }
179
180    fn boxed(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
181        let mut v = ((payload.len() + 8) as u32).to_be_bytes().to_vec();
182        v.extend_from_slice(kind);
183        v.extend_from_slice(payload);
184        v
185    }
186}