decode/lib.rs
1//! `decode` — video decode → BGRA frames, trait-based.
2//!
3//! # Why a trait
4//!
5//! The recorder decodes MP4s through `GStreamer` today (the workspace's
6//! single media stack — see CLAUDE.md "Stack" section and AUT-144).
7//! Multiple backends still exist as a contract: the [`gstreamer_pipe`]
8//! CLI-subprocess implementation, a future in-process `gstreamer-rs`
9//! bindings implementation for encode (`appsrc`-fed), and the [`mock`]
10//! deterministic test source. The *consumer* (wisp's
11//! `VideoTexture::upload_bgra` path) is uniform: it wants a stream of
12//! BGRA frames at known dimensions, ticked at known timestamps.
13//!
14//! [`VideoStream`] is that uniform contract. Any implementation can be
15//! swapped in at runtime without changing the player loop.
16//!
17//! # Layers
18//!
19//! - [`VideoStream`] — pull-based interface returning [`VideoFrame`] values.
20//! - [`mock::MockVideoStream`] — deterministic test source. No external
21//! deps; useful for tests and as the harness target for examples until
22//! M-DEC.2 wires in a real codec.
23//!
24//! # Quick start
25//!
26//! ```rust
27//! use decode::{VideoFrame, VideoStream, mock::MockVideoStream};
28//!
29//! let mut stream = MockVideoStream::scrolling_gradient(64, 36, 8);
30//! let frame: VideoFrame = stream.next_frame().expect("first frame");
31//! assert_eq!(frame.width, 64);
32//! assert_eq!(frame.bgra.len(), (64 * 36 * 4) as usize);
33//! ```
34
35pub mod editor_stream;
36pub mod gstreamer_pipe;
37pub mod mock;
38
39pub use editor_stream::EditorVideoStream;
40
41/// One decoded frame in BGRA8 layout, the canonical wgpu input format for
42/// per-frame texture uploads.
43#[derive(Clone)]
44pub struct VideoFrame {
45 /// Frame width in pixels.
46 pub width: u32,
47 /// Frame height in pixels.
48 pub height: u32,
49 /// Tightly packed BGRA bytes, row-major. Length must equal
50 /// `(width * height * 4) as usize`.
51 pub bgra: Vec<u8>,
52 /// Presentation timestamp in seconds since the start of the stream.
53 pub pts_seconds: f64,
54 /// Zero-based frame index (monotonic across the stream).
55 pub frame_index: u64,
56}
57
58impl std::fmt::Debug for VideoFrame {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 f.debug_struct("VideoFrame")
61 .field("width", &self.width)
62 .field("height", &self.height)
63 .field("bgra_len", &self.bgra.len())
64 .field("pts_seconds", &self.pts_seconds)
65 .field("frame_index", &self.frame_index)
66 .finish()
67 }
68}
69
70/// Pull-based source of decoded video frames.
71///
72/// Implementations may be lazy (stream from disk on each `next_frame`) or
73/// eager (decode the whole stream up front). The contract is just:
74/// successive calls return successive frames; `None` signals end-of-stream.
75///
76/// Frame dimensions are reported by [`width`](Self::width) /
77/// [`height`](Self::height) and must remain constant across the stream
78/// (we don't support resolution changes mid-playback in v1).
79pub trait VideoStream {
80 /// Frame width in pixels. Constant across the stream.
81 fn width(&self) -> u32;
82
83 /// Frame height in pixels. Constant across the stream.
84 fn height(&self) -> u32;
85
86 /// Source frame rate (frames per second). Used by the player loop to
87 /// time uploads; backends without a known fixed rate may return their
88 /// nominal rate (e.g. `AVFoundation`'s `nominalFrameRate` on the track).
89 fn frame_rate(&self) -> f32;
90
91 /// Pull the next frame. Returns `None` at end-of-stream.
92 fn next_frame(&mut self) -> Option<VideoFrame>;
93
94 /// Total frame count, if known up-front. `None` for live streams.
95 fn frame_count_hint(&self) -> Option<u64> {
96 None
97 }
98}