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}