1#![cfg_attr(not(target_os = "macos"), allow(dead_code))]
31
32use std::cell::RefCell;
33use std::num::NonZero;
34use std::path::{Path, PathBuf};
35use std::sync::mpsc;
36use std::time::{Duration, Instant};
37
38mod pixels;
39
40#[cfg(target_os = "macos")]
41mod mac;
42#[cfg(target_os = "macos")]
43use mac as sys;
44
45#[cfg(not(target_os = "macos"))]
46mod other;
47#[cfg(not(target_os = "macos"))]
48use other as sys;
49
50pub use falcon_ffmpeg::Info;
51pub use pixels::{Frame, quarter_turns};
52
53#[derive(Clone, Debug, PartialEq)]
55pub struct Error(pub String);
56
57impl std::fmt::Display for Error {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 f.write_str(&self.0)
60 }
61}
62
63impl std::error::Error for Error {}
64
65impl From<falcon_ffmpeg::Error> for Error {
66 fn from(e: falcon_ffmpeg::Error) -> Self {
67 Error(e.0)
68 }
69}
70
71pub type Result<T> = std::result::Result<T, Error>;
72
73pub const RATES: [f32; 11] = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 3.0, 4.0, 8.0];
75
76pub fn step_rate(rate: f32, up: bool) -> f32 {
79 if up {
80 RATES.iter().copied().find(|&r| r > rate + 1e-4).unwrap_or(rate)
81 } else {
82 RATES.iter().rev().copied().find(|&r| r < rate - 1e-4).unwrap_or(rate)
83 }
84}
85
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum Backend {
89 System,
91 Ffmpeg,
93}
94
95pub fn ffmpeg_available() -> bool {
97 falcon_ffmpeg::available()
98}
99
100pub fn ffmpeg_status() -> std::result::Result<(String, PathBuf), String> {
103 falcon_ffmpeg::status().map_err(|e| e.to_string())
104}
105
106pub fn needs_ffmpeg(ext: &str) -> bool {
109 falcon_ffmpeg::handles(ext)
110}
111
112const NO_FRAME_LIMIT: Duration = Duration::from_millis(1500);
114
115fn switch_for_no_frame(elapsed: Duration, had_frame: bool, ffmpeg: bool) -> bool {
119 ffmpeg && !had_frame && elapsed >= NO_FRAME_LIMIT
120}
121
122fn either<T>(system: impl FnOnce() -> Result<T>, ffmpeg: impl FnOnce() -> falcon_ffmpeg::Result<T>) -> Result<T> {
125 match system() {
126 Ok(v) => Ok(v),
127 Err(e) if !ffmpeg_available() => Err(e),
128 Err(_) => ffmpeg().map_err(Error::from),
129 }
130}
131
132pub fn probe(path: &Path) -> Result<Info> {
135 either(|| sys::probe(path), || falcon_ffmpeg::probe(path))
136}
137
138pub fn frame_at(path: &Path, secs: f64) -> Result<Frame> {
141 either(|| sys::frame_at(path, secs), || falcon_ffmpeg::frame_at(path, secs))
142}
143
144pub struct Grabber {
150 grab: RefCell<Grab>,
151 path: std::path::PathBuf,
152 max_side: u32,
153}
154
155enum Grab {
156 System(sys::Grabber),
157 Ffmpeg(falcon_ffmpeg::Grabber),
158}
159
160impl Grabber {
161 pub fn open(path: &Path, max_side: u32) -> Result<Grabber> {
163 let grab = either(
164 || sys::Grabber::open(path, max_side).map(Grab::System),
165 || falcon_ffmpeg::Grabber::open(path, max_side).map(Grab::Ffmpeg),
166 )?;
167 Ok(Grabber { grab: RefCell::new(grab), path: path.to_path_buf(), max_side })
168 }
169
170 pub fn grab(&self, secs: f64) -> Result<Frame> {
175 let mut grab = self.grab.borrow_mut();
176 match &mut *grab {
177 Grab::Ffmpeg(g) => g.grab(secs).map_err(Error::from),
178 Grab::System(g) => match g.grab(secs) {
179 Ok(f) => Ok(f),
180 Err(e) if !ffmpeg_available() => Err(e),
181 Err(e) => {
182 let Ok(mut g) = falcon_ffmpeg::Grabber::open(&self.path, self.max_side) else { return Err(e) };
183 let f = g.grab(secs).map_err(Error::from);
184 *grab = Grab::Ffmpeg(g);
185 f
186 }
187 },
188 }
189 }
190}
191
192struct Sound(falcon_ffmpeg::AudioOut);
195
196impl Iterator for Sound {
197 type Item = rodio::Sample;
198
199 fn next(&mut self) -> Option<rodio::Sample> {
200 self.0.next_sample().map(|s| s as rodio::Sample)
201 }
202}
203
204impl rodio::Source for Sound {
205 fn current_span_len(&self) -> Option<usize> {
206 None
207 }
208
209 fn channels(&self) -> rodio::ChannelCount {
210 NonZero::new(falcon_ffmpeg::AudioOut::CHANNELS).unwrap_or(NonZero::<u16>::MIN)
211 }
212
213 fn sample_rate(&self) -> rodio::SampleRate {
214 NonZero::new(falcon_ffmpeg::AudioOut::RATE).unwrap_or(NonZero::<u32>::MIN)
215 }
216
217 fn total_duration(&self) -> Option<std::time::Duration> {
218 None
219 }
220}
221
222type Opened = falcon_ffmpeg::Result<(falcon_ffmpeg::Player, Option<falcon_ffmpeg::AudioOut>)>;
224
225#[cfg_attr(windows, allow(clippy::large_enum_variant))]
227enum Readied {
228 System(sys::Prepared),
229 Ffmpeg(falcon_ffmpeg::Player, Option<falcon_ffmpeg::AudioOut>),
230}
231
232pub struct Prepared {
236 readied: Readied,
237 path: PathBuf,
238 max_side: u32,
239}
240
241impl Prepared {
242 pub fn new(path: &Path, max_side: u32) -> Result<Prepared> {
245 let readied = match sys::Prepared::new(path) {
246 Ok(p) => Readied::System(p),
247 Err(e) if !ffmpeg_available() => return Err(e),
248 Err(_) => {
249 let (player, sound) = falcon_ffmpeg::Player::open_preview(path, true, max_side)?;
250 Readied::Ffmpeg(player, sound)
251 }
252 };
253 Ok(Prepared { readied, path: path.to_path_buf(), max_side })
254 }
255
256 pub fn start(self) -> Result<Video> {
260 if !sys::on_main_thread() {
261 return Err(Error("video playback runs on the main thread".into()));
262 }
263 let (inner, held_sound) = match self.readied {
264 Readied::System(p) => (Inner::System(sys::Video::start(p, Some(self.max_side))?), None),
265 Readied::Ffmpeg(p, sound) => (Inner::Ffmpeg(p), sound),
266 };
267 let mut video = Video {
268 inner,
269 path: self.path,
270 mixer: None,
271 volume: 1.0,
272 muted: false,
273 fallback_error: None,
274 opened: Instant::now(),
275 had_frame: false,
276 held_frame: None,
277 preview_side: Some(self.max_side),
278 held_sound,
279 opening: None,
280 };
281 video.apply_mute();
282 Ok(video)
283 }
284}
285
286enum Inner {
287 System(sys::Video),
288 Ffmpeg(falcon_ffmpeg::Player),
289}
290
291pub struct Video {
298 inner: Inner,
299 path: PathBuf,
300 mixer: Option<rodio::mixer::Mixer>,
302 volume: f32,
303 muted: bool,
304 fallback_error: Option<String>,
306 opened: Instant,
308 had_frame: bool,
310 held_frame: Option<Frame>,
312 preview_side: Option<u32>,
315 held_sound: Option<falcon_ffmpeg::AudioOut>,
317 opening: Option<mpsc::Receiver<Opened>>,
319}
320
321impl Video {
322 pub fn open(path: &Path) -> Result<Video> {
326 Video::open_with(path, None)
327 }
328
329 pub fn open_with(path: &Path, mixer: Option<&rodio::mixer::Mixer>) -> Result<Video> {
332 if !sys::on_main_thread() {
333 return Err(Error("video playback runs on the main thread".into()));
334 }
335 let mixer = mixer.cloned();
336 let inner = match sys::Video::open(path) {
337 Ok(v) => Inner::System(v),
338 Err(e) if !ffmpeg_available() => return Err(e),
339 Err(_) => Inner::Ffmpeg(open_ffmpeg(path, mixer.as_ref())?),
340 };
341 Ok(Video { inner, path: path.to_path_buf(), mixer, volume: 1.0, muted: false, fallback_error: None, opened: Instant::now(), had_frame: false, held_frame: None, preview_side: None, held_sound: None, opening: None })
342 }
343
344 pub fn backend(&self) -> Backend {
346 match self.inner {
347 Inner::System(_) => Backend::System,
348 Inner::Ffmpeg(_) => Backend::Ffmpeg,
349 }
350 }
351
352 pub fn info(&self) -> &Info {
353 match &self.inner {
354 Inner::System(v) => v.info(),
355 Inner::Ffmpeg(p) => p.info(),
356 }
357 }
358
359 pub fn is_ready(&self) -> bool {
361 match &self.inner {
362 Inner::System(v) => v.is_ready(),
363 Inner::Ffmpeg(p) => p.is_ready(),
364 }
365 }
366
367 pub fn error(&self) -> Option<String> {
371 match &self.inner {
372 Inner::System(v) => v.error().filter(|_| self.fallback_error.is_some() || !ffmpeg_available()),
373 Inner::Ffmpeg(_) => None,
374 }
375 }
376
377 pub fn play(&mut self) {
379 match &mut self.inner {
380 Inner::System(v) => v.play(),
381 Inner::Ffmpeg(p) => p.play(),
382 }
383 }
384
385 pub fn pause(&mut self) {
386 match &mut self.inner {
387 Inner::System(v) => v.pause(),
388 Inner::Ffmpeg(p) => p.pause(),
389 }
390 }
391
392 pub fn toggle(&mut self) {
393 match &mut self.inner {
394 Inner::System(v) => v.toggle(),
395 Inner::Ffmpeg(p) => p.toggle(),
396 }
397 }
398
399 pub fn is_playing(&self) -> bool {
400 match &self.inner {
401 Inner::System(v) => v.is_playing(),
402 Inner::Ffmpeg(p) => p.is_playing(),
403 }
404 }
405
406 pub fn seek(&mut self, secs: f64) {
408 match &mut self.inner {
409 Inner::System(v) => v.seek(secs),
410 Inner::Ffmpeg(p) => p.seek(secs),
411 }
412 }
413
414 pub fn step(&mut self, frames: i32) {
417 match &mut self.inner {
418 Inner::System(v) => v.step(frames),
419 Inner::Ffmpeg(p) => p.step(frames),
420 }
421 }
422
423 pub fn rate(&self) -> f32 {
424 match &self.inner {
425 Inner::System(v) => v.rate(),
426 Inner::Ffmpeg(p) => p.rate(),
427 }
428 }
429
430 pub fn set_rate(&mut self, rate: f32) {
431 let rate = rate.clamp(RATES[0], RATES[RATES.len() - 1]);
432 match &mut self.inner {
433 Inner::System(v) => v.set_rate(rate),
434 Inner::Ffmpeg(p) => p.set_rate(rate),
435 }
436 }
437
438 pub fn set_volume(&mut self, volume: f32) {
440 self.volume = volume.clamp(0.0, 1.0);
441 match &mut self.inner {
442 Inner::System(v) => v.set_volume(self.volume),
443 Inner::Ffmpeg(p) => p.set_volume(self.volume),
444 }
445 }
446
447 pub fn set_muted(&mut self, muted: bool) {
448 self.muted = muted;
449 self.apply_mute();
450 }
451
452 fn apply_mute(&mut self) {
455 match &mut self.inner {
456 Inner::System(v) => v.set_muted(self.muted),
457 Inner::Ffmpeg(p) => p.set_muted(self.muted || self.held_sound.is_some()),
458 }
459 }
460
461 pub fn needs_mixer(&self) -> bool {
463 self.preview_side.is_some() && self.mixer.is_none()
464 }
465
466 pub fn set_mixer(&mut self, mixer: rodio::mixer::Mixer) {
469 if let Some(s) = self.held_sound.take() {
470 mixer.add(Sound(s));
471 }
472 self.mixer = Some(mixer);
473 self.apply_mute();
474 }
475
476 pub fn set_looping(&mut self, looping: bool) {
477 match &mut self.inner {
478 Inner::System(v) => v.set_looping(looping),
479 Inner::Ffmpeg(p) => p.set_looping(looping),
480 }
481 }
482
483 pub fn looping(&self) -> bool {
484 match &self.inner {
485 Inner::System(v) => v.looping(),
486 Inner::Ffmpeg(p) => p.looping(),
487 }
488 }
489
490 pub fn current_time(&self) -> f64 {
492 match &self.inner {
493 Inner::System(v) => v.current_time(),
494 Inner::Ffmpeg(p) => p.current_time(),
495 }
496 }
497
498 pub fn tick(&mut self) {
502 self.adopt_opened();
503 if let Inner::System(v) = &self.inner
504 && v.error().is_some()
505 && self.fallback_error.is_none()
506 && self.opening.is_none()
507 && ffmpeg_available()
508 {
509 self.fall_back();
510 }
511 self.check_first_frame();
512 match &mut self.inner {
513 Inner::System(v) => v.tick(),
514 Inner::Ffmpeg(p) => p.tick(),
515 }
516 }
517
518 fn check_first_frame(&mut self) {
522 let Inner::System(v) = &mut self.inner else { return };
523 if self.fallback_error.is_some() || self.opening.is_some() || !switch_for_no_frame(self.opened.elapsed(), self.had_frame, ffmpeg_available()) {
524 return;
525 }
526 if let Some(f) = v.frame() {
527 self.had_frame = true;
528 self.held_frame = Some(f);
529 return;
530 }
531 self.fall_back();
532 }
533
534 #[doc(hidden)]
537 pub fn expire_first_frame_time(&mut self) {
538 self.opened = Instant::now().checked_sub(NO_FRAME_LIMIT).unwrap_or(self.opened);
539 }
540
541 fn fall_back(&mut self) {
546 if let Some(side) = self.preview_side {
547 let (tx, rx) = mpsc::channel();
548 let path = self.path.clone();
549 std::thread::spawn(move || {
550 let _ = tx.send(falcon_ffmpeg::Player::open_preview(&path, true, side));
551 });
552 self.opening = Some(rx);
553 return;
554 }
555 match open_ffmpeg(&self.path, self.mixer.as_ref()) {
556 Ok(p) => self.take_over(p),
557 Err(e) => self.fallback_error = Some(e.0),
558 }
559 }
560
561 fn adopt_opened(&mut self) {
563 let Some(rx) = &self.opening else { return };
564 let opened = match rx.try_recv() {
565 Ok(r) => r,
566 Err(mpsc::TryRecvError::Empty) => return,
567 Err(mpsc::TryRecvError::Disconnected) => Err(falcon_ffmpeg::Error("the FFmpeg worker stopped".into())),
568 };
569 self.opening = None;
570 match opened {
571 Ok((p, sound)) => {
572 match (&self.mixer, sound) {
573 (Some(m), Some(s)) => m.add(Sound(s)),
574 (None, s) => self.held_sound = s,
575 (Some(_), None) => {}
576 }
577 self.take_over(p);
578 }
579 Err(e) => self.fallback_error = Some(e.0),
580 }
581 }
582
583 fn take_over(&mut self, mut p: falcon_ffmpeg::Player) {
585 let Inner::System(old) = &self.inner else { return };
586 let (rate, looping, playing, at) = (old.rate(), old.looping(), old.is_playing(), old.current_time());
587 p.set_volume(self.volume);
588 p.set_muted(self.muted || self.held_sound.is_some());
589 p.set_rate(rate);
590 p.set_looping(looping);
591 if at > 0.0 {
592 p.seek(at);
593 }
594 if playing {
595 p.play();
596 }
597 self.inner = Inner::Ffmpeg(p);
598 }
599
600 pub fn current_frame(&self) -> Result<Frame> {
605 match &self.inner {
606 Inner::System(v) => v.current_frame(),
607 Inner::Ffmpeg(p) => p.current_frame().map_err(Error::from),
608 }
609 }
610
611 pub fn frame(&mut self) -> Option<Frame> {
613 if let Some(f) = self.held_frame.take() {
614 return Some(f);
615 }
616 match &mut self.inner {
617 Inner::System(v) => {
618 let f = v.frame();
619 self.had_frame |= f.is_some();
620 f
621 }
622 Inner::Ffmpeg(p) => p.frame(),
623 }
624 }
625}
626
627fn open_ffmpeg(path: &Path, mixer: Option<&rodio::mixer::Mixer>) -> Result<falcon_ffmpeg::Player> {
629 let (player, sound) = falcon_ffmpeg::Player::open(path, mixer.is_some())?;
630 if let (Some(m), Some(s)) = (mixer, sound) {
631 m.add(Sound(s));
632 }
633 Ok(player)
634}
635
636#[cfg(test)]
637mod tests {
638 use super::*;
639
640 #[test]
641 fn rates_step_and_stop_at_the_ends() {
642 assert_eq!(step_rate(1.0, true), 1.25);
643 assert_eq!(step_rate(1.0, false), 0.75);
644 assert_eq!(step_rate(8.0, true), 8.0);
645 assert_eq!(step_rate(0.25, false), 0.25);
646 assert_eq!(step_rate(2.0, true), 3.0);
647 assert_eq!(step_rate(1.1, true), 1.25);
648 assert_eq!(step_rate(1.1, false), 1.0);
649 }
650
651 #[test]
652 fn a_player_with_no_picture_after_the_limit_switches() {
653 let (before, at, after) = (Duration::from_millis(1499), NO_FRAME_LIMIT, Duration::from_secs(60));
654 assert!(!switch_for_no_frame(Duration::ZERO, false, true));
655 assert!(!switch_for_no_frame(before, false, true), "still within its time");
656 assert!(switch_for_no_frame(at, false, true));
657 assert!(switch_for_no_frame(after, false, true));
658 }
659
660 #[test]
661 fn a_player_that_drew_a_picture_stays() {
662 assert!(!switch_for_no_frame(NO_FRAME_LIMIT, true, true));
664 assert!(!switch_for_no_frame(Duration::from_secs(3600), true, true));
665 }
666
667 #[test]
668 fn without_ffmpeg_there_is_nothing_to_switch_to() {
669 assert!(!switch_for_no_frame(NO_FRAME_LIMIT, false, false));
670 assert!(!switch_for_no_frame(Duration::from_secs(60), false, false));
671 }
672
673 #[test]
674 fn missing_files_are_refused_by_both() {
675 let p = Path::new("/nonexistent/falcon-video-test.mkv");
676 assert!(probe(p).is_err());
677 assert!(frame_at(p, 0.0).is_err());
678 assert!(Grabber::open(p, 64).is_err());
679 }
680}