Skip to main content

decode/
mock.rs

1//! Deterministic mock video sources.
2//!
3//! These implement [`crate::VideoStream`] without any external decoder
4//! dependency, so we can:
5//!
6//! - exercise the player loop end-to-end in tests,
7//! - drive wisp examples that prove the GPU path,
8//! - keep `cargo check` / `just gate` clean on machines without a system
9//!   `FFmpeg` / `AVFoundation` toolchain.
10
11use crate::{VideoFrame, VideoStream};
12
13/// A scrolling-gradient mock: each frame phase-shifts a smooth RGB gradient
14/// across the surface. Visually obvious motion; mathematically deterministic.
15pub struct MockVideoStream {
16    width: u32,
17    height: u32,
18    frame_rate: f32,
19    total_frames: u64,
20    next_index: u64,
21}
22
23impl MockVideoStream {
24    /// Build a scrolling-gradient stream with the given dimensions and
25    /// total frame count. Plays at 30 fps.
26    #[must_use]
27    pub fn scrolling_gradient(width: u32, height: u32, total_frames: u64) -> Self {
28        Self {
29            width,
30            height,
31            frame_rate: 30.0,
32            total_frames,
33            next_index: 0,
34        }
35    }
36
37    /// Override the frame rate (used by player-pacing tests).
38    #[must_use]
39    pub fn with_frame_rate(mut self, fps: f32) -> Self {
40        self.frame_rate = fps;
41        self
42    }
43}
44
45impl VideoStream for MockVideoStream {
46    fn width(&self) -> u32 {
47        self.width
48    }
49
50    fn height(&self) -> u32 {
51        self.height
52    }
53
54    fn frame_rate(&self) -> f32 {
55        self.frame_rate
56    }
57
58    fn frame_count_hint(&self) -> Option<u64> {
59        Some(self.total_frames)
60    }
61
62    fn next_frame(&mut self) -> Option<VideoFrame> {
63        if self.next_index >= self.total_frames {
64            return None;
65        }
66        let frame = synthesize(
67            self.width,
68            self.height,
69            self.next_index,
70            self.total_frames,
71            self.frame_rate,
72        );
73        self.next_index += 1;
74        Some(frame)
75    }
76}
77
78#[allow(
79    clippy::cast_possible_truncation,
80    clippy::cast_precision_loss,
81    clippy::cast_sign_loss,
82    clippy::many_single_char_names,
83    reason = "byte values bounded by rem_euclid(256.0); single-letter names match gradient math conventions"
84)]
85fn synthesize(width: u32, height: u32, idx: u64, total: u64, fps: f32) -> VideoFrame {
86    let mut bgra = Vec::with_capacity((width * height * 4) as usize);
87    let phase = (idx as f32) / (total.max(1) as f32);
88    for y in 0..height {
89        for x in 0..width {
90            let u = (x as f32) / (width as f32);
91            let v = (y as f32) / (height as f32);
92            let r = ((u + phase) * 255.0).rem_euclid(256.0) as u8;
93            let g = ((v + phase) * 255.0).rem_euclid(256.0) as u8;
94            let b = (((u + v + phase) * 0.5) * 255.0).rem_euclid(256.0) as u8;
95            // BGRA8 packing: B, G, R, A.
96            bgra.push(b);
97            bgra.push(g);
98            bgra.push(r);
99            bgra.push(255);
100        }
101    }
102    VideoFrame {
103        width,
104        height,
105        bgra,
106        pts_seconds: f64::from(idx as u32) / f64::from(fps),
107        frame_index: idx,
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn mock_stream_yields_exact_count_then_terminates() {
117        let mut s = MockVideoStream::scrolling_gradient(8, 8, 4);
118        for expected in 0..4 {
119            let f = s.next_frame().expect("frame");
120            assert_eq!(f.frame_index, expected);
121        }
122        assert!(s.next_frame().is_none());
123    }
124
125    #[test]
126    fn frame_dimensions_match_buffer_length() {
127        let mut s = MockVideoStream::scrolling_gradient(16, 9, 1);
128        let f = s.next_frame().unwrap();
129        assert_eq!(f.width, 16);
130        assert_eq!(f.height, 9);
131        assert_eq!(f.bgra.len(), (16 * 9 * 4) as usize);
132    }
133
134    #[test]
135    fn frame_rate_drives_pts() {
136        let mut s = MockVideoStream::scrolling_gradient(4, 4, 3).with_frame_rate(60.0);
137        let f0 = s.next_frame().unwrap();
138        let f1 = s.next_frame().unwrap();
139        let f2 = s.next_frame().unwrap();
140        assert!((f0.pts_seconds - 0.0).abs() < 1e-9);
141        assert!((f1.pts_seconds - (1.0 / 60.0)).abs() < 1e-9);
142        assert!((f2.pts_seconds - (2.0 / 60.0)).abs() < 1e-9);
143    }
144
145    #[test]
146    fn frame_count_hint_matches_total() {
147        let s = MockVideoStream::scrolling_gradient(2, 2, 42);
148        assert_eq!(s.frame_count_hint(), Some(42));
149    }
150
151    #[test]
152    fn motion_is_visible_between_frames() {
153        // Adjacent frames must differ; a static stream would be a regression.
154        let mut s = MockVideoStream::scrolling_gradient(32, 32, 4);
155        let f0 = s.next_frame().unwrap();
156        let f1 = s.next_frame().unwrap();
157        assert_ne!(
158            f0.bgra, f1.bgra,
159            "scrolling gradient must move between frames"
160        );
161    }
162}