electron screenshot 截图项目文档
使用 C++获取屏幕图片
运行项目
- 环境安装
1. vs2020
2. nodejs
3. node-gyp
- 安装模块
npm install
- 编译 main.cpp
npm run gyp:configure
npm run gyp:build
- 启动 electron
npm start
把 C++代码编译成.node 文件
修改 main.cpp 代码后需要
npm run gyp:rebuild
如果是首次编译 main.cpp
npm run gyp:configure
npm run gyp:build
快捷键
截图 F1
取消(退出截图) ESC
已知问题
- 绘制矩形窗口的时候,从下往上绘制,绘制完成后移动窗口可以把矩形移动到窗口以外
- 缩放比在 125% 175% 图片会失真的问题 (系统的问题)
c++直接返回 rgba 数据 让 JavaScript 端处理
https://chat.openai.com/c/58ef482d-16f3-49e8-b07d-f3edb95b69c6
性能优化
c++ 返回的图片是 BGRA 格式
如果直接返回需要 JavaScript 进行转换,比较耗时(4k 32 毫秒)
故在 c++端转换 (4k 16 毫秒)
convertBGRAtoRGBA(pixelDataArray);
export default function convertBGRAtoRGBA(buffer) {
for (let i = 0; i < buffer.length; i += 4) {
// Swap the red and blue channels
const temp = buffer[i];
buffer[i] = buffer[i + 2];
buffer[i + 2] = temp;
}
return buffer;
}
GetDIBits(hdcWindow, hbmCapture, 0, height, rgbaData.data(), &bi, DIB_RGB_COLORS);
auto conversion_start = std::chrono::high_resolution_clock::now();
// 转换从BGRA到RGBA
for (size_t i = 0; i < rgbaData.size(); i += 4) {
std::swap(rgbaData[i], rgbaData[i + 2]);
}
auto conversion_end = std::chrono::high_resolution_clock::now();
log << "Time for [BGRA to RGBA conversion]: " << std::chrono::duration<double, std::milli>(conversion_end - conversion_start).count() << " ms\n";
把 rgba 像素绘制到 canvas 上, 开始使用了 Uint8ClampedArray.from 比较耗时(4k 69 毫秒)
改成 new Uint8ClampedArray(pixelDataArray.buffer) 直接使用 buffer 耗时可以降到到 21 毫秒
// const data = Uint8ClampedArray.from(pixelDataArray);
console.time("创建一个Image Data对象");
const data = new Uint8ClampedArray(pixelDataArray.buffer);
// 创建一个Image Data对象
const image = new ImageData(data, primaryMonitor.width, primaryMonitor.height);
// 将图像数据绘制到canvas上
backgroundCtx.putImageData(image, 0, 0);
截图以前是写入文件,耗时 140 毫秒,改成内存共享,无需写入文件
使用 c++插件实现跨进程内存共享插件 shared_memory,优化 140 毫秒,渲染进程读取图片内存依然需要 50 毫秒
截图一开始返回的 bmp 位图文件,改成 rgba 像素返回,canvas 无需转成,直接 putImageData 即可
由 90 毫秒加载图片 优化到 20 毫秒
当缩放比在 1.75 也就是屏幕不可以被缩放比整除的时候会出现屏幕坐标和宽度错误
使用 ffi-napi 调用 win32 获取 屏幕的句柄,然后获取 Rect,算出来 x,y,width,height
使用 ffi-napi 获取屏幕耗时较长 4k 30 毫秒,是同步任务
使用 node.js worker 获取,这里需要 rebuild 一下 ffi-napi 才可以在 worker 使用
当然也可以使用 fork child process 子进程
获取屏幕是截图耗时较长,并且是同步任务 (220 毫秒)
使用 worker 或者 child process 获取
获取桌面所有的窗口是一个 electron api 是异步的,也是耗时任务 (4k 70 毫秒)
Promise.all 跟获取屏幕截图、获取屏幕窗口大小(ffi-napi)、desktopCapturer
放到一个 Promise.all 中,这样子就可以同时做三件事情,减少等待时间
electron 渲染进程读取图片内存 buffer 之后,通过 preload.js contextBridge 返回数据 导致耗时 50 毫秒
- 去掉 contextBridge
- 设置运行访问 node.js
- 代码改成 window.electronAPI = {} 方式直接暴露
mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, "preload.js"),
nodeIntegration: true,
contextIsolation: false,
sandbox: false,
webSecurity: false,
spellcheck: false, // 关闭拼写检查错误
},
});
const { contextBridge, ipcRenderer } = require("electron");
const sharedMemory = require("../shared_memory/build/Release/shared_memory");
// 这里不使用 contextBridge
// 原因是 getImageData 转换需要50~90毫秒
window.electronAPI = {
getImageData(length) {},
};
// contextBridge.exposeInMainWorld("electronAPI", {
// getImageData(length) {
// });
共享内存跨进程无法访问,获取到的数据为空,获取共享内存显示操作错误等
- 父子进程或者 worker 读写数据,必须先在父进程 调用下面 sharedMemory.createOrOpenSharedMemory 方法
sharedMemory.closeSharedMemory();
sharedMemory.createOrOpenSharedMemory(
Math.ceil(width * scaleFactor * height * scaleFactor * 4)
);
然后在子进程或 worker 调用 sharedMemory.writeToSharedMemory 写入数据
const addon = require("../screenshot/build/Release/screenshot");
const sharedMemory = require("../shared_memory/build/Release/shared_memory");
process.on("message", () => {
console.time("addon.captureScreen");
const bmpData = addon.captureScreen();
console.log("bmpData.length", bmpData.length);
sharedMemory.closeSharedMemory();
sharedMemory.createOrOpenSharedMemory(bmpData.length);
sharedMemory.writeToSharedMemory(bmpData);
console.timeEnd("addon.captureScreen");
process.send(bmpData.length);
});
- 非父子派生进程,非 worker 线程读写,
必须要先 sharedMemory.createOrOpenSharedMemory 创建或者打开内存
然后再读取 数据
读取的数据必须是要和写入的数据大小一致
window.electronAPI = {
getImageData(length) {
console.time("读取图片完整耗时");
console.time("创建或者打开内存");
sharedMemory.createOrOpenSharedMemory(length);
console.timeEnd("创建或者打开内存");
console.time("读取内存");
const imageData = sharedMemory.readFromSharedMemory();
console.timeEnd("读取内存");
console.time("关闭内存");
sharedMemory.closeSharedMemory();
console.timeEnd("关闭内存");
console.log("imageData", imageData.length, Date.now());
console.timeEnd("读取图片完整耗时");
return imageData;
},
electron 渲染进程启用 共享内存 SharedArrayBuffer
// https://stackoverflow.com/questions/71770369/use-sharearraybuffer-from-an-electron-app
// Enable SharedArrayBuffer
mainWindow.webContents.session.webRequest.onHeadersReceived(
(details, callback) => {
details.responseHeaders["Cross-Origin-Opener-Policy"] = ["same-origin"];
details.responseHeaders["Cross-Origin-Embedder-Policy"] = ["require-corp"];
callback({ responseHeaders: details.responseHeaders });
}
);
ffi-napi 在 electron 20.3.8 以上版本(不包含)引入报错
"Error in native callback" using ffi-napi in electron and electron-builder
stack overflow https://stackoverflow.com/questions/75668307/error-in-native-callback-using-ffi-napi-in-electron-and-electron-builder
使用小于等于 20.3.8 版本的 electron
获取桌面任务栏窗口
- 使用 Window Detective 获取到任务栏对应的窗口类
- 使用 ffi-napi 通过“Window 类”获取到任务栏窗口句柄
- 通过窗口句柄遍历所有的子窗口,句柄
- 通过 ffi-napi 根据窗口句柄然会窗口坐标 (top left right bottom)
- 最后转成 x,y,width,height