1use std::fmt;
9
10use prost::bytes::Bytes;
11use xxhash_rust::xxh3::xxh3_64;
12
13pub const BYTES_PER_PIXEL: usize = 3;
15
16#[derive(Clone, PartialEq, Eq)]
22pub struct Frame {
23 width: u32,
24 height: u32,
25 pixels: Bytes,
26 seq: u32,
27 timestamp_us: u64,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum FrameError {
33 Empty,
35 SizeMismatch { expected: usize, actual: usize },
37}
38
39impl fmt::Display for FrameError {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 FrameError::Empty => write!(f, "the frame has no pixels"),
43 FrameError::SizeMismatch { expected, actual } => write!(
44 f,
45 "an RGB888 frame of these dimensions has {expected} bytes, got {actual}"
46 ),
47 }
48 }
49}
50
51impl std::error::Error for FrameError {}
52
53impl Frame {
54 pub fn new(width: u32, height: u32, pixels: Bytes) -> Result<Self, FrameError> {
56 if width == 0 || height == 0 {
57 return Err(FrameError::Empty);
58 }
59 let expected = width as usize * height as usize * BYTES_PER_PIXEL;
60 if pixels.len() != expected {
61 return Err(FrameError::SizeMismatch {
62 expected,
63 actual: pixels.len(),
64 });
65 }
66 Ok(Self {
67 width,
68 height,
69 pixels,
70 seq: 0,
71 timestamp_us: 0,
72 })
73 }
74
75 pub fn with_timing(self, seq: u32, timestamp_us: u64) -> Self {
77 Self {
78 seq,
79 timestamp_us,
80 ..self
81 }
82 }
83
84 pub fn width(&self) -> u32 {
85 self.width
86 }
87
88 pub fn height(&self) -> u32 {
89 self.height
90 }
91
92 pub fn pixels(&self) -> &[u8] {
94 &self.pixels
95 }
96
97 pub fn row(&self, y: u32) -> &[u8] {
103 assert!(
104 y < self.height,
105 "row {y} of a frame {} rows high",
106 self.height
107 );
108 let stride = self.width as usize * BYTES_PER_PIXEL;
109 &self.pixels[y as usize * stride..(y as usize + 1) * stride]
110 }
111
112 pub fn seq(&self) -> u32 {
114 self.seq
115 }
116
117 pub fn timestamp_us(&self) -> u64 {
120 self.timestamp_us
121 }
122}
123
124impl fmt::Debug for Frame {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 f.debug_struct("Frame")
127 .field("width", &self.width)
128 .field("height", &self.height)
129 .field("bytes", &self.pixels.len())
130 .field("seq", &self.seq)
131 .field("timestamp_us", &self.timestamp_us)
132 .finish()
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
138pub struct FrameKey {
139 pub width: u32,
140 pub height: u32,
141 pub hash: u64,
142}
143
144#[derive(Debug, Clone, Copy, PartialEq)]
146pub struct FrameHasher {
147 status_bar_fraction: f32,
148}
149
150impl FrameHasher {
151 pub const DEFAULT_STATUS_BAR_FRACTION: f32 = 0.06;
155
156 pub fn new(status_bar_fraction: f32) -> Self {
159 let fraction = if status_bar_fraction.is_nan() {
160 0.0
161 } else {
162 status_bar_fraction.clamp(0.0, 1.0)
163 };
164 Self {
165 status_bar_fraction: fraction,
166 }
167 }
168
169 pub fn status_bar_rows(&self, height: u32) -> u32 {
171 ((height as f32 * self.status_bar_fraction).round() as u32).min(height)
172 }
173
174 pub fn key(&self, frame: &Frame) -> FrameKey {
176 let stride = frame.width as usize * BYTES_PER_PIXEL;
177 let skipped = self.status_bar_rows(frame.height) as usize * stride;
178 FrameKey {
179 width: frame.width,
180 height: frame.height,
181 hash: xxh3_64(&frame.pixels[skipped..]),
182 }
183 }
184}
185
186impl Default for FrameHasher {
187 fn default() -> Self {
188 Self::new(Self::DEFAULT_STATUS_BAR_FRACTION)
189 }
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub enum Observation {
195 First,
197 Unchanged,
199 Changed,
201}
202
203#[derive(Debug, Clone)]
206pub struct ChangeDetector {
207 hasher: FrameHasher,
208 last: Option<FrameKey>,
209}
210
211impl ChangeDetector {
212 pub fn new(hasher: FrameHasher) -> Self {
213 Self { hasher, last: None }
214 }
215
216 pub fn observe(&mut self, frame: &Frame) -> Observation {
218 let key = self.hasher.key(frame);
219 match self.last.replace(key) {
220 None => Observation::First,
221 Some(last) if last == key => Observation::Unchanged,
222 Some(_) => Observation::Changed,
223 }
224 }
225
226 pub fn reset(&mut self) {
228 self.last = None;
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 const W: u32 = 4;
237 const H: u32 = 50;
238
239 fn pixels(fill: u8) -> Vec<u8> {
240 vec![fill; W as usize * H as usize * BYTES_PER_PIXEL]
241 }
242
243 fn frame(data: Vec<u8>) -> Frame {
244 Frame::new(W, H, Bytes::from(data)).unwrap()
245 }
246
247 fn poke(data: &mut [u8], row: u32, col: u32) {
249 data[(row * W + col) as usize * BYTES_PER_PIXEL] ^= 0xff;
250 }
251
252 #[test]
253 fn a_frame_must_hold_width_times_height_rgb_pixels() {
254 assert_eq!(
255 Frame::new(0, 0, Bytes::new()).unwrap_err(),
256 FrameError::Empty
257 );
258 assert_eq!(
259 Frame::new(W, 0, Bytes::new()).unwrap_err(),
260 FrameError::Empty
261 );
262 let short = pixels(1)[1..].to_vec();
263 assert_eq!(
264 Frame::new(W, H, Bytes::from(short)).unwrap_err(),
265 FrameError::SizeMismatch {
266 expected: 600,
267 actual: 599
268 }
269 );
270 let rgba = vec![0u8; W as usize * H as usize * 4];
272 assert!(matches!(
273 Frame::new(W, H, Bytes::from(rgba)),
274 Err(FrameError::SizeMismatch { .. })
275 ));
276 let f = frame(pixels(1)).with_timing(7, 1_000);
277 assert_eq!((f.width(), f.height(), f.pixels().len()), (W, H, 600));
278 assert_eq!((f.seq(), f.timestamp_us()), (7, 1_000));
279 }
280
281 #[test]
282 fn rows_are_top_down() {
283 let mut data = pixels(0);
284 data[..W as usize * BYTES_PER_PIXEL].fill(9);
285 let f = frame(data);
286 assert!(
287 f.row(0).iter().all(|&b| b == 9),
288 "row 0 is the first buffer row"
289 );
290 assert!(f.row(H - 1).iter().all(|&b| b == 0));
291 assert_eq!(f.row(1).len(), W as usize * BYTES_PER_PIXEL);
292 }
293
294 #[test]
295 fn the_status_bar_share_becomes_whole_rows() {
296 let hasher = FrameHasher::default();
297 assert_eq!(hasher.status_bar_rows(2400), 144);
298 assert_eq!(hasher.status_bar_rows(1200), 72);
299 assert_eq!(FrameHasher::new(0.0).status_bar_rows(1200), 0);
300 assert_eq!(FrameHasher::new(2.0).status_bar_rows(1200), 1200);
301 assert_eq!(FrameHasher::new(-1.0).status_bar_rows(1200), 0);
302 assert_eq!(FrameHasher::new(f32::NAN).status_bar_rows(1200), 0);
303 }
304
305 #[test]
306 fn the_first_frame_then_identical_content_is_unchanged() {
307 let mut d = ChangeDetector::new(FrameHasher::new(0.0));
308 assert_eq!(d.observe(&frame(pixels(10))), Observation::First);
309 assert_eq!(d.observe(&frame(pixels(10))), Observation::Unchanged);
310 }
311
312 #[test]
313 fn one_changed_pixel_is_a_change_and_becomes_the_new_reference() {
314 let a = pixels(10);
315 let mut b = a.clone();
316 poke(&mut b, 30, 3);
317 let mut d = ChangeDetector::new(FrameHasher::new(0.0));
318 d.observe(&frame(a.clone()));
319 assert_eq!(d.observe(&frame(b.clone())), Observation::Changed);
320 assert_eq!(d.observe(&frame(b)), Observation::Unchanged);
321 assert_eq!(d.observe(&frame(a)), Observation::Changed);
322 }
323
324 #[test]
325 fn a_change_in_the_status_bar_is_unchanged() {
326 let base = pixels(10);
328 let mut clock = base.clone();
329 poke(&mut clock, 4, 0);
330 let mut content = base.clone();
331 poke(&mut content, 5, 0);
332 let mut d = ChangeDetector::new(FrameHasher::new(0.1));
333 d.observe(&frame(base));
334 assert_eq!(d.observe(&frame(clock)), Observation::Unchanged);
335 assert_eq!(d.observe(&frame(content)), Observation::Changed);
336 }
337
338 #[test]
339 fn the_status_bar_is_at_the_start_of_the_buffer() {
340 let base = pixels(10);
343 let mut bottom = base.clone();
344 poke(&mut bottom, H - 1, 2);
345 let mut d = ChangeDetector::new(FrameHasher::new(0.1));
346 d.observe(&frame(base));
347 assert_eq!(d.observe(&frame(bottom)), Observation::Changed);
348 }
349
350 #[test]
351 fn a_dimension_change_is_a_change() {
352 let mut d = ChangeDetector::new(FrameHasher::default());
353 d.observe(&frame(pixels(10)));
354 let rotated = Frame::new(H, W, Bytes::from(pixels(10))).unwrap();
355 assert_eq!(d.observe(&rotated), Observation::Changed);
356 }
357
358 #[test]
359 fn reset_starts_over() {
360 let mut d = ChangeDetector::new(FrameHasher::default());
361 d.observe(&frame(pixels(10)));
362 d.reset();
363 assert_eq!(d.observe(&frame(pixels(10))), Observation::First);
364 }
365
366 #[test]
367 fn keys_carry_the_dimensions() {
368 let key = FrameHasher::default().key(&frame(pixels(3)));
369 assert_eq!((key.width, key.height), (W, H));
370 assert_eq!(key, FrameHasher::default().key(&frame(pixels(3))));
371 assert_ne!(key.hash, FrameHasher::default().key(&frame(pixels(4))).hash);
372 }
373}