Skip to main content

oxframe_2d/
video.rs

1//! A video clip as a node.
2//!
3//! The node shows the frame of its clip at `time` (seconds into the clip),
4//! a signal the scene advances while the clip plays: `play` starts a task
5//! that adds `playback_rate × dt` every frame. So the frame is a function
6//! of the scene's own steps, and a seek or a render shows the same frame
7//! every time.
8//!
9//! Decoding is not done here: this crate also builds for the browser. The
10//! native engine installs an opener backed by ffmpeg ([`set_video_opener`]);
11//! without one, the node draws a placeholder box.
12
13use std::cell::{Cell, RefCell};
14use std::path::{Path, PathBuf};
15use std::rc::Rc;
16
17use crate::image::RasterImage;
18
19/// What a clip is.
20#[derive(Debug, Clone, Copy, PartialEq)]
21pub struct VideoInfo {
22    /// Width in pixels.
23    pub width: u32,
24    /// Height in pixels.
25    pub height: u32,
26    /// Frames per second of the clip.
27    pub fps: f64,
28    /// Length in seconds.
29    pub duration: f64,
30    /// The file has an audio stream.
31    pub has_audio: bool,
32}
33
34/// Frames of one clip, by index. Implemented by the native ffmpeg decoder.
35pub trait VideoSource {
36    /// Size, frame rate and length.
37    fn info(&self) -> VideoInfo;
38    /// Frame `index` (0 is the first), straight-alpha RGBA. `None` past the
39    /// end or when decoding failed.
40    fn frame(&self, index: u64) -> Option<Rc<RasterImage>>;
41}
42
43/// Opens a clip by path.
44pub type VideoOpener = Rc<dyn Fn(&Path) -> Result<Rc<dyn VideoSource>, String>>;
45
46thread_local! {
47    static OPENER: RefCell<Option<VideoOpener>> = const { RefCell::new(None) };
48}
49
50/// Installs the function that opens clips for [`Video`](crate::Video)
51/// nodes. The engine does this when it is built with the `video` feature.
52pub fn set_video_opener(opener: VideoOpener) {
53    OPENER.with(|o| *o.borrow_mut() = Some(opener));
54}
55
56/// The clip at `path`, or a placeholder and the reason it is one.
57pub(crate) fn open(path: &Path) -> (Rc<dyn VideoSource>, Option<String>) {
58    let opened = OPENER.with(|o| o.borrow().clone()).map(|open| open(path));
59    match opened {
60        Some(Ok(source)) => (source, None),
61        Some(Err(e)) => (Rc::new(Missing), Some(e)),
62        None => (
63            Rc::new(Missing),
64            Some("video decoding needs the native engine with the `video` feature".into()),
65        ),
66    }
67}
68
69/// A clip that could not be opened: a 16:9 box with no frames.
70struct Missing;
71
72impl VideoSource for Missing {
73    fn info(&self) -> VideoInfo {
74        VideoInfo {
75            width: 1280,
76            height: 720,
77            fps: 30.0,
78            duration: 0.0,
79            has_audio: false,
80        }
81    }
82
83    fn frame(&self, _: u64) -> Option<Rc<RasterImage>> {
84        None
85    }
86}
87
88/// Playback of one node, shared by its handle and the task that plays it.
89#[doc(hidden)]
90#[derive(Default)]
91pub struct Playback {
92    pub(crate) playing: Cell<bool>,
93    /// Bumped by every `play`, so an older task stops.
94    pub(crate) generation: Cell<u32>,
95    /// `seek` moved the time: the sound starts again from there.
96    pub(crate) jumped: Cell<bool>,
97    /// The sound being recorded: (clip index, scene time, start in file).
98    pub(crate) segment: Cell<Option<(usize, f64, f64)>>,
99}
100
101/// Stored on a video node.
102#[doc(hidden)]
103pub struct VideoData {
104    pub(crate) source: Rc<dyn VideoSource>,
105    pub(crate) path: PathBuf,
106    /// Why there are no frames, for `inspect` and `check`.
107    pub(crate) problem: Option<String>,
108    pub(crate) muted: bool,
109    pub(crate) playback: Rc<Playback>,
110}
111
112impl VideoData {
113    /// Index of the frame shown at clip time `t`.
114    pub(crate) fn index_at(&self, t: f64) -> u64 {
115        let info = self.source.info();
116        let last = (info.duration * info.fps).ceil().max(1.0) as u64 - 1;
117        ((t.max(0.0) * info.fps + 1e-6).floor() as u64).min(last)
118    }
119}