Skip to content

Commit

Permalink
feat: 支持获取友好的显示器名称 (#188)
Browse files Browse the repository at this point in the history
* feat: 优化windows 代码

* feat: windows 获取友好的显示器名称

* feat: macos切换底层依赖到objc2

* chore: 修改版本号

* chore: 格式化

---------

Co-authored-by: nashaofu <[email protected]>
  • Loading branch information
nashaofu and nashaofu authored Feb 4, 2025
1 parent d236975 commit a6f508a
Show file tree
Hide file tree
Showing 14 changed files with 531 additions and 546 deletions.
19 changes: 10 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "xcap"
version = "0.2.2"
version = "0.3.0"
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 (WIP)."
license = "Apache-2.0"
Expand All @@ -18,22 +18,23 @@ image = ["image/default"]
[dependencies]
image = { version = "0.25", default-features = false, features = ["png"] }
log = "0.4"
scopeguard = "1.2"
thiserror = "2.0"

[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.10"
core-graphics = "0.24"
objc2-app-kit = { version = "0.2.2", features = [
"libc",
"NSWorkspace",
"NSRunningApplication",
] }
objc2 = "0.6"
objc2-app-kit = "0.3"
objc2-core-foundation = "0.3"
objc2-core-graphics = "0.3"
objc2-foundation = "0.3"

[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.58", features = [
widestring = "1.1"
windows = { version = "0.59", features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_Graphics_Dwm",
"Win32_Devices_Display",
"Win32_System_LibraryLoader",
"Win32_UI_WindowsAndMessaging",
"Win32_Storage_Xps",
Expand Down
18 changes: 9 additions & 9 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ pub enum XCapError {
StdTimeSystemTimeError(#[from] std::time::SystemTimeError),

#[cfg(target_os = "macos")]
#[error("CoreGraphicsDisplayCGError {0}")]
CoreGraphicsDisplayCGError(core_graphics::display::CGError),
#[error("Objc2CoreGraphicsCGError {:?}", 0)]
Objc2CoreGraphicsCGError(objc2_core_graphics::CGError),

#[cfg(target_os = "windows")]
#[error(transparent)]
WindowsCoreError(#[from] windows::core::Error),
#[cfg(target_os = "windows")]
#[error(transparent)]
StdStringFromUtf16Error(#[from] std::string::FromUtf16Error),
Utf16Error(#[from] widestring::error::Utf16Error),
}

impl XCapError {
Expand All @@ -49,12 +49,12 @@ impl XCapError {
}
}

#[cfg(target_os = "macos")]
impl From<core_graphics::display::CGError> for XCapError {
fn from(value: core_graphics::display::CGError) -> Self {
XCapError::CoreGraphicsDisplayCGError(value)
}
}
// #[cfg(target_os = "macos")]
// impl From<core_graphics::display::CGError> for XCapError {
// fn from(value: core_graphics::display::CGError) -> Self {
// XCapError::CoreGraphicsDisplayCGError(value)
// }
// }

pub type XCapResult<T> = Result<T, XCapError>;

Expand Down
31 changes: 0 additions & 31 deletions src/macos/boxed.rs

This file was deleted.

56 changes: 33 additions & 23 deletions src/macos/capture.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use core_graphics::{
display::{kCGWindowImageDefault, CGWindowID, CGWindowListOption},
geometry::CGRect,
window::create_image,
};
use image::RgbaImage;
use objc2_core_foundation::CGRect;
use objc2_core_graphics::{
CGDataProviderCopyData, CGImageGetBytesPerRow, CGImageGetDataProvider, CGImageGetHeight,
CGImageGetWidth, CGWindowID, CGWindowImageOption, CGWindowListCreateImage, CGWindowListOption,
};

use crate::error::{XCapError, XCapResult};

Expand All @@ -12,26 +12,36 @@ pub fn capture(
list_option: CGWindowListOption,
window_id: CGWindowID,
) -> XCapResult<RgbaImage> {
let cg_image = create_image(cg_rect, list_option, window_id, kCGWindowImageDefault)
.ok_or_else(|| XCapError::new(format!("Capture failed {} {:?}", window_id, cg_rect)))?;
unsafe {
let cg_image = CGWindowListCreateImage(
cg_rect,
list_option,
window_id,
CGWindowImageOption::Default,
);

let width = cg_image.width();
let height = cg_image.height();
let bytes = Vec::from(cg_image.data().bytes());
let width = CGImageGetWidth(cg_image.as_deref());
let height = CGImageGetHeight(cg_image.as_deref());
let data_provider = CGImageGetDataProvider(cg_image.as_deref());
let data = CGDataProviderCopyData(data_provider.as_deref())
.ok_or_else(|| XCapError::new("Failed to copy data"))?
.to_vec();
let bytes_per_row = CGImageGetBytesPerRow(cg_image.as_deref());

// Some platforms e.g. MacOS can have extra bytes at the end of each row.
// See
// https://github.com/nashaofu/xcap/issues/29
// https://github.com/nashaofu/xcap/issues/38
let mut buffer = Vec::with_capacity(width * height * 4);
for row in bytes.chunks_exact(cg_image.bytes_per_row()) {
buffer.extend_from_slice(&row[..width * 4]);
}
// Some platforms e.g. MacOS can have extra bytes at the end of each row.
// See
// https://github.com/nashaofu/xcap/issues/29
// https://github.com/nashaofu/xcap/issues/38
let mut buffer = Vec::with_capacity(width * height * 4);
for row in data.chunks_exact(bytes_per_row) {
buffer.extend_from_slice(&row[..width * 4]);
}

for bgra in buffer.chunks_exact_mut(4) {
bgra.swap(0, 2);
}
for bgra in buffer.chunks_exact_mut(4) {
bgra.swap(0, 2);
}

RgbaImage::from_raw(width as u32, height as u32, buffer)
.ok_or_else(|| XCapError::new("RgbaImage::from_raw failed"))
RgbaImage::from_raw(width as u32, height as u32, buffer)
.ok_or_else(|| XCapError::new("RgbaImage::from_raw failed"))
}
}
163 changes: 96 additions & 67 deletions src/macos/impl_monitor.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
use core_graphics::display::{
kCGNullWindowID, kCGWindowListOptionAll, CGDirectDisplayID, CGDisplay, CGDisplayMode, CGError,
CGPoint,
};
use image::RgbaImage;
use objc2::MainThreadMarker;
use objc2_app_kit::NSScreen;
use objc2_core_foundation::{CGPoint, CGRect};
use objc2_core_graphics::{
CGDirectDisplayID, CGDisplayBounds, CGDisplayCopyDisplayMode, CGDisplayIsActive,
CGDisplayIsMain, CGDisplayModeGetPixelWidth, CGDisplayModeGetRefreshRate, CGDisplayRotation,
CGError, CGGetActiveDisplayList, CGGetDisplaysWithPoint, CGWindowListOption,
};
use objc2_foundation::{NSNumber, NSString};

use crate::error::{XCapError, XCapResult};

use super::{capture::capture, impl_video_recorder::ImplVideoRecorder};

#[derive(Debug, Clone)]
pub(crate) struct ImplMonitor {
pub cg_display: CGDisplay,
pub cg_direct_display_id: CGDirectDisplayID,
pub id: u32,
pub name: String,
pub x: i32,
Expand All @@ -23,53 +28,90 @@ pub(crate) struct ImplMonitor {
pub is_primary: bool,
}

#[link(name = "CoreGraphics", kind = "framework")]
extern "C" {
fn CGGetDisplaysWithPoint(
point: CGPoint,
max_displays: u32,
displays: *mut CGDirectDisplayID,
display_count: *mut u32,
) -> CGError;
fn get_display_friendly_name(display_id: CGDirectDisplayID) -> XCapResult<String> {
let screens = NSScreen::screens(unsafe { MainThreadMarker::new_unchecked() });
for screen in screens {
let device_description = screen.deviceDescription();
let screen_number = device_description
.objectForKey(&NSString::from_str("NSScreenNumber"))
.ok_or(XCapError::new("Get NSScreenNumber failed"))?;

let screen_id = screen_number
.downcast::<NSNumber>()
.map_err(|err| XCapError::new(format!("{:?}", err)))?
.unsignedIntValue();

if screen_id == display_id {
unsafe { return Ok(screen.localizedName().to_string()) };
}
}

Err(XCapError::new(format!(
"Get display {} friendly name failed",
display_id
)))
}

impl ImplMonitor {
pub(super) fn new(id: CGDirectDisplayID) -> XCapResult<ImplMonitor> {
let cg_display = CGDisplay::new(id);
let screen_num = cg_display.model_number();
let cg_rect = cg_display.bounds();
let cg_display_mode = get_cg_display_mode(cg_display)?;
let pixel_width = cg_display_mode.pixel_width();
let scale_factor = pixel_width as f32 / cg_rect.size.width as f32;

Ok(ImplMonitor {
cg_display,
id: cg_display.id,
name: format!("Monitor #{screen_num}"),
x: cg_rect.origin.x as i32,
y: cg_rect.origin.y as i32,
width: cg_rect.size.width as u32,
height: cg_rect.size.height as u32,
rotation: cg_display.rotation() as f32,
scale_factor,
frequency: cg_display_mode.refresh_rate() as f32,
is_primary: cg_display.is_main(),
})
unsafe {
let CGRect { origin, size } = CGDisplayBounds(id);

let rotation = CGDisplayRotation(id) as f32;

let display_mode = CGDisplayCopyDisplayMode(id);
let pixel_width = CGDisplayModeGetPixelWidth(display_mode.as_deref());
let scale_factor = pixel_width as f32 / size.width as f32;
let frequency = CGDisplayModeGetRefreshRate(display_mode.as_deref()) as f32;
let is_primary = CGDisplayIsMain(id);

Ok(ImplMonitor {
cg_direct_display_id: id,
id,
name: get_display_friendly_name(id).unwrap_or(format!("Unknown Monitor {}", id)),
x: origin.x as i32,
y: origin.y as i32,
width: size.width as u32,
height: size.height as u32,
rotation,
scale_factor,
frequency,
is_primary,
})
}
}
pub fn all() -> XCapResult<Vec<ImplMonitor>> {
// active vs online https://developer.apple.com/documentation/coregraphics/1454964-cggetonlinedisplaylist?language=objc
let display_ids = CGDisplay::active_displays()?;
let max_displays: u32 = 16;
let mut active_displays: Vec<CGDirectDisplayID> = vec![0; max_displays as usize];
let mut display_count: u32 = 0;

let mut impl_monitors: Vec<ImplMonitor> = Vec::with_capacity(display_ids.len());
let cg_error = unsafe {
CGGetActiveDisplayList(
max_displays,
active_displays.as_mut_ptr(),
&mut display_count,
)
};

for display_id in display_ids {
if cg_error != CGError::Success {
return Err(XCapError::new(format!(
"CGGetActiveDisplayList failed: {:?}",
cg_error
)));
}

active_displays.truncate(display_count as usize);

let mut impl_monitors = Vec::with_capacity(active_displays.len());

for display in active_displays {
// 运行过程中,如果遇到显示器插拔,可能会导致调用报错
// 对于报错的情况,就把报错的情况给排除掉
// https://github.com/nashaofu/xcap/issues/118
if let Ok(impl_monitor) = ImplMonitor::new(display_id) {
if let Ok(impl_monitor) = ImplMonitor::new(display) {
impl_monitors.push(impl_monitor);
} else {
log::error!("ImplMonitor::new({}) failed", display_id);
log::error!("ImplMonitor::new({}) failed", display);
}
}

Expand All @@ -81,6 +123,7 @@ impl ImplMonitor {
x: x as f64,
y: y as f64,
};

let max_displays: u32 = 16;
let mut display_ids: Vec<CGDirectDisplayID> = vec![0; max_displays as usize];
let mut display_count: u32 = 0;
Expand All @@ -94,43 +137,29 @@ impl ImplMonitor {
)
};

if cg_error != 0 {
return Err(XCapError::CoreGraphicsDisplayCGError(cg_error));
if cg_error != CGError::Success {
return Err(XCapError::new(format!(
"CGGetDisplaysWithPoint failed: {:?}",
cg_error
)));
}

if display_count == 0 {
return Err(XCapError::new("Get displays from point failed"));
}

let display_id = display_ids
.first()
.ok_or(XCapError::new("Monitor not found"))?;

let impl_monitor = ImplMonitor::new(*display_id)?;

if !impl_monitor.cg_display.is_active() {
Err(XCapError::new("Monitor is not active"))
if let Some(&display_id) = display_ids.first() {
if unsafe { !CGDisplayIsActive(display_id) } {
return Err(XCapError::new("Monitor is not active"));
}
ImplMonitor::new(display_id)
} else {
Ok(impl_monitor)
Err(XCapError::new("Monitor not found"))
}
}
}

fn get_cg_display_mode(cg_display: CGDisplay) -> XCapResult<CGDisplayMode> {
let cg_display_mode = cg_display
.display_mode()
.ok_or_else(|| XCapError::new("Get display mode failed"))?;

Ok(cg_display_mode)
}

impl ImplMonitor {
pub fn capture_image(&self) -> XCapResult<RgbaImage> {
capture(
self.cg_display.bounds(),
kCGWindowListOptionAll,
kCGNullWindowID,
)
let cg_rect = unsafe { CGDisplayBounds(self.cg_direct_display_id) };

capture(cg_rect, CGWindowListOption::OptionAll, 0)
}

pub fn video_recorder(&self) -> XCapResult<ImplVideoRecorder> {
Expand Down
Loading

0 comments on commit a6f508a

Please sign in to comment.