Electron调用原生模块-截图方案优化

截图功能,起初很自然地使用了electron提供的API: desktopCapturer。但实际上,该API并不是专门用来截图的,最终实现效果也并不具有很好的体验。本文会介绍基于desktopCapturer的截图方案,以及后面专门为mac和windows设计的优化方案。

DesktopCapturer

思路

  1. 新建一个BrowserWindow;
  2. 在窗口加载完成,调用desktopCapturer获取当前桌面录屏的一帧,同时修改当前窗口大小为全屏
  3. 在窗口绘制两个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截图主要有几种做法:

  1. Nircmd命令行工具(http://www.nirsoft.net/utils/nircmd.html) ,第三方的windows命令行工具,nircmd.exe savescreenshot "f:\temp\shot.png" 会将当前桌面保存到指定文件
  2. Nodejs库:screenshot-desktopnode-desktop-screenshot,这样的Nodejs库有不少,但实际实现原理差不多,通过exec调用其自身包含的bat或cmd工具。而且也只能全屏截图或传送参数来裁剪指定区域。
  3. 在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文件

关键工具:

反编译工具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,并转储为IL文件;
  3. 大概熟悉下IL语法,对比源代码,进行修改;
  4. 运行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-ffiedge-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工具

参考:
electron 使用 Node.js 原生模块

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,192评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,186评论 1 303
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,844评论 0 252
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,471评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,876评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,891评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,068评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,791评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,539评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,772评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,250评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,577评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,244评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,146评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,949评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,995评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,812评论 2 276

推荐阅读更多精彩内容