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 ],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");23const rootPath = path.resolve(__dirname, "..");45module.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: false25 },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);56let 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}1314const 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: ffmpegStaticModulePath7 },8 {9 from: path.resolve(ffmpegStaticModulePath, "package.json"),10 to: ffmpegStaticModulePath11 },12 {13 from: platformArchPath,14 to: platformArchPath15 }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.