1pub mod ffpipe;
7pub mod mf;
8pub mod proxy;
9pub mod thumbs;
10pub mod waveform;
11pub mod ytdlp;
12
13use crate::model::Asset;
14use std::collections::HashMap;
15
16pub const SAMPLE_RATE: u32 = 48000;
17pub const CHANNELS: usize = 2;
18
19#[derive(Clone, Default)]
21pub struct Frame {
22 pub width: u32,
23 pub height: u32,
24 pub pts: f64,
26 pub rgba: Vec<u8>,
27}
28
29impl Frame {
30 pub fn new(width: u32, height: u32) -> Self {
31 Self { width, height, pts: 0.0, rgba: vec![0; (width * height * 4) as usize] }
32 }
33 pub fn resize(&mut self, width: u32, height: u32) {
35 if self.width != width || self.height != height {
36 self.width = width;
37 self.height = height;
38 self.rgba.clear();
39 self.rgba.resize((width * height * 4) as usize, 0);
40 }
41 }
42 pub fn fill(&mut self, rgba: [u8; 4]) {
43 for px in self.rgba.chunks_exact_mut(4) {
44 px.copy_from_slice(&rgba);
45 }
46 }
47 pub fn stride(&self) -> usize {
48 self.width as usize * 4
49 }
50 pub fn is_empty(&self) -> bool {
51 self.width == 0 || self.height == 0
52 }
53}
54
55pub trait VideoSource: Send {
56 fn size(&self) -> (u32, u32);
58 fn frame_at(&mut self, t: f64, w: u32, h: u32, out: &mut Frame) -> bool;
63}
64
65pub trait AudioSource: Send {
66 fn duration(&self) -> f64;
67 fn read_at(&mut self, t: f64, out: &mut [f32]);
70}
71
72#[derive(Clone, Copy, PartialEq, Eq, Debug)]
73pub enum Backend {
74 Auto,
75 Mf,
76 Ffmpeg,
77}
78
79impl Backend {
80 pub fn parse(s: &str) -> Self {
81 match s {
82 "mf" => Backend::Mf,
83 "ffmpeg" => Backend::Ffmpeg,
84 _ => Backend::Auto,
85 }
86 }
87}
88
89pub fn ext(path: &str) -> String {
91 std::path::Path::new(path).extension().map(|e| e.to_string_lossy().to_ascii_lowercase()).unwrap_or_default()
92}
93
94pub fn is_image_path(path: &str) -> bool {
95 matches!(ext(path).as_str(), "png" | "jpg" | "jpeg" | "bmp" | "gif" | "webp" | "tif" | "tiff" | "tga" | "psd")
96}
97
98pub fn probe(path: &str, backend: Backend) -> Result<Asset, String> {
101 match backend {
102 Backend::Ffmpeg => ffpipe::probe(path),
103 Backend::Mf => mf::probe(path),
104 Backend::Auto => {
105 if ffpipe::ffprobe_exe().is_some() {
106 ffpipe::probe(path).or_else(|e| mf::probe(path).map_err(|e2| format!("{e}; {e2}")))
107 } else {
108 mf::probe(path).or_else(|e| ffpipe::probe(path).map_err(|e2| format!("{e}; {e2}")))
109 }
110 }
111 }
112}
113
114pub fn open_video(path: &str, backend: Backend) -> Result<Box<dyn VideoSource>, String> {
115 if is_image_path(path) {
116 return ffpipe::open_video(path);
117 }
118 match backend {
119 Backend::Ffmpeg => ffpipe::open_video(path),
120 Backend::Mf => mf::open_video(path),
121 Backend::Auto => mf::open_video(path).or_else(|e| ffpipe::open_video(path).map_err(|e2| format!("{e}; {e2}"))),
122 }
123}
124
125pub fn open_audio(path: &str, stream: usize, backend: Backend) -> Result<Box<dyn AudioSource>, String> {
126 match backend {
127 Backend::Ffmpeg => ffpipe::open_audio(path, stream),
128 Backend::Mf => mf::open_audio(path, stream),
129 Backend::Auto => mf::open_audio(path, stream)
130 .or_else(|e| ffpipe::open_audio(path, stream).map_err(|e2| format!("{e}; {e2}"))),
131 }
132}
133
134pub struct DecoderPool {
137 backend: Backend,
138 videos: HashMap<String, (u64, Option<Box<dyn VideoSource>>)>,
139 audios: HashMap<(String, usize), (u64, Option<Box<dyn AudioSource>>)>,
140 tick: u64,
143 proxies: std::collections::HashMap<String, String>,
146}
147
148const POOL_VIDEOS: usize = 16;
150const POOL_AUDIOS: usize = 32;
151
152impl DecoderPool {
153 pub fn new(backend: Backend) -> Self {
154 Self { backend, videos: HashMap::new(), audios: HashMap::new(), tick: 0, proxies: HashMap::new() }
155 }
156 pub fn set_proxies(&mut self, map: HashMap<String, String>) {
158 if map != self.proxies {
159 self.proxies = map;
160 self.clear();
161 }
162 }
163 pub fn set_backend(&mut self, b: Backend) {
164 if b != self.backend {
165 self.backend = b;
166 self.clear();
167 }
168 }
169 pub fn video(&mut self, path: &str) -> Option<&mut (dyn VideoSource + 'static)> {
170 let path = self.proxies.get(path).cloned().unwrap_or_else(|| path.to_string());
172 let path = path.as_str();
173 let b = self.backend;
174 self.tick += 1;
175 let tick = self.tick;
176 let e = self.videos.entry(path.to_string()).or_insert_with(|| (tick, open_video(path, b).ok()));
177 e.0 = tick;
178 let hit = e.1.is_some();
179 if hit && self.videos.values().filter(|(_, v)| v.is_some()).count() > POOL_VIDEOS {
180 let evict = self
181 .videos
182 .iter()
183 .filter(|(p, (_, v))| v.is_some() && p.as_str() != path)
184 .min_by_key(|(_, (t, _))| *t)
185 .map(|(p, _)| p.clone());
186 if let Some(p) = evict {
187 self.videos.remove(&p);
188 }
189 }
190 self.videos.get_mut(path).and_then(|(_, v)| v.as_deref_mut())
191 }
192 pub fn audio(&mut self, path: &str, stream: usize) -> Option<&mut (dyn AudioSource + 'static)> {
193 let b = self.backend;
194 self.tick += 1;
195 let tick = self.tick;
196 let key = (path.to_string(), stream);
197 let e = self.audios.entry(key.clone()).or_insert_with(|| (tick, open_audio(path, stream, b).ok()));
198 e.0 = tick;
199 let hit = e.1.is_some();
200 if hit && self.audios.values().filter(|(_, v)| v.is_some()).count() > POOL_AUDIOS {
201 let evict = self
202 .audios
203 .iter()
204 .filter(|(k, (_, v))| v.is_some() && **k != key)
205 .min_by_key(|(_, (t, _))| *t)
206 .map(|(k, _)| k.clone());
207 if let Some(k) = evict {
208 self.audios.remove(&k);
209 }
210 }
211 self.audios.get_mut(&key).and_then(|(_, v)| v.as_deref_mut())
212 }
213 #[cfg(test)]
215 pub fn insert_video(&mut self, path: &str, v: Box<dyn VideoSource>) {
216 self.videos.insert(path.to_string(), (self.tick, Some(v)));
217 }
218 #[cfg(test)]
219 pub fn insert_audio(&mut self, path: &str, stream: usize, a: Box<dyn AudioSource>) {
220 self.audios.insert((path.to_string(), stream), (self.tick, Some(a)));
221 }
222 pub fn clear(&mut self) {
225 self.videos.clear();
226 self.audios.clear();
227 }
228}