Electron調(diào)用原生模塊-截圖方案優(yōu)化

截圖功能琳拭,起初很自然地使用了electron提供的API: desktopCapturer浩村。但實(shí)際上,該API并不是專門用來截圖的护蝶,最終實(shí)現(xiàn)效果也并不具有很好的體驗(yàn)华烟。本文會介紹基于desktopCapturer的截圖方案,以及后面專門為mac和windows設(shè)計(jì)的優(yōu)化方案持灰。

DesktopCapturer

思路

  1. 新建一個(gè)BrowserWindow盔夜;
  2. 在窗口加載完成,調(diào)用desktopCapturer獲取當(dāng)前桌面錄屏的一幀堤魁,同時(shí)修改當(dāng)前窗口大小為全屏
  3. 在窗口繪制兩個(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截圖主要有幾種做法:

  1. Nircmd命令行工具(http://www.nirsoft.net/utils/nircmd.html) 逢慌,第三方的windows命令行工具,nircmd.exe savescreenshot "f:\temp\shot.png" 會將當(dāng)前桌面保存到指定文件
  2. Nodejs庫:screenshot-desktop间狂,node-desktop-screenshot攻泼,這樣的Nodejs庫有不少,但實(shí)際實(shí)現(xiàn)原理差不多鉴象,通過exec調(diào)用其自身包含的bat或cmd工具忙菠。而且也只能全屏截圖或傳送參數(shù)來裁剪指定區(qū)域。
  3. 在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)鍵工具:

反編譯工具ILSpy

windows自帶IL編譯工具ilasm和ildasm:https://docs.microsoft.com/en-us/dotnet/framework/tools/ilasm-exe-il-assembler

步驟:

  1. 將dll文件拖入ILSpy蜕便,查看C++源碼,找到需要修改的部分贩幻;
  2. 使用ildasm工具打開dll轿腺,并轉(zhuǎn)儲為IL文件;
  3. 大概熟悉下IL語法两嘴,對比源代碼,進(jìn)行修改族壳;
  4. 運(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-ffiedge-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工具

參考:
electron 使用 Node.js 原生模塊

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末郑什,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蒲肋,更是在濱河造成了極大的恐慌蹦误,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肉津,死亡現(xiàn)場離奇詭異强胰,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)妹沙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門偶洋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人距糖,你說我怎么就攤上這事玄窝。” “怎么了悍引?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵恩脂,是天一觀的道長。 經(jīng)常有香客問我趣斤,道長俩块,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任浓领,我火速辦了婚禮玉凯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘联贩。我一直安慰自己漫仆,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布泪幌。 她就那樣靜靜地躺著盲厌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪祸泪。 梳的紋絲不亂的頭發(fā)上吗浩,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機(jī)與錄音浴滴,去河邊找鬼拓萌。 笑死,一個(gè)胖子當(dāng)著我的面吹牛升略,可吹牛的內(nèi)容都是我干的微王。 我是一名探鬼主播屡限,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼炕倘!你這毒婦竟也來了钧大?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤罩旋,失蹤者是張志新(化名)和其女友劉穎啊央,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體涨醋,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瓜饥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了浴骂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乓土。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖溯警,靈堂內(nèi)的尸體忽然破棺而出趣苏,到底是詐尸還是另有隱情,我是刑警寧澤梯轻,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布食磕,位于F島的核電站,受9級特大地震影響喳挑,放射性物質(zhì)發(fā)生泄漏彬伦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一蟀悦、第九天 我趴在偏房一處隱蔽的房頂上張望媚朦。 院中可真熱鬧,春花似錦日戈、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至唯袄,卻和暖如春弯屈,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背恋拷。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工资厉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蔬顾。 一個(gè)月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓宴偿,卻偏偏與公主長得像湘捎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子窄刘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容