1use std::fs::File;
5use std::io::{Read, Seek, SeekFrom};
6use std::path::Path;
7
8
9pub const AUDIO_EXTENSIONS: [&str; 6] = ["mp3", "wav", "flac", "ogg", "aac", "m4a"];
11pub 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
23pub 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
42pub 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
70pub 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
90fn 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 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}