截图功能,起初很自然地使用了electron提供的API: desktopCapturer。但实际上,该API并不是专门用来截图的,最终实现效果也并不具有很好的体验。本文会介绍基于desktopCapturer的截图方案,以及后面专门为mac和windows设计的优化方案。
DesktopCapturer
思路
- 新建一个BrowserWindow;
- 在窗口加载完成,调用desktopCapturer获取当前桌面录屏的一帧,同时修改当前窗口大小为全屏
- 在窗口绘制两个canvas,一个用于遮罩,一个用于显示裁剪区域
附一下desktopCapturer的使用:
onCapture: function() {
const self = this;
const desktopCapturer = Electron.desktopCapturer;
const display = Electron.screen.getPrimaryDisplay();
const size = display.size;
desktopCapturer.getSources({types: ['screen']}, function(error, sources) {
if (error) throw error;
const sourceId = sources[0].id;
navigator.webkitGetUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: sourceId,
minWidth: size.width,
maxWidth: size.width,
minHeight: size.height,
maxHeight: size.height,
},
},
}, function(stream) {
const video = ReactDOM.findDOMNode(self.refs.captureVideo);
const canvas = ReactDOM.findDOMNode(self.refs.captureCanvas);
const context = canvas.getContext('2d');
video.addEventListener('play', function() {
video.pause();
canvas.setAttribute('width', size.width);
canvas.setAttribute('height', size.height);
context.drawImage(video, 0, 0, size.width, size.height);
self.executeAction(windowAction.windowResize, {
window: cfg.GLB.CAPTURE_WINDOW,
width: size.width,
height: size.height + 85,
onTop: true,
fullscreen: true,
});
});
video.addEventListener('canplay', function() {
video.play();
});
video.setAttribute('src', URL.createObjectURL(stream));
/*
setTimeout(function() {
video.play()
}, 500);*/
}, function(e) {
console.error('getUserMediaError');
});
});
},
Mac截图
mac的优化方案很简单,使用mac自带的命令screencapture -i
screencapture是mac自带的截图命令,有-i
和-w
两种模式,分别是自由截图和窗口截图;
screencapture -i filePath
,指定要保存的路径
screencapture -i -x filePath
,关闭截图完成后的提示音
Windows截图
windows截图的方案研究了很久,因为windows本身没有提供类似mac上的截图命令。调研发现网上针对windows截图主要有几种做法:
- Nircmd命令行工具(http://www.nirsoft.net/utils/nircmd.html) ,第三方的windows命令行工具,
nircmd.exe savescreenshot "f:\temp\shot.png"
会将当前桌面保存到指定文件 - Nodejs库:screenshot-desktop,node-desktop-screenshot,这样的Nodejs库有不少,但实际实现原理差不多,通过exec调用其自身包含的bat或cmd工具。而且也只能全屏截图或传送参数来裁剪指定区域。
- 在Electron项目中调用原生模块。研究的Electron成熟产品大多采用了这种方法,如eagle、bearychat等。这种方法还可以细分成三种:
- 调用native代码编译的.node文件
- 通过node-ffi、edge-atom-shell等模块,在nodejs中直接写C++代码调用dll
- 通过命令行执行.exe文件去调用dll
三种方案中,前两者都是简单的全屏截图,无法提供裁剪、编辑等功能。接下来分析第三种方案的具体实现。
通过exe调用dll
这是项目目前采用的方案,nodejs中通过child_process
的execFile方法去执行exe文件,exe调用同级目录下的dll,调出截图工具。
const libPath = path.join(__dirname, 'capture.exe').replace('app.asar', 'app.asar.unpacked');
clipboard.clear();
const exec = require('child_process').execFile;
exec(libPath, (err, stdout, stderr) => {
if (err) log.error('capture error', err);
log.info('capture finished', clipboard.readImage().isEmpty());
const image = clipboard.readImage();
if (!image.isEmpty()) {
// 传给UI层处理
}
})
},
将exe和dll文件打包到app.asar.unpacked
目录下,通过绝对路径去执行。exe和dll是网上找的的,调用并不复杂。
问题是dll中有部分不适用的地方,需要进行修改,这就涉及到dll文件的反编译和重编译问题了,然而,我这块已经完全还给了大学老师,连环境和编译工具都一时想不起来,折腾了很久..后来参考 如何修改被编译后DLL文件
关键工具:
windows自带IL编译工具ilasm和ildasm:https://docs.microsoft.com/en-us/dotnet/framework/tools/ilasm-exe-il-assembler
步骤:
- 将dll文件拖入ILSpy,查看C++源码,找到需要修改的部分;
- 使用ildasm工具打开dll,并转储为IL文件;
- 大概熟悉下IL语法,对比源代码,进行修改;
- 运行ilasm命令,将IL重新编译成dll;
封装addon
electron作为跨平台PC开发框架,其提供了众多原生API,但毕竟需求各异,很多时候,我们仍需要实现基于C的底层业务。electron提供了nodejs调用原生模块的解决方案:使用Node原生模块
配置好node-gyp
的环境后,将c++代码暴露出供node调用的接口,修改biding.gyp。编译生成当前electron环境的addon模块,即.node文件。
node-gyp rebuild --runtime=electron --target_arch=ia32 --target=1.7.11 --disturl=https://atom.io/download/atom-shell
考虑到deadline和对C++的弱鸡,这个方案基本没考虑。但也有发现一些现成项目里的addon模块,试过直接调用其screenshot.node
,打包运行后,报错:
'xxx/screenshot.node' was compiled against a different Node.js version using
NODE_MODULE_VERSION 53. This version of Node.js requires
NODE_MODULE_VERSION 54. Please try re-compiling or re-installing
...
错误日志很明确,screenshot.node编译使用的electron版本与当前项目不一致。53对应的是electron 1.6.x版本,54对应的是1.7.x版本,具体对应关系可以查看https://github.com/lgeiger/electron-abi
因为electron的更新日志提到1.6.x版本有安全漏洞,也就不去降级来尝试解决了。而且一旦更改大版本,项目中其他的原生模块也需要rebuild。
Nodejs调用dll
因为未找到能实现windows截图的Node原生模块,我们转而研究node-ffi和edge-atom-shell这种Node原生模块,两者均能实现nodejs与C的通信。
当然,在install之前,仍得先配置后node-gyp的环境,以确保install后的ffi模块能在当前环境使用。
e.g
图片管理应用eagle中就大量运用了edge-atom-shell:
edge = require('edge-atom-shell');
NiuNiuCaptureInit = edge.func(function () {
/*#r "mscorlib.dll"
using System.Threading.Tasks;
using System;
using System.Text;
using System.Runtime.InteropServices;
public class Startup
{
public delegate void Callback();
public Callback callbackInstance;
[DllImport("NiuniuCapturex64.dll", EntryPoint = "StartScreenCapture")]
static extern IntPtr StartScreenCapture(StringBuilder fileName, Callback callback);
[DllImport("NiuniuCapturex64.dll", EntryPoint = "InitScreenCapture")]
static extern IntPtr InitScreenCapture(string auth);
public async Task<object> Invoke(dynamic input)
{
return 123;
}
}
*/});
NiuNiuCaptureInit({}, function (error, result) {});
NiuNiuCapture = edge.func(function () {/*
#r "mscorlib.dll"
using System.Threading.Tasks;
using System;
using System.Text;
using System.Runtime.InteropServices;
public class Startup
{
public delegate void Callback();
public Callback callbackInstance;
[DllImport("NiuniuCapturex64.dll", EntryPoint = "StartScreenCapture")]
static extern IntPtr StartScreenCapture(StringBuilder fileName, Callback callback);
[DllImport("NiuniuCapturex64.dll", EntryPoint = "InitScreenCapture")]
static extern IntPtr InitScreenCapture(string auth);
public async Task<object> Invoke(dynamic input)
{
callbackInstance = new Callback(delegate () {
((Func<object,Task<object>>)input.event_handler)(123);
});
StringBuilder data = new StringBuilder(input.path);
InitScreenCapture("niuniu");
StartScreenCapture(data, callbackInstance);
return 123;
}
}
*/});
其他
虽然最终使用了exe去执行dll的方案,但因为exe的打开、关闭也需要一定的延时,导致截图功能的响应不是很快。
寻找解决方案期间,也有些其他觉得不错的开源项目,只是未想到如何利用:
screenshot,一个利用微信截图dll的C#和python工具
网友评论
exec(`screencapture -i ${logPath}`,(error,stdout,stderr)=>{
console.info("截图 :",error,stdout,stderr);
});
我这么写,路径下一直没有截图,不知道路径有什么要求吗
2. 命令运行后,可以自由截图,效果等同于在mac上使用快捷键 command+shift+4
3. 截完就可以在filepath找到你的截图