├── .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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 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 | ![demo](https://github.com/khomin/electron_camera_ffmpeg/blob/master/demo.png) 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 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 | --------------------------------------------------------------------------------