美文网首页Mac OS开发
Electron调用原生模块-截图方案优化

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

作者: charles0427 | 来源:发表于2018-05-09 15:03 被阅读0次

    截图功能,起初很自然地使用了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 原生模块

    相关文章

      网友评论

      • Joeycopy:楼主 我还是不太明白mac怎么实现截图,用命令,需要怎么用,能说明一下吗
        Joeycopy:@charles0427 console.info("路径:",logPath);
        exec(`screencapture -i ${logPath}`,(error,stdout,stderr)=>{
        console.info("截图 :",error,stdout,stderr);
        });
        我这么写,路径下一直没有截图,不知道路径有什么要求吗
        charles0427:1. 通过node的exec执行命令 screencapture -i filePath;
        2. 命令运行后,可以自由截图,效果等同于在mac上使用快捷键 command+shift+4
        3. 截完就可以在filepath找到你的截图
      • 亮神_:有个用canvas实现的方案也不错
        charles0427:@亮神_ emm...能捕捉到整个桌面么,有方案的具体地址不
      • IT人故事会:,楼主的心血啊!谢谢博主

      本文标题:Electron调用原生模块-截图方案优化

      本文链接:https://www.haomeiwen.com/subject/cbonrftx.html