Skip to content

Commit

Permalink
feat: window添加refresh API (#149)
Browse files Browse the repository at this point in the history
* feat: window添加refresh方法

* fix: 修复macos window内存泄漏

* chore: 更新版本号
  • Loading branch information
nashaofu authored Aug 25, 2024
1 parent daff24c commit 784eec1
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 27 deletions.
23 changes: 13 additions & 10 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -19,26 +24,24 @@
// ]

// 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"
]
}
}

// 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"
}
14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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",
Expand All @@ -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"
70 changes: 70 additions & 0 deletions examples/window_record.rs
Original file line number Diff line number Diff line change
@@ -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("Chrome") {
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
}
19 changes: 19 additions & 0 deletions src/linux/impl_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RgbaImage> {
capture_window(self)
}
Expand Down
31 changes: 31 additions & 0 deletions src/macos/boxed.rs
Original file line number Diff line number Diff line change
@@ -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 }
}
}
85 changes: 75 additions & 10 deletions src/macos/impl_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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,
Expand Down Expand Up @@ -160,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,
Expand All @@ -179,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;
Expand Down Expand Up @@ -250,9 +248,76 @@ impl ImplWindow {
}

impl ImplWindow {
pub fn refresh(&mut self) -> XCapResult<()> {
unsafe {
let impl_monitors = ImplMonitor::all()?;

let box_cf_array_ref = BoxCFArrayRef::new(CGWindowListCopyWindowInfo(
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
kCGNullWindowID,
));

if box_cf_array_ref.is_null() {
return Err(XCapError::new("Run CGWindowListCopyWindowInfo error"));
}

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(());
}
}

Err(XCapError::new("Not Found window"))
}
}
pub fn capture_image(&self) -> XCapResult<RgbaImage> {
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,
)
Expand Down
1 change: 1 addition & 0 deletions src/macos/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod boxed;
mod capture;

pub mod impl_monitor;
Expand Down
3 changes: 3 additions & 0 deletions src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ impl Window {
}

impl Window {
pub fn refresh(&mut self) -> XCapResult<()> {
self.impl_window.refresh()
}
pub fn capture_image(&self) -> XCapResult<RgbaImage> {
self.impl_window.capture_image()
}
Expand Down
20 changes: 20 additions & 0 deletions src/windows/impl_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RgbaImage> {
// TODO: 在win10之后,不同窗口有不同的dpi,所以可能存在截图不全或者截图有较大空白,实际窗口没有填充满图片
capture_window(
Expand Down

0 comments on commit 784eec1

Please sign in to comment.