Navigate back to the homepage

🏗️ Building Electron with FFmpeg

Rahul Tarak
January 11th, 2021 · 4 min read

At Modfy, we are developing a desktop wrapper for modfy.video and I wanted to share how to package FFmpeg with Electron using Webpack. I think there is a lot of good work based around this, but it wasn’t very clearly documented, and I ran into a lot of issues, so I thought I’d document what I used here.

The groundwork

Electron

I really like typescript and react, so I’d recommend using this boilerplate, as it does a lot of the heavy lifting in that regard.

https://github.com/diego3g/electron-typescript-react

1git clone https://github.com/diego3g/electron-typescript-react.git project

FFmpeg

To ship a binary like FFmpeg with an application, we need to use the concept of static binaries. These are essentially binaries compiled with all their dependencies into one file that can be directly executed. Thankfully we don’t need to make these binaries ourselves, as other people have done the great work of compiling these binaries.

For example, the linux binaries are compiled here: https://johnvansickle.com/ffmpeg/

These binaries can be used wherever you’d like inside any ffmpeg wrapper, such as fluent-ffmpeg, or calling them yourself inside a child.spawn . For the rest of the tutorial we are going to demo this using fluent-ffmpeg, but it shouldn’t really change anything.

We can find all these static binaries bundled together for us in this repo: https://github.com/pietrop/ffmpeg-static-electron

It does a great job setting things up, but can be improved for the final compile step to actually import the files into electron correctly.

Building

Electron-builder

The boilerplate uses electron-builder to build out electron, which simplifies quite a bit but can still be a lot to learn and figure out. The documentation on electron.build is a good place to start.

The boilerplate will give you this within your build step in package.json, which is where all the electron build configs will go.

1"build": {
2 "appId": "your.id",
3 "mac": {
4 "category": "public.app-category.video"
5 },
6 "directories": {
7 "output": "packages"
8 },
9 "files": [
10 "package.json",
11 "dist/**"
12 ]
13 },

While this is a very basic starting point, there is a lot more to add here to actually get the build working. There are tons of good tutorials on this, but I’ll still go over it here briefly for posterity.

For each operating system you want to build to, you must choose your targets. For example, for Linux we want a build target as .deb, for debain operation systems. (https://www.electron.build/configuration/linux)

Within each target, we can add more changes and customization specific to that target.

For .dmg on MacOS, we need to make a few customizations to get the current dmg installer.

The contents determine how the dmg looks on MacOS when mounted.

1"dmg": {
2 "icon": "build/icon.icns",
3 "iconSize": 100,
4 "contents": [
5 {
6 "x": 380,
7 "y": 280,
8 "type": "link",
9 "path": "/Applications"
10 },
11 {
12 "x": 110,
13 "y": 280,
14 "type": "file"
15 }
16 ]
17 },

Build resources

For extra resources related to the build itself, we need to make a folder called build at the root of our project, where we can store these files.

We can add this directory to the build like this:

1"directories": {
2 "output": "packages",
3 "buildResources": "build"
4 },

This build folder will contain icons and other build assets.

Generating icons

For the build to work correctly we need two icon files; one .icns, and one .ico, for MacOS and Windows respectively. The Linux icon is generated from the MacOS icon.

The image should be square to make these icons. For me, making these icons was quite a bit of a pain, and I had to try various tools, so you may have to experiment a bit.

For icns I would recommend https://www.npmjs.com/package/make-icns

1npx mk-icns <png-file-path> <destination-directory>

Other options: https://www.electron.build/icons

A template of a final build config (not the exact config for modfy)

1"build": {
2 "appId": "com.app.id",
3 "productName": "Modfy",
4 "copyright": "Copyright © 2020 Modfy Inc",
5 "mac": {
6 "category": "public.app-category.video",
7 "artifactName": "${productName}-${version}-${arch}.${ext}"
8 },
9 "linux": {
10 "category": "Chat;GNOME;GTK;Network;InstantMessaging",
11 "packageCategory": "GNOME;GTK;Network;InstantMessaging",
12 "description": "Your app description",
13 "target": [
14 "deb",
15 "AppImage",
16 "snap"
17 ],
18 "maintainer": "John doe <[email protected]>",
19 "artifactName": "${productName}-${version}-${arch}.${ext}"
20 },
21 "deb": {
22 "synopsis": "Modfy Desktop App"
23 },
24 "snap": {
25 "synopsis": "Modfy Desktop App"
26 },
27 "dmg": {
28 "icon": "build/icon.icns",
29 "iconSize": 100,
30 "contents": [
31 {
32 "x": 380,
33 "y": 280,
34 "type": "link",
35 "path": "/Applications"
36 },
37 {
38 "x": 110,
39 "y": 280,
40 "type": "file"
41 }
42 ]
43 },
44 "win": {
45 "target": [
46 {
47 "target": "nsis",
48 "arch": [
49 "x64",
50 "ia32"
51 ]
52 }
53 ],
54 "icon": "build/icon.ico",
55 "artifactName": "${productName}-${version}.${ext}",
56 "publisherName": "Modfy Inc."
57 },
58 "directories": {
59 "output": "packages",
60 "buildResources": "build"
61 },
62 "files": [
63 "package.json",
64 "dist/**/*",
65 ],
66 },

Webpack

The boilerplate comes with a good base webpack config, but we need to modify it to deal with ffmpeg correctly.

The base webpack config in webpack/electron.config.js:

1const path = require("path");
2
3const rootPath = path.resolve(__dirname, "..");
4
5module.exports = {
6 resolve: {
7 extensions: [".tsx", ".ts", ".js"]
8 },
9 devtool: "source-map",
10 entry: path.resolve(rootPath, "electron", "main.ts"),
11 target: "electron-main",
12 module: {
13 rules: [
14 {
15 test: /\.(js|ts|tsx)$/,
16 exclude: /node_modules/,
17 use: {
18 loader: "babel-loader"
19 }
20 }
21 ]
22 },
23 node: {
24 __dirname: false
25 },
26 output: {
27 path: path.resolve(rootPath, "dist"),
28 filename: "[name].js"
29 }
30};

First the obvious step, change the entry point to whatever your main process file is. If you are using a preload, then that should be its only entry point.

1entry: {
2 main: path.resolve(rootPath, 'src', 'mainProcess', 'main.ts'),
3 preload: path.resolve(rootPath, 'src', 'mainProcess', 'preload.ts')
4 },

Webpack + FFmpeg Static

Now we can move on to the meat of how to configure ffmpeg-static-electron and webpack correctly!

First, we need to make the ffmpeg-static-electron package an ‘external’, which means it will not be bundled into the files itself.

1externals: {
2 'ffmpeg-static-electron': 'commonjs2 ffmpeg-static-electron'
3 },

Now that we have configured the package to be an ‘external’, we should copy over the package files into the dist folder. We can be smart here, and only copy over the corresponding OS files, rather than copy of all of them. We can use this kind of selective copying in a few places, which will be highlighted later on.

To copy files in webpack, we have to use copy-webpack-plugin.

Let’s find the paths of the files we want first; we want the index.js, package.json files regardless, but we also want /bin/{os}/{arch}/ffmpeg.

1const ffmpegStaticModulePath = path.join(
2 "node_modules",
3 "ffmpeg-static-electron"
4);
5
6let platform = os.platform();
7// patch for compatibilit with electron-builder, for smart built process.
8if (platform == "darwin") {
9 platform = "mac";
10} else if (platform == "win32") {
11 platform = "win";
12}
13
14const platformArchPath = path.join(
15 ffmpegStaticModulePath,
16 "bin",
17 platform,
18 os.arch()
19);

This will create the file paths we need to copy over our files.

1plugins: [
2 new CopyPlugin({
3 patterns: [
4 {
5 from: path.resolve(ffmpegStaticModulePath, "index.js"),
6 to: ffmpegStaticModulePath
7 },
8 {
9 from: path.resolve(ffmpegStaticModulePath, "package.json"),
10 to: ffmpegStaticModulePath
11 },
12 {
13 from: platformArchPath,
14 to: platformArchPath
15 }
16 ]
17 })
18];

Now we have created a dist/node_modules/ folder with FFmpeg. This is very important, as this is the folder we will be using for development.

The last step with webpack is to make the copied files an executable file.

For this, we need to add a build hook (yes webpack has hooks now). So we can use webpack-hook-plugin to create this build hook.

1new WebpackHookPlugin({
2 onBuildStart: ['echo "Webpack Start"'],
3 onBuildExit: [`chmod a+x ${path.resolve("dist", platformArchPath, "ffmpeg")}`]
4});

This command should make the files executable.

Moving FFmpeg into electron bundle

Now the final step and this one is a bit weird.

Electron builder by default will not put any folder called node_modules inside the app or app.asar unless they are listed as dependencies (This is a good time to move all the dependencies to dev-dependencies, as they are compiled with webpack)

I was able to work around this by using extraResources instead of files in the build config (https://www.electron.build/configuration/contents.html#extraresources).

This will put the files in the Contents/Resources folder for MacOS, and the resources folder for Linux and Windows.

As package resolution will check ../node_modules/, this will not cause a problem and you will be able to use your app.

1extraResources": [
2 {
3 "from": "dist/node_modules/ffmpeg-static-electron/",
4 "to": "node_modules/ffmpeg-static-electron"
5 }
6 ]

At this point, you should be good to go with your app, and you can use electron-builder --dir to check and electron-builder to compile for the current operating system.


If you want to use modfy.video’s desktop app (which is currently an early alpha preview), then join our discord server to get access.

At the end I wanted to add, I am by no means an Electron expert. This was my first main experience building an Electron app, and I wanted to document my experience. Feel free to reach out to me on Twitter or [email protected] to tell me what I could improve.

More articles from Modfy Inc.

Be an active protagonist in your life

My journey making active choices in 2020 and their consequences

December 31st, 2020 · 7 min read
© 2020–2021 Modfy Inc.
Link to $https://twitter.com/modfydotvideoLink to $https://github.com/modfyLink to $https://www.linkedin.com/company/modfy/