How can I bundle a precompiled binary with electro

2019-01-22 05:21发布

问题:

I am trying to include a precompiled binary with an electron app. I began with electron quick start app and modified my renderer.js file to include this code that is triggered when a file is dropped on the body:

spawn = require('child_process').spawn,
  ffmpeg = spawn('node_modules/.bin/ffmpeg', ['-i', clips[0], '-an', '-q:v', '1', '-vcodec', 'libx264', '-y', '-pix_fmt', 'yuv420p', '-vf', 'setsar=1,scale=trunc(iw/2)*2:trunc(ih/2)*2,crop=in_w:in_h-50:0:50', '/tmp/out21321.mp4']);

ffmpeg.stdout.on('data', data => {
  console.log(`stdout: ${data}`);
});
ffmpeg.stderr.on('data', data => {
  console.log(`stderr: ${data}`);
});

I have placed my precompiled ffmpeg binary in node_modules/.bin/. Everything works great in the dev panel, but when I use electron-packager to set up the app, it throws a spawn error ENOENT to the console when triggered. I did find a very similar question on SO, but the question doesn't seem to be definitively answered. The npm page on electron-packager does show that they can be bundled, but I cannot find any documentation on how to do so.

回答1:

The problem is that electron-builder or electron-packager will bundle your dependency into the asar file. It seems that if the dependency has a binary into node_modules/.bin it is smart enough to not package it.

This is the documentation for asar packaging for electron-builder on that topic. It says

Node modules, that must be unpacked, will be detected automatically

I understand that it is related to existing binaries in node_modules/.bin.

If the module you are using is not automatically unpacked you can disable asar archiving completely or explicitly tell electron-builder to not pack certain files. You do so in your package.json file like this:

  "build": {
    "asarUnpack": [
      "**/app/node_modules/some-module/*"
    ],

For your particular case

I ran into the same issue with ffmpeg and this is what I've done:

  • Use ffmpeg-static. This package bundles statically compiled ffmpeg binaries for Windows, Mac and Linux. It also provides a way to get the full path of the binary for the OS you are running: require('ffmpeg-static').path
  • This will work fine in development, but we still need to troubleshoot the distribution problem.
  • Tell electron-builder to not pack the ffmpeg-static module:

    "build": { "asarUnpack": [ "**/app/node_modules/ffmpeg-static/*" ],

  • Now we need to slightly change the code to get the right path to ffmpeg with this code: require('ffmpeg-static').path.replace('app.asar', 'app.asar.unpacked') (if we are in development the replace() won't replace anything which is fine).

If you are using webpack (or other javascript bundler)

I ran into the issue that require('ffmpeg-static').path was returning a relative path in the renderer process. But the issue seemed to be that webpack changes the way the module is required and that prevents ffmpeg-static to provide a full path. In the Dev Tools the require('ffmpeg-static').path was working fine when run manually, but when doing the same in the bundled code I was always getting a relative path. So this is what I did.

  • In the main process add this before opening the BrowserWindow: global.ffmpegpath = require('ffmpeg-static').path.replace('app.asar', 'app.asar.unpacked'). The code that runs in the main process is not bundled by webpack so I always get a full path with this code.
  • In the renderer process pick the value this way: require('electron').remote.getGlobal('ffmpegpath')


回答2:

If anyone happens to need an answer to this question: I do have a solution to this, but I have no idea if this is considered best practice. I couldn't find any good documentation for including 3rd party precompiled binaries, so I just fiddled with it until it finally worked. Here's what I did (starting with the electron quick start, node.js v6):

From the app directory I ran the following commands to include the ffmpeg binary as a module:

mkdir node_modules/ffmpeg
cp /usr/local/bin/ffmpeg node_modules/ffmpeg/
ln -s ../ffmpeg/ffmpeg node_modules/.bin/ffmpeg

(replace /usr/local/bin/ffmpeg with your current binary path, download it from here) Placing the link allowed electron-packager to include the binary I saved to node_modules/ffmpeg/.

Then to get the bundled app path I installed the npm package app-root-dir by running the following command:

npm i -S app-root-dir

Since I could then get the app path, I just appended the subfolder for my binary and spawned from there. This is the code that I placed in renderer.js:.

var appRootDir = require('app-root-dir').get();
var ffmpegpath=appRootDir+'/node_modules/ffmpeg/ffmpeg';
console.log(ffmpegpath);

const
    spawn = require( 'child_process' ).spawn,
    ffmpeg = spawn( ffmpegpath, ['-i',clips_input[0]]);  //add whatever switches you need here

ffmpeg.stdout.on( 'data', data => {
     console.log( `stdout: ${data}` );
    });
   ffmpeg.stderr.on( 'data', data => {
console.log( `stderr: ${data}` );
    });


回答3:

I know I'm a bit late but just wanted to mention ffbinaries npm package I created a while ago exactly for this purpose.

It'll allow you to download ffmpeg/ffplay/ffserver/ffprobe binaries to specified location either during application boot (so you don't need to bundle it with your application) or in a CI setup. It can autodetect platform, you can also specify it manually.



回答4:

This is how I would do it:

Taking cues from tsuriga's answer, here is my code:

Note: replace or add OS path accordingly.

  • Create a directory ./resources/mac/bin
  • Place you binaries inside this folder
  • Create file ./app/binaries.js and paste the following code:
'use strict';

import path from 'path';
import { remote } from 'electron';
import getPlatform from './get-platform';

const IS_PROD = process.env.NODE_ENV === 'production';
const root = process.cwd();
const { isPackaged, getAppPath } = remote.app;

const binariesPath =
  IS_PROD && isPackaged
    ? path.join(path.dirname(getAppPath()), '..', './Resources', './bin')
    : path.join(root, './resources', getPlatform(), './bin');

export const execPath = path.resolve(path.join(binariesPath, './exec-file-name'));
  • Create file ./app/get-platform.js and paste the following code:
'use strict';

import { platform } from 'os';

export default () => {
  switch (platform()) {
    case 'aix':
    case 'freebsd':
    case 'linux':
    case 'openbsd':
    case 'android':
      return 'linux';
    case 'darwin':
    case 'sunos':
      return 'mac';
    case 'win32':
      return 'win';
  }
};
  • Add the following code inside the ./package.json file:
"build": {
....

 "extraFiles": [
      {
        "from": "resources/mac/bin",
        "to": "Resources/bin",
        "filter": [
          "**/*"
        ]
      }
    ],

....
},
  • import binary file path as:
import { execPath } from './binaries';

#your program code:
var command = spawn(execPath, arg, {});

Why this is better?

  • Most of the answers require an additional package called app-root-dir

  • The original answer doesn't handle the (env=production) build or the pre-packed versions properly. He/she has only taken care of development and post-packaged versions.