Skip to main content

oxideav_scene/
source.rs

1//! Source / Sink plumbing.
2//!
3//! A scene is a media producer — drive it with a [`SceneRenderer`]
4//! and it yields a stream of [`RenderedFrame`]s at the scene's
5//! [`framerate`](crate::Scene::framerate). The [`SceneSource`] trait
6//! formalises that contract so scenes can slot into the same pipe
7//! topology that oxideav uses for live decoders, capture devices,
8//! and file readers.
9//!
10//! A [`SceneSink`] consumes `RenderedFrame`s and does something with
11//! them — encode + mux to a file, push to an RTMP endpoint, render
12//! to a window, emit PDF operators to a writer, etc. The sink trait
13//! is deliberately thin: `init` once, `push` per frame, `finalise`
14//! once. Format negotiation happens upfront via [`SourceFormat`].
15//!
16//! [`drive`] is the glue: it runs the pull loop until the source is
17//! exhausted or the sink errors out.
18
19use oxideav_core::{Error, Rational, Result, TimeBase};
20
21use crate::duration::{SceneDuration, TimeStamp};
22use crate::object::Canvas;
23use crate::render::{RenderedFrame, SceneRenderer};
24use crate::scene::Scene;
25
26/// Format contract between a [`SceneSource`] and a [`SceneSink`].
27///
28/// Everything a sink needs to set up encoders / muxers / windows
29/// before the first frame arrives.
30#[derive(Clone, Debug)]
31pub struct SourceFormat {
32    pub canvas: Canvas,
33    pub framerate: Rational,
34    pub time_base: TimeBase,
35    pub sample_rate: u32,
36    /// Whether the source has a known end. `Finite(n)` lets sinks
37    /// size output containers; `Indefinite` signals a streaming
38    /// source that runs until externally stopped.
39    pub duration: SceneDuration,
40    /// Pages-mode flag — `true` when the source scene carries a
41    /// non-empty [`Scene::pages`] list. Lets paged-content sinks
42    /// (PDF, multi-page TIFF) reject timeline scenes early in
43    /// `init()`, and lets video sinks reject paged scenes.
44    pub paged: bool,
45}
46
47impl SourceFormat {
48    /// Build from a scene's current state. The renderer consumes
49    /// this at `init()` time so downstream encoder settings match
50    /// the scene's declarations.
51    pub fn from_scene(scene: &Scene) -> Self {
52        SourceFormat {
53            canvas: scene.canvas,
54            framerate: scene.framerate,
55            time_base: scene.time_base,
56            sample_rate: scene.sample_rate,
57            duration: scene.duration,
58            paged: scene.is_paged(),
59        }
60    }
61}
62
63/// Pull-based source of rendered frames.
64///
65/// Implementors typically wrap a [`Scene`] + [`SceneRenderer`] and
66/// advance an internal frame counter per `pull()`. The first call
67/// after `prepare` emits frame 0 at timestamp 0; each subsequent
68/// call advances by `1 / framerate`.
69///
70/// Sources are **not** required to be seekable — a streaming
71/// compositor source is forward-only. Sources that can seek should
72/// expose it via an inherent method, not this trait.
73pub trait SceneSource {
74    /// Declared format. Constant across a session.
75    fn format(&self) -> SourceFormat;
76
77    /// Produce the next rendered tick. Returns `Ok(None)` when the
78    /// source is exhausted (finite scene reached its end). For
79    /// indefinite sources, this never returns `None`.
80    fn pull(&mut self) -> Result<Option<RenderedFrame>>;
81}
82
83/// Push-based sink for rendered frames.
84///
85/// Implementors set up encoders / muxers in [`init`], receive one
86/// frame per [`push`] call, then release any buffered state in
87/// [`finalise`]. A sink that fails mid-stream should return the
88/// error from `push` — [`drive`] will call `finalise` regardless.
89///
90/// [`init`]: SceneSink::init
91/// [`push`]: SceneSink::push
92/// [`finalise`]: SceneSink::finalise
93pub trait SceneSink {
94    /// Called once before the first `push`. The sink may return
95    /// `Error::Unsupported` if it can't handle the format.
96    fn init(&mut self, format: &SourceFormat) -> Result<()>;
97
98    /// Consume one rendered frame. Time is embedded in the frame's
99    /// video pts + audio sample count; the sink itself doesn't need
100    /// to track timestamps separately.
101    fn push(&mut self, frame: RenderedFrame) -> Result<()>;
102
103    /// Flush + close. No more `push` calls after this. Always
104    /// called by `drive`, even on error paths.
105    fn finalise(&mut self) -> Result<()>;
106}
107
108/// Pull-loop helper. Drives `source` → `sink` until the source is
109/// exhausted or either side errors out. `finalise` is always
110/// called on the sink; `init` happens before the first pull.
111pub fn drive(source: &mut dyn SceneSource, sink: &mut dyn SceneSink) -> Result<()> {
112    let fmt = source.format();
113    sink.init(&fmt)?;
114    let result = drive_loop(source, sink);
115    let fin = sink.finalise();
116    result.and(fin)
117}
118
119fn drive_loop(source: &mut dyn SceneSource, sink: &mut dyn SceneSink) -> Result<()> {
120    loop {
121        match source.pull()? {
122            Some(frame) => sink.push(frame)?,
123            None => return Ok(()),
124        }
125    }
126}
127
128/// Default [`SceneSource`] implementation wrapping a scene + a
129/// renderer. Advances one frame per `pull` at the scene's declared
130/// framerate; emits `None` when a finite scene's last frame has
131/// been yielded.
132pub struct RenderedSource<R: SceneRenderer> {
133    scene: Scene,
134    renderer: R,
135    next_frame: u64,
136    total_frames: Option<u64>,
137    prepared: bool,
138}
139
140impl<R: SceneRenderer> RenderedSource<R> {
141    /// Take ownership of `scene` + `renderer`. Does not call
142    /// `prepare` on the renderer — that happens lazily on the first
143    /// `pull`.
144    pub fn new(scene: Scene, renderer: R) -> Self {
145        let total_frames = scene.frame_count();
146        RenderedSource {
147            scene,
148            renderer,
149            next_frame: 0,
150            total_frames,
151            prepared: false,
152        }
153    }
154
155    /// Access the underlying scene (read-only). Useful for tests +
156    /// compositors that want to inspect state between pulls.
157    pub fn scene(&self) -> &Scene {
158        &self.scene
159    }
160
161    /// Mutate the scene between pulls. The streaming-compositor use
162    /// case uses this to apply `Operation`s pulled from a control
163    /// channel. Mid-stream mutations MUST NOT shift earlier
164    /// timestamps — append-only operations (new keyframes after
165    /// `next_timestamp()`, new objects, removed-in-future) are
166    /// safe; rewriting existing keyframes is not.
167    pub fn scene_mut(&mut self) -> &mut Scene {
168        &mut self.scene
169    }
170
171    /// Timestamp of the next frame to be pulled.
172    pub fn next_timestamp(&self) -> TimeStamp {
173        self.scene.frame_to_timestamp(self.next_frame)
174    }
175}
176
177impl<R: SceneRenderer> SceneSource for RenderedSource<R> {
178    fn format(&self) -> SourceFormat {
179        SourceFormat::from_scene(&self.scene)
180    }
181
182    fn pull(&mut self) -> Result<Option<RenderedFrame>> {
183        if let Some(total) = self.total_frames {
184            if self.next_frame >= total {
185                return Ok(None);
186            }
187        }
188        if !self.prepared {
189            self.renderer.prepare(&self.scene)?;
190            self.prepared = true;
191        }
192        let t = self.next_timestamp();
193        let frame = self.renderer.render_at(&self.scene, t)?;
194        self.next_frame += 1;
195        Ok(Some(frame))
196    }
197}
198
199/// Discarding sink — useful for correctness tests + dry runs that
200/// exercise the pull loop without wiring an encoder. Records a
201/// frame + byte counter so callers can assert progress.
202#[derive(Default)]
203pub struct NullSink {
204    pub frames_received: u64,
205    pub bytes_received: u64,
206    pub format_seen: Option<SourceFormat>,
207}
208
209impl SceneSink for NullSink {
210    fn init(&mut self, format: &SourceFormat) -> Result<()> {
211        self.format_seen = Some(format.clone());
212        Ok(())
213    }
214
215    fn push(&mut self, frame: RenderedFrame) -> Result<()> {
216        self.frames_received += 1;
217        if let Some(v) = frame.video.as_ref() {
218            self.bytes_received += v.planes.iter().map(|p| p.data.len() as u64).sum::<u64>();
219        }
220        self.bytes_received += (frame.audio.len() * std::mem::size_of::<f32>()) as u64;
221        Ok(())
222    }
223
224    fn finalise(&mut self) -> Result<()> {
225        Ok(())
226    }
227}
228
229/// Sink that forwards to a closure. Handy for tests + one-off
230/// integrations where a full trait impl is overkill.
231pub struct FnSink<F>
232where
233    F: FnMut(&SourceFormat, RenderedFrame) -> Result<()>,
234{
235    format: Option<SourceFormat>,
236    cb: F,
237}
238
239impl<F> FnSink<F>
240where
241    F: FnMut(&SourceFormat, RenderedFrame) -> Result<()>,
242{
243    pub fn new(cb: F) -> Self {
244        FnSink { format: None, cb }
245    }
246}
247
248impl<F> SceneSink for FnSink<F>
249where
250    F: FnMut(&SourceFormat, RenderedFrame) -> Result<()>,
251{
252    fn init(&mut self, format: &SourceFormat) -> Result<()> {
253        self.format = Some(format.clone());
254        Ok(())
255    }
256
257    fn push(&mut self, frame: RenderedFrame) -> Result<()> {
258        let fmt = self.format.as_ref().ok_or_else(|| {
259            Error::invalid("FnSink: push before init — call SceneSink::init first")
260        })?;
261        (self.cb)(fmt, frame)
262    }
263
264    fn finalise(&mut self) -> Result<()> {
265        Ok(())
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use crate::render::StubRenderer;
273
274    /// Trivial `SceneSource` that emits 3 empty frames and stops.
275    struct CountingSource {
276        fmt: SourceFormat,
277        left: u32,
278    }
279
280    impl SceneSource for CountingSource {
281        fn format(&self) -> SourceFormat {
282            self.fmt.clone()
283        }
284        fn pull(&mut self) -> Result<Option<RenderedFrame>> {
285            if self.left == 0 {
286                return Ok(None);
287            }
288            self.left -= 1;
289            Ok(Some(RenderedFrame::default()))
290        }
291    }
292
293    #[test]
294    fn drive_runs_until_source_empty() {
295        let scene = Scene::default();
296        let fmt = SourceFormat::from_scene(&scene);
297        let mut src = CountingSource { fmt, left: 3 };
298        let mut sink = NullSink::default();
299        drive(&mut src, &mut sink).unwrap();
300        assert_eq!(sink.frames_received, 3);
301        assert!(sink.format_seen.is_some());
302    }
303
304    #[test]
305    fn rendered_source_stops_at_frame_count() {
306        // 3 frames at 30 fps = 100 ms → Finite(100).
307        let scene = Scene {
308            duration: SceneDuration::Finite(100),
309            ..Scene::default()
310        };
311        // 3 frames expected (0, 33, 66 ms; 100 ms is past the end).
312        assert_eq!(scene.frame_count(), Some(3));
313        // StubRenderer returns Unsupported, so we can't actually
314        // pull successfully — but the frame-counting bookkeeping is
315        // what matters here. Confirm via next_timestamp.
316        let src = RenderedSource::new(scene, StubRenderer);
317        assert_eq!(src.next_timestamp(), 0);
318    }
319
320    #[test]
321    fn fn_sink_forwards_to_closure() {
322        let mut count = 0u32;
323        let mut sink = FnSink::new(|_fmt, _frame| {
324            count += 1;
325            Ok(())
326        });
327        let fmt = SourceFormat::from_scene(&Scene::default());
328        sink.init(&fmt).unwrap();
329        sink.push(RenderedFrame::default()).unwrap();
330        sink.push(RenderedFrame::default()).unwrap();
331        sink.finalise().unwrap();
332        assert_eq!(count, 2);
333    }
334
335    #[test]
336    fn fn_sink_rejects_push_before_init() {
337        let mut sink = FnSink::new(|_fmt, _frame| Ok(()));
338        let err = sink.push(RenderedFrame::default()).unwrap_err();
339        assert!(matches!(err, Error::InvalidData(_)));
340    }
341}