├── .editorconfig
├── .erb
├── configs
│ ├── .eslintrc
│ ├── webpack.config.base.ts
│ ├── webpack.config.eslint.ts
│ ├── webpack.config.main.prod.ts
│ ├── webpack.config.renderer.dev.dll.ts
│ ├── webpack.config.renderer.dev.ts
│ ├── webpack.config.renderer.prod.ts
│ └── webpack.paths.ts
├── img
│ ├── erb-banner.svg
│ └── erb-logo.png
├── mocks
│ └── fileMock.js
└── scripts
│ ├── .eslintrc
│ ├── check-build-exists.ts
│ ├── check-native-dep.js
│ ├── check-node-env.js
│ ├── check-port-in-use.js
│ ├── clean.js
│ ├── delete-source-maps.js
│ ├── electron-rebuild.js
│ ├── link-modules.ts
│ └── notarize.js
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .husky
└── pre-commit
├── .vscode
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── README.md
├── assets
├── assets.d.ts
├── entitlements.mac.plist
├── icon.icns
├── icon.ico
├── icon.png
├── icon.svg
└── icons
│ ├── 1024x1024.png
│ ├── 128x128.png
│ ├── 16x16.png
│ ├── 24x24.png
│ ├── 256x256.png
│ ├── 32x32.png
│ ├── 48x48.png
│ ├── 512x512.png
│ ├── 64x64.png
│ └── 96x96.png
├── demo.png
├── package-lock.json
├── package.json
├── release
└── app
│ ├── package-lock.json
│ ├── package.json
│ └── yarn.lock
├── src
├── __tests__
│ └── App.test.tsx
├── main
│ ├── main.ts
│ ├── menu.ts
│ ├── preload.js
│ └── util.ts
├── package-lock.json
├── package.json
└── renderer
│ ├── App.css
│ ├── App.tsx
│ ├── index.ejs
│ └── index.tsx
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.erb/configs/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "no-console": "off",
4 | "global-require": "off",
5 | "import/no-dynamic-require": "off"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.base.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Base webpack config used across other specific configs
3 | */
4 |
5 | import webpack from 'webpack';
6 | import webpackPaths from './webpack.paths';
7 | import { dependencies as externals } from '../../release/app/package.json';
8 |
9 | const configuration: webpack.Configuration = {
10 | externals: [...Object.keys(externals || {})],
11 |
12 | stats: 'errors-only',
13 |
14 | module: {
15 | rules: [
16 | {
17 | test: /\.[jt]sx?$/,
18 | exclude: /node_modules/,
19 | use: {
20 | loader: 'ts-loader',
21 | options: {
22 | // Remove this line to enable type checking in webpack builds
23 | transpileOnly: true,
24 | },
25 | },
26 | },
27 | ],
28 | },
29 |
30 | output: {
31 | path: webpackPaths.srcPath,
32 | // https://github.com/webpack/webpack/issues/1114
33 | library: {
34 | type: 'commonjs2',
35 | },
36 | },
37 |
38 | /**
39 | * Determine the array of extensions that should be used to resolve modules.
40 | */
41 | resolve: {
42 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
43 | modules: [webpackPaths.srcPath, 'node_modules'],
44 | },
45 |
46 | plugins: [
47 | new webpack.EnvironmentPlugin({
48 | NODE_ENV: 'production',
49 | }),
50 | ],
51 | };
52 |
53 | export default configuration;
54 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.eslint.ts:
--------------------------------------------------------------------------------
1 | /* eslint import/no-unresolved: off, import/no-self-import: off */
2 |
3 | module.exports = require('./webpack.config.renderer.dev').default;
4 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.main.prod.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Webpack config for production electron main process
3 | */
4 |
5 | import path from 'path';
6 | import webpack from 'webpack';
7 | import { merge } from 'webpack-merge';
8 | import TerserPlugin from 'terser-webpack-plugin';
9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
10 | import baseConfig from './webpack.config.base';
11 | import webpackPaths from './webpack.paths';
12 | import checkNodeEnv from '../scripts/check-node-env';
13 | import deleteSourceMaps from '../scripts/delete-source-maps';
14 |
15 | checkNodeEnv('production');
16 | deleteSourceMaps();
17 |
18 | const devtoolsConfig =
19 | process.env.DEBUG_PROD === 'true'
20 | ? {
21 | devtool: 'source-map',
22 | }
23 | : {};
24 |
25 | const configuration: webpack.Configuration = {
26 | ...devtoolsConfig,
27 |
28 | mode: 'production',
29 |
30 | target: 'electron-main',
31 |
32 | entry: {
33 | main: path.join(webpackPaths.srcMainPath, 'main.ts'),
34 | preload: path.join(webpackPaths.srcMainPath, 'preload.js'),
35 | },
36 |
37 | output: {
38 | path: webpackPaths.distMainPath,
39 | filename: '[name].js',
40 | },
41 |
42 | optimization: {
43 | minimizer: [
44 | new TerserPlugin({
45 | parallel: true,
46 | }),
47 | ],
48 | },
49 |
50 | plugins: [
51 | new BundleAnalyzerPlugin({
52 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
53 | }),
54 |
55 | /**
56 | * Create global constants which can be configured at compile time.
57 | *
58 | * Useful for allowing different behaviour between development builds and
59 | * release builds
60 | *
61 | * NODE_ENV should be production so that modules do not perform certain
62 | * development checks
63 | */
64 | new webpack.EnvironmentPlugin({
65 | NODE_ENV: 'production',
66 | DEBUG_PROD: false,
67 | START_MINIMIZED: false,
68 | }),
69 | ],
70 |
71 | /**
72 | * Disables webpack processing of __dirname and __filename.
73 | * If you run the bundle in node.js it falls back to these values of node.js.
74 | * https://github.com/webpack/webpack/issues/2010
75 | */
76 | node: {
77 | __dirname: false,
78 | __filename: false,
79 | },
80 | };
81 |
82 | export default merge(baseConfig, configuration);
83 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.renderer.dev.dll.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Builds the DLL for development electron renderer process
3 | */
4 |
5 | import webpack from 'webpack';
6 | import path from 'path';
7 | import { merge } from 'webpack-merge';
8 | import baseConfig from './webpack.config.base';
9 | import webpackPaths from './webpack.paths';
10 | import { dependencies } from '../../package.json';
11 | import checkNodeEnv from '../scripts/check-node-env';
12 |
13 | checkNodeEnv('development');
14 |
15 | const dist = webpackPaths.dllPath;
16 |
17 | const configuration: webpack.Configuration = {
18 | context: webpackPaths.rootPath,
19 |
20 | devtool: 'eval',
21 |
22 | mode: 'development',
23 |
24 | target: 'electron-renderer',
25 |
26 | externals: ['fsevents', 'crypto-browserify'],
27 |
28 | /**
29 | * Use `module` from `webpack.config.renderer.dev.js`
30 | */
31 | module: require('./webpack.config.renderer.dev').default.module,
32 |
33 | entry: {
34 | renderer: Object.keys(dependencies || {}),
35 | },
36 |
37 | output: {
38 | path: dist,
39 | filename: '[name].dev.dll.js',
40 | library: {
41 | name: 'renderer',
42 | type: 'var',
43 | },
44 | },
45 |
46 | plugins: [
47 | new webpack.DllPlugin({
48 | path: path.join(dist, '[name].json'),
49 | name: '[name]',
50 | }),
51 |
52 | /**
53 | * Create global constants which can be configured at compile time.
54 | *
55 | * Useful for allowing different behaviour between development builds and
56 | * release builds
57 | *
58 | * NODE_ENV should be production so that modules do not perform certain
59 | * development checks
60 | */
61 | new webpack.EnvironmentPlugin({
62 | NODE_ENV: 'development',
63 | }),
64 |
65 | new webpack.LoaderOptionsPlugin({
66 | debug: true,
67 | options: {
68 | context: webpackPaths.srcPath,
69 | output: {
70 | path: webpackPaths.dllPath,
71 | },
72 | },
73 | }),
74 | ],
75 | };
76 |
77 | export default merge(baseConfig, configuration);
78 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.renderer.dev.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import fs from 'fs';
3 | import webpack from 'webpack';
4 | import HtmlWebpackPlugin from 'html-webpack-plugin';
5 | import chalk from 'chalk';
6 | import { merge } from 'webpack-merge';
7 | import { spawn, execSync } from 'child_process';
8 | import baseConfig from './webpack.config.base';
9 | import webpackPaths from './webpack.paths';
10 | import checkNodeEnv from '../scripts/check-node-env';
11 | import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
12 |
13 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
14 | // at the dev webpack config is not accidentally run in a production environment
15 | if (process.env.NODE_ENV === 'production') {
16 | checkNodeEnv('development');
17 | }
18 |
19 | const port = process.env.PORT || 1212;
20 | const manifest = path.resolve(webpackPaths.dllPath, 'renderer.json');
21 | const requiredByDLLConfig = module.parent!.filename.includes(
22 | 'webpack.config.renderer.dev.dll'
23 | );
24 |
25 | /**
26 | * Warn if the DLL is not built
27 | */
28 | if (
29 | !requiredByDLLConfig &&
30 | !(fs.existsSync(webpackPaths.dllPath) && fs.existsSync(manifest))
31 | ) {
32 | console.log(
33 | chalk.black.bgYellow.bold(
34 | 'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"'
35 | )
36 | );
37 | execSync('npm run postinstall');
38 | }
39 |
40 | const configuration: webpack.Configuration = {
41 | devtool: 'inline-source-map',
42 |
43 | mode: 'development',
44 |
45 | target: ['web', 'electron-renderer'],
46 |
47 | entry: [
48 | `webpack-dev-server/client?http://localhost:${port}/dist`,
49 | 'webpack/hot/only-dev-server',
50 | path.join(webpackPaths.srcRendererPath, 'index.tsx'),
51 | ],
52 |
53 | output: {
54 | path: webpackPaths.distRendererPath,
55 | publicPath: '/',
56 | filename: 'renderer.dev.js',
57 | library: {
58 | type: 'umd',
59 | },
60 | },
61 |
62 | module: {
63 | rules: [
64 | {
65 | test: /\.s?css$/,
66 | use: [
67 | 'style-loader',
68 | {
69 | loader: 'css-loader',
70 | options: {
71 | modules: true,
72 | sourceMap: true,
73 | importLoaders: 1,
74 | },
75 | },
76 | 'sass-loader',
77 | ],
78 | include: /\.module\.s?(c|a)ss$/,
79 | },
80 | {
81 | test: /\.s?css$/,
82 | use: ['style-loader', 'css-loader', 'sass-loader'],
83 | exclude: /\.module\.s?(c|a)ss$/,
84 | },
85 | // Fonts
86 | {
87 | test: /\.(woff|woff2|eot|ttf|otf)$/i,
88 | type: 'asset/resource',
89 | },
90 | // Images
91 | {
92 | test: /\.(png|svg|jpg|jpeg|gif)$/i,
93 | type: 'asset/resource',
94 | },
95 | ],
96 | },
97 | plugins: [
98 | ...(requiredByDLLConfig
99 | ? []
100 | : [
101 | new webpack.DllReferencePlugin({
102 | context: webpackPaths.dllPath,
103 | manifest: require(manifest),
104 | sourceType: 'var',
105 | }),
106 | ]),
107 |
108 | new webpack.NoEmitOnErrorsPlugin(),
109 |
110 | /**
111 | * Create global constants which can be configured at compile time.
112 | *
113 | * Useful for allowing different behaviour between development builds and
114 | * release builds
115 | *
116 | * NODE_ENV should be production so that modules do not perform certain
117 | * development checks
118 | *
119 | * By default, use 'development' as NODE_ENV. This can be overriden with
120 | * 'staging', for example, by changing the ENV variables in the npm scripts
121 | */
122 | new webpack.EnvironmentPlugin({
123 | NODE_ENV: 'development',
124 | }),
125 |
126 | new webpack.LoaderOptionsPlugin({
127 | debug: true,
128 | }),
129 |
130 | new ReactRefreshWebpackPlugin(),
131 |
132 | new HtmlWebpackPlugin({
133 | filename: path.join('index.html'),
134 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
135 | minify: {
136 | collapseWhitespace: true,
137 | removeAttributeQuotes: true,
138 | removeComments: true,
139 | },
140 | isBrowser: false,
141 | env: process.env.NODE_ENV,
142 | isDevelopment: process.env.NODE_ENV !== 'production',
143 | nodeModules: webpackPaths.appNodeModulesPath,
144 | }),
145 | ],
146 |
147 | node: {
148 | __dirname: false,
149 | __filename: false,
150 | },
151 |
152 | // @ts-ignore
153 | devServer: {
154 | port,
155 | compress: true,
156 | hot: true,
157 | headers: { 'Access-Control-Allow-Origin': '*' },
158 | static: {
159 | publicPath: '/',
160 | },
161 | historyApiFallback: {
162 | verbose: true,
163 | },
164 | onBeforeSetupMiddleware() {
165 | console.log('Starting Main Process...');
166 | spawn('npm', ['run', 'start:main'], {
167 | shell: true,
168 | env: process.env,
169 | stdio: 'inherit',
170 | })
171 | .on('close', (code: number) => process.exit(code!))
172 | .on('error', (spawnError) => console.error(spawnError));
173 | },
174 | },
175 | };
176 |
177 | export default merge(baseConfig, configuration);
178 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.config.renderer.prod.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Build config for electron renderer process
3 | */
4 |
5 | import path from 'path';
6 | import webpack from 'webpack';
7 | import HtmlWebpackPlugin from 'html-webpack-plugin';
8 | import MiniCssExtractPlugin from 'mini-css-extract-plugin';
9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
10 | import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
11 | import { merge } from 'webpack-merge';
12 | import TerserPlugin from 'terser-webpack-plugin';
13 | import baseConfig from './webpack.config.base';
14 | import webpackPaths from './webpack.paths';
15 | import checkNodeEnv from '../scripts/check-node-env';
16 | import deleteSourceMaps from '../scripts/delete-source-maps';
17 |
18 | checkNodeEnv('production');
19 | deleteSourceMaps();
20 |
21 | const devtoolsConfig =
22 | process.env.DEBUG_PROD === 'true'
23 | ? {
24 | devtool: 'source-map',
25 | }
26 | : {};
27 |
28 | const configuration: webpack.Configuration = {
29 | ...devtoolsConfig,
30 |
31 | mode: 'production',
32 |
33 | target: ['web', 'electron-renderer'],
34 |
35 | entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')],
36 |
37 | output: {
38 | path: webpackPaths.distRendererPath,
39 | publicPath: './',
40 | filename: 'renderer.js',
41 | library: {
42 | type: 'umd',
43 | },
44 | },
45 |
46 | module: {
47 | rules: [
48 | {
49 | test: /\.s?(a|c)ss$/,
50 | use: [
51 | MiniCssExtractPlugin.loader,
52 | {
53 | loader: 'css-loader',
54 | options: {
55 | modules: true,
56 | sourceMap: true,
57 | importLoaders: 1,
58 | },
59 | },
60 | 'sass-loader',
61 | ],
62 | include: /\.module\.s?(c|a)ss$/,
63 | },
64 | {
65 | test: /\.s?(a|c)ss$/,
66 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
67 | exclude: /\.module\.s?(c|a)ss$/,
68 | },
69 | // Fonts
70 | {
71 | test: /\.(woff|woff2|eot|ttf|otf)$/i,
72 | type: 'asset/resource',
73 | },
74 | // Images
75 | {
76 | test: /\.(png|svg|jpg|jpeg|gif)$/i,
77 | type: 'asset/resource',
78 | },
79 | ],
80 | },
81 |
82 | optimization: {
83 | minimize: true,
84 | minimizer: [
85 | new TerserPlugin({
86 | parallel: true,
87 | }),
88 | new CssMinimizerPlugin(),
89 | ],
90 | },
91 |
92 | plugins: [
93 | /**
94 | * Create global constants which can be configured at compile time.
95 | *
96 | * Useful for allowing different behaviour between development builds and
97 | * release builds
98 | *
99 | * NODE_ENV should be production so that modules do not perform certain
100 | * development checks
101 | */
102 | new webpack.EnvironmentPlugin({
103 | NODE_ENV: 'production',
104 | DEBUG_PROD: false,
105 | }),
106 |
107 | new MiniCssExtractPlugin({
108 | filename: 'style.css',
109 | }),
110 |
111 | new BundleAnalyzerPlugin({
112 | analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
113 | }),
114 |
115 | new HtmlWebpackPlugin({
116 | filename: 'index.html',
117 | template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
118 | minify: {
119 | collapseWhitespace: true,
120 | removeAttributeQuotes: true,
121 | removeComments: true,
122 | },
123 | isBrowser: false,
124 | isDevelopment: process.env.NODE_ENV !== 'production',
125 | }),
126 | ],
127 | };
128 |
129 | export default merge(baseConfig, configuration);
130 |
--------------------------------------------------------------------------------
/.erb/configs/webpack.paths.ts:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const rootPath = path.join(__dirname, '../..');
4 |
5 | const dllPath = path.join(__dirname, '../dll');
6 |
7 | const srcPath = path.join(rootPath, 'src');
8 | const srcMainPath = path.join(srcPath, 'main');
9 | const srcRendererPath = path.join(srcPath, 'renderer');
10 |
11 | const releasePath = path.join(rootPath, 'release');
12 | const appPath = path.join(releasePath, 'app');
13 | const appPackagePath = path.join(appPath, 'package.json');
14 | const appNodeModulesPath = path.join(appPath, 'node_modules');
15 | const srcNodeModulesPath = path.join(srcPath, 'node_modules');
16 |
17 | const distPath = path.join(appPath, 'dist');
18 | const distMainPath = path.join(distPath, 'main');
19 | const distRendererPath = path.join(distPath, 'renderer');
20 |
21 | const buildPath = path.join(releasePath, 'build');
22 |
23 | export default {
24 | rootPath,
25 | dllPath,
26 | srcPath,
27 | srcMainPath,
28 | srcRendererPath,
29 | releasePath,
30 | appPath,
31 | appPackagePath,
32 | appNodeModulesPath,
33 | srcNodeModulesPath,
34 | distPath,
35 | distMainPath,
36 | distRendererPath,
37 | buildPath,
38 | };
39 |
--------------------------------------------------------------------------------
/.erb/img/erb-banner.svg:
--------------------------------------------------------------------------------
1 |
33 |
--------------------------------------------------------------------------------
/.erb/img/erb-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khomin/electron_camera_ffmpeg/dc6b3c24308342eab9abf7593848df6d3fc70f4e/.erb/img/erb-logo.png
--------------------------------------------------------------------------------
/.erb/mocks/fileMock.js:
--------------------------------------------------------------------------------
1 | export default 'test-file-stub';
2 |
--------------------------------------------------------------------------------
/.erb/scripts/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "no-console": "off",
4 | "global-require": "off",
5 | "import/no-dynamic-require": "off",
6 | "import/no-extraneous-dependencies": "off"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.erb/scripts/check-build-exists.ts:
--------------------------------------------------------------------------------
1 | // Check if the renderer and main bundles are built
2 | import path from 'path';
3 | import chalk from 'chalk';
4 | import fs from 'fs';
5 | import webpackPaths from '../configs/webpack.paths';
6 |
7 | const mainPath = path.join(webpackPaths.distMainPath, 'main.js');
8 | const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js');
9 |
10 | if (!fs.existsSync(mainPath)) {
11 | throw new Error(
12 | chalk.whiteBright.bgRed.bold(
13 | 'The main process is not built yet. Build it by running "npm run build:main"'
14 | )
15 | );
16 | }
17 |
18 | if (!fs.existsSync(rendererPath)) {
19 | throw new Error(
20 | chalk.whiteBright.bgRed.bold(
21 | 'The renderer process is not built yet. Build it by running "npm run build:renderer"'
22 | )
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/.erb/scripts/check-native-dep.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import chalk from 'chalk';
3 | import { execSync } from 'child_process';
4 | import { dependencies } from '../../package.json';
5 |
6 | if (dependencies) {
7 | const dependenciesKeys = Object.keys(dependencies);
8 | const nativeDeps = fs
9 | .readdirSync('node_modules')
10 | .filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`));
11 | if (nativeDeps.length === 0) {
12 | process.exit(0);
13 | }
14 | try {
15 | // Find the reason for why the dependency is installed. If it is installed
16 | // because of a devDependency then that is okay. Warn when it is installed
17 | // because of a dependency
18 | const { dependencies: dependenciesObject } = JSON.parse(
19 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString()
20 | );
21 | const rootDependencies = Object.keys(dependenciesObject);
22 | const filteredRootDependencies = rootDependencies.filter((rootDependency) =>
23 | dependenciesKeys.includes(rootDependency)
24 | );
25 | if (filteredRootDependencies.length > 0) {
26 | const plural = filteredRootDependencies.length > 1;
27 | console.log(`
28 | ${chalk.whiteBright.bgYellow.bold(
29 | 'Webpack does not work with native dependencies.'
30 | )}
31 | ${chalk.bold(filteredRootDependencies.join(', '))} ${
32 | plural ? 'are native dependencies' : 'is a native dependency'
33 | } and should be installed inside of the "./release/app" folder.
34 | First, uninstall the packages from "./package.json":
35 | ${chalk.whiteBright.bgGreen.bold('npm uninstall your-package')}
36 | ${chalk.bold(
37 | 'Then, instead of installing the package to the root "./package.json":'
38 | )}
39 | ${chalk.whiteBright.bgRed.bold('npm install your-package')}
40 | ${chalk.bold('Install the package to "./release/app/package.json"')}
41 | ${chalk.whiteBright.bgGreen.bold('cd ./release/app && npm install your-package')}
42 | Read more about native dependencies at:
43 | ${chalk.bold(
44 | 'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure'
45 | )}
46 | `);
47 | process.exit(1);
48 | }
49 | } catch (e) {
50 | console.log('Native dependencies could not be checked');
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/.erb/scripts/check-node-env.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk';
2 |
3 | export default function checkNodeEnv(expectedEnv) {
4 | if (!expectedEnv) {
5 | throw new Error('"expectedEnv" not set');
6 | }
7 |
8 | if (process.env.NODE_ENV !== expectedEnv) {
9 | console.log(
10 | chalk.whiteBright.bgRed.bold(
11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`
12 | )
13 | );
14 | process.exit(2);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.erb/scripts/check-port-in-use.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk';
2 | import detectPort from 'detect-port';
3 |
4 | const port = process.env.PORT || '1212';
5 |
6 | detectPort(port, (err, availablePort) => {
7 | if (port !== String(availablePort)) {
8 | throw new Error(
9 | chalk.whiteBright.bgRed.bold(
10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start`
11 | )
12 | );
13 | } else {
14 | process.exit(0);
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/.erb/scripts/clean.js:
--------------------------------------------------------------------------------
1 | import rimraf from 'rimraf';
2 | import webpackPaths from '../configs/webpack.paths.ts';
3 | import process from 'process';
4 |
5 | const args = process.argv.slice(2);
6 | const commandMap = {
7 | dist: webpackPaths.distPath,
8 | release: webpackPaths.releasePath,
9 | dll: webpackPaths.dllPath,
10 | };
11 |
12 | args.forEach((x) => {
13 | const pathToRemove = commandMap[x];
14 | if (pathToRemove !== undefined) {
15 | rimraf.sync(pathToRemove);
16 | }
17 | });
18 |
--------------------------------------------------------------------------------
/.erb/scripts/delete-source-maps.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import rimraf from 'rimraf';
3 | import webpackPaths from '../configs/webpack.paths';
4 |
5 | export default function deleteSourceMaps() {
6 | rimraf.sync(path.join(webpackPaths.distMainPath, '*.js.map'));
7 | rimraf.sync(path.join(webpackPaths.distRendererPath, '*.js.map'));
8 | }
9 |
--------------------------------------------------------------------------------
/.erb/scripts/electron-rebuild.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import { execSync } from 'child_process';
3 | import fs from 'fs';
4 | import { dependencies } from '../../release/app/package.json';
5 | import webpackPaths from '../configs/webpack.paths';
6 |
7 | if (
8 | Object.keys(dependencies || {}).length > 0 &&
9 | fs.existsSync(webpackPaths.appNodeModulesPath)
10 | ) {
11 | const electronRebuildCmd =
12 | '../../node_modules/.bin/electron-rebuild --parallel --force --types prod,dev,optional --module-dir .';
13 | const cmd =
14 | process.platform === 'win32'
15 | ? electronRebuildCmd.replace(/\//g, '\\')
16 | : electronRebuildCmd;
17 | execSync(cmd, {
18 | cwd: webpackPaths.appPath,
19 | stdio: 'inherit',
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/.erb/scripts/link-modules.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import webpackPaths from '../configs/webpack.paths';
3 |
4 | const srcNodeModulesPath = webpackPaths.srcNodeModulesPath;
5 | const appNodeModulesPath = webpackPaths.appNodeModulesPath
6 |
7 | if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) {
8 | fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction');
9 | }
10 |
--------------------------------------------------------------------------------
/.erb/scripts/notarize.js:
--------------------------------------------------------------------------------
1 | const { notarize } = require('electron-notarize');
2 | const { build } = require('../../package.json');
3 |
4 | exports.default = async function notarizeMacos(context) {
5 | const { electronPlatformName, appOutDir } = context;
6 | if (electronPlatformName !== 'darwin') {
7 | return;
8 | }
9 |
10 | if (process.env.CI !== "true") {
11 | console.warn('Skipping notarizing step. Packaging is not running in CI');
12 | return;
13 | }
14 |
15 | if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) {
16 | console.warn('Skipping notarizing step. APPLE_ID and APPLE_ID_PASS env variables must be set');
17 | return;
18 | }
19 |
20 | const appName = context.packager.appInfo.productFilename;
21 |
22 | await notarize({
23 | appBundleId: build.appId,
24 | appPath: `${appOutDir}/${appName}.app`,
25 | appleId: process.env.APPLE_ID,
26 | appleIdPassword: process.env.APPLE_ID_PASS,
27 | });
28 | };
29 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Coverage directory used by tools like istanbul
11 | coverage
12 | .eslintcache
13 |
14 | # Dependency directory
15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
16 | node_modules
17 |
18 | # OSX
19 | .DS_Store
20 |
21 | release/app/dist
22 | release/build
23 | .erb/dll
24 |
25 | .idea
26 | npm-debug.log.*
27 | *.css.d.ts
28 | *.sass.d.ts
29 | *.scss.d.ts
30 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: 'erb',
3 | rules: {
4 | // A temporary hack related to IDE not resolving correct package.json
5 | 'import/no-extraneous-dependencies': 'off',
6 | 'import/no-unresolved': 'error',
7 | // Since React 17 and typescript 4.1 you can safely disable the rule
8 | 'react/react-in-jsx-scope': 'off',
9 | },
10 | parserOptions: {
11 | ecmaVersion: 2020,
12 | sourceType: 'module',
13 | project: './tsconfig.json',
14 | tsconfigRootDir: __dirname,
15 | createDefaultProgram: true,
16 | },
17 | settings: {
18 | 'import/resolver': {
19 | // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below
20 | node: {},
21 | webpack: {
22 | config: require.resolve('./.erb/configs/webpack.config.eslint.ts'),
23 | },
24 | typescript: {},
25 | },
26 | 'import/parsers': {
27 | '@typescript-eslint/parser': ['.ts', '.tsx'],
28 | },
29 | },
30 | };
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Coverage directory used by tools like istanbul
11 | coverage
12 | .eslintcache
13 |
14 | # Dependency directory
15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
16 | node_modules
17 |
18 | # OSX
19 | .DS_Store
20 |
21 | release/app/dist
22 | release/build
23 | .erb/dll
24 |
25 | .idea
26 | npm-debug.log.*
27 | *.css.d.ts
28 | *.sass.d.ts
29 | *.scss.d.ts
30 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["dbaeumer.vscode-eslint", "EditorConfig.EditorConfig"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Electron: Main",
6 | "type": "node",
7 | "request": "launch",
8 | "protocol": "inspector",
9 | "runtimeExecutable": "yarn",
10 | // "runtimeArgs": [
11 | // "run start:main --inspect=5858 --remote-debugging-port=9223"
12 | // ],
13 | // "runtimeArgs": ["start:main", "--inspect=5858", "--remote-debugging-port=9223"],
14 | "runtimeArgs": ["start:main"],
15 | "preLaunchTask": "Start Webpack Dev"
16 | },
17 | {
18 | "name": "Electron: Renderer",
19 | "type": "chrome",
20 | "request": "attach",
21 | "port": 9223,
22 | "webRoot": "${workspaceFolder}",
23 | "timeout": 15000
24 | }
25 | ],
26 | "compounds": [
27 | {
28 | "name": "Electron: All",
29 | "configurations": ["Electron: Main", "Electron: Renderer"]
30 | }
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.associations": {
3 | "*.qml": "c",
4 | ".eslintrc": "jsonc",
5 | ".prettierrc": "jsonc",
6 | ".eslintignore": "ignore",
7 | "ios": "cpp",
8 | "string": "cpp",
9 | "thread": "cpp",
10 | "__bit_reference": "cpp",
11 | "__config": "cpp",
12 | "__debug": "cpp",
13 | "__errc": "cpp",
14 | "__functional_base": "cpp",
15 | "__hash_table": "cpp",
16 | "__locale": "cpp",
17 | "__mutex_base": "cpp",
18 | "__node_handle": "cpp",
19 | "__nullptr": "cpp",
20 | "__split_buffer": "cpp",
21 | "__string": "cpp",
22 | "__threading_support": "cpp",
23 | "__tree": "cpp",
24 | "__tuple": "cpp",
25 | "algorithm": "cpp",
26 | "array": "cpp",
27 | "atomic": "cpp",
28 | "bit": "cpp",
29 | "bitset": "cpp",
30 | "cctype": "cpp",
31 | "chrono": "cpp",
32 | "cmath": "cpp",
33 | "complex": "cpp",
34 | "condition_variable": "cpp",
35 | "cstdarg": "cpp",
36 | "cstddef": "cpp",
37 | "cstdint": "cpp",
38 | "cstdio": "cpp",
39 | "cstdlib": "cpp",
40 | "cstring": "cpp",
41 | "ctime": "cpp",
42 | "cwchar": "cpp",
43 | "cwctype": "cpp",
44 | "deque": "cpp",
45 | "exception": "cpp",
46 | "fstream": "cpp",
47 | "functional": "cpp",
48 | "initializer_list": "cpp",
49 | "iomanip": "cpp",
50 | "iosfwd": "cpp",
51 | "iostream": "cpp",
52 | "istream": "cpp",
53 | "iterator": "cpp",
54 | "limits": "cpp",
55 | "list": "cpp",
56 | "locale": "cpp",
57 | "memory": "cpp",
58 | "mutex": "cpp",
59 | "new": "cpp",
60 | "optional": "cpp",
61 | "ostream": "cpp",
62 | "queue": "cpp",
63 | "ratio": "cpp",
64 | "set": "cpp",
65 | "sstream": "cpp",
66 | "stack": "cpp",
67 | "stdexcept": "cpp",
68 | "streambuf": "cpp",
69 | "string_view": "cpp",
70 | "system_error": "cpp",
71 | "tuple": "cpp",
72 | "type_traits": "cpp",
73 | "typeinfo": "cpp",
74 | "unordered_map": "cpp",
75 | "utility": "cpp",
76 | "vector": "cpp",
77 | "filesystem": "cpp"
78 | },
79 |
80 | "javascript.validate.enable": false,
81 | "javascript.format.enable": false,
82 | "typescript.format.enable": false,
83 |
84 | "search.exclude": {
85 | ".git": true,
86 | ".eslintcache": true,
87 | ".erb/dll": true,
88 | "release/{build,app/dist}": true,
89 | "node_modules": true,
90 | "npm-debug.log.*": true,
91 | "test/**/__snapshots__": true,
92 | "package-lock.json": true,
93 | "*.{css,sass,scss}.d.ts": true
94 | },
95 | "cmake.configureOnOpen": true
96 | }
97 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "npm",
6 | "label": "Start Webpack Dev",
7 | "script": "start:renderer",
8 | "options": {
9 | "cwd": "${workspaceFolder}"
10 | },
11 | "isBackground": true,
12 | "problemMatcher": {
13 | "owner": "custom",
14 | "pattern": {
15 | "regexp": "____________"
16 | },
17 | "background": {
18 | "activeOnStart": true,
19 | "beginsPattern": "Compiling\\.\\.\\.$",
20 | "endsPattern": "(Compiled successfully|Failed to compile)\\.$"
21 | }
22 | }
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # electron_camera_ffmpeg
2 | An example of using Electron and a native ffmpeg addon to access a webcamera
3 |
4 | This guide may be helpful to someone who is trying to find a way
to work with Electron if they need to use a c++ library or code
5 | I was looking for a more realistic example than a simple 'hello world' and i didn't succeed
6 | So let me share my experience
7 |
8 | 
9 |
10 | We have three layers:
11 | - main (launches BrowserWindow, listens for signals and is considered a backend)
12 | - rendering (runs React JS, handles UI events, shows video frame and info)
13 | - native (responsible for ffmpeg, starts/stops the video, sends a callback to the main layer)
14 |
15 | Render thread cannot directly access the main thread and vice versa
16 | All communications must be done through the ipcMain/ipcRenderer modules
17 | (it provides methods to allow synchronous and asynchronous messages to be sent from these layers)
18 |
19 | Set the listener in main.ts
20 | It will receive events from Render thread and pass them to native layer
21 | ```
22 | ipcMain.on('ipc-example', async (event, arg) => {
23 | if(arg.type == 'startCamera') {
24 | addon.setCameraEnabled()
25 | } else if(arg.type == 'stopCamera') {
26 | addon.setCameraDisable()
27 | } else if(arg.type == 'setDimention') {
28 | addon.setDimention(arg.width, arg.height)
29 | }
30 | });
31 | ```
32 | Listen to responses from Native and translate them to Render thread
33 | We set the listener callback just a couple of lines below
34 | So we have a completed chain:
35 | Render -> Main -> Native
36 | Native -> Main -> Render
37 |
38 | ```
39 | addon.setCb(function(data) {
40 | if(data.type == 'stats') {
41 | mainWindow.webContents.send('ipc-example_stats', data)
42 | } else if(data.type == 'frame') {
43 | mainWindow.webContents.send('ipc-example_frame', data)
44 | }
45 | });
46 | ```
47 | Now it's time to see what's on the render thread
48 | We will send events 'startCamera', 'stopCamera' and 'setDimention
49 | A simple React.Component class and props for handling UI logic
50 | I hope everything is clear from the names
51 | ```
52 | export default class Root extends React.Component {
53 | constructor(props) {
54 | super(props);
55 | this.state = {
56 | videoActive: false,
57 | buttonText: 'Start video',
58 | packets: 0,
59 | errors: 0,
60 | resolution: 0,
61 | frame: null,
62 | frameBytes: 0,
63 | frameWidth: 1000,
64 | frameHeight: 1000
65 | };
66 | }
67 | ```
68 | To send messages from Render to Main use this construct
69 | ```
70 | window.electron.ipcRenderer.startCamera()
71 | ```
72 | And to set a listener on certain channel
73 | ```
74 | ipcRenderer.on('ipc-name', (cb) => {}
75 | ```
76 |
77 | So, the full code is:
78 | ```
79 | componentDidMount() {
80 | window.electron.ipcRenderer.on('ipc-example_stats', (data) => {
81 | this.setState({ videoActive: data.is_active == true } )
82 | this.setState({ buttonText: data.is_active == true ? 'stop video' : 'start video'} )
83 | this.setState({ packets: data.packet_cnt } )
84 | this.setState({ errors: data.err_cnt } )
85 | });
86 | window.electron.ipcRenderer.on('ipc-example_frame', (data) => {
87 | this.setState({ resolution: data.width + 'x' + data.height} )
88 | this.setState({ frame: data.data} )
89 | this.setState({ frameBytes: data.data.byteLength } )
90 | this.setState({ frameWidth: data.width } )
91 | this.setState({ frameHeight: data.height } )
92 | this.updateFrame()
93 | });
94 | }
95 | ```
96 | And when the class is no longer needed, we have to remove these listeners:
97 | ```
98 | componentWillUnmount() {
99 | window.electron.ipcRenderer.removeListener('ipc-example_stats')
100 | window.electron.ipcRenderer.removeListener('ipc-example_frame')
101 | }
102 | ```
103 | We may have noticed the this.updateFrame() method
104 | This is where the canvas is loaded with a video frame:
105 | ```
106 | updateFrame() {
107 | var canvas = document.getElementById("frameCanvas");
108 | var ctx = canvas.getContext("2d");
109 | var data = this.state.frame
110 | var len = this.state.frameBytes
111 | var frameHeight = this.state.frameHeight
112 | var frameWidth = this.state.frameWidth
113 | if(data == null || len == 0 || frameHeight == 0 || frameWidth == 0) return
114 |
115 | var imageData = ctx.createImageData(frameWidth, frameHeight);
116 | const data_img = imageData.data;
117 | var pixels = new Uint8Array(data)
118 | var i = 0; // cursor for RGBA buffer
119 | var t = 0; // cursor for RGB buffer
120 | var _len = data_img.length
121 | for(; i < _len; i += 4) {
122 | data_img[i] = pixels[t+2]
123 | data_img[i+1] = pixels[t+1]
124 | data_img[i+2] = pixels[t]
125 | data_img[i+3] = 255
126 | t += 4;
127 | }
128 | ctx.putImageData(imageData, 0, 0);
129 | }
130 | ```
131 | The UI will look like this:
132 | ```
133 | render() {
134 | return (
135 |
136 |
137 |
138 |
139 | {/*
140 | button enable/disable video
141 | */}
142 |
151 |
152 |
153 | {/*
154 | statistics
155 | */}
156 |
157 |
158 |
159 |
160 |
161 |
166 |
167 | );
168 | }
169 | }
170 | ```
171 | Now let's look at the native layer
172 | Most of the work is in it
173 | First time I thought it would be really hard
174 | Especially concerning linking and compiling libraries
175 | But it turned out to be quite simple, since the 'node-gyb build'
176 | does its job perfectly and there is not much difference compared to the bare cmake
177 |
178 | The entry point is "Init"
179 | In this place we create m_video and set the listeners
180 | We cannot send data to JS right away
181 | V8 imposes restrictions on access to threads
182 | Thus it is impossible to pass data from other thread to main without synchronization
183 | Thread-safe methods called Napi::ThreadSafeFunction are used for this task
184 |
185 | The strategy is to store the data from the callback into a queue
186 | And process this queue from Napi::ThreadSafeFunction:
187 | ```
188 | Napi::Object Init(Napi::Env env, Napi::Object exports) {
189 | m_video = new Video();
190 | m_video->setStatusCallBack(([&](VideStats stats) {
191 | if(threadCtx == NULL) return;
192 | std::lock_guardlk(threadCtx->m_data_lock);
193 | auto data = new DataItemStats();
194 | data->type = DataItemType::DataStats;
195 | data->stats = new VideStats();
196 | data->stats->is_active = stats.is_active;
197 | data->stats->packet_cnt = stats.packet_cnt;
198 | data->stats->err_cnt = stats.err_cnt;
199 | threadCtx->m_data_queue.push(data);
200 | threadCtx->m_data_cv.notify_one();
201 | }));
202 | m_video->setFrameCallBack(([&](AVFrame* frame, uint32_t bufSize) {
203 | if(frame != NULL) {
204 | std::lock_guardlk(threadCtx->m_data_lock);
205 | auto data = new DataItemFrame();
206 | data->type = DataItemType::DataFrame;
207 | data->frame = new uint8_t[bufSize];
208 | data->frame_buf_size = bufSize;
209 | data->width = frame->width;
210 | data->height = frame->height;
211 | memcpy(data->frame, (uint8_t*)frame->data[0], bufSize);
212 | threadCtx->m_data_queue.push(data);
213 | threadCtx->m_data_cv.notify_one();
214 | } else {
215 | std::cout << "frameCallback: frame == null" << std::endl;
216 | }
217 | }));
218 |
219 | exports["setCb"] = Napi::Function::New(env, setCallback, std::string("setCallback"));
220 | exports.Set(Napi::String::New(env, "setCameraEnabled"), Napi::Function::New(env, StartVideo));
221 | exports.Set(Napi::String::New(env, "setCameraDisable"), Napi::Function::New(env, StopVideo));
222 | exports.Set(Napi::String::New(env, "setDimention"), Napi::Function::New(env, SetDimention));
223 | ```
224 | Inside the queue, use these classes:
225 | ```
226 | class DataItem {
227 | public:
228 | DataItemType type;
229 | };
230 | ```
231 | And since we have different data types (frames, info)
232 | The best way is to extend derived classes
233 | ```
234 | class DataItemStats : public DataItem {
235 | public:
236 | VideStats* stats;
237 | };
238 | class DataItemFrame : public DataItem {
239 | public:
240 | uint8_t* frame;
241 | uint32_t frame_buf_size;
242 | int width;
243 | int height;
244 | };
245 | ```
246 | All data is collected inside one class for convenience:
247 | ```
248 | struct ThreadCtx {
249 | ThreadCtx(Napi::Env env) {};
250 | std::thread nativeThread;
251 | Napi::ThreadSafeFunction tsfn;
252 | bool toCancel = false;
253 | std::queue m_data_queue;
254 | std::mutex m_data_lock;
255 | std::condition_variable m_data_cv;
256 | };
257 | ```
258 | And the methods that were described above in - exports["setCb"]
259 | Must have an implementation:
260 | ```
261 | Napi::Value setCallback(const Napi::CallbackInfo& info) {
262 | auto env = info.Env();
263 | threadCtx = new ThreadCtx(env);
264 | // a safe function
265 | threadCtx->tsfn = Napi::ThreadSafeFunction::New(
266 | env,
267 | info[0].As(),
268 | "CallbackMethod",
269 | 0, 1 ,
270 | threadCtx,
271 | [&]( Napi::Env, void *finalizeData, ThreadCtx *context ) {
272 | threadCtx->nativeThread.join();
273 | },
274 | (void*)nullptr
275 | );
276 |
277 | // a thread for the queue
278 | // it calls threadCtx->tsfn.BlockingCall
279 | // and sends a json to js layer
280 | threadCtx->nativeThread = std::thread([&]{
281 | auto callbackStats = [](Napi::Env env, Napi::Function cb, char* buffer) {
282 | auto data = (DataItemStats*)buffer;
283 | if(data == NULL) return;
284 |
285 | Napi::Object obj = Napi::Object::New(env);
286 | obj.Set("type", std::string("stats"));
287 | obj.Set("is_active", std::to_string(data->stats->is_active));
288 | obj.Set("packet_cnt", std::to_string(data->stats->packet_cnt));
289 | obj.Set("err_cnt", std::to_string(data->stats->err_cnt));
290 | cb.Call({obj});
291 | delete data->stats;
292 | delete data;
293 | };
294 | auto callbackFrame = [](Napi::Env env, Napi::Function cb, char* buffer) {
295 | auto data = (DataItemFrame*)buffer;
296 | if(data == NULL) return;
297 |
298 | napi_value arrayBuffer;
299 | void* yourPointer = malloc(data->frame_buf_size);
300 | napi_create_arraybuffer(env, data->frame_buf_size, &yourPointer, &arrayBuffer);
301 | memcpy((uint8_t*)yourPointer, data->frame, data->frame_buf_size);
302 |
303 | Napi::Object obj = Napi::Object::New(env);
304 | obj.Set("type", std::string("frame"));
305 | obj.Set("data", arrayBuffer);
306 | obj.Set("width", data->width);
307 | obj.Set("height", data->height);
308 | cb.Call({obj});
309 | delete data->frame;
310 | delete data;
311 | };
312 | while(!threadCtx->toCancel) {
313 | DataItem* data_item = NULL;
314 | std::unique_lock lk(threadCtx->m_data_lock);
315 | threadCtx->m_data_cv.wait(lk, [&] {
316 | return !threadCtx->m_data_queue.empty();
317 | });
318 |
319 | while(!threadCtx->m_data_queue.empty()) {
320 | data_item = threadCtx->m_data_queue.front();
321 | threadCtx->m_data_queue.pop();
322 | if(data_item == NULL) continue;
323 |
324 | if(data_item->type == DataItemType::DataStats) {
325 | napi_status status = threadCtx->tsfn.BlockingCall((char*)data_item, callbackStats);
326 | if (status != napi_ok) {
327 | // Handle error
328 | break;
329 | }
330 | } else if(data_item->type == DataItemType::DataFrame) {
331 | napi_status status = threadCtx->tsfn.BlockingCall((char*)data_item, callbackFrame);
332 | if (status != napi_ok) {
333 | // Handle error
334 | break;
335 | }
336 | }
337 | }
338 | }
339 | threadCtx->tsfn.Release();
340 | });
341 | return Napi::String::New(info.Env(), std::string("SimpleAsyncWorker for seconds queued.").c_str());
342 | };
343 | ```
344 | And a couple of methods that don't need a queue:
345 | ```
346 | Napi::Boolean StartVideo(const Napi::CallbackInfo& info) {
347 | std::cout << "Command: startCamera\n";
348 | if(!m_video->isStarted()) {
349 | m_video->startVideoCamera();
350 | }
351 | Napi::Env env = info.Env();
352 | return Napi::Boolean::New(env, true);
353 | }
354 | Napi::Boolean StopVideo(const Napi::CallbackInfo& info) {
355 | std::cout << "Command: stopCamera\n";
356 | if(m_video->isStarted()) {
357 | m_video->stopVideo();
358 | }
359 | Napi::Env env = info.Env();
360 | return Napi::Boolean::New(env, true);
361 | }
362 | Napi::Value SetDimention(const Napi::CallbackInfo& info) {
363 | if(m_video == NULL || !m_video->isStarted()) {
364 | std::cout << "Command: setDimention -camera is not started!\n";
365 | } else if(info.Length() == 2) {
366 | int width = info[0].As().ToNumber();
367 | int height = info[1].As().ToNumber();;
368 | std::cout << "Command: setDimention: " << ",width=" << width << ",height=" << height << std::endl;
369 | m_video->setResolution(width, height);
370 | } else {
371 | std::cout << "Command: setDimention missed arguments\n";
372 | }
373 | return Napi::Number::New(info.Env(), true);
374 | }
375 | ```
376 | At the end should be this define
377 | NODE_API_MODULE(, ):
378 | ```
379 | NODE_API_MODULE(addon, Init)
380 | ```
381 | The c++ addon itself is included as a submodule and will be cloned automatically
382 | (https://github.com/khomin/electron_ffmpeg_addon_camera)
383 | But it has to be built independently
384 |
385 | Keep in mind
386 | The build ffmpeg is not in the repository (because of its relatively large size)
387 | You must build ffmpeg as a shared library
388 | And then edit the path in binding.gyp (src/native/binding.gyp)
389 | ```
390 | 'libraries': [
391 | '../src/ffmpeg_mac/lib/libavcodec.58.91.100.dylib',
392 | '../src/ffmpeg_mac/lib/libavdevice.58.10.100.dylib',
393 | '../src/ffmpeg_mac/lib/libavfilter.7.85.100.dylib',
394 | '../src/ffmpeg_mac/lib/libavformat.58.45.100.dylib',
395 | '../src/ffmpeg_mac/lib/libavutil.56.51.100.dylib',
396 | '../src/ffmpeg_mac/lib/libpostproc.55.7.100.dylib',
397 | '../src/ffmpeg_mac/lib/libswresample.3.7.100.dylib',
398 | '../src/ffmpeg_mac/lib/libswscale.5.7.100.dylib',
399 | ],
400 | ```
401 | The project is written on macos
402 | If you need Windows/Linux support, you must specify the appropriate methods for avformat_open_input
403 | You can see the exact location by this code
404 | ```
405 | const char* VideoSource::getDeviceFamily() {
406 | #ifdef _WIN32
407 | const char *device_family = "dshow";
408 | #elif __APPLE__
409 | const char *device_family = "avfoundation";
410 | #elif __linux__
411 | const char *device_family = "v4l2";
412 | #endif
413 | return device_family;
414 | }
415 | ```
416 | If you have any question you can contact me over email
417 | khominvladimir@yandex.ru
418 |
419 | Install
420 | Clone the repo and install dependencies:
421 | ```bash
422 | git clone --recursive https://github.com/khomin/electron_camera_ffmpeg.git
423 | cd ./electron_camera_ffmpeg
424 | npm install
425 | ```
426 | Then go to native submodule and build the native addon:
427 | ```bash
428 | cd ./src/native
429 | node-gyp configure
430 | node-gyp build
431 | ```
432 | ## License
433 | MIT
434 |
435 | ## Inspirational Projects
436 | [Electron React Boilerplate](https://github.com/electron-react-boilerplate)
437 |
438 | [Node-ffmpeg](https://github.com/luuvish/node-ffmpeg)
439 |
--------------------------------------------------------------------------------
/assets/assets.d.ts:
--------------------------------------------------------------------------------
1 | type Styles = Record;
2 |
3 | declare module '*.svg' {
4 | const content: string;
5 | export default content;
6 | }
7 |
8 | declare module '*.png' {
9 | const content: string;
10 | export default content;
11 | }
12 |
13 | declare module '*.jpg' {
14 | const content: string;
15 | export default content;
16 | }
17 |
18 | declare module '*.scss' {
19 | const content: Styles;
20 | export default content;
21 | }
22 |
23 | declare module '*.sass' {
24 | const content: Styles;
25 | export default content;
26 | }
27 |
28 | declare module '*.css' {
29 | const content: Styles;
30 | export default content;
31 | }
32 |
--------------------------------------------------------------------------------
/assets/entitlements.mac.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.cs.allow-unsigned-executable-memory
6 |
7 | com.apple.security.cs.allow-jit
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/assets/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khomin/electron_camera_ffmpeg/dc6b3c24308342eab9abf7593848df6d3fc70f4e/assets/icon.icns
--------------------------------------------------------------------------------
/assets/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khomin/electron_camera_ffmpeg/dc6b3c24308342eab9abf7593848df6d3fc70f4e/assets/icon.ico
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khomin/electron_camera_ffmpeg/dc6b3c24308342eab9abf7593848df6d3fc70f4e/assets/icon.png
--------------------------------------------------------------------------------
/assets/icon.svg:
--------------------------------------------------------------------------------
1 |
24 |
--------------------------------------------------------------------------------
/assets/icons/1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khomin/electron_camera_ffmpeg/dc6b3c24308342eab9abf7593848df6d3fc70f4e/assets/icons/1024x1024.png
--------------------------------------------------------------------------------
/assets/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khomin/electron_camera_ffmpeg/dc6b3c24308342eab9abf7593848df6d3fc70f4e/assets/icons/128x128.png
--------------------------------------------------------------------------------
/assets/icons/16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khomin/electron_camera_ffmpeg/dc6b3c24308342eab9abf7593848df6d3fc70f4e/assets/icons/16x16.png
--------------------------------------------------------------------------------
/assets/icons/24x24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khomin/electron_camera_ffmpeg/dc6b3c24308342eab9abf7593848df6d3fc70f4e/assets/icons/24x24.png
--------------------------------------------------------------------------------
/assets/icons/256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khomin/electron_camera_ffmpeg/dc6b3c24308342eab9abf7593848df6d3fc70f4e/assets/icons/256x256.png
--------------------------------------------------------------------------------
/assets/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khomin/electron_camera_ffmpeg/dc6b3c24308342eab9abf7593848df6d3fc70f4e/assets/icons/32x32.png
--------------------------------------------------------------------------------
/assets/icons/48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khomin/electron_camera_ffmpeg/dc6b3c24308342eab9abf7593848df6d3fc70f4e/assets/icons/48x48.png
--------------------------------------------------------------------------------
/assets/icons/512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khomin/electron_camera_ffmpeg/dc6b3c24308342eab9abf7593848df6d3fc70f4e/assets/icons/512x512.png
--------------------------------------------------------------------------------
/assets/icons/64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khomin/electron_camera_ffmpeg/dc6b3c24308342eab9abf7593848df6d3fc70f4e/assets/icons/64x64.png
--------------------------------------------------------------------------------
/assets/icons/96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khomin/electron_camera_ffmpeg/dc6b3c24308342eab9abf7593848df6d3fc70f4e/assets/icons/96x96.png
--------------------------------------------------------------------------------
/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khomin/electron_camera_ffmpeg/dc6b3c24308342eab9abf7593848df6d3fc70f4e/demo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "electron-react-boilerplate",
3 | "description": "A foundation for scalable desktop apps",
4 | "scripts": {
5 | "build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
6 | "build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
7 | "build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
8 | "rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
9 | "lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx",
10 | "package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never",
11 | "postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts && opencollective-postinstall",
12 | "start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run start:renderer",
13 | "start:main": "cross-env NODE_ENV=development electron -r ts-node/register/transpile-only ./src/main/main.ts",
14 | "start-main-debug": "yarn start-main-dev --inspect=5858 --remote-debugging-port=9223",
15 | "start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
16 | "test": "jest",
17 | "prepare": "husky install"
18 | },
19 | "lint-staged": {
20 | "*.{js,jsx,ts,tsx}": [
21 | "cross-env NODE_ENV=development eslint --cache"
22 | ],
23 | "*.json,.{eslintrc,prettierrc}": [
24 | "prettier --ignore-path .eslintignore --parser json --write"
25 | ],
26 | "*.{css,scss}": [
27 | "prettier --ignore-path .eslintignore --single-quote --write"
28 | ],
29 | "*.{html,md,yml}": [
30 | "prettier --ignore-path .eslintignore --single-quote --write"
31 | ]
32 | },
33 | "build": {
34 | "productName": "ElectronReact",
35 | "appId": "org.erb.ElectronReact",
36 | "asar": true,
37 | "asarUnpack": "**\\*.{node,dll}",
38 | "files": [
39 | "dist",
40 | "node_modules",
41 | "package.json"
42 | ],
43 | "afterSign": ".erb/scripts/notarize.js",
44 | "mac": {
45 | "target": {
46 | "target": "default",
47 | "arch": [
48 | "arm64",
49 | "x64"
50 | ]
51 | },
52 | "type": "distribution",
53 | "hardenedRuntime": true,
54 | "entitlements": "assets/entitlements.mac.plist",
55 | "entitlementsInherit": "assets/entitlements.mac.plist",
56 | "gatekeeperAssess": false
57 | },
58 | "dmg": {
59 | "contents": [
60 | {
61 | "x": 130,
62 | "y": 220
63 | },
64 | {
65 | "x": 410,
66 | "y": 220,
67 | "type": "link",
68 | "path": "/Applications"
69 | }
70 | ]
71 | },
72 | "win": {
73 | "target": [
74 | "nsis"
75 | ]
76 | },
77 | "linux": {
78 | "target": [
79 | "AppImage"
80 | ],
81 | "category": "Development"
82 | },
83 | "directories": {
84 | "app": "release/app",
85 | "buildResources": "assets",
86 | "output": "release/build"
87 | },
88 | "extraResources": [
89 | "./assets/**"
90 | ],
91 | "publish": {
92 | "provider": "github",
93 | "owner": "electron-react-boilerplate",
94 | "repo": "electron-react-boilerplate"
95 | }
96 | },
97 | "repository": {
98 | "type": "git",
99 | "url": "git+https://github.com/electron-react-boilerplate/electron-react-boilerplate.git"
100 | },
101 | "author": {
102 | "name": "Electron React Boilerplate Maintainers",
103 | "email": "electronreactboilerplate@gmail.com",
104 | "url": "https://electron-react-boilerplate.js.org"
105 | },
106 | "contributors": [
107 | {
108 | "name": "Amila Welihinda",
109 | "email": "amilajack@gmail.com",
110 | "url": "https://github.com/amilajack"
111 | },
112 | {
113 | "name": "John Tran",
114 | "email": "jptran318@gmail.com",
115 | "url": "https://github.com/jooohhn"
116 | }
117 | ],
118 | "license": "MIT",
119 | "bugs": {
120 | "url": "https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues"
121 | },
122 | "keywords": [
123 | "electron",
124 | "boilerplate",
125 | "react",
126 | "typescript",
127 | "ts",
128 | "sass",
129 | "webpack",
130 | "hot",
131 | "reload"
132 | ],
133 | "homepage": "https://github.com/electron-react-boilerplate/electron-react-boilerplate#readme",
134 | "jest": {
135 | "testURL": "http://localhost/",
136 | "testEnvironment": "jsdom",
137 | "transform": {
138 | "\\.(ts|tsx|js|jsx)$": "ts-jest"
139 | },
140 | "moduleNameMapper": {
141 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/.erb/mocks/fileMock.js",
142 | "\\.(css|less|sass|scss)$": "identity-obj-proxy"
143 | },
144 | "moduleFileExtensions": [
145 | "js",
146 | "jsx",
147 | "ts",
148 | "tsx",
149 | "json"
150 | ],
151 | "moduleDirectories": [
152 | "node_modules",
153 | "release/app/node_modules"
154 | ],
155 | "testPathIgnorePatterns": [
156 | "release/app/dist"
157 | ],
158 | "setupFiles": [
159 | "./.erb/scripts/check-build-exists.ts"
160 | ]
161 | },
162 | "devDependencies": {
163 | "@pmmmwh/react-refresh-webpack-plugin": "0.5.4",
164 | "@teamsupercell/typings-for-css-modules-loader": "^2.5.1",
165 | "@testing-library/jest-dom": "^5.16.1",
166 | "@testing-library/react": "^12.1.2",
167 | "@types/jest": "^27.0.3",
168 | "@types/node": "17.0.5",
169 | "@types/react": "^17.0.38",
170 | "@types/react-dom": "^17.0.11",
171 | "@types/react-test-renderer": "^17.0.1",
172 | "@types/terser-webpack-plugin": "^5.0.4",
173 | "@types/webpack-env": "^1.16.3",
174 | "@typescript-eslint/eslint-plugin": "^5.8.1",
175 | "@typescript-eslint/parser": "^5.8.1",
176 | "browserslist-config-erb": "^0.0.3",
177 | "chalk": "^4.1.2",
178 | "concurrently": "^6.5.1",
179 | "core-js": "^3.20.1",
180 | "cross-env": "^7.0.3",
181 | "css-loader": "^6.5.1",
182 | "css-minimizer-webpack-plugin": "^3.3.1",
183 | "detect-port": "^1.3.0",
184 | "electron": "^16.0.5",
185 | "electron-builder": "22.13.1",
186 | "electron-devtools-installer": "^3.2.0",
187 | "electron-notarize": "^1.1.1",
188 | "electron-rebuild": "^3.2.5",
189 | "eslint": "^8.5.0",
190 | "eslint-config-airbnb-base": "^15.0.0",
191 | "eslint-config-erb": "^4.0.3",
192 | "eslint-import-resolver-typescript": "^2.5.0",
193 | "eslint-import-resolver-webpack": "^0.13.2",
194 | "eslint-plugin-compat": "^4.0.0",
195 | "eslint-plugin-import": "^2.25.3",
196 | "eslint-plugin-jest": "^25.3.2",
197 | "eslint-plugin-jsx-a11y": "^6.5.1",
198 | "eslint-plugin-promise": "^6.0.0",
199 | "eslint-plugin-react": "^7.28.0",
200 | "eslint-plugin-react-hooks": "^4.3.0",
201 | "file-loader": "^6.2.0",
202 | "html-webpack-plugin": "^5.5.0",
203 | "husky": "^7.0.4",
204 | "identity-obj-proxy": "^3.0.0",
205 | "jest": "^27.4.5",
206 | "lint-staged": "^12.1.4",
207 | "mini-css-extract-plugin": "^2.4.5",
208 | "opencollective-postinstall": "^2.0.3",
209 | "prettier": "^2.5.1",
210 | "react-refresh": "^0.11.0",
211 | "react-refresh-typescript": "^2.0.3",
212 | "react-test-renderer": "^17.0.2",
213 | "rimraf": "^3.0.2",
214 | "sass": "^1.45.1",
215 | "sass-loader": "^12.4.0",
216 | "style-loader": "^3.3.1",
217 | "terser-webpack-plugin": "^5.3.0",
218 | "ts-jest": "^27.1.2",
219 | "ts-loader": "^9.2.6",
220 | "ts-node": "^10.4.0",
221 | "typescript": "^4.5.4",
222 | "url-loader": "^4.1.1",
223 | "webpack": "^5.65.0",
224 | "webpack-bundle-analyzer": "^4.5.0",
225 | "webpack-cli": "^4.9.1",
226 | "webpack-dev-server": "^4.7.1",
227 | "webpack-merge": "^5.8.0"
228 | },
229 | "dependencies": {
230 | "electron-debug": "^3.2.0",
231 | "electron-log": "^4.4.4",
232 | "electron-updater": "^4.6.5",
233 | "history": "^5.2.0",
234 | "react": "^17.0.2",
235 | "react-bootstrap": "^2.1.2",
236 | "react-dom": "^17.0.2",
237 | "react-router-dom": "^6.2.1"
238 | },
239 | "devEngines": {
240 | "node": ">=14.x",
241 | "npm": ">=7.x"
242 | },
243 | "collective": {
244 | "url": "https://opencollective.com/electron-react-boilerplate-594"
245 | },
246 | "browserslist": [],
247 | "prettier": {
248 | "overrides": [
249 | {
250 | "files": [
251 | ".prettierrc",
252 | ".eslintrc"
253 | ],
254 | "options": {
255 | "parser": "json"
256 | }
257 | }
258 | ],
259 | "singleQuote": true
260 | }
261 | }
262 |
--------------------------------------------------------------------------------
/release/app/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "electron-react-boilerplate",
3 | "version": "4.5.0",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "electron-react-boilerplate",
9 | "version": "4.5.0",
10 | "hasInstallScript": true,
11 | "license": "MIT"
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/release/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "electron-react-boilerplate",
3 | "version": "4.5.0",
4 | "description": "A foundation for scalable desktop apps",
5 | "main": "./dist/main/main.js",
6 | "author": {
7 | "name": "Electron React Boilerplate Maintainers",
8 | "email": "electronreactboilerplate@gmail.com",
9 | "url": "https://github.com/electron-react-boilerplate"
10 | },
11 | "scripts": {
12 | "electron-rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js",
13 | "link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts",
14 | "postinstall": "npm run electron-rebuild && npm run link-modules"
15 | },
16 | "dependencies": {},
17 | "license": "MIT"
18 | }
19 |
--------------------------------------------------------------------------------
/release/app/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/__tests__/App.test.tsx:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 | import { render } from '@testing-library/react';
3 | import App from '../renderer/App';
4 |
5 | describe('App', () => {
6 | it('should render', () => {
7 | expect(render()).toBeTruthy();
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/src/main/main.ts:
--------------------------------------------------------------------------------
1 | /* eslint global-require: off, no-console: off, promise/always-return: off */
2 |
3 | /**
4 | * This module executes inside of electron's main process. You can start
5 | * electron renderer process from here and communicate with the other processes
6 | * through IPC.
7 | *
8 | * When running `npm run build` or `npm run build:main`, this file is compiled to
9 | * `./src/main.js` using webpack. This gives us some performance wins.
10 | */
11 | import path from 'path';
12 | import { app, BrowserWindow, shell, ipcMain } from 'electron';
13 | import { autoUpdater } from 'electron-updater';
14 | import log from 'electron-log';
15 | import MenuBuilder from './menu';
16 | import { resolveHtmlPath } from './util';
17 |
18 | import addon from '../native/build/Release/hello';
19 |
20 | addon.setCb(function(data) {
21 | if(data.type == 'stats') {
22 | // var log = `test: js stats, is_active=${ data.is_active }, packet_cnt=${ data.packet_cnt }, err_cnt=${ data.err_cnt }`;
23 | // console.log(log);
24 | mainWindow.webContents.send('ipc-example_stats', data)
25 |
26 | } else if(data.type == 'frame') {
27 | // var log = `test: js frame, data=${ data.data.byteLength }`;
28 | // console.log(log);
29 |
30 | mainWindow.webContents.send('ipc-example_frame', data)
31 | }
32 | });
33 |
34 | export default class AppUpdater {
35 | constructor() {
36 | log.transports.file.level = 'info';
37 | autoUpdater.logger = log;
38 | autoUpdater.checkForUpdatesAndNotify();
39 | }
40 | }
41 |
42 | let mainWindow: BrowserWindow | null = null;
43 |
44 | ipcMain.on('ipc-example', async (event, arg) => {
45 | //console.log('ipcMain.on: = ' + "event=" + event + ", arg=" + arg)
46 | if(arg.type == 'startCamera') {
47 | addon.setCameraEnabled()
48 | } else if(arg.type == 'stopCamera') {
49 | addon.setCameraDisable()
50 | } else if(arg.type == 'setDimention') {
51 | addon.setDimention(arg.width, arg.height)
52 | }
53 | });
54 |
55 | if (process.env.NODE_ENV === 'production') {
56 | const sourceMapSupport = require('source-map-support');
57 | sourceMapSupport.install();
58 | }
59 |
60 | const isDevelopment =
61 | process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true';
62 |
63 | if (isDevelopment) {
64 | require('electron-debug')();
65 | }
66 |
67 | const installExtensions = async () => {
68 | const installer = require('electron-devtools-installer');
69 | const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
70 | const extensions = ['REACT_DEVELOPER_TOOLS'];
71 |
72 | return installer
73 | .default(
74 | extensions.map((name) => installer[name]),
75 | forceDownload
76 | )
77 | .catch(console.log);
78 | };
79 |
80 | const createWindow = async () => {
81 | if (isDevelopment) {
82 | await installExtensions();
83 | }
84 |
85 | const RESOURCES_PATH = app.isPackaged
86 | ? path.join(process.resourcesPath, 'assets')
87 | : path.join(__dirname, '../../assets');
88 |
89 | const getAssetPath = (...paths: string[]): string => {
90 | return path.join(RESOURCES_PATH, ...paths);
91 | };
92 |
93 | mainWindow = new BrowserWindow({
94 | show: false,
95 | width: 1024,
96 | height: 728,
97 | icon: getAssetPath('icon.png'),
98 | webPreferences: {
99 | preload: path.join(__dirname, 'preload.js'),
100 | },
101 | });
102 |
103 | mainWindow.loadURL(resolveHtmlPath('index.html'));
104 |
105 | mainWindow.on('ready-to-show', () => {
106 | if (!mainWindow) {
107 | throw new Error('"mainWindow" is not defined');
108 | }
109 | if (process.env.START_MINIMIZED) {
110 | mainWindow.minimize();
111 | } else {
112 | mainWindow.show();
113 | }
114 | });
115 |
116 | mainWindow.on('closed', () => {
117 | mainWindow = null;
118 | });
119 |
120 | const menuBuilder = new MenuBuilder(mainWindow);
121 | menuBuilder.buildMenu();
122 |
123 | // Open urls in the user's browser
124 | mainWindow.webContents.setWindowOpenHandler((edata) => {
125 | shell.openExternal(edata.url);
126 | return { action: 'deny' };
127 | });
128 |
129 | // Remove this if your app does not use auto updates
130 | // eslint-disable-next-line
131 | new AppUpdater();
132 | };
133 |
134 | /**
135 | * Add event listeners...
136 | */
137 |
138 | app.on('window-all-closed', () => {
139 | // Respect the OSX convention of having the application in memory even
140 | // after all windows have been closed
141 | if (process.platform !== 'darwin') {
142 | app.quit();
143 | }
144 | });
145 |
146 | app
147 | .whenReady()
148 | .then(() => {
149 | createWindow();
150 | app.on('activate', () => {
151 | // On macOS it's common to re-create a window in the app when the
152 | // dock icon is clicked and there are no other windows open.
153 | if (mainWindow === null) createWindow();
154 | });
155 | })
156 | .catch(console.log);
157 |
--------------------------------------------------------------------------------
/src/main/menu.ts:
--------------------------------------------------------------------------------
1 | import {
2 | app,
3 | Menu,
4 | shell,
5 | BrowserWindow,
6 | MenuItemConstructorOptions,
7 | } from 'electron';
8 |
9 | interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions {
10 | selector?: string;
11 | submenu?: DarwinMenuItemConstructorOptions[] | Menu;
12 | }
13 |
14 | console.log("test: type=" + process.env.NODE_ENV) // dev
15 |
16 | export default class MenuBuilder {
17 | mainWindow: BrowserWindow;
18 |
19 | constructor(mainWindow: BrowserWindow) {
20 | this.mainWindow = mainWindow;
21 | }
22 |
23 | buildMenu(): Menu {
24 | if (
25 | process.env.NODE_ENV === 'development' ||
26 | process.env.DEBUG_PROD === 'true'
27 | ) {
28 | this.setupDevelopmentEnvironment();
29 | }
30 |
31 | const template =
32 | process.platform === 'darwin'
33 | ? this.buildDarwinTemplate()
34 | : this.buildDefaultTemplate();
35 |
36 | const menu = Menu.buildFromTemplate(template);
37 | Menu.setApplicationMenu(menu);
38 |
39 | return menu;
40 | }
41 |
42 | setupDevelopmentEnvironment(): void {
43 | this.mainWindow.webContents.on('context-menu', (_, props) => {
44 | const { x, y } = props;
45 |
46 | Menu.buildFromTemplate([
47 | {
48 | label: 'Inspect element',
49 | click: () => {
50 | this.mainWindow.webContents.inspectElement(x, y);
51 | },
52 | },
53 | ]).popup({ window: this.mainWindow });
54 | });
55 | }
56 |
57 | buildDarwinTemplate(): MenuItemConstructorOptions[] {
58 | const subMenuAbout: DarwinMenuItemConstructorOptions = {
59 | label: 'Electron',
60 | submenu: [
61 | {
62 | label: 'About ElectronReact',
63 | selector: 'orderFrontStandardAboutPanel:',
64 | },
65 | { type: 'separator' },
66 | { label: 'Services', submenu: [] },
67 | { type: 'separator' },
68 | {
69 | label: 'Hide ElectronReact',
70 | accelerator: 'Command+H',
71 | selector: 'hide:',
72 | },
73 | {
74 | label: 'Hide Others',
75 | accelerator: 'Command+Shift+H',
76 | selector: 'hideOtherApplications:',
77 | },
78 | { label: 'Show All', selector: 'unhideAllApplications:' },
79 | { type: 'separator' },
80 | {
81 | label: 'Quit',
82 | accelerator: 'Command+Q',
83 | click: () => {
84 | app.quit();
85 | },
86 | },
87 | ],
88 | };
89 | const subMenuEdit: DarwinMenuItemConstructorOptions = {
90 | label: 'Edit',
91 | submenu: [
92 | { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' },
93 | { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' },
94 | { type: 'separator' },
95 | { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' },
96 | { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' },
97 | { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' },
98 | {
99 | label: 'Select All',
100 | accelerator: 'Command+A',
101 | selector: 'selectAll:',
102 | },
103 | ],
104 | };
105 | const subMenuViewDev: MenuItemConstructorOptions = {
106 | label: 'View',
107 | submenu: [
108 | {
109 | label: 'Reload',
110 | accelerator: 'Command+R',
111 | click: () => {
112 | this.mainWindow.webContents.reload();
113 | },
114 | },
115 | {
116 | label: 'Toggle Full Screen',
117 | accelerator: 'Ctrl+Command+F',
118 | click: () => {
119 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
120 | },
121 | },
122 | {
123 | label: 'Toggle Developer Tools',
124 | accelerator: 'Alt+Command+I',
125 | click: () => {
126 | this.mainWindow.webContents.toggleDevTools();
127 | },
128 | },
129 | ],
130 | };
131 | const subMenuViewProd: MenuItemConstructorOptions = {
132 | label: 'View',
133 | submenu: [
134 | {
135 | label: 'Toggle Full Screen',
136 | accelerator: 'Ctrl+Command+F',
137 | click: () => {
138 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
139 | },
140 | },
141 | ],
142 | };
143 | const subMenuWindow: DarwinMenuItemConstructorOptions = {
144 | label: 'Window',
145 | submenu: [
146 | {
147 | label: 'Minimize',
148 | accelerator: 'Command+M',
149 | selector: 'performMiniaturize:',
150 | },
151 | { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' },
152 | { type: 'separator' },
153 | { label: 'Bring All to Front', selector: 'arrangeInFront:' },
154 | ],
155 | };
156 | const subMenuHelp: MenuItemConstructorOptions = {
157 | label: 'Help',
158 | submenu: [
159 | {
160 | label: 'Learn More',
161 | click() {
162 | shell.openExternal('https://electronjs.org');
163 | },
164 | },
165 | {
166 | label: 'Documentation',
167 | click() {
168 | shell.openExternal(
169 | 'https://github.com/electron/electron/tree/main/docs#readme'
170 | );
171 | },
172 | },
173 | {
174 | label: 'Community Discussions',
175 | click() {
176 | shell.openExternal('https://www.electronjs.org/community');
177 | },
178 | },
179 | {
180 | label: 'Search Issues',
181 | click() {
182 | shell.openExternal('https://github.com/electron/electron/issues');
183 | },
184 | },
185 | ],
186 | };
187 |
188 | const subMenuView =
189 | process.env.NODE_ENV === 'development' ||
190 | process.env.DEBUG_PROD === 'true'
191 | ? subMenuViewDev
192 | : subMenuViewProd;
193 |
194 | return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp];
195 | }
196 |
197 | buildDefaultTemplate() {
198 | const templateDefault = [
199 | {
200 | label: '&File',
201 | submenu: [
202 | {
203 | label: '&Open',
204 | accelerator: 'Ctrl+O',
205 | },
206 | {
207 | label: '&Close',
208 | accelerator: 'Ctrl+W',
209 | click: () => {
210 | this.mainWindow.close();
211 | },
212 | },
213 | ],
214 | },
215 | {
216 | label: '&View',
217 | submenu:
218 | process.env.NODE_ENV === 'development' ||
219 | process.env.DEBUG_PROD === 'true'
220 | ? [
221 | {
222 | label: '&Reload',
223 | accelerator: 'Ctrl+R',
224 | click: () => {
225 | this.mainWindow.webContents.reload();
226 | },
227 | },
228 | {
229 | label: 'Toggle &Full Screen',
230 | accelerator: 'F11',
231 | click: () => {
232 | this.mainWindow.setFullScreen(
233 | !this.mainWindow.isFullScreen()
234 | );
235 | },
236 | },
237 | {
238 | label: 'Toggle &Developer Tools',
239 | accelerator: 'Alt+Ctrl+I',
240 | click: () => {
241 | this.mainWindow.webContents.toggleDevTools();
242 | },
243 | },
244 | ]
245 | : [
246 | {
247 | label: 'Toggle &Full Screen',
248 | accelerator: 'F11',
249 | click: () => {
250 | this.mainWindow.setFullScreen(
251 | !this.mainWindow.isFullScreen()
252 | );
253 | },
254 | },
255 | ],
256 | },
257 | {
258 | label: 'Help',
259 | submenu: [
260 | {
261 | label: 'Learn More',
262 | click() {
263 | shell.openExternal('https://electronjs.org');
264 | },
265 | },
266 | {
267 | label: 'Documentation',
268 | click() {
269 | shell.openExternal(
270 | 'https://github.com/electron/electron/tree/main/docs#readme'
271 | );
272 | },
273 | },
274 | {
275 | label: 'Community Discussions',
276 | click() {
277 | shell.openExternal('https://www.electronjs.org/community');
278 | },
279 | },
280 | {
281 | label: 'Search Issues',
282 | click() {
283 | shell.openExternal('https://github.com/electron/electron/issues');
284 | },
285 | },
286 | ],
287 | },
288 | ];
289 |
290 | return templateDefault;
291 | }
292 | }
293 |
--------------------------------------------------------------------------------
/src/main/preload.js:
--------------------------------------------------------------------------------
1 | const { contextBridge, ipcRenderer } = require('electron');
2 |
3 | var addon = require('../native/build/Release/hello.node')
4 |
5 | contextBridge.exposeInMainWorld('electron', {
6 | ipcRenderer: {
7 | stopCamera() {
8 | var command = []
9 | command.type = 'stopCamera'
10 | ipcRenderer.send('ipc-example', command);
11 | },
12 | startCamera() {
13 | var command = []
14 | command.type = 'startCamera'
15 | ipcRenderer.send('ipc-example', command);
16 | },
17 | setDimention(width, height) {
18 | var command = []
19 | command.type = 'setDimention'
20 | command.width = width
21 | command.height = height
22 | ipcRenderer.send('ipc-example', command);
23 | },
24 | on(channel, func) {
25 | const validChannels = ['ipc-example', 'ipc-example_stats', 'ipc-example_frame'];
26 | if (validChannels.includes(channel)) {
27 | // Deliberately strip event as it includes `sender`
28 | ipcRenderer.on(channel, (event, ...args) => func(...args));
29 | }
30 | },
31 | once(channel, func) {
32 | const validChannels = ['ipc-example'];
33 | if (validChannels.includes(channel)) {
34 | // Deliberately strip event as it includes `sender`
35 | ipcRenderer.once(channel, (event, ...args) => func(...args));
36 | }
37 | },
38 | removeListener(channel) {
39 | ipcRenderer.removeAllListeners(channel)
40 | }
41 | },
42 | });
43 |
--------------------------------------------------------------------------------
/src/main/util.ts:
--------------------------------------------------------------------------------
1 | /* eslint import/prefer-default-export: off, import/no-mutable-exports: off */
2 | import { URL } from 'url';
3 | import path from 'path';
4 |
5 | export let resolveHtmlPath: (htmlFileName: string) => string;
6 |
7 | if (process.env.NODE_ENV === 'development') {
8 | const port = process.env.PORT || 1212;
9 | resolveHtmlPath = (htmlFileName: string) => {
10 | const url = new URL(`http://localhost:${port}`);
11 | url.pathname = htmlFileName;
12 | return url.href;
13 | };
14 | } else {
15 | resolveHtmlPath = (htmlFileName: string) => {
16 | return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`;
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/src/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "src",
3 | "lockfileVersion": 2,
4 | "requires": true,
5 | "packages": {
6 | "": {
7 | "dependencies": {
8 | "node-addon-api": "^4.3.0"
9 | }
10 | },
11 | "node_modules/node-addon-api": {
12 | "version": "4.3.0",
13 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
14 | "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="
15 | }
16 | },
17 | "dependencies": {
18 | "node-addon-api": {
19 | "version": "4.3.0",
20 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
21 | "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "node-addon-api": "^4.3.0"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/renderer/App.css:
--------------------------------------------------------------------------------
1 | /*
2 | * @NOTE: Prepend a `~` to css file paths that are in your node_modules
3 | * See https://github.com/webpack-contrib/sass-loader#imports
4 | */
5 | body {
6 | color: black;
7 | height: 100vh;
8 | background-color: black;
9 | font-family: sans-serif;
10 | overflow-y: hidden;
11 | display: flex;
12 | justify-content: center;
13 | align-items: center;
14 | }
15 |
16 | button {
17 | background-color: white;
18 | padding: 10px 20px;
19 | border-radius: 10px;
20 | border: none;
21 | appearance: none;
22 | font-size: 1.3rem;
23 | box-shadow: 0px 8px 28px -6px rgba(24, 39, 75, 0.12),
24 | 0px 18px 88px -4px rgba(24, 39, 75, 0.14);
25 | transition: all ease-in 0.1s;
26 | cursor: pointer;
27 | opacity: 0.9;
28 | }
29 |
30 | button:hover {
31 | transform: scale(1.05);
32 | opacity: 1;
33 | }
34 |
35 | li {
36 | list-style: none;
37 | }
38 |
39 | a {
40 | text-decoration: none;
41 | height: fit-content;
42 | width: fit-content;
43 | margin: 10px;
44 | }
45 |
46 | a:hover {
47 | opacity: 1;
48 | text-decoration: none;
49 | }
50 |
51 | .Hello {
52 | display: flex;
53 | justify-content: center;
54 | align-items: center;
55 | margin: 20px 0;
56 | }
57 |
58 | .text_caption {
59 | color: white;
60 | align-items: center;
61 | display: flex;
62 | margin-left: 8px;
63 | }
64 |
65 | .status {
66 | display: flex;
67 | align-self: center;
68 | margin-left: 8px;
69 | width: 32px;
70 | height: 32px;
71 | background-color: red;
72 | border-radius: 50%;
73 | }
74 |
75 | .topPanel {
76 | height: 40px;
77 | width: auto;
78 | color: #233F67;
79 | display: flex;
80 | background-color: #233F67;
81 | position: fixed;
82 | top: 0;
83 | right: 0;
84 | left: 0;
85 | width: auto;
86 | justify-content: left;
87 | }
88 |
89 | .button {
90 | height: 40px;
91 | width: auto;
92 | display: flex;
93 | margin-left: 8px;
94 | text-align: center;
95 | }
96 |
97 | .videoFrame {
98 | display: flex;
99 | position: fixed;
100 | top: 90px;
101 | bottom: 0;
102 | left: 0;
103 | right: 0
104 | }
105 |
106 | .stats {
107 | height: 50px;
108 | width: auto;
109 | display: flex;
110 | align-self: center;
111 | background-color: #252525;
112 | position: fixed;
113 | top: 40px;
114 | right: 0;
115 | left: 0;
116 | }
--------------------------------------------------------------------------------
/src/renderer/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button, Col, Row } from 'react-bootstrap';
3 | import './App.css';
4 |
5 | export default class Root extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | videoActive: false,
10 | buttonText: 'Start video',
11 | packets: 0,
12 | errors: 0,
13 | resolution: 0,
14 | frame: null,
15 | frameBytes: 0,
16 | frameWidth: 1000,
17 | frameHeight: 1000
18 | };
19 | }
20 |
21 | componentDidMount() {
22 | window.electron.ipcRenderer.on('ipc-example_stats', (data) => {
23 | this.setState({ videoActive: data.is_active == true } )
24 | this.setState({ buttonText: data.is_active == true ? 'stop video' : 'start video'} )
25 | this.setState({ packets: data.packet_cnt } )
26 | this.setState({ errors: data.err_cnt } )
27 | // var log = `test: ui stats, is_active=${ data.is_active }, packet_cnt=${ data.packet_cnt }, err_cnt=${ data.err_cnt }`;
28 | // console.log(log);
29 | });
30 | window.electron.ipcRenderer.on('ipc-example_frame', (data) => {
31 | this.setState({ resolution: data.width + 'x' + data.height} )
32 | this.setState({ frame: data.data} )
33 | this.setState({ frameBytes: data.data.byteLength } )
34 | this.setState({ frameWidth: data.width } )
35 | this.setState({ frameHeight: data.height } )
36 | this.updateFrame()
37 | //var log = `test: ui frame, width=${data.width}, height=${data.height}, data=${data.data.byteLength}`;
38 | //console.log(log);
39 | });
40 | }
41 |
42 | componentWillUnmount() {
43 | window.electron.ipcRenderer.removeListener('ipc-example_stats')
44 | window.electron.ipcRenderer.removeListener('ipc-example_frame')
45 | }
46 |
47 | updateFrame() {
48 | var canvas = document.getElementById("frameCanvas");
49 | var ctx = canvas.getContext("2d");
50 |
51 | // const ctx = canvasRef.getContext('2d')
52 | var data = this.state.frame
53 | var len = this.state.frameBytes
54 | var frameHeight = this.state.frameHeight
55 | var frameWidth = this.state.frameWidth
56 | if(data == null || len == 0 || frameHeight == 0 || frameWidth == 0) return
57 |
58 | var imageData = ctx.createImageData(frameWidth, frameHeight);
59 | const data_img = imageData.data;
60 | var pixels = new Uint8Array(data)
61 | var i = 0; // cursor for RGBA buffer
62 | var t = 0; // cursor for RGB buffer
63 | var _len = data_img.length
64 | for(; i < _len; i += 4) {
65 | data_img[i] = pixels[t+2]
66 | data_img[i+1] = pixels[t+1]
67 | data_img[i+2] = pixels[t]
68 | data_img[i+3] = 255
69 | t += 4;
70 | }
71 | ctx.putImageData(imageData, 0, 0);
72 | }
73 |
74 | render() {
75 | return (
76 |
77 |
78 |
79 |
80 | {/*
81 | button enable/disable video
82 | */}
83 |
92 |
93 | {/*
94 | button 1920x1080
95 | */}
96 | { this.state.videoActive &&
97 |
102 | }
103 | {/*
104 | button 1280x1024
105 | */}
106 | { this.state.videoActive &&
107 |
112 | }
113 |
114 |
115 | {/*
116 | statistics
117 | */}
118 |
119 |
120 |
121 |
122 |
123 |
128 |
129 | );
130 | }
131 | }
--------------------------------------------------------------------------------
/src/renderer/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 | Hello Electron React!
10 |
11 |
12 |
13 |
14 |
17 |
18 |
--------------------------------------------------------------------------------
/src/renderer/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import App from './App';
4 |
5 | render(, document.getElementById('root'));
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2021",
4 | "module": "commonjs",
5 | "lib": ["dom", "esnext"],
6 | "declaration": true,
7 | "declarationMap": true,
8 | "jsx": "react-jsx",
9 | "strict": true,
10 | "pretty": true,
11 | "sourceMap": true,
12 | "baseUrl": "./src",
13 | "noUnusedLocals": true,
14 | "noUnusedParameters": true,
15 | "noImplicitReturns": true,
16 | "noFallthroughCasesInSwitch": true,
17 | "moduleResolution": "node",
18 | "esModuleInterop": true,
19 | "allowSyntheticDefaultImports": true,
20 | "resolveJsonModule": true,
21 | "allowJs": true,
22 | "outDir": "release/app/dist"
23 | },
24 | "exclude": ["test", "release/build", "release/app/dist", ".erb/dll"]
25 | }
26 |
--------------------------------------------------------------------------------