From c035498652329bc9e260f7cff03eed9f8840eadf Mon Sep 17 00:00:00 2001 From: nashaofu Date: Wed, 21 Aug 2024 10:07:55 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20window=E6=B7=BB=E5=8A=A0refresh?= =?UTF-8?q?=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .devcontainer/devcontainer.json | 23 ++++++----- examples/window_record.rs | 70 ++++++++++++++++++++++++++++++++ src/linux/impl_window.rs | 19 +++++++++ src/macos/impl_window.rs | 71 +++++++++++++++++++++++++++++++++ src/window.rs | 3 ++ src/windows/impl_window.rs | 20 ++++++++++ 6 files changed, 196 insertions(+), 10 deletions(-) create mode 100644 examples/window_record.rs diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8d66cb5..a9149d4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,9 +4,14 @@ "name": "Rust", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile "image": "mcr.microsoft.com/devcontainers/rust:1-1-bullseye", - // Features to add to the dev container. More info: https://containers.dev/features. "features": { - "ghcr.io/devcontainers/features/desktop-lite:1": {} + "ghcr.io/devcontainers/features/desktop-lite:1": { + "version": "latest", + "noVncVersion": "1.2.0", + "password": "noPassword", + "webPort": "6080", + "vncPort": "5901" + } }, // Use 'mounts' to make the cargo cache persistent in a Docker Volume. @@ -19,13 +24,17 @@ // ] // Use 'forwardPorts' to make a list of ports inside the container available locally. - "forwardPorts": [6080], + "forwardPorts": [6080, 5901], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "rustc --version", + + // Configure tool-specific properties. "customizations": { "vscode": { "extensions": [ "wengerk.highlight-bad-chars", "streetsidesoftware.code-spell-checker", - "serayuzgur.crates", "EditorConfig.EditorConfig", "tamasfe.even-better-toml", "rust-lang.rust-analyzer" @@ -33,12 +42,6 @@ } } - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "rustc --version", - - // Configure tool-specific properties. - // "customizations": {}, - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "root" } diff --git a/examples/window_record.rs b/examples/window_record.rs new file mode 100644 index 0000000..6274759 --- /dev/null +++ b/examples/window_record.rs @@ -0,0 +1,70 @@ +use fs_extra::dir; +use std::{ + thread, + time::{Duration, Instant}, +}; +use xcap::Window; + +fn main() { + let windows = Window::all().unwrap(); + + dir::create_all("target/windows", true).unwrap(); + + let mut i = 0; + for window in &windows { + // 最小化的窗口不能截屏 + if window.is_minimized() { + continue; + } + + if window.title().contains("起始页") { + break; + } + + println!( + "Window: {:?} {:?} {:?}", + window.title(), + (window.x(), window.y(), window.width(), window.height()), + (window.is_minimized(), window.is_maximized()) + ); + + i += 1; + } + + let mut win = windows.get(i).unwrap().clone(); + println!("{:?}", win); + + let mut i = 0; + let frame = 20; + let start = Instant::now(); + let fps = 1000 / frame; + + loop { + i += 1; + let time = Instant::now(); + win.refresh().unwrap(); + let image = win.capture_image().unwrap(); + image + .save(format!("target/windows/window-{}.png", i,)) + .unwrap(); + let sleep_time = fps * i - start.elapsed().as_millis() as i128; + println!( + "sleep_time: {:?} current_step_time: {:?}", + sleep_time, + time.elapsed() + ); + if sleep_time > 0 { + thread::sleep(Duration::from_millis(sleep_time as u64)); + } + + if i >= 900 { + break; + } + } + + println!("time {:?}", start.elapsed()); + let actual_fps = 900 / start.elapsed().as_secs(); + println!("actual fps: {}", actual_fps); + + // ffmpeg -framerate {actual_fps} -i window-%d.png -c:v libx264 -pix_fmt yuv420p output.mp4 +} diff --git a/src/linux/impl_window.rs b/src/linux/impl_window.rs index 4bd72e7..346a1da 100644 --- a/src/linux/impl_window.rs +++ b/src/linux/impl_window.rs @@ -235,6 +235,25 @@ impl ImplWindow { } impl ImplWindow { + pub fn refresh(&mut self) -> XCapResult<()> { + let (conn, _) = Connection::connect(None)?; + let impl_monitors = ImplMonitor::all()?; + let impl_window = ImplWindow::new(&conn, &self.window, &impl_monitors)?; + + self.window = impl_window.window; + self.id = impl_window.id; + self.title = impl_window.title; + self.app_name = impl_window.app_name; + self.current_monitor = impl_window.current_monitor; + self.x = impl_window.x; + self.y = impl_window.y; + self.width = impl_window.width; + self.height = impl_window.height; + self.is_minimized = impl_window.is_minimized; + self.is_maximized = impl_window.is_maximized; + + Ok(()) + } pub fn capture_image(&self) -> XCapResult { capture_window(self) } diff --git a/src/macos/impl_window.rs b/src/macos/impl_window.rs index cb4f8d1..2bf2356 100644 --- a/src/macos/impl_window.rs +++ b/src/macos/impl_window.rs @@ -120,6 +120,39 @@ fn get_window_cg_rect(window_cf_dictionary_ref: CFDictionaryRef) -> XCapResult XCapResult { + unsafe { + let cg_window_list_copy_window_info = CGWindowListCopyWindowInfo( + kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, + kCGNullWindowID, + ); + + if cg_window_list_copy_window_info.is_null() { + return Err(XCapError::new("Run CGWindowListCopyWindowInfo error")); + } + + let num_windows = CFArrayGetCount(cg_window_list_copy_window_info); + + for i in 0..num_windows { + let window_cf_dictionary_ref = + CFArrayGetValueAtIndex(cg_window_list_copy_window_info, i) as CFDictionaryRef; + + if window_cf_dictionary_ref.is_null() { + continue; + } + + let k_cg_window_number = + get_cf_number_u32_value(window_cf_dictionary_ref, "kCGWindowNumber")?; + + if k_cg_window_number == window_id { + return Ok(window_cf_dictionary_ref); + } + } + + Err(XCapError::new("Not Found window")) + } +} + impl ImplWindow { pub fn new( window_cf_dictionary_ref: CFDictionaryRef, @@ -250,6 +283,44 @@ impl ImplWindow { } impl ImplWindow { + pub fn refresh(&mut self) -> XCapResult<()> { + let impl_monitors = ImplMonitor::all()?; + + let window_cf_dictionary_ref: *const core_foundation::dictionary::__CFDictionary = + get_window_cf_dictionary_ref(self.id)?; + + let window_name = match get_cf_string_value(window_cf_dictionary_ref, "kCGWindowName") { + Ok(window_name) => window_name, + _ => return Err(XCapError::new("Get window name failed")), + }; + + let window_owner_name = + match get_cf_string_value(window_cf_dictionary_ref, "kCGWindowOwnerName") { + Ok(window_owner_name) => window_owner_name, + _ => return Err(XCapError::new("Get window owner name failed")), + }; + + let impl_window = ImplWindow::new( + window_cf_dictionary_ref, + &impl_monitors, + window_name, + window_owner_name, + )?; + + self.window_cf_dictionary_ref = impl_window.window_cf_dictionary_ref; + self.id = impl_window.id; + self.title = impl_window.title; + self.app_name = impl_window.app_name; + self.current_monitor = impl_window.current_monitor; + self.x = impl_window.x; + self.y = impl_window.y; + self.width = impl_window.width; + self.height = impl_window.height; + self.is_minimized = impl_window.is_minimized; + self.is_maximized = impl_window.is_maximized; + + Ok(()) + } pub fn capture_image(&self) -> XCapResult { capture( get_window_cg_rect(self.window_cf_dictionary_ref)?, diff --git a/src/window.rs b/src/window.rs index a41bff4..a72e5e2 100644 --- a/src/window.rs +++ b/src/window.rs @@ -74,6 +74,9 @@ impl Window { } impl Window { + pub fn refresh(&mut self) -> XCapResult<()> { + self.impl_window.refresh() + } pub fn capture_image(&self) -> XCapResult { self.impl_window.capture_image() } diff --git a/src/windows/impl_window.rs b/src/windows/impl_window.rs index 83eab94..116cad4 100644 --- a/src/windows/impl_window.rs +++ b/src/windows/impl_window.rs @@ -361,6 +361,26 @@ impl ImplWindow { } impl ImplWindow { + pub fn refresh(&mut self) -> XCapResult<()> { + let impl_window = ImplWindow::new(self.hwnd)?; + + self.hwnd = impl_window.hwnd; + self.window_info = impl_window.window_info; + self.id = impl_window.id; + self.title = impl_window.title; + self.app_name = impl_window.app_name; + self.process_id = impl_window.process_id; + self.current_monitor = impl_window.current_monitor; + self.x = impl_window.x; + self.y = impl_window.y; + self.width = impl_window.width; + self.height = impl_window.height; + self.is_minimized = impl_window.is_minimized; + self.is_maximized = impl_window.is_maximized; + + Ok(()) + } + pub fn capture_image(&self) -> XCapResult { // TODO: 在win10之后,不同窗口有不同的dpi,所以可能存在截图不全或者截图有较大空白,实际窗口没有填充满图片 capture_window( From 878b7ce03129e67e268f16bae10e97c79219cfcf Mon Sep 17 00:00:00 2001 From: nashaofu Date: Sun, 25 Aug 2024 18:41:22 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dmacos=20window?= =?UTF-8?q?=E5=86=85=E5=AD=98=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/macos/boxed.rs | 31 +++++++++ src/macos/impl_window.rs | 144 +++++++++++++++++++-------------------- src/macos/mod.rs | 1 + 3 files changed, 101 insertions(+), 75 deletions(-) create mode 100644 src/macos/boxed.rs diff --git a/src/macos/boxed.rs b/src/macos/boxed.rs new file mode 100644 index 0000000..aae49e0 --- /dev/null +++ b/src/macos/boxed.rs @@ -0,0 +1,31 @@ +use core_foundation::{ + array::CFArrayRef, + base::{CFRelease, ToVoid}, +}; +use std::ops::Deref; + +#[derive(Debug)] +pub(super) struct BoxCFArrayRef { + cf_array_ref: CFArrayRef, +} + +impl Deref for BoxCFArrayRef { + type Target = CFArrayRef; + fn deref(&self) -> &Self::Target { + &self.cf_array_ref + } +} + +impl Drop for BoxCFArrayRef { + fn drop(&mut self) { + unsafe { + CFRelease(self.cf_array_ref.to_void()); + } + } +} + +impl BoxCFArrayRef { + pub fn new(cf_array_ref: CFArrayRef) -> Self { + BoxCFArrayRef { cf_array_ref } + } +} diff --git a/src/macos/impl_window.rs b/src/macos/impl_window.rs index 2bf2356..8365f80 100644 --- a/src/macos/impl_window.rs +++ b/src/macos/impl_window.rs @@ -8,7 +8,7 @@ use core_foundation::{ use core_graphics::{ display::{ kCGWindowListExcludeDesktopElements, kCGWindowListOptionIncludingWindow, - kCGWindowListOptionOnScreenOnly, CGDisplay, CGPoint, CGWindowListCopyWindowInfo, + kCGWindowListOptionOnScreenOnly, CGDisplay, CGPoint, CGSize, CGWindowListCopyWindowInfo, }, geometry::CGRect, window::{kCGNullWindowID, kCGWindowSharingNone}, @@ -18,11 +18,10 @@ use std::ffi::c_void; use crate::{error::XCapResult, XCapError}; -use super::{capture::capture, impl_monitor::ImplMonitor}; +use super::{boxed::BoxCFArrayRef, capture::capture, impl_monitor::ImplMonitor}; #[derive(Debug, Clone)] pub(crate) struct ImplWindow { - pub window_cf_dictionary_ref: CFDictionaryRef, pub id: u32, pub title: String, pub app_name: String, @@ -120,39 +119,6 @@ fn get_window_cg_rect(window_cf_dictionary_ref: CFDictionaryRef) -> XCapResult XCapResult { - unsafe { - let cg_window_list_copy_window_info = CGWindowListCopyWindowInfo( - kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, - kCGNullWindowID, - ); - - if cg_window_list_copy_window_info.is_null() { - return Err(XCapError::new("Run CGWindowListCopyWindowInfo error")); - } - - let num_windows = CFArrayGetCount(cg_window_list_copy_window_info); - - for i in 0..num_windows { - let window_cf_dictionary_ref = - CFArrayGetValueAtIndex(cg_window_list_copy_window_info, i) as CFDictionaryRef; - - if window_cf_dictionary_ref.is_null() { - continue; - } - - let k_cg_window_number = - get_cf_number_u32_value(window_cf_dictionary_ref, "kCGWindowNumber")?; - - if k_cg_window_number == window_id { - return Ok(window_cf_dictionary_ref); - } - } - - Err(XCapError::new("Not Found window")) - } -} - impl ImplWindow { pub fn new( window_cf_dictionary_ref: CFDictionaryRef, @@ -193,7 +159,6 @@ impl ImplWindow { !get_cf_bool_value(window_cf_dictionary_ref, "kCGWindowIsOnscreen")? && !is_maximized; Ok(ImplWindow { - window_cf_dictionary_ref, id, title: window_name, app_name: window_owner_name, @@ -212,20 +177,20 @@ impl ImplWindow { let impl_monitors = ImplMonitor::all()?; let mut impl_windows = Vec::new(); - let cg_window_list_copy_window_info = CGWindowListCopyWindowInfo( + let box_cf_array_ref = BoxCFArrayRef::new(CGWindowListCopyWindowInfo( kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID, - ); + )); - if cg_window_list_copy_window_info.is_null() { + if box_cf_array_ref.is_null() { return Ok(impl_windows); } - let num_windows = CFArrayGetCount(cg_window_list_copy_window_info); + let num_windows = CFArrayGetCount(*box_cf_array_ref); for i in 0..num_windows { let window_cf_dictionary_ref = - CFArrayGetValueAtIndex(cg_window_list_copy_window_info, i) as CFDictionaryRef; + CFArrayGetValueAtIndex(*box_cf_array_ref, i) as CFDictionaryRef; if window_cf_dictionary_ref.is_null() { continue; @@ -284,46 +249,75 @@ impl ImplWindow { impl ImplWindow { pub fn refresh(&mut self) -> XCapResult<()> { - let impl_monitors = ImplMonitor::all()?; + unsafe { + let impl_monitors = ImplMonitor::all()?; - let window_cf_dictionary_ref: *const core_foundation::dictionary::__CFDictionary = - get_window_cf_dictionary_ref(self.id)?; + let box_cf_array_ref = BoxCFArrayRef::new(CGWindowListCopyWindowInfo( + kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, + kCGNullWindowID, + )); - let window_name = match get_cf_string_value(window_cf_dictionary_ref, "kCGWindowName") { - Ok(window_name) => window_name, - _ => return Err(XCapError::new("Get window name failed")), - }; + if box_cf_array_ref.is_null() { + return Err(XCapError::new("Run CGWindowListCopyWindowInfo error")); + } - let window_owner_name = - match get_cf_string_value(window_cf_dictionary_ref, "kCGWindowOwnerName") { - Ok(window_owner_name) => window_owner_name, - _ => return Err(XCapError::new("Get window owner name failed")), - }; + let num_windows = CFArrayGetCount(*box_cf_array_ref); + + for i in 0..num_windows { + let window_cf_dictionary_ref = + CFArrayGetValueAtIndex(*box_cf_array_ref, i) as CFDictionaryRef; + + if window_cf_dictionary_ref.is_null() { + continue; + } + + let k_cg_window_number = + get_cf_number_u32_value(window_cf_dictionary_ref, "kCGWindowNumber")?; + + if k_cg_window_number == self.id { + let window_name = + match get_cf_string_value(window_cf_dictionary_ref, "kCGWindowName") { + Ok(window_name) => window_name, + _ => return Err(XCapError::new("Get window name failed")), + }; + + let window_owner_name = + match get_cf_string_value(window_cf_dictionary_ref, "kCGWindowOwnerName") { + Ok(window_owner_name) => window_owner_name, + _ => return Err(XCapError::new("Get window owner name failed")), + }; + + let impl_window = ImplWindow::new( + window_cf_dictionary_ref, + &impl_monitors, + window_name, + window_owner_name, + )?; + + self.id = impl_window.id; + self.title = impl_window.title; + self.app_name = impl_window.app_name; + self.current_monitor = impl_window.current_monitor; + self.x = impl_window.x; + self.y = impl_window.y; + self.width = impl_window.width; + self.height = impl_window.height; + self.is_minimized = impl_window.is_minimized; + self.is_maximized = impl_window.is_maximized; + + return Ok(()); + } + } - let impl_window = ImplWindow::new( - window_cf_dictionary_ref, - &impl_monitors, - window_name, - window_owner_name, - )?; - - self.window_cf_dictionary_ref = impl_window.window_cf_dictionary_ref; - self.id = impl_window.id; - self.title = impl_window.title; - self.app_name = impl_window.app_name; - self.current_monitor = impl_window.current_monitor; - self.x = impl_window.x; - self.y = impl_window.y; - self.width = impl_window.width; - self.height = impl_window.height; - self.is_minimized = impl_window.is_minimized; - self.is_maximized = impl_window.is_maximized; - - Ok(()) + Err(XCapError::new("Not Found window")) + } } pub fn capture_image(&self) -> XCapResult { capture( - get_window_cg_rect(self.window_cf_dictionary_ref)?, + CGRect::new( + &CGPoint::new(self.x as f64, self.y as f64), + &CGSize::new(self.width as f64, self.height as f64), + ), kCGWindowListOptionIncludingWindow, self.id, ) diff --git a/src/macos/mod.rs b/src/macos/mod.rs index 42f354e..262de57 100644 --- a/src/macos/mod.rs +++ b/src/macos/mod.rs @@ -1,3 +1,4 @@ +mod boxed; mod capture; pub mod impl_monitor; From 3a0bbc51d1d0df26162b59dd8cb723f2b378cf90 Mon Sep 17 00:00:00 2001 From: nashaofu Date: Sun, 25 Aug 2024 18:57:09 +0800 Subject: [PATCH 3/3] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 14 +++++++------- examples/window_record.rs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 02712bf..b32a718 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xcap" -version = "0.0.12" +version = "0.0.13" edition = "2021" description = "XCap is a cross-platform screen capture library written in Rust. It supports Linux (X11, Wayland), MacOS, and Windows. XCap supports screenshot and video recording (to be implemented)." license = "Apache-2.0" @@ -14,15 +14,15 @@ keywords = ["screen", "monitor", "window", "capture", "image"] [dependencies] image = "0.25" log = "0.4" -sysinfo = "0.30.5" +sysinfo = "0.31" thiserror = "1.0" [target.'cfg(target_os = "macos")'.dependencies] -core-foundation = "0.9" -core-graphics = "0.23" +core-foundation = "0.10" +core-graphics = "0.24" [target.'cfg(target_os = "windows")'.dependencies] -windows = { version = "0.54", features = [ +windows = { version = "0.58", features = [ "Win32_Foundation", "Win32_Graphics_Gdi", "Win32_Graphics_Dwm", @@ -35,8 +35,8 @@ windows = { version = "0.54", features = [ [target.'cfg(target_os="linux")'.dependencies] percent-encoding = "2.3" -xcb = { version = "1.3", features = ["randr"] } +xcb = { version = "1.4", features = ["randr"] } dbus = { version = "0.9", features = ["vendored"] } [dev-dependencies] -fs_extra = "1.3.0" +fs_extra = "1.3" diff --git a/examples/window_record.rs b/examples/window_record.rs index 6274759..9326fe5 100644 --- a/examples/window_record.rs +++ b/examples/window_record.rs @@ -17,7 +17,7 @@ fn main() { continue; } - if window.title().contains("起始页") { + if window.title().contains("Chrome") { break; }