use crate::pcc::types::{Frame, FrameCapture, QualityConfig};
use device_query::{DeviceQuery, DeviceState};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CursorSample {
pub x: u32,
pub y: u32,
}
pub trait CursorSampler: Send {
fn sample(&mut self, width: u32, height: u32) -> Option<CursorSample>;
}
pub struct NoCursorSampler;
impl CursorSampler for NoCursorSampler {
fn sample(&mut self, _width: u32, _height: u32) -> Option<CursorSample> {
None
}
}
pub struct PlatformCursorSampler {
origin: (i32, i32),
}
impl PlatformCursorSampler {
pub fn new(origin: (i32, i32)) -> Self {
Self { origin }
}
}
impl CursorSampler for PlatformCursorSampler {
fn sample(&mut self, width: u32, height: u32) -> Option<CursorSample> {
let state = DeviceState::checked_new()?;
let (gx, gy) = state.get_mouse().coords;
let (x, y) = (gx - self.origin.0, gy - self.origin.1);
if x < 0 || y < 0 || x >= width as i32 || y >= height as i32 {
return None;
}
Some(CursorSample {
x: x as u32,
y: y as u32,
})
}
}
pub struct SweepSampler {
step: u32,
}
impl SweepSampler {
pub fn new() -> Self {
Self { step: 0 }
}
}
impl Default for SweepSampler {
fn default() -> Self {
Self::new()
}
}
impl CursorSampler for SweepSampler {
fn sample(&mut self, width: u32, height: u32) -> Option<CursorSample> {
if width == 0 || height == 0 {
return None;
}
let x = self.step % width;
let y = (self.step / width.max(1)) % height;
self.step = self.step.wrapping_add(37);
Some(CursorSample { x, y })
}
}
use anyhow::{Context, Result};
use screenshots::Screen;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::SystemTime;
use tracing::{debug, info};
pub struct ScreenCapture {
screen: Screen,
frame_counter: AtomicU64,
region: Option<(u32, u32, u32, u32)>,
window: Option<WindowShare>,
}
#[derive(Debug, Clone)]
struct WindowShare {
needle: String,
app: Option<String>,
last: (u32, u32),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CaptureTarget {
Display(usize),
Region {
x: u32,
y: u32,
width: u32,
height: u32,
},
Window(String),
Application(String),
}
impl CaptureTarget {
pub fn parse_region(spec: &str) -> Result<(u32, u32, u32, u32)> {
let parts: Vec<&str> = spec.split(',').collect();
let nums: Result<Vec<u32>, _> = parts.iter().map(|p| p.trim().parse()).collect();
let nums = nums.map_err(|_| {
anyhow::anyhow!("--region must be x,y,w,h in display pixels, got '{spec}'")
})?;
if nums.len() != 4 || nums[2] == 0 || nums[3] == 0 {
anyhow::bail!("--region must be x,y,w,h with nonzero size, got '{spec}'");
}
Ok((nums[0], nums[1], nums[2], nums[3]))
}
}
impl ScreenCapture {
pub fn new() -> Result<Self> {
Self::open_target(&CaptureTarget::Display(0))
}
pub fn open_display(index: usize) -> Result<Self> {
Self::open_target(&CaptureTarget::Display(index))
}
pub fn open_target(target: &CaptureTarget) -> Result<Self> {
if let CaptureTarget::Window(id) = target {
let needle = id.to_ascii_lowercase();
let (w, h) =
find_window(&needle, None).context(format!("no visible window matches '{id}'"))?;
let screen = Screen::all()
.context("Failed to enumerate screens")?
.into_iter()
.next()
.context("No screens found")?;
info!("Sharing window matching '{id}' ({w}x{h})");
return Ok(Self {
screen,
frame_counter: AtomicU64::new(0),
region: None,
window: Some(WindowShare {
needle,
app: None,
last: (w, h),
}),
});
}
if let CaptureTarget::Application(id) = target {
let needle = id.to_ascii_lowercase();
let (w, h) = find_window(&needle, Some(&needle))
.context(format!("no visible window matches application '{id}'"))?;
let screen = Screen::all()
.context("Failed to enumerate screens")?
.into_iter()
.next()
.context("No screens found")?;
info!("Sharing application matching '{id}' ({w}x{h})");
return Ok(Self {
screen,
frame_counter: AtomicU64::new(0),
region: None,
window: Some(WindowShare {
needle: needle.clone(),
app: Some(needle),
last: (w, h),
}),
});
}
let screens = Screen::all().context("Failed to enumerate screens")?;
let (screen, region) = match target {
CaptureTarget::Display(i) => (
screens.into_iter().nth(*i).context(format!(
"display {i} not found ({} display(s) available)",
Screen::all().map(|s| s.len()).unwrap_or(0)
))?,
None,
),
CaptureTarget::Region {
x,
y,
width,
height,
} => {
let screen = screens.into_iter().next().context("No screens found")?;
let (dw, dh) = (screen.display_info.width, screen.display_info.height);
let x = (*x).min(dw.saturating_sub(1));
let y = (*y).min(dh.saturating_sub(1));
let width = (*width).min(dw.saturating_sub(x)).max(1);
let height = (*height).min(dh.saturating_sub(y)).max(1);
let probe = screen
.capture_area(x as i32, y as i32, width, height)
.context("verifying the platform honors region capture")?;
let (width, height) = (probe.width(), probe.height());
if width == 0 || height == 0 {
anyhow::bail!(
"platform returned an empty image for region {x},{y}: region capture unsupported here"
);
}
(screen, Some((x, y, width, height)))
}
CaptureTarget::Window(id) | CaptureTarget::Application(id) => {
unreachable!("window/application resolved above, id={id}")
}
};
info!(
"Screen capture initialized: {}x{} (scale: {}){}",
screen.display_info.width,
screen.display_info.height,
screen.display_info.scale_factor,
region
.map(|(x, y, w, h)| format!(" region {x},{y} {w}x{h}"))
.unwrap_or_default(),
);
Ok(Self {
screen,
frame_counter: AtomicU64::new(0),
region,
window: None,
})
}
pub fn origin(&self) -> (i32, i32) {
if self.window.is_some() {
return (0, 0);
}
match self.region {
Some((x, y, _, _)) => (x as i32, y as i32),
None => (self.screen.display_info.x, self.screen.display_info.y),
}
}
pub fn list_displays() -> Vec<(usize, u32, u32)> {
Screen::all()
.unwrap_or_default()
.iter()
.enumerate()
.map(|(i, s)| (i, s.display_info.width, s.display_info.height))
.collect()
}
pub fn width(&self) -> u32 {
if let Some(w) = &self.window {
return w.last.0;
}
self.region
.map(|(_, _, w, _)| w)
.unwrap_or(self.screen.display_info.width)
}
pub fn height(&self) -> u32 {
if let Some(w) = &self.window {
return w.last.1;
}
self.region
.map(|(_, _, _, h)| h)
.unwrap_or(self.screen.display_info.height)
}
}
fn find_window(needle: &str, app: Option<&str>) -> Result<(u32, u32)> {
let windows = xcap::Window::all().context("enumerating windows")?;
for w in &windows {
let title = w.title().unwrap_or_default().to_ascii_lowercase();
if !title.contains(needle) {
continue;
}
if let Some(a) = app {
let name = w.app_name().unwrap_or_default().to_ascii_lowercase();
if !name.contains(a) && !title.contains(a) {
continue;
}
}
if w.is_minimized().unwrap_or(false) {
continue;
}
let (width, height) = (w.width().unwrap_or(0), w.height().unwrap_or(0));
if width == 0 || height == 0 {
continue;
}
return Ok((width, height));
}
anyhow::bail!("no visible window matches")
}
pub fn list_windows() -> Vec<(String, String, u32, u32)> {
let Ok(windows) = xcap::Window::all() else {
return Vec::new();
};
let mut out: Vec<(String, String, u32, u32)> = windows
.iter()
.filter(|w| !w.is_minimized().unwrap_or(true))
.filter_map(|w| {
let (width, height) = (w.width().unwrap_or(0), w.height().unwrap_or(0));
if width == 0 || height == 0 {
return None;
}
Some((
w.title().unwrap_or_default(),
w.app_name().unwrap_or_default(),
width,
height,
))
})
.collect();
out.sort_by_key(|(_, _, w, h)| w * h);
out
}
fn capture_window_frame(counter: &AtomicU64, share: &WindowShare) -> Result<Frame> {
let (width, height, rgba_data): (u32, u32, Vec<u8>) = match find_live_window(share)
.and_then(|w| w.capture_image().context("capturing window pixels"))
{
Ok(image) => (image.width(), image.height(), image.into_raw()),
Err(_) => {
let (w, h) = share.last;
let mut raw = Vec::with_capacity((w * h * 4) as usize);
for _ in 0..w * h {
raw.extend_from_slice(&[128, 128, 128, 255]);
}
(w, h, raw)
}
};
let mut rgb_data = Vec::with_capacity((width * height * 3) as usize);
for pixel in rgba_data.as_chunks::<4>().0 {
rgb_data.push(pixel[0]);
rgb_data.push(pixel[1]);
rgb_data.push(pixel[2]);
}
let id = counter.fetch_add(1, Ordering::Relaxed);
Ok(Frame {
id,
timestamp: SystemTime::now(),
pts_us: 0,
width,
height,
data: rgb_data,
})
}
fn find_live_window(share: &WindowShare) -> Result<xcap::Window> {
let windows = xcap::Window::all().context("enumerating windows")?;
for w in windows {
let title = w.title().unwrap_or_default().to_ascii_lowercase();
if !title.contains(&share.needle) {
continue;
}
if let Some(a) = &share.app {
let name = w.app_name().unwrap_or_default().to_ascii_lowercase();
if !name.contains(a) && !title.contains(a) {
continue;
}
}
if w.is_minimized().unwrap_or(false) {
continue;
}
if w.width().unwrap_or(0) == 0 || w.height().unwrap_or(0) == 0 {
continue;
}
return Ok(w);
}
anyhow::bail!("shared window is not visible right now")
}
impl FrameCapture for ScreenCapture {
fn capture_frame(&self) -> Result<Frame> {
if let Some(share) = &self.window {
return capture_window_frame(&self.frame_counter, share);
}
let image = match self.region {
Some((x, y, w, h)) => self
.screen
.capture_area(x as i32, y as i32, w, h)
.context("Failed to capture screen region")?,
None => self.screen.capture().context("Failed to capture screen")?,
};
let width = image.width();
let height = image.height();
let rgba_data = image.into_raw();
let mut rgb_data = Vec::with_capacity((width * height * 3) as usize);
for pixel in rgba_data.as_chunks::<4>().0 {
rgb_data.push(pixel[0]); rgb_data.push(pixel[1]); rgb_data.push(pixel[2]); }
let id = self.frame_counter.fetch_add(1, Ordering::Relaxed);
debug!("Captured frame {}: {}x{}", id, width, height);
Ok(Frame {
id,
timestamp: SystemTime::now(),
pts_us: 0,
width,
height,
data: rgb_data,
})
}
fn supported_configs(&self) -> Vec<QualityConfig> {
vec![
QualityConfig {
target_fps: 30,
max_fps: 60,
quality: 0.8,
},
QualityConfig {
target_fps: 60,
max_fps: 60,
quality: 1.0,
},
]
}
}
pub struct SyntheticCapture {
width: u32,
height: u32,
frame_counter: AtomicU64,
}
impl SyntheticCapture {
pub fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
frame_counter: AtomicU64::new(0),
}
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
}
impl FrameCapture for SyntheticCapture {
fn capture_frame(&self) -> Result<Frame> {
let id = self.frame_counter.fetch_add(1, Ordering::Relaxed);
let (width, height) = (self.width, self.height);
let mut data = vec![30u8; (width * height * 3) as usize];
for y in 0..height {
let shade = (y * 40 / height.max(1)) as u8;
for x in 0..width {
let idx = ((y * width + x) * 3) as usize;
data[idx] = 20 + shade;
data[idx + 1] = 20 + shade / 2;
data[idx + 2] = 40;
}
}
let box_size: u32 = (width.min(height) / 8).max(10);
let period_x = (width - box_size).max(1);
let period_y = (height - box_size).max(1);
let t = id % (2 * period_x.max(period_y) as u64).max(1);
let bx = triangle_wave(t, period_x as u64) as u32;
let by = triangle_wave(t, period_y as u64) as u32;
for dy in 0..box_size.min(height) {
for dx in 0..box_size.min(width) {
let x = (bx + dx).min(width - 1);
let y = (by + dy).min(height - 1);
let idx = ((y * width + x) * 3) as usize;
data[idx] = 220;
data[idx + 1] = 60;
data[idx + 2] = 60;
}
}
debug!("Generated synthetic frame {}: {}x{}", id, width, height);
Ok(Frame {
id,
timestamp: SystemTime::now(),
pts_us: 0,
width,
height,
data,
})
}
fn supported_configs(&self) -> Vec<QualityConfig> {
vec![QualityConfig::default()]
}
}
fn triangle_wave(t: u64, period: u64) -> u64 {
let period = period.max(1);
let cycle = 2 * period;
let phase = t % cycle;
if phase <= period {
phase
} else {
cycle - phase
}
}
pub enum CaptureSource {
Screen(ScreenCapture),
Synthetic(SyntheticCapture),
}
impl CaptureSource {
pub fn open(force_synthetic: bool) -> Result<Self> {
Self::open_target(force_synthetic, &CaptureTarget::Display(0))
}
pub fn open_target(force_synthetic: bool, target: &CaptureTarget) -> Result<Self> {
if force_synthetic {
info!("Using synthetic test-pattern capture (requested)");
return Ok(Self::Synthetic(SyntheticCapture::new(1280, 720)));
}
match ScreenCapture::open_target(target) {
Ok(capture) => Ok(Self::Screen(capture)),
Err(e) => {
if matches!(
target,
CaptureTarget::Window(_) | CaptureTarget::Application(_)
) {
return Err(e);
}
info!("No real display available ({e}); falling back to synthetic test pattern");
Ok(Self::Synthetic(SyntheticCapture::new(1280, 720)))
}
}
}
pub fn width(&self) -> u32 {
match self {
Self::Screen(c) => c.width(),
Self::Synthetic(c) => c.width(),
}
}
pub fn origin(&self) -> (i32, i32) {
match self {
Self::Screen(c) => c.origin(),
Self::Synthetic(_) => (0, 0),
}
}
pub fn height(&self) -> u32 {
match self {
Self::Screen(c) => c.height(),
Self::Synthetic(c) => c.height(),
}
}
pub fn capture_frame(&self) -> Result<Frame> {
match self {
Self::Screen(c) => c.capture_frame(),
Self::Synthetic(c) => c.capture_frame(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_screen_capture_creation() {
let _capture = ScreenCapture::new();
}
#[test]
fn test_synthetic_capture_produces_correctly_sized_frames() {
let capture = SyntheticCapture::new(64, 48);
let frame = capture.capture_frame().unwrap();
assert_eq!(frame.data.len(), 64 * 48 * 3);
}
#[test]
fn test_synthetic_capture_changes_between_frames() {
let capture = SyntheticCapture::new(64, 48);
let frame1 = capture.capture_frame().unwrap();
let frame2 = capture.capture_frame().unwrap();
assert_ne!(frame1.data, frame2.data, "the bouncing box should move");
}
#[test]
fn window_share_fails_fast_on_no_match() {
let err = ScreenCapture::open_target(&CaptureTarget::Window("no such window xyz".into()))
.err()
.map(|e| e.to_string())
.expect("bogus title must fail");
assert!(err.contains("no visible window"), "unhelpful: {err}");
let err = ScreenCapture::open_target(&CaptureTarget::Application("no such app xyz".into()))
.err()
.map(|e| e.to_string())
.expect("bogus app must fail");
assert!(err.contains("no visible window"), "unhelpful: {err}");
}
#[test]
fn window_list_and_live_capture_agree() {
let wins = list_windows();
let Some((title, _, w, h)) = wins.first() else {
return;
};
assert!(*w > 0 && *h > 0);
let needle = title.chars().take(4).collect::<String>();
if needle.trim().is_empty() {
return;
}
let cap = ScreenCapture::open_target(&CaptureTarget::Window(needle));
if let Ok(cap) = cap {
let frame = cap.capture_frame().unwrap();
assert_eq!(
frame.data.len(),
frame.width as usize * frame.height as usize * 3
);
assert!(frame.width > 0 && frame.height > 0);
}
}
#[test]
fn platform_sampler_never_returns_out_of_bounds() {
let mut s = PlatformCursorSampler::new((0, 0));
for _ in 0..50 {
if let Some(c) = s.sample(1512, 982) {
assert!(c.x < 1512 && c.y < 982);
}
}
let mut far = PlatformCursorSampler::new((1_000_000, 1_000_000));
assert!(far.sample(64, 48).is_none());
}
#[test]
fn sweep_sampler_walks_inside_the_frame() {
let mut s = SweepSampler::new();
for _ in 0..200 {
let c = s.sample(64, 48).expect("nonempty frame has a cursor");
assert!(c.x < 64 && c.y < 48);
}
assert!(SweepSampler::new().sample(0, 48).is_none());
let mut n = NoCursorSampler;
assert!(n.sample(64, 48).is_none());
}
#[test]
fn region_spec_parses_and_rejects_garbage() {
assert_eq!(
CaptureTarget::parse_region("100,200,1280,720").unwrap(),
(100, 200, 1280, 720)
);
assert_eq!(
CaptureTarget::parse_region(" 0, 0, 64, 48 ").unwrap(),
(0, 0, 64, 48)
);
for bad in ["", "1,2,3", "1,2,3,4,5", "a,b,c,d", "0,0,0,10", "0,0,10,0"] {
assert!(
CaptureTarget::parse_region(bad).is_err(),
"{bad:?} must not parse"
);
}
}
#[test]
fn synthetic_mode_ignores_the_target() {
for target in [
CaptureTarget::Display(3),
CaptureTarget::Region {
x: 10,
y: 10,
width: 64,
height: 48,
},
CaptureTarget::Window("x".into()),
] {
let source = CaptureSource::open_target(true, &target).unwrap();
assert_eq!((source.width(), source.height()), (1280, 720));
}
}
#[test]
fn region_clamps_to_the_display() {
let Ok(screens) = screenshots::Screen::all() else {
return;
};
let Some(first) = screens.first() else {
return;
};
let (dw, dh) = (first.display_info.width, first.display_info.height);
let cap = ScreenCapture::open_target(&CaptureTarget::Region {
x: dw + 1000,
y: dh + 1000,
width: 10_000,
height: 10_000,
})
.unwrap();
assert!(cap.width() >= 1 && cap.width() <= dw.max(2));
assert!(cap.height() >= 1 && cap.height() <= dh.max(2));
let frame = cap.capture_frame().unwrap();
assert_eq!((frame.width, frame.height), (cap.width(), cap.height()));
assert_eq!(
frame.data.len(),
frame.width as usize * frame.height as usize * 3
);
}
#[test]
fn test_capture_source_falls_back_when_forced() {
let source = CaptureSource::open(true).unwrap();
assert_eq!(source.width(), 1280);
assert_eq!(source.height(), 720);
}
}