1use crate::{VideoFrame, VideoStream};
12
13pub 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 #[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 #[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 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 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}