use crate::screenshot::ImageFormat;
use crate::{Error, ScreenInfo, Screenshot};
#[cfg(any(target_os = "linux", target_os = "windows"))]
use brightness::blocking::{Brightness, brightness_devices};
use std::io::Cursor;
#[cfg(target_os = "linux")]
use wayland_sys as _;
#[expect(
clippy::needless_pass_by_value,
reason = "must own its argument to be passed directly to `Result::map_err`."
)]
fn map_xcap_error(error: xcap::XCapError) -> Error {
Error::Platform(error.to_string())
}
fn monitors() -> Result<Vec<xcap::Monitor>, Error> {
xcap::Monitor::all().map_err(map_xcap_error)
}
fn monitor_by_id(id: u32) -> Result<xcap::Monitor, Error> {
for monitor in monitors()? {
if monitor.id().map_err(map_xcap_error)? == id {
return Ok(monitor);
}
}
Err(Error::MonitorNotFound)
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
fn map_brightness_error(error: &brightness::Error) -> Error {
Error::Platform(format!("brightness backend error: {error}"))
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
fn first_brightness_device() -> Result<brightness::blocking::BrightnessDevice, Error> {
let mut first_error = None;
for device in brightness_devices() {
match device {
Ok(device) => return Ok(device),
Err(error) if first_error.is_none() => first_error = Some(error),
Err(_) => {}
}
}
first_error.map_or(Err(Error::MonitorNotFound), |error| {
Err(map_brightness_error(&error))
})
}
pub fn screens() -> Result<Vec<ScreenInfo>, Error> {
let monitors = monitors()?;
let mut infos = Vec::with_capacity(monitors.len());
for monitor in monitors {
infos.push(ScreenInfo::new(
monitor.id().map_err(map_xcap_error)?,
monitor.name().map_err(map_xcap_error)?,
monitor.width().map_err(map_xcap_error)?,
monitor.height().map_err(map_xcap_error)?,
monitor.scale_factor().map_err(map_xcap_error)?,
monitor.is_primary().map_err(map_xcap_error)?,
));
}
Ok(infos)
}
pub fn max_refresh_rate_hz() -> Result<f32, Error> {
let mut max_refresh_rate = None;
for monitor in monitors()? {
let refresh_rate = monitor.frequency().map_err(map_xcap_error)?;
if refresh_rate.is_finite() && refresh_rate > 0.0 {
max_refresh_rate = Some(
max_refresh_rate.map_or(refresh_rate, |current: f32| current.max(refresh_rate)),
);
}
}
max_refresh_rate.ok_or(Error::MonitorNotFound)
}
#[allow(clippy::unused_async)]
#[allow(clippy::cast_precision_loss)]
pub async fn get_brightness() -> Result<f32, Error> {
#[cfg(target_os = "macos")]
{
super::apple::get_macos_brightness()
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
{
blocking::unblock(|| {
let device = first_brightness_device()?;
let percentage = device.get().map_err(|error| map_brightness_error(&error))?;
Ok(((percentage as f32) / 100.0).clamp(0.0, 1.0))
})
.await
}
}
#[allow(clippy::unused_async)]
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub async fn set_brightness(val: f32) -> Result<(), Error> {
#[cfg(target_os = "macos")]
{
super::apple::set_macos_brightness(val)
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
{
blocking::unblock(move || {
let device = first_brightness_device()?;
let percentage = (val.clamp(0.0, 1.0) * 100.0).round() as u32;
device
.set(percentage.min(100))
.map_err(|error| map_brightness_error(&error))
})
.await
}
}
pub fn screenshot(display: &ScreenInfo, format: ImageFormat) -> Result<Screenshot, Error> {
#[cfg(not(target_os = "macos"))]
if matches!(format, ImageFormat::Heif | ImageFormat::Avif) {
return Err(Error::Unsupported);
}
#[cfg(target_os = "macos")]
if matches!(format, ImageFormat::Heif | ImageFormat::Avif) {
return super::apple::screenshot(display, format);
}
let monitor = monitor_by_id(display.id())?;
let image = monitor.capture_image().map_err(map_xcap_error)?;
let width = image.width();
let height = image.height();
let mut buffer = Vec::new();
let mut cursor = Cursor::new(&mut buffer);
image
.write_to(&mut cursor, xcap::image::ImageFormat::Png)
.map_err(|e| Error::Encoding(e.to_string()))?;
Ok(Screenshot::new(buffer, width, height, ImageFormat::Png))
}
#[cfg(not(target_os = "macos"))]
use crate::frame::ScreenFrame;
#[cfg(not(target_os = "macos"))]
use crate::stream::StreamConfig;
#[cfg(not(target_os = "macos"))]
use std::sync::Arc;
#[cfg(not(target_os = "macos"))]
use wgpu::{Device, Extent3d, Queue, TextureDimension, TextureFormat, TextureUsages};
#[cfg(not(target_os = "macos"))]
pub struct ScreenStreamInner {
monitor: xcap::Monitor,
device: Arc<Device>,
queue: Arc<Queue>,
width: u32,
height: u32,
}
#[cfg(not(target_os = "macos"))]
impl ScreenStreamInner {
pub fn new(
display: &ScreenInfo,
device: Arc<Device>,
queue: Arc<Queue>,
_config: &StreamConfig,
) -> Result<Self, Error> {
let monitor = monitor_by_id(display.id())?;
let width = display.width();
let height = display.height();
Ok(Self {
monitor,
device,
queue,
width,
height,
})
}
#[allow(clippy::unused_async)]
#[allow(
clippy::future_not_send,
reason = "the Windows capture session is a thread-affine `*mut c_void`, so `ScreenStream` is deliberately not `Sync` and these futures cannot be `Send`."
)]
pub async fn next_frame(&self) -> Option<ScreenFrame> {
self.try_next_frame()
}
pub fn try_next_frame(&self) -> Option<ScreenFrame> {
let image = self.monitor.capture_image().ok()?;
let width = image.width();
let height = image.height();
let rgba = image.into_raw();
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("ScreenCapture"),
size: Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8UnormSrgb,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
view_formats: &[],
});
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&rgba,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(width * 4),
rows_per_image: Some(height),
},
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
let timestamp_ns = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX));
Some(ScreenFrame::from_texture(
Arc::new(texture),
width,
height,
TextureFormat::Rgba8UnormSrgb,
timestamp_ns,
))
}
pub const fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
}