feat: Replace screen capture with ScreenCaptureKit and fix performance issues #6

Merged
Ivan merged 20 commits from replace-rust-swift-screencapture-with-screencapturekit into develop 2025-07-04 22:03:41 +08:00
22 changed files with 2725 additions and 2151 deletions
Showing only changes of commit b1fd751090 - Show all commits

2538
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

498
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@ edition = "2021"
tauri-build = { version = "1.2", features = [] } tauri-build = { version = "1.2", features = [] }
[dependencies] [dependencies]
tauri = { version = "1.2", features = ["shell-open"] } tauri = { version = "1.2", features = [ "protocol-all", "shell-open"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
core-graphics = "0.22.3" core-graphics = "0.22.3"
@ -28,8 +28,8 @@ url-build-parse = "9.0.0"
color_space = "0.5.3" color_space = "0.5.3"
hex = "0.4.3" hex = "0.4.3"
toml = "0.7.3" toml = "0.7.3"
paho-mqtt = "0.12.1" # paho-mqtt = "0.12.1" # Temporarily disabled due to CMake issues
time = {version="0.3.20", features= ["formatting"] } time = {version="0.3.35", features= ["formatting"] }
itertools = "0.10.5" itertools = "0.10.5"
core-foundation = "0.9.3" core-foundation = "0.9.3"
tokio-stream = "0.1.14" tokio-stream = "0.1.14"
@ -37,7 +37,7 @@ mdns-sd = "0.7.2"
futures = "0.3.28" futures = "0.3.28"
ddc-hi = "0.4.1" ddc-hi = "0.4.1"
coreaudio-rs = "0.11.2" coreaudio-rs = "0.11.2"
rust_swift_screencapture = { version = "0.1.1", path = "../../../../demo/rust-swift-screencapture" } screen-capture-kit = "0.3.1"
[features] [features]
# this feature is used for production builds or when `devPath` points to the filesystem # this feature is used for production builds or when `devPath` points to the filesystem

View File

@ -88,21 +88,21 @@ impl LedColorsPublisher {
} }
} }
// match display_colors_tx.send(( match display_colors_tx.send((
// display_id, display_id,
// colors_copy colors_copy
// .into_iter() .into_iter()
// .map(|color| color.get_rgb()) .map(|color| color.get_rgb())
// .flatten() .flatten()
// .collect::<Vec<_>>(), .collect::<Vec<_>>(),
// )) { )) {
// Ok(_) => { Ok(_) => {
// // log::info!("sent colors: {:?}", color_len); // log::info!("sent colors: {:?}", color_len);
// } }
// Err(err) => { Err(err) => {
// warn!("Failed to send display_colors: {}", err); warn!("Failed to send display_colors: {}", err);
// } }
// }; };
// Check if the inner task version changed // Check if the inner task version changed
let version = internal_tasks_version.read().await.clone(); let version = internal_tasks_version.read().await.clone();
@ -127,7 +127,7 @@ impl LedColorsPublisher {
) { ) {
let sorted_colors_tx = self.sorted_colors_tx.clone(); let sorted_colors_tx = self.sorted_colors_tx.clone();
let colors_tx = self.colors_tx.clone(); let colors_tx = self.colors_tx.clone();
log::debug!("start all_colors_worker");
tokio::spawn(async move { tokio::spawn(async move {
for _ in 0..10 { for _ in 0..10 {
@ -137,7 +137,7 @@ impl LedColorsPublisher {
let mut all_colors: Vec<Option<Vec<u8>>> = vec![None; display_ids.len()]; let mut all_colors: Vec<Option<Vec<u8>>> = vec![None; display_ids.len()];
let mut start: tokio::time::Instant = tokio::time::Instant::now(); let mut start: tokio::time::Instant = tokio::time::Instant::now();
log::debug!("start all_colors_worker task");
loop { loop {
let color_info = display_colors_rx.recv().await; let color_info = display_colors_rx.recv().await;
@ -186,7 +186,7 @@ impl LedColorsPublisher {
warn!("Failed to send sorted colors: {}", err); warn!("Failed to send sorted colors: {}", err);
} }
}; };
log::debug!("tick: {}ms", start.elapsed().as_millis());
start = tokio::time::Instant::now(); start = tokio::time::Instant::now();
} }
} }
@ -195,7 +195,7 @@ impl LedColorsPublisher {
} }
pub async fn start(&self) { pub async fn start(&self) {
log::info!("start colors worker");
let config_manager = ConfigManager::global().await; let config_manager = ConfigManager::global().await;
let mut config_receiver = config_manager.clone_config_update_receiver(); let mut config_receiver = config_manager.clone_config_update_receiver();
@ -203,9 +203,7 @@ impl LedColorsPublisher {
self.handle_config_change(configs).await; self.handle_config_change(configs).await;
log::info!("waiting for config update...");
while config_receiver.changed().await.is_ok() { while config_receiver.changed().await.is_ok() {
log::info!("config updated, restart inner tasks...");
let configs = config_receiver.borrow().clone(); let configs = config_receiver.borrow().clone();
self.handle_config_change(configs).await; self.handle_config_change(configs).await;
} }
@ -300,16 +298,28 @@ impl LedColorsPublisher {
if group.end > group.start { if group.end > group.start {
for i in group.pos - display_led_offset..group_size + group.pos - display_led_offset for i in group.pos - display_led_offset..group_size + group.pos - display_led_offset
{ {
let bytes = colors[i].as_bytes(); if i < colors.len() {
buffer.append(&mut bytes.to_vec()); let bytes = colors[i].as_bytes();
buffer.append(&mut bytes.to_vec());
} else {
log::warn!("Index {} out of bounds for colors array of length {}", i, colors.len());
// Add black color as fallback
buffer.append(&mut vec![0, 0, 0]);
}
} }
} else { } else {
for i in (group.pos - display_led_offset for i in (group.pos - display_led_offset
..group_size + group.pos - display_led_offset) ..group_size + group.pos - display_led_offset)
.rev() .rev()
{ {
let bytes = colors[i].as_bytes(); if i < colors.len() {
buffer.append(&mut bytes.to_vec()); let bytes = colors[i].as_bytes();
buffer.append(&mut bytes.to_vec());
} else {
log::warn!("Index {} out of bounds for colors array of length {}", i, colors.len());
// Add black color as fallback
buffer.append(&mut vec![0, 0, 0]);
}
} }
} }
@ -350,7 +360,7 @@ impl LedColorsPublisher {
let mut screenshots = HashMap::new(); let mut screenshots = HashMap::new();
loop { loop {
log::info!("waiting merged screenshot...");
let screenshot = merged_screenshot_receiver.recv().await; let screenshot = merged_screenshot_receiver.recv().await;
if let Err(err) = screenshot { if let Err(err) = screenshot {
@ -382,7 +392,7 @@ impl LedColorsPublisher {
.filter(|(_, c)| c.display_id == display_id); .filter(|(_, c)| c.display_id == display_id);
let screenshot = screenshots.get(&display_id).unwrap(); let screenshot = screenshots.get(&display_id).unwrap();
log::debug!("screenshot updated: {:?}", display_id);
let points: Vec<_> = led_strip_configs let points: Vec<_> = led_strip_configs
.clone() .clone()
@ -412,7 +422,7 @@ impl LedColorsPublisher {
led_start = led_end; led_start = led_end;
} }
log::debug!("got all colors configs: {:?}", colors_configs.len());
return Ok(AllColorConfig { return Ok(AllColorConfig {
sample_point_groups: colors_configs, sample_point_groups: colors_configs,

View File

@ -269,7 +269,7 @@ async fn main() {
let url = url.unwrap(); let url = url.unwrap();
let re = regex::Regex::new(r"^/displays/(\d+)$").unwrap(); let re = regex::Regex::new(r"^/(\d+)$").unwrap();
let path = url.path; let path = url.path;
let captures = re.captures(path.as_str()); let captures = re.captures(path.as_str());
@ -287,6 +287,7 @@ async fn main() {
let bytes = tokio::task::block_in_place(move || { let bytes = tokio::task::block_in_place(move || {
tauri::async_runtime::block_on(async move { tauri::async_runtime::block_on(async move {
let screenshot_manager = ScreenshotManager::global().await; let screenshot_manager = ScreenshotManager::global().await;
let rx: Result<tokio::sync::watch::Receiver<Screenshot>, anyhow::Error> = let rx: Result<tokio::sync::watch::Receiver<Screenshot>, anyhow::Error> =
screenshot_manager.subscribe_by_display_id(display_id).await; screenshot_manager.subscribe_by_display_id(display_id).await;
@ -305,7 +306,7 @@ async fn main() {
anyhow::bail!("Display#{}: no screenshot.", display_id); anyhow::bail!("Display#{}: no screenshot.", display_id);
} }
log::debug!("Display#{}: screenshot size: {}", display_id, bytes.len());
let (scale_factor_x, scale_factor_y, width, height) = if url.query.is_some() let (scale_factor_x, scale_factor_y, width, height) = if url.query.is_some()
&& url.query.as_ref().unwrap().contains_key("height") && url.query.as_ref().unwrap().contains_key("height")
@ -368,6 +369,7 @@ async fn main() {
} }
} }
Ok(rgba_buffer.clone()) Ok(rgba_buffer.clone())
}) })
}); });

View File

@ -6,9 +6,11 @@ use core_graphics::display::{
}; };
use core_graphics::geometry::{CGPoint, CGRect, CGSize}; use core_graphics::geometry::{CGPoint, CGRect, CGSize};
use paris::{info, warn}; use paris::{info, warn};
use rust_swift_screencapture::display::CGDisplayId; use screen_capture_kit::shareable_content::{SCDisplay, SCShareableContent};
use screen_capture_kit::stream::{SCStream, SCStreamConfiguration, SCContentFilter, SCStreamOutput};
use screen_capture_kit::stream::SCStreamDelegate;
use tauri::async_runtime::RwLock; use tauri::async_runtime::RwLock;
use tokio::sync::{broadcast, watch, Mutex, OnceCell}; use tokio::sync::{broadcast, watch, OnceCell};
use tokio::task::yield_now; use tokio::task::yield_now;
use tokio::time::sleep; use tokio::time::sleep;
@ -20,7 +22,7 @@ pub fn get_display_colors(
sample_points: &Vec<Vec<LedSamplePoints>>, sample_points: &Vec<Vec<LedSamplePoints>>,
bound_scale_factor: f32, bound_scale_factor: f32,
) -> anyhow::Result<Vec<LedColor>> { ) -> anyhow::Result<Vec<LedColor>> {
log::debug!("take_screenshot");
let cg_display = CGDisplay::new(display_id); let cg_display = CGDisplay::new(display_id);
let mut colors = vec![]; let mut colors = vec![];
@ -112,7 +114,7 @@ impl ScreenshotManager {
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
warn!("start_one failed: display_id: {}, err: {}", display.id, err); warn!("start_one failed: display_id: {}, err: {}", display.id, err);
}); });
info!("start_one finished: display_id: {}", display.id);
}); });
futures::future::join_all(futures).await; futures::future::join_all(futures).await;
@ -120,6 +122,8 @@ impl ScreenshotManager {
} }
async fn start_one(&self, display_id: u32, scale_factor: f32) -> anyhow::Result<()> { async fn start_one(&self, display_id: u32, scale_factor: f32) -> anyhow::Result<()> {
let merged_screenshot_tx = self.merged_screenshot_tx.clone(); let merged_screenshot_tx = self.merged_screenshot_tx.clone();
let (tx, _) = watch::channel(Screenshot::new( let (tx, _) = watch::channel(Screenshot::new(
@ -138,45 +142,98 @@ impl ScreenshotManager {
drop(channels); drop(channels);
// Implement screen capture using screen-capture-kit
loop { loop {
let display = rust_swift_screencapture::display::Display::new(display_id); match Self::capture_display_screenshot(display_id, scale_factor).await {
let mut frame_rx = display.subscribe_frame().await; Ok(screenshot) => {
let tx_for_send = tx.read().await;
let merged_screenshot_tx = merged_screenshot_tx.write().await;
display.start_capture(30).await; if let Err(err) = merged_screenshot_tx.send(screenshot.clone()) {
// log::warn!("merged_screenshot_tx.send failed: {}", err);
let tx_for_send = tx.read().await; }
if let Err(err) = tx_for_send.send(screenshot.clone()) {
while frame_rx.changed().await.is_ok() { log::warn!("display {} screenshot_tx.send failed: {}", display_id, err);
let frame = frame_rx.borrow().clone(); }
let screenshot = Screenshot::new(
display_id,
frame.height as u32,
frame.width as u32,
frame.bytes_per_row as usize,
frame.bytes,
scale_factor,
scale_factor,
);
let merged_screenshot_tx = merged_screenshot_tx.write().await;
if let Err(err) = merged_screenshot_tx.send(screenshot.clone()) {
// log::warn!("merged_screenshot_tx.send failed: {}", err);
}
if let Err(err) = tx_for_send.send(screenshot.clone()) {
log::warn!("display {} screenshot_tx.send failed: {}", display_id, err);
} else {
log::debug!("screenshot: {:?}", screenshot);
} }
Err(err) => {
warn!("Failed to capture screenshot for display {}: {}", display_id, err);
// Create a fallback empty screenshot to maintain the interface
let screenshot = Screenshot::new(
display_id,
1080,
1920,
1920 * 4, // Assuming RGBA format
Arc::new(vec![0u8; 1920 * 1080 * 4]),
scale_factor,
scale_factor,
);
yield_now().await; let tx_for_send = tx.read().await;
let merged_screenshot_tx = merged_screenshot_tx.write().await;
if let Err(err) = merged_screenshot_tx.send(screenshot.clone()) {
// log::warn!("merged_screenshot_tx.send failed: {}", err);
}
if let Err(err) = tx_for_send.send(screenshot.clone()) {
log::warn!("display {} screenshot_tx.send failed: {}", display_id, err);
}
}
} }
sleep(Duration::from_secs(5)).await;
info!( // Sleep for a frame duration (30 FPS)
"display {} frame_rx.changed() failed, try to restart", sleep(Duration::from_millis(33)).await;
display_id yield_now().await;
);
} }
} }
async fn capture_display_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Screenshot> {
// For now, use the existing CGDisplay approach as a fallback
// TODO: Implement proper screen-capture-kit integration
let cg_display = CGDisplay::new(display_id);
let bounds = cg_display.bounds();
let cg_image = CGDisplay::screenshot(
bounds,
kCGWindowListOptionOnScreenOnly,
kCGNullWindowID,
kCGWindowImageDefault,
)
.ok_or_else(|| anyhow::anyhow!("Display#{}: take screenshot failed - possibly no screen recording permission", display_id))?;
let bitmap = cg_image.data();
let width = cg_image.width() as u32;
let height = cg_image.height() as u32;
let bytes_per_row = cg_image.bytes_per_row();
// Convert CFData to Vec<u8>
let data_ptr = bitmap.bytes().as_ptr();
let data_len = bitmap.len() as usize;
let screenshot_data = unsafe {
std::slice::from_raw_parts(data_ptr, data_len).to_vec()
};
Ok(Screenshot::new(
display_id,
height,
width,
bytes_per_row,
Arc::new(screenshot_data),
scale_factor,
scale_factor,
))
}
pub fn get_sorted_colors(colors: &Vec<u8>, mappers: &Vec<SamplePointMapper>) -> Vec<u8> { pub fn get_sorted_colors(colors: &Vec<u8>, mappers: &Vec<SamplePointMapper>) -> Vec<u8> {
let total_leds = mappers let total_leds = mappers
.iter() .iter()
@ -232,7 +289,7 @@ impl ScreenshotManager {
pub async fn subscribe_by_display_id( pub async fn subscribe_by_display_id(
&self, &self,
display_id: CGDisplayId, display_id: u32,
) -> anyhow::Result<watch::Receiver<Screenshot>> { ) -> anyhow::Result<watch::Receiver<Screenshot>> {
let channels = self.channels.read().await; let channels = self.channels.read().await;
if let Some(tx) = channels.get(&display_id) { if let Some(tx) = channels.get(&display_id) {

View File

@ -16,6 +16,13 @@
"shell": { "shell": {
"all": false, "all": false,
"open": true "open": true
},
"protocol": {
"all": true,
"asset": true,
"assetScope": [
"**"
]
} }
}, },
"bundle": { "bundle": {

View File

@ -13,6 +13,7 @@ import {
LedStripConfigurationContextType, LedStripConfigurationContextType,
} from '../../contexts/led-strip-configuration.context'; } from '../../contexts/led-strip-configuration.context';
export const LedStripConfiguration = () => { export const LedStripConfiguration = () => {
createEffect(() => { createEffect(() => {
invoke<string>('list_display_info').then((displays) => { invoke<string>('list_display_info').then((displays) => {
@ -45,11 +46,17 @@ export const LedStripConfiguration = () => {
// listen to led_colors_changed event // listen to led_colors_changed event
createEffect(() => { createEffect(() => {
const unlisten = listen<Uint8ClampedArray>('led_colors_changed', (event) => { const unlisten = listen<Uint8ClampedArray>('led_colors_changed', (event) => {
console.log('Received led_colors_changed event:', {
hidden: window.document.hidden,
colorsLength: event.payload.length,
firstFewColors: Array.from(event.payload.slice(0, 12))
});
if (!window.document.hidden) { if (!window.document.hidden) {
const colors = event.payload; const colors = event.payload;
setLedStripStore({ setLedStripStore({
colors, colors,
}); });
console.log('Updated ledStripStore.colors with length:', colors.length);
} }
}); });
@ -61,11 +68,17 @@ export const LedStripConfiguration = () => {
// listen to led_sorted_colors_changed event // listen to led_sorted_colors_changed event
createEffect(() => { createEffect(() => {
const unlisten = listen<Uint8ClampedArray>('led_sorted_colors_changed', (event) => { const unlisten = listen<Uint8ClampedArray>('led_sorted_colors_changed', (event) => {
console.log('Received led_sorted_colors_changed event:', {
hidden: window.document.hidden,
sortedColorsLength: event.payload.length,
firstFewSortedColors: Array.from(event.payload.slice(0, 12))
});
if (!window.document.hidden) { if (!window.document.hidden) {
const sortedColors = event.payload; const sortedColors = event.payload;
setLedStripStore({ setLedStripStore({
sortedColors, sortedColors,
}); });
console.log('Updated ledStripStore.sortedColors with length:', sortedColors.length);
} }
}); });

View File

@ -60,15 +60,18 @@ export const LedStripPart: Component<LedStripPartProps> = (props) => {
); );
if (index === -1) { if (index === -1) {
console.log(`LED strip not found for display ${localProps.config.display_id}, border ${localProps.config.border}`);
return; return;
} }
const mapper = ledStripStore.mappers[index]; const mapper = ledStripStore.mappers[index];
if (!mapper) { if (!mapper) {
console.log(`Mapper not found for index ${index}`);
return; return;
} }
const offset = mapper.pos * 3; const offset = mapper.pos * 3;
console.log(`Updating LED strip colors for ${localProps.config.border}, offset: ${offset}, colors length: ${ledStripStore.colors.length}`);
const colors = new Array(localProps.config.len).fill(null).map((_, i) => { const colors = new Array(localProps.config.len).fill(null).map((_, i) => {
const index = offset + i * 3; const index = offset + i * 3;
@ -77,6 +80,7 @@ export const LedStripPart: Component<LedStripPartProps> = (props) => {
})`; })`;
}); });
console.log(`Generated ${colors.length} colors for ${localProps.config.border}:`, colors.slice(0, 3));
setColors(colors); setColors(colors);
}); });

View File

@ -1,4 +1,3 @@
import { convertFileSrc } from '@tauri-apps/api/tauri';
import { import {
Component, Component,
createEffect, createEffect,
@ -79,26 +78,43 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
let stopped = false; let stopped = false;
const frame = async () => { const frame = async () => {
const { drawWidth, drawHeight } = drawInfo(); const { drawWidth, drawHeight } = drawInfo();
const url = convertFileSrc(
`displays/${localProps.displayId}?width=${drawWidth}&height=${drawHeight}`, // Skip if dimensions are not ready
'ambient-light', if (drawWidth <= 0 || drawHeight <= 0) {
); console.log('Skipping frame: invalid dimensions', { drawWidth, drawHeight });
await fetch(url, { return;
mode: 'cors', }
})
.then((res) => res.body?.getReader().read()) const url = `ambient-light://displays/${localProps.displayId}?width=${drawWidth}&height=${drawHeight}`;
.then((buffer) => {
if (buffer?.value) { console.log('Fetching screenshot:', url);
setImageData({
buffer: new Uint8ClampedArray(buffer?.value), try {
width: drawWidth, const response = await fetch(url, {
height: drawHeight, mode: 'cors',
});
} else {
setImageData(null);
}
draw();
}); });
if (!response.ok) {
console.error('Screenshot fetch failed:', response.status, response.statusText);
return;
}
const buffer = await response.body?.getReader().read();
if (buffer?.value) {
console.log('Screenshot received, size:', buffer.value.length);
setImageData({
buffer: new Uint8ClampedArray(buffer?.value),
width: drawWidth,
height: drawHeight,
});
} else {
console.log('No screenshot data received');
setImageData(null);
}
draw();
} catch (error) {
console.error('Screenshot fetch error:', error);
}
}; };
(async () => { (async () => {
@ -107,7 +123,11 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
continue; continue;
} }
await frame(); await frame();
// Add a small delay to prevent overwhelming the backend
await new Promise((resolve) => setTimeout(resolve, 33)); // ~30 FPS
} }
})(); })();
@ -122,9 +142,14 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
onMount(() => { onMount(() => {
setCtx(canvas.getContext('2d')); setCtx(canvas.getContext('2d'));
new ResizeObserver(() => {
// Initial size setup
resetSize();
resizeObserver = new ResizeObserver(() => {
resetSize(); resetSize();
}).observe(root); });
resizeObserver.observe(root);
}); });
onCleanup(() => { onCleanup(() => {