use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::time::SystemTime;
pub const BYTES_PER_PIXEL: usize = 3;
pub const MAX_FRAME_BYTES: usize = 100_000_000;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Frame {
pub id: u64,
pub timestamp: SystemTime,
pub pts_us: u64,
pub width: u32,
pub height: u32,
pub data: Vec<u8>,
}
impl Frame {
pub fn new(id: u64, width: u32, height: u32, data: Vec<u8>) -> Result<Self> {
Self::with_pts(id, width, height, data, 0)
}
pub fn with_pts(id: u64, width: u32, height: u32, data: Vec<u8>, pts_us: u64) -> Result<Self> {
let expected = rgb_len(width, height)?;
if data.len() != expected {
anyhow::bail!(
"frame buffer length mismatch: {width}x{height} needs {expected} bytes, got {}",
data.len()
);
}
Ok(Self {
id,
timestamp: SystemTime::now(),
pts_us,
width,
height,
data,
})
}
pub fn encode(&self) -> Result<Vec<u8>> {
Ok(bincode::serialize(self)?)
}
pub fn decode(data: &[u8]) -> Result<Self> {
let frame: Self = bincode::deserialize(data)?;
frame.validate()?;
Ok(frame)
}
pub fn validate(&self) -> Result<()> {
let expected = rgb_len(self.width, self.height)?;
if self.data.len() != expected {
anyhow::bail!(
"frame buffer length mismatch: {}x{} needs {expected} bytes, got {}",
self.width,
self.height,
self.data.len()
);
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PixelChange {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
pub data: Vec<u8>,
}
impl PixelChange {
pub fn validate(&self, frame_width: u32, frame_height: u32) -> Result<()> {
let end_x = self
.x
.checked_add(self.width)
.ok_or_else(|| anyhow::anyhow!("rectangle x overflow: x={}", self.x))?;
let end_y = self
.y
.checked_add(self.height)
.ok_or_else(|| anyhow::anyhow!("rectangle y overflow: y={}", self.y))?;
if end_x > frame_width || end_y > frame_height {
anyhow::bail!(
"rectangle ({},{}) {}x{} exceeds frame {frame_width}x{frame_height}",
self.x,
self.y,
self.width,
self.height
);
}
let expected = (self.width as usize)
.checked_mul(self.height as usize)
.and_then(|px| px.checked_mul(BYTES_PER_PIXEL))
.ok_or_else(|| {
anyhow::anyhow!(
"rectangle pixel count overflow: {}x{}",
self.width,
self.height
)
})?;
if self.data.len() != expected {
anyhow::bail!(
"rectangle payload length mismatch: {}x{} needs {expected} bytes, got {}",
self.width,
self.height,
self.data.len()
);
}
Ok(())
}
pub fn pixels(&self) -> u64 {
self.width as u64 * self.height as u64
}
pub fn copy_from(&self, src: &[u8], src_width: u32, dst: &mut [u8], dst_width: u32) {
let row_bytes = self.width as usize * BYTES_PER_PIXEL;
for row in 0..self.height as usize {
let from =
((self.y as usize + row) * src_width as usize + self.x as usize) * BYTES_PER_PIXEL;
let to =
((self.y as usize + row) * dst_width as usize + self.x as usize) * BYTES_PER_PIXEL;
dst[to..to + row_bytes].copy_from_slice(&src[from..from + row_bytes]);
}
}
}
pub fn rgb_len(width: u32, height: u32) -> Result<usize> {
let bytes = (width as usize)
.checked_mul(height as usize)
.and_then(|px| px.checked_mul(BYTES_PER_PIXEL))
.ok_or_else(|| {
anyhow::anyhow!("frame dimensions overflow: {width}x{height} ({} pixels)", {
(width as u64) * (height as u64)
})
})?;
if bytes > MAX_FRAME_BYTES {
anyhow::bail!(
"frame budget exceeded: max_frame_bytes={MAX_FRAME_BYTES}, requested={bytes} \
({width}x{height})"
);
}
Ok(bytes)
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct QualityConfig {
pub target_fps: u32,
pub max_fps: u32,
pub quality: f32,
}
impl Default for QualityConfig {
fn default() -> Self {
Self {
target_fps: 30,
max_fps: 60,
quality: 0.8,
}
}
}
pub trait PixelChangeDetector {
fn detect_changes(&self, previous: &Frame, current: &Frame) -> Result<Vec<PixelChange>>;
fn set_threshold(&mut self, threshold: u8);
fn set_block_size(&mut self, block_size: u32) -> Result<()>;
}
pub trait FrameCapture {
fn capture_frame(&self) -> Result<Frame>;
fn supported_configs(&self) -> Vec<QualityConfig>;
}
#[cfg(test)]
mod tests {
use super::*;
fn frame(w: u32, h: u32) -> Frame {
Frame::new(0, w, h, vec![0; rgb_len(w, h).unwrap()]).unwrap()
}
#[test]
fn rect_rejects_row_wrapping() {
let f = frame(4, 2);
let bad = PixelChange {
x: 3,
y: 0,
width: 2,
height: 1,
data: vec![255; 6],
};
let err = bad.validate(f.width, f.height).unwrap_err().to_string();
assert!(err.contains("exceeds frame"), "unhelpful error: {err}");
}
#[test]
fn rect_rejects_wrong_payload_length() {
let f = frame(4, 2);
let bad = PixelChange {
x: 0,
y: 0,
width: 2,
height: 2,
data: vec![255; 6], };
let err = bad.validate(f.width, f.height).unwrap_err().to_string();
assert!(err.contains("payload length mismatch"), "unhelpful: {err}");
}
#[test]
fn rect_rejects_coordinate_overflow() {
let f = frame(4, 2);
let bad = PixelChange {
x: u32::MAX,
y: 0,
width: 1,
height: 1,
data: vec![0; 3],
};
assert!(bad.validate(f.width, f.height).is_err());
}
#[test]
fn frame_new_rejects_mismatched_buffer() {
assert!(Frame::new(0, 4, 4, vec![0; 10]).is_err());
}
#[test]
fn frame_budget_is_reported_with_numbers() {
let err = rgb_len(20_000, 20_000).unwrap_err().to_string();
assert!(
err.contains("max_frame_bytes=100000000"),
"unhelpful: {err}"
);
assert!(err.contains("requested=1200000000"), "unhelpful: {err}");
}
#[test]
fn copy_from_blits_exact_rect() {
let src = frame(4, 2);
let mut src = src.data;
for b in src.iter_mut() {
*b = 9;
}
let mut dst = vec![0u8; 24];
let change = PixelChange {
x: 1,
y: 1,
width: 2,
height: 1,
data: vec![],
};
change.copy_from(&src, 4, &mut dst, 4);
assert!(dst[12..15].iter().all(|&b| b == 0));
assert!(dst[15..21].iter().all(|&b| b == 9));
assert!(dst[21..].iter().all(|&b| b == 0));
}
}