截圖功能琳拭,起初很自然地使用了electron提供的API: desktopCapturer浩村。但實(shí)際上,該API并不是專門用來截圖的护蝶,最終實(shí)現(xiàn)效果也并不具有很好的體驗(yàn)华烟。本文會介紹基于desktopCapturer的截圖方案,以及后面專門為mac和windows設(shè)計(jì)的優(yōu)化方案持灰。
DesktopCapturer
思路
- 新建一個(gè)BrowserWindow盔夜;
- 在窗口加載完成,調(diào)用desktopCapturer獲取當(dāng)前桌面錄屏的一幀堤魁,同時(shí)修改當(dāng)前窗口大小為全屏
- 在窗口繪制兩個(gè)canvas喂链,一個(gè)用于遮罩,一個(gè)用于顯示裁剪區(qū)域
附一下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的優(yōu)化方案很簡單妥泉,使用mac自帶的命令screencapture -i
screencapture是mac自帶的截圖命令椭微,有-i
和-w
兩種模式,分別是自由截圖和窗口截圖盲链;
screencapture -i filePath
蝇率,指定要保存的路徑
screencapture -i -x filePath
检诗,關(guān)閉截圖完成后的提示音
Windows截圖
windows截圖的方案研究了很久,因?yàn)閣indows本身沒有提供類似mac上的截圖命令瓢剿。調(diào)研發(fā)現(xiàn)網(wǎng)上針對windows截圖主要有幾種做法:
- Nircmd命令行工具(http://www.nirsoft.net/utils/nircmd.html) 逢慌,第三方的windows命令行工具,
nircmd.exe savescreenshot "f:\temp\shot.png"
會將當(dāng)前桌面保存到指定文件 - Nodejs庫:screenshot-desktop间狂,node-desktop-screenshot攻泼,這樣的Nodejs庫有不少,但實(shí)際實(shí)現(xiàn)原理差不多鉴象,通過exec調(diào)用其自身包含的bat或cmd工具忙菠。而且也只能全屏截圖或傳送參數(shù)來裁剪指定區(qū)域。
- 在Electron項(xiàng)目中調(diào)用原生模塊纺弊。研究的Electron成熟產(chǎn)品大多采用了這種方法牛欢,如eagle、bearychat等淆游。這種方法還可以細(xì)分成三種:
- 調(diào)用native代碼編譯的.node文件
- 通過node-ffi傍睹、edge-atom-shell等模塊,在nodejs中直接寫C++代碼調(diào)用dll
- 通過命令行執(zhí)行.exe文件去調(diào)用dll
三種方案中犹菱,前兩者都是簡單的全屏截圖拾稳,無法提供裁剪、編輯等功能腊脱。接下來分析第三種方案的具體實(shí)現(xiàn)访得。
通過exe調(diào)用dll
這是項(xiàng)目目前采用的方案,nodejs中通過child_process
的execFile方法去執(zhí)行exe文件陕凹,exe調(diào)用同級目錄下的dll悍抑,調(diào)出截圖工具。
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
目錄下杜耙,通過絕對路徑去執(zhí)行搜骡。exe和dll是網(wǎng)上找的的,調(diào)用并不復(fù)雜泥技。
問題是dll中有部分不適用的地方浆兰,需要進(jìn)行修改,這就涉及到dll文件的反編譯和重編譯問題了珊豹,然而簸呈,我這塊已經(jīng)完全還給了大學(xué)老師,連環(huán)境和編譯工具都一時(shí)想不起來店茶,折騰了很久..后來參考 如何修改被編譯后DLL文件
關(guān)鍵工具:
windows自帶IL編譯工具ilasm和ildasm:https://docs.microsoft.com/en-us/dotnet/framework/tools/ilasm-exe-il-assembler
步驟:
- 將dll文件拖入ILSpy蜕便,查看C++源碼,找到需要修改的部分贩幻;
- 使用ildasm工具打開dll轿腺,并轉(zhuǎn)儲為IL文件;
- 大概熟悉下IL語法两嘴,對比源代碼,進(jìn)行修改族壳;
- 運(yùn)行ilasm命令憔辫,將IL重新編譯成dll;
封裝addon
electron作為跨平臺PC開發(fā)框架仿荆,其提供了眾多原生API贰您,但畢竟需求各異,很多時(shí)候拢操,我們?nèi)孕枰獙?shí)現(xiàn)基于C的底層業(yè)務(wù)锦亦。electron提供了nodejs調(diào)用原生模塊的解決方案:使用Node原生模塊
配置好node-gyp
的環(huán)境后,將c++代碼暴露出供node調(diào)用的接口令境,修改biding.gyp杠园。編譯生成當(dāng)前electron環(huán)境的addon模塊,即.node文件舔庶。
node-gyp rebuild --runtime=electron --target_arch=ia32 --target=1.7.11 --disturl=https://atom.io/download/atom-shell
考慮到deadline和對C++的弱雞抛蚁,這個(gè)方案基本沒考慮。但也有發(fā)現(xiàn)一些現(xiàn)成項(xiàng)目里的addon模塊栖茉,試過直接調(diào)用其screenshot.node
篮绿,打包運(yùn)行后,報(bào)錯(cuò):
'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
...
錯(cuò)誤日志很明確吕漂,screenshot.node編譯使用的electron版本與當(dāng)前項(xiàng)目不一致。53對應(yīng)的是electron 1.6.x版本尘应,54對應(yīng)的是1.7.x版本惶凝,具體對應(yīng)關(guān)系可以查看https://github.com/lgeiger/electron-abi
因?yàn)閑lectron的更新日志提到1.6.x版本有安全漏洞,也就不去降級來嘗試解決了犬钢。而且一旦更改大版本苍鲜,項(xiàng)目中其他的原生模塊也需要rebuild。
Nodejs調(diào)用dll
因?yàn)槲凑业侥軐?shí)現(xiàn)windows截圖的Node原生模塊玷犹,我們轉(zhuǎn)而研究node-ffi和edge-atom-shell這種Node原生模塊混滔,兩者均能實(shí)現(xiàn)nodejs與C的通信。
當(dāng)然歹颓,在install之前坯屿,仍得先配置后node-gyp的環(huán)境,以確保install后的ffi模塊能在當(dāng)前環(huán)境使用巍扛。
e.g
圖片管理應(yīng)用eagle中就大量運(yùn)用了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去執(zhí)行dll的方案领跛,但因?yàn)閑xe的打開、關(guān)閉也需要一定的延時(shí)撤奸,導(dǎo)致截圖功能的響應(yīng)不是很快吠昭。
尋找解決方案期間喊括,也有些其他覺得不錯(cuò)的開源項(xiàng)目,只是未想到如何利用:
screenshot矢棚,一個(gè)利用微信截圖dll的C#和python工具