#![allow(unsafe_code)]
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use image::RgbaImage;
use windows_capture::capture::{
CaptureControlError, Context, GraphicsCaptureApiError, GraphicsCaptureApiHandler,
};
use windows_capture::frame::Frame as WgcFrame;
use windows_capture::graphics_capture_api::InternalCaptureControl;
use windows_capture::settings::{
ColorFormat, CursorCaptureSettings, DirtyRegionSettings, DrawBorderSettings,
MinimumUpdateIntervalSettings, SecondaryWindowSettings, Settings,
};
use windows_capture::window::Window as CaptureWindow;
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::HiDpi::GetDpiForWindow;
use crate::backend::capture as xcap_capture;
use crate::backend::WindowTarget;
use crate::error::ComputerError;
use crate::image_util;
use crate::types::{Frame, RawCapture};
pub(crate) const SOURCE_WGC: &str = "wgc";
const FRAME_TIMEOUT: Duration = Duration::from_secs(5);
const FRAME_POLL: Duration = Duration::from_millis(10);
pub(crate) fn capture_window(target: &WindowTarget) -> Result<RawCapture, ComputerError> {
if target.bounds.width == 0 || target.bounds.height == 0 {
return Err(ComputerError::InvalidArguments(
"window bounds are empty".into(),
));
}
match capture_wgc(target) {
Ok(raw) => Ok(raw),
Err(error) => fallback_crop(target, &error),
}
}
fn fallback_crop(
target: &WindowTarget,
cause: &ComputerError,
) -> Result<RawCapture, ComputerError> {
let mut raw = xcap_capture::crop_window(target.bounds)?;
raw.warnings.push(format!(
"Windows.Graphics.Capture failed ({cause}); cropped the desktop image instead, so covered \
pixels may be wrong"
));
Ok(raw)
}
fn capture_wgc(target: &WindowTarget) -> Result<RawCapture, ComputerError> {
let raw_hwnd = target.window_id as *mut std::ffi::c_void;
let dpi = unsafe { GetDpiForWindow(HWND(raw_hwnd)) };
let geo = image_util::window_placement(
target.bounds.x,
target.bounds.y,
target.bounds.width,
target.bounds.height,
dpi,
);
let window = CaptureWindow::from_raw_hwnd(raw_hwnd);
let sink: Arc<Mutex<Option<RgbaImage>>> = Arc::new(Mutex::new(None));
let settings = Settings::new(
window,
CursorCaptureSettings::WithoutCursor,
DrawBorderSettings::WithoutBorder,
SecondaryWindowSettings::Exclude,
MinimumUpdateIntervalSettings::Default,
DirtyRegionSettings::Default,
ColorFormat::Rgba8,
Arc::clone(&sink),
);
let image = grab_frame(settings, sink)?;
let mut warnings = Vec::new();
if let Some(warning) = image_util::size_mismatch_warning(image.width(), image.height(), &geo) {
warnings.push(warning);
}
Ok(RawCapture {
frame: Frame::from_image(image, geo.scale),
origin_x: geo.origin_x,
origin_y: geo.origin_y,
logical_width: geo.logical_width,
logical_height: geo.logical_height,
capture_source: SOURCE_WGC.into(),
occluded: false,
warnings,
})
}
struct SingleFrame {
slot: Arc<Mutex<Option<RgbaImage>>>,
}
impl GraphicsCaptureApiHandler for SingleFrame {
type Flags = Arc<Mutex<Option<RgbaImage>>>;
type Error = Box<dyn std::error::Error + Send + Sync>;
fn new(ctx: Context<Self::Flags>) -> Result<Self, Self::Error> {
Ok(Self { slot: ctx.flags })
}
fn on_frame_arrived(
&mut self,
frame: &mut WgcFrame,
capture_control: InternalCaptureControl,
) -> Result<(), Self::Error> {
let image = copy_frame(frame)?;
if let Ok(mut slot) = self.slot.lock() {
if slot.is_none() {
*slot = Some(image);
}
}
capture_control.stop();
Ok(())
}
fn on_closed(&mut self) -> Result<(), Self::Error> {
Ok(())
}
}
fn copy_frame(frame: &mut WgcFrame) -> Result<RgbaImage, Box<dyn std::error::Error + Send + Sync>> {
let mut buffer = frame.buffer()?;
let width = buffer.width();
let height = buffer.height();
let pixels = buffer.as_raw_buffer().to_vec();
RgbaImage::from_raw(width, height, pixels)
.ok_or_else(|| "captured frame dimensions do not match its pixel buffer".into())
}
fn grab_frame(
settings: Settings<Arc<Mutex<Option<RgbaImage>>>, CaptureWindow>,
slot: Arc<Mutex<Option<RgbaImage>>>,
) -> Result<RgbaImage, ComputerError> {
let control = SingleFrame::start_free_threaded(settings).map_err(start_error)?;
let image = wait_for_frame(&slot);
control.stop().map_err(stop_error)?;
image
}
fn wait_for_frame(slot: &Arc<Mutex<Option<RgbaImage>>>) -> Result<RgbaImage, ComputerError> {
let deadline = Instant::now() + FRAME_TIMEOUT;
loop {
if let Ok(mut guard) = slot.lock() {
if let Some(image) = guard.take() {
return Ok(image);
}
}
if Instant::now() >= deadline {
return Err(ComputerError::ExecutionFailed(format!(
"Windows.Graphics.Capture produced no frame within {}s",
FRAME_TIMEOUT.as_secs()
)));
}
std::thread::sleep(FRAME_POLL);
}
}
fn start_error(
error: GraphicsCaptureApiError<Box<dyn std::error::Error + Send + Sync>>,
) -> ComputerError {
ComputerError::ExecutionFailed(format!("Windows.Graphics.Capture: {error}"))
}
fn stop_error(
error: CaptureControlError<Box<dyn std::error::Error + Send + Sync>>,
) -> ComputerError {
ComputerError::ExecutionFailed(format!("Windows.Graphics.Capture shutdown: {error}"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::target::WindowBounds;
fn bounds(x: i32, y: i32, width: u32, height: u32) -> WindowBounds {
WindowBounds {
x,
y,
width,
height,
}
}
#[test]
fn empty_bounds_are_refused_before_any_capture() {
let target = WindowTarget {
window_id: 1,
pid: 1,
bounds: bounds(0, 0, 0, 0),
};
let Err(error) = capture_window(&target) else {
panic!("empty bounds must be refused");
};
assert!(
matches!(error, ComputerError::InvalidArguments(_)),
"{error:?}"
);
}
}