pref: 截图改用自定义协议传递。
This commit is contained in:
parent
5df7f54bed
commit
85ef261c51
17
src-tauri/Cargo.lock
generated
17
src-tauri/Cargo.lock
generated
@ -2653,12 +2653,14 @@ dependencies = [
|
|||||||
"env_logger",
|
"env_logger",
|
||||||
"log",
|
"log",
|
||||||
"paris",
|
"paris",
|
||||||
|
"percent-encoding",
|
||||||
"png",
|
"png",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"url-build-parse",
|
||||||
"webp",
|
"webp",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2913,6 +2915,21 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "url-build-parse"
|
||||||
|
version = "9.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "434debbcbb26557225df912efe7949c6d1237defa81ae14d05a9cd8b7ffa2776"
|
||||||
|
dependencies = [
|
||||||
|
"url-search-params",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "url-search-params"
|
||||||
|
version = "9.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e84890540c25e90ae611bfbab7d8bfcd138f29af6495027478945c1427abf532"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf-8"
|
name = "utf-8"
|
||||||
version = "0.7.6"
|
version = "0.7.6"
|
||||||
|
@ -26,6 +26,8 @@ tokio = {version = "1.26.0", features = ["full"] }
|
|||||||
paris = { version = "1.5", features = ["timestamps", "macros"] }
|
paris = { version = "1.5", features = ["timestamps", "macros"] }
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
env_logger = "0.10.0"
|
env_logger = "0.10.0"
|
||||||
|
percent-encoding = "2.2.0"
|
||||||
|
url-build-parse = "9.0.0"
|
||||||
|
|
||||||
[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
|
||||||
|
@ -9,10 +9,11 @@ use core_graphics::display::{
|
|||||||
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
|
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
|
||||||
};
|
};
|
||||||
use display_info::DisplayInfo;
|
use display_info::DisplayInfo;
|
||||||
use paris::error;
|
use paris::{error, info};
|
||||||
use screenshot_manager::ScreenshotManager;
|
use screenshot_manager::ScreenshotManager;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::to_string;
|
use serde_json::to_string;
|
||||||
|
use tauri::{http::ResponseBuilder, regex};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
#[serde(remote = "DisplayInfo")]
|
#[serde(remote = "DisplayInfo")]
|
||||||
@ -165,6 +166,122 @@ async fn main() {
|
|||||||
list_display_info,
|
list_display_info,
|
||||||
subscribe_encoded_screenshot_updated
|
subscribe_encoded_screenshot_updated
|
||||||
])
|
])
|
||||||
|
.register_uri_scheme_protocol("ambient-light", move |_app, request| {
|
||||||
|
info!("request: {:?}", request.uri());
|
||||||
|
// prepare our response
|
||||||
|
let response = ResponseBuilder::new().header("Access-Control-Allow-Origin", "*");
|
||||||
|
// get the file path
|
||||||
|
let uri = request.uri();
|
||||||
|
|
||||||
|
let uri = percent_encoding::percent_decode_str(uri)
|
||||||
|
.decode_utf8()
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let url = url_build_parse::parse_url(uri.as_str());
|
||||||
|
|
||||||
|
if let Err(err) = url {
|
||||||
|
error!("url parse error: {}", err);
|
||||||
|
return response
|
||||||
|
.status(500)
|
||||||
|
.mimetype("text/plain")
|
||||||
|
.body("Parse uri failed.".as_bytes().to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = url.unwrap();
|
||||||
|
|
||||||
|
let re = regex::Regex::new(r"^/displays/(\d+)$").unwrap();
|
||||||
|
let path = url.path;
|
||||||
|
let captures = re.captures(path.as_str());
|
||||||
|
|
||||||
|
if let None = captures {
|
||||||
|
error!("path not matched: {:?}", path);
|
||||||
|
return response
|
||||||
|
.status(404)
|
||||||
|
.mimetype("text/plain")
|
||||||
|
.body("Path Not Found.".as_bytes().to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
let captures = captures.unwrap();
|
||||||
|
|
||||||
|
let display_id = captures[1].parse::<u32>().unwrap();
|
||||||
|
|
||||||
|
let bytes = tokio::task::block_in_place(move || {
|
||||||
|
tauri::async_runtime::block_on(async move {
|
||||||
|
let screenshot_manager = ScreenshotManager::global().await;
|
||||||
|
let channels = screenshot_manager.channels.read().await;
|
||||||
|
if let Some(rx) = channels.get(&display_id) {
|
||||||
|
let rx = rx.clone();
|
||||||
|
let screenshot = rx.borrow().clone();
|
||||||
|
let bytes = screenshot.bytes.read().await;
|
||||||
|
|
||||||
|
let (scale_factor, width, height) = if url.query.is_some()
|
||||||
|
&& url.query.as_ref().unwrap().contains_key("height")
|
||||||
|
&& url.query.as_ref().unwrap().contains_key("width")
|
||||||
|
{
|
||||||
|
let width =
|
||||||
|
url.query.as_ref().unwrap()["width"].parse::<u32>().unwrap();
|
||||||
|
let height = url.query.as_ref().unwrap()["height"]
|
||||||
|
.parse::<u32>()
|
||||||
|
.unwrap();
|
||||||
|
(screenshot.width as f32 / width as f32, width, height)
|
||||||
|
} else {
|
||||||
|
info!("scale by scale_factor");
|
||||||
|
let scale_factor = screenshot.scale_factor;
|
||||||
|
(
|
||||||
|
scale_factor,
|
||||||
|
(screenshot.width as f32 / scale_factor) as u32,
|
||||||
|
(screenshot.height as f32 / scale_factor) as u32,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
info!(
|
||||||
|
"scale by query. width: {}, height: {}, scale_factor: {}, len: {}",
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
screenshot.width as f32 / width as f32,
|
||||||
|
width * height * 4,
|
||||||
|
);
|
||||||
|
|
||||||
|
let bytes_per_row = screenshot.bytes_per_row as f32;
|
||||||
|
|
||||||
|
let mut rgba_buffer = vec![0u8; (width * height * 4) as usize];
|
||||||
|
|
||||||
|
for y in 0..height {
|
||||||
|
for x in 0..width {
|
||||||
|
let offset = ((y as f32) * scale_factor) as usize * bytes_per_row as usize
|
||||||
|
+ ((x as f32) * scale_factor) as usize * 4;
|
||||||
|
let b = bytes[offset];
|
||||||
|
let g = bytes[offset + 1];
|
||||||
|
let r = bytes[offset + 2];
|
||||||
|
let a = bytes[offset + 3];
|
||||||
|
let offset_2 = (y * width + x) as usize * 4;
|
||||||
|
rgba_buffer[offset_2] = r;
|
||||||
|
rgba_buffer[offset_2 + 1] = g;
|
||||||
|
rgba_buffer[offset_2 + 2] = b;
|
||||||
|
rgba_buffer[offset_2 + 3] = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(rgba_buffer.clone())
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("Display#{}: not found", display_id);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Ok(bytes) = bytes {
|
||||||
|
return response
|
||||||
|
.mimetype("octet/stream")
|
||||||
|
.status(200)
|
||||||
|
.body(bytes.to_vec());
|
||||||
|
}
|
||||||
|
let err = bytes.unwrap_err();
|
||||||
|
error!("request screenshot bin data failed: {}", err);
|
||||||
|
return response
|
||||||
|
.mimetype("text/plain")
|
||||||
|
.status(500)
|
||||||
|
.body(err.to_string().into_bytes());
|
||||||
|
})
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Scr
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct ScreenshotManager {
|
pub struct ScreenshotManager {
|
||||||
channels: Arc<RwLock<HashMap<u32, watch::Receiver<Screenshot>>>>,
|
pub channels: Arc<RwLock<HashMap<u32, watch::Receiver<Screenshot>>>>,
|
||||||
encode_listeners: Arc<RwLock<HashMap<u32, Vec<Window>>>>,
|
encode_listeners: Arc<RwLock<HashMap<u32, Vec<Window>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { createEffect } from 'solid-js';
|
import { createEffect } from 'solid-js';
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { convertFileSrc, invoke } from '@tauri-apps/api/tauri';
|
||||||
import { DisplayView } from './components/\u0016display-view';
|
import { DisplayView } from './components/display-view';
|
||||||
import { DisplayListContainer } from './components/display-list-container';
|
import { DisplayListContainer } from './components/display-list-container';
|
||||||
import { displayStore, setDisplayStore } from './stores/display.store';
|
import { displayStore, setDisplayStore } from './stores/display.store';
|
||||||
|
import { path } from '@tauri-apps/api';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
@ -9,15 +9,21 @@ type DisplayViewProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DisplayView: Component<DisplayViewProps> = (props) => {
|
export const DisplayView: Component<DisplayViewProps> = (props) => {
|
||||||
|
const size = createMemo(() => ({
|
||||||
|
width: props.display.width * displayStore.viewScale,
|
||||||
|
height: props.display.height * displayStore.viewScale,
|
||||||
|
}));
|
||||||
const style = createMemo(() => ({
|
const style = createMemo(() => ({
|
||||||
width: `${props.display.width * displayStore.viewScale}px`,
|
|
||||||
height: `${props.display.height * displayStore.viewScale}px`,
|
|
||||||
top: `${props.display.y * displayStore.viewScale}px`,
|
top: `${props.display.y * displayStore.viewScale}px`,
|
||||||
left: `${props.display.x * displayStore.viewScale}px`,
|
left: `${props.display.x * displayStore.viewScale}px`,
|
||||||
}));
|
}));
|
||||||
return (
|
return (
|
||||||
<section class="absolute bg-gray-300" style={style()}>
|
<section class="absolute bg-gray-300" style={style()}>
|
||||||
<ScreenView displayId={props.display.id} />
|
<ScreenView
|
||||||
|
displayId={props.display.id}
|
||||||
|
height={size().height}
|
||||||
|
width={size().width}
|
||||||
|
/>
|
||||||
<DisplayInfoPanel
|
<DisplayInfoPanel
|
||||||
display={props.display}
|
display={props.display}
|
||||||
class="absolute bg-slate-50/10 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded backdrop-blur w-1/3 min-w-[300px] text-black"
|
class="absolute bg-slate-50/10 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded backdrop-blur w-1/3 min-w-[300px] text-black"
|
@ -1,17 +1,21 @@
|
|||||||
import { invoke } from '@tauri-apps/api';
|
import { invoke } from '@tauri-apps/api';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/tauri';
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
createEffect,
|
createEffect,
|
||||||
createSignal,
|
createSignal,
|
||||||
JSX,
|
JSX,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
|
onMount,
|
||||||
splitProps,
|
splitProps,
|
||||||
} from 'solid-js';
|
} from 'solid-js';
|
||||||
|
|
||||||
type ScreenViewProps = {
|
type ScreenViewProps = {
|
||||||
displayId: number;
|
displayId: number;
|
||||||
} & JSX.HTMLAttributes<HTMLImageElement>;
|
height: number;
|
||||||
|
width: number;
|
||||||
|
} & Omit<JSX.HTMLAttributes<HTMLCanvasElement>, 'height' | 'width'>;
|
||||||
|
|
||||||
async function subscribeScreenshotUpdate(displayId: number) {
|
async function subscribeScreenshotUpdate(displayId: number) {
|
||||||
await invoke('subscribe_encoded_screenshot_updated', {
|
await invoke('subscribe_encoded_screenshot_updated', {
|
||||||
@ -21,20 +25,48 @@ async function subscribeScreenshotUpdate(displayId: number) {
|
|||||||
|
|
||||||
export const ScreenView: Component<ScreenViewProps> = (props) => {
|
export const ScreenView: Component<ScreenViewProps> = (props) => {
|
||||||
const [localProps, rootProps] = splitProps(props, ['displayId']);
|
const [localProps, rootProps] = splitProps(props, ['displayId']);
|
||||||
const [image, setImage] = createSignal<string>();
|
let canvas: HTMLCanvasElement;
|
||||||
|
const [ctx, setCtx] = createSignal<CanvasRenderingContext2D | null>(null);
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const unlisten = listen<{ base64_image: string; display_id: number }>(
|
const unlisten = listen<{
|
||||||
'encoded-screenshot-updated',
|
base64_image: string;
|
||||||
(event) => {
|
display_id: number;
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
}>('encoded-screenshot-updated', (event) => {
|
||||||
if (event.payload.display_id === localProps.displayId) {
|
if (event.payload.display_id === localProps.displayId) {
|
||||||
setImage(event.payload.base64_image);
|
const url = convertFileSrc(
|
||||||
|
`displays/${localProps.displayId}?width=${canvas.width}&height=${canvas.height}`,
|
||||||
|
'ambient-light',
|
||||||
|
);
|
||||||
|
fetch(url, {
|
||||||
|
mode: 'cors',
|
||||||
|
})
|
||||||
|
.then((res) => res.body?.getReader().read())
|
||||||
|
.then((buffer) => {
|
||||||
|
console.log(buffer?.value?.length);
|
||||||
|
|
||||||
|
let _ctx = ctx();
|
||||||
|
if (_ctx && buffer?.value) {
|
||||||
|
_ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
const img = new ImageData(
|
||||||
|
new Uint8ClampedArray(buffer.value),
|
||||||
|
canvas.width,
|
||||||
|
canvas.height,
|
||||||
|
);
|
||||||
|
_ctx.putImageData(img, 0, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(event.payload.display_id, localProps.displayId);
|
// console.log(event.payload.display_id, localProps.displayId);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
subscribeScreenshotUpdate(localProps.displayId);
|
subscribeScreenshotUpdate(localProps.displayId);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
setCtx(canvas.getContext('2d'));
|
||||||
|
});
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
unlisten.then((unlisten) => {
|
unlisten.then((unlisten) => {
|
||||||
unlisten();
|
unlisten();
|
||||||
@ -42,5 +74,5 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return <img src={image()} class="object-contain" {...rootProps} />;
|
return <canvas ref={canvas!} class="object-contain" {...rootProps} />;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user