├── .electron-vue ├── build.js ├── dev-client.js ├── dev-main.js ├── dev-runner.js ├── webpack.main.config.js └── webpack.renderer.config.js ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── appveyor.yml ├── cmake-tool ├── appCMakeJSConfig.js ├── buildSystem.js ├── cMake.js ├── cmLog.js ├── cpp │ └── win_delay_load_hook.cc ├── electron-abi.js ├── environment.js ├── index.js ├── node-addon.cmake ├── processHelpers.js ├── targetOptions.js ├── toolset.js └── vsDetect.js ├── ffplay.png ├── licenses └── FFmpeg.md ├── node-ffplay ├── CMakeLists.txt └── src │ ├── player.cc │ ├── player.h │ └── wrap.cc ├── package.json ├── resources └── icons │ ├── 256x256.png │ ├── icon.icns │ └── icon.ico ├── src ├── components │ ├── app-bar.vue │ ├── app.vue │ ├── dialogs.vue │ └── xplayer │ │ ├── controller.vue │ │ ├── detection-canvas.js │ │ ├── index.vue │ │ ├── info-panel.vue │ │ ├── loading.vue │ │ ├── play-list.vue │ │ ├── settings.vue │ │ ├── test_decode.js │ │ ├── video.vue │ │ └── yuv-canvas.js ├── ffplay.js ├── i18n │ ├── en.yml │ ├── index.js │ └── zh-cn.yml ├── import-dll.js ├── index.ejs ├── index.js ├── main │ ├── index.dev.js │ └── index.js └── store │ ├── index.js │ └── video.js └── test ├── e2e ├── index.js ├── specs │ └── Launch.spec.js └── utils.js └── unit ├── index.js ├── karma.conf.js └── specs └── LandingPage.spec.js /.electron-vue/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | const chalk = require('chalk') 6 | const del = require('del') 7 | const webpack = require('webpack') 8 | const Multispinner = require('multispinner') 9 | 10 | const mainConfig = require('./webpack.main.config') 11 | const rendererConfig = require('./webpack.renderer.config') 12 | 13 | const doneLog = chalk.bgGreen.white(' DONE ') + ' ' 14 | const errorLog = chalk.bgRed.white(' ERROR ') + ' ' 15 | const okayLog = chalk.bgBlue.white(' OKAY ') + ' ' 16 | 17 | if (process.env.BUILD_TARGET === 'clean') clean() 18 | else build() 19 | 20 | function clean () { 21 | del.sync(['release/*']) 22 | console.log(`\n${doneLog}\n`) 23 | process.exit() 24 | } 25 | 26 | function build () { 27 | 28 | del.sync(['dist/*', '!.gitkeep']) 29 | 30 | const tasks = ['main', 'renderer'] 31 | const m = new Multispinner(tasks, { 32 | preText: 'building', 33 | postText: 'process' 34 | }) 35 | 36 | let results = '' 37 | 38 | m.on('success', () => { 39 | process.stdout.write('\x1B[2J\x1B[0f') 40 | console.log(`\n\n${results}`) 41 | console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`) 42 | process.exit() 43 | }) 44 | 45 | pack(mainConfig).then(result => { 46 | results += result + '\n\n' 47 | m.success('main') 48 | }).catch(err => { 49 | m.error('main') 50 | console.log(`\n ${errorLog}failed to build main process`) 51 | console.error(`\n${err}\n`) 52 | process.exit(1) 53 | }) 54 | 55 | pack(rendererConfig).then(result => { 56 | results += result + '\n\n' 57 | m.success('renderer') 58 | }).catch(err => { 59 | m.error('renderer') 60 | console.log(`\n ${errorLog}failed to build renderer process`) 61 | console.error(`\n${err}\n`) 62 | process.exit(1) 63 | }) 64 | } 65 | 66 | function pack (config) { 67 | return new Promise((resolve, reject) => { 68 | config.mode = 'production' 69 | webpack(config, (err, stats) => { 70 | if (err) reject(err.stack || err) 71 | else if (stats.hasErrors()) { 72 | let err = '' 73 | 74 | stats.toString({ 75 | chunks: false, 76 | colors: true 77 | }) 78 | .split(/\r?\n/) 79 | .forEach(line => { 80 | err += ` ${line}\n` 81 | }) 82 | 83 | reject(err) 84 | } else { 85 | resolve(stats.toString({ 86 | chunks: false, 87 | colors: true 88 | })) 89 | } 90 | }) 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /.electron-vue/dev-client.js: -------------------------------------------------------------------------------- 1 | const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 2 | 3 | hotClient.subscribe(event => { 4 | /** 5 | * Reload browser when HTMLWebpackPlugin emits a new index.html 6 | * 7 | * Currently disabled until jantimon/html-webpack-plugin#680 is resolved. 8 | * https://github.com/SimulatedGREG/electron-vue/issues/437 9 | * https://github.com/jantimon/html-webpack-plugin/issues/680 10 | */ 11 | // if (event.action === 'reload') { 12 | // window.location.reload() 13 | // } 14 | 15 | /** 16 | * Notify `mainWindow` when `main` process is compiling, 17 | * giving notice for an expected reload of the `electron` process 18 | */ 19 | if (event.action === 'compiling') { 20 | document.body.innerHTML += ` 21 | 34 | 35 |
36 | Compiling Main Process... 37 |
38 | ` 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /.electron-vue/dev-main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is used specifically and only for development. It installs 3 | * `electron-debug` & `vue-devtools`. There shouldn't be any need to 4 | * modify this file, but it can be used to extend your development 5 | * environment. 6 | */ 7 | 8 | /* eslint-disable */ 9 | 10 | process.env.NODE_ENV = 'development'; 11 | 12 | // Install `electron-debug` with `devtron` 13 | require('electron-debug')() 14 | 15 | // Install `vue-devtools` 16 | require('electron').app.on('ready', () => { 17 | let installExtension = require('electron-devtools-installer') 18 | installExtension.default(installExtension.VUEJS_DEVTOOLS) 19 | .then(() => {}) 20 | .catch(err => { 21 | console.log('Unable to install `vue-devtools`: \n', err) 22 | }) 23 | }) 24 | 25 | // Require `main` process to boot app 26 | require('../src/main') 27 | -------------------------------------------------------------------------------- /.electron-vue/dev-runner.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const chalk = require('chalk') 4 | const electron = require('electron') 5 | const path = require('path') 6 | const { spawn } = require('child_process') 7 | const webpack = require('webpack') 8 | const WebpackDevServer = require('webpack-dev-server') 9 | const webpackHotMiddleware = require('webpack-hot-middleware') 10 | 11 | const mainConfig = require('./webpack.main.config') 12 | const rendererConfig = require('./webpack.renderer.config') 13 | 14 | let electronProcess = null 15 | let manualRestart = false 16 | let hotMiddleware 17 | 18 | function logStats (proc, data) { 19 | let log = '' 20 | 21 | log += chalk.yellow.bold(`┏ ${proc} Process ${new Array((19 - proc.length) + 1).join('-')}`) 22 | log += '\n\n' 23 | 24 | if (typeof data === 'object') { 25 | data.toString({ 26 | colors: true, 27 | chunks: false 28 | }).split(/\r?\n/).forEach(line => { 29 | log += ' ' + line + '\n' 30 | }) 31 | } else { 32 | log += ` ${data}\n` 33 | } 34 | 35 | log += '\n' + chalk.yellow.bold(`┗ ${new Array(28 + 1).join('-')}`) + '\n' 36 | 37 | console.log(log) 38 | } 39 | 40 | function startMain () { 41 | return new Promise((resolve, reject) => { 42 | mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main) 43 | mainConfig.mode = 'development' 44 | const compiler = webpack(mainConfig) 45 | 46 | compiler.hooks.watchRun.tapAsync('watch-run', (compilation, done) => { 47 | logStats('Main', chalk.white.bold('compiling...')) 48 | hotMiddleware.publish({ action: 'compiling' }) 49 | done() 50 | }) 51 | 52 | compiler.watch({}, (err, stats) => { 53 | if (err) { 54 | console.log(err) 55 | return 56 | } 57 | 58 | logStats('Main', stats) 59 | 60 | if (electronProcess && electronProcess.kill) { 61 | manualRestart = true 62 | process.kill(electronProcess.pid) 63 | electronProcess = null 64 | startElectron() 65 | 66 | setTimeout(() => { 67 | manualRestart = false 68 | }, 5000) 69 | } 70 | 71 | resolve() 72 | }) 73 | }) 74 | } 75 | 76 | function startMainForTest () { 77 | return new Promise((resolve, reject) => { 78 | mainConfig.entry.main = path.join(__dirname, '../src/main/test') 79 | mainConfig.mode = 'development' 80 | const compiler = webpack(mainConfig) 81 | 82 | compiler.hooks.watchRun.tapAsync('watch-run', (compilation, done) => { 83 | logStats('Main', chalk.white.bold('compiling...')) 84 | done() 85 | }) 86 | 87 | compiler.watch({}, (err, stats) => { 88 | if (err) { 89 | console.log(err) 90 | return 91 | } 92 | 93 | logStats('Main', stats) 94 | resolve() 95 | }) 96 | }) 97 | } 98 | 99 | function startRenderer () { 100 | return new Promise((resolve, reject) => { 101 | rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer) 102 | rendererConfig.mode = 'development' 103 | const compiler = webpack(rendererConfig) 104 | hotMiddleware = webpackHotMiddleware(compiler, { 105 | log: false, 106 | heartbeat: 2500 107 | }) 108 | 109 | compiler.hooks.compilation.tap('compilation', compilation => { 110 | compilation.hooks.htmlWebpackPluginAfterEmit.tapAsync('html-webpack-plugin-after-emit', (data, cb) => { 111 | hotMiddleware.publish({ action: 'reload' }) 112 | cb() 113 | }) 114 | }) 115 | 116 | compiler.hooks.done.tap('done', stats => { 117 | logStats('Renderer', stats) 118 | }) 119 | 120 | const server = new WebpackDevServer( 121 | compiler, 122 | { 123 | contentBase: path.join(__dirname, '../'), 124 | quiet: true, 125 | before (app, ctx) { 126 | app.use(hotMiddleware) 127 | ctx.middleware.waitUntilValid(() => { 128 | resolve() 129 | }) 130 | } 131 | } 132 | ) 133 | 134 | server.listen(9080) 135 | }) 136 | } 137 | 138 | function startElectron () { 139 | var args = [ 140 | '--inspect=5858', 141 | path.join(__dirname, '../dist/electron/main.js') 142 | ] 143 | 144 | // detect yarn or npm and process commandline args accordingly 145 | if (process.env.npm_execpath.endsWith('yarn.js')) { 146 | args = args.concat(process.argv.slice(3)) 147 | } else if (process.env.npm_execpath.endsWith('npm-cli.js')) { 148 | args = args.concat(process.argv.slice(2)) 149 | } 150 | 151 | electronProcess = spawn(electron, args) 152 | 153 | electronProcess.stdout.on('data', data => { 154 | electronLog(data, 'blue') 155 | }) 156 | electronProcess.stderr.on('data', data => { 157 | electronLog(data, 'red') 158 | }) 159 | 160 | electronProcess.on('close', () => { 161 | if (!manualRestart) process.exit() 162 | }) 163 | } 164 | 165 | function electronLog (data, color) { 166 | let log = '' 167 | data = data.toString().split(/\r?\n/) 168 | data.forEach(line => { 169 | log += ` ${line}\n` 170 | }) 171 | if (/[0-9A-z]+/.test(log)) { 172 | console.log( 173 | chalk[color].bold('┏ Electron -------------------') + 174 | '\n\n' + 175 | log + 176 | chalk[color].bold('┗ ----------------------------') + 177 | '\n' 178 | ) 179 | } 180 | } 181 | 182 | function init () { 183 | 184 | startRenderer() 185 | .then(() => startMain()) 186 | .then(() => startElectron()) 187 | .catch(err => { 188 | console.error(err) 189 | }) 190 | } 191 | 192 | function initForTest () { 193 | startMainForTest() 194 | .then(() => startElectron()) 195 | .catch(err => { 196 | console.error(err) 197 | }) 198 | } 199 | 200 | if (process.argv[2] == 'test-main') { 201 | initForTest() 202 | } else { 203 | init() 204 | } 205 | -------------------------------------------------------------------------------- /.electron-vue/webpack.main.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'main' 4 | 5 | const path = require('path') 6 | const { dependencies } = require('../package.json') 7 | const webpack = require('webpack') 8 | 9 | const MinifyPlugin = require("babel-minify-webpack-plugin") 10 | 11 | let mainConfig = { 12 | entry: { 13 | main: path.join(__dirname, '../src/main') 14 | }, 15 | externals: [ 16 | ...Object.keys(dependencies || {}) 17 | ], 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.(js)$/, 22 | enforce: 'pre', 23 | exclude: /node_modules/, 24 | use: { 25 | loader: 'eslint-loader', 26 | options: { 27 | formatter: require('eslint-friendly-formatter') 28 | } 29 | } 30 | }, 31 | { 32 | test: /\.js$/, 33 | exclude: /node_modules/, 34 | use: [ 35 | { 36 | loader: 'babel-loader', 37 | options: { 38 | presets: [['@babel/env', { 39 | 'targets': { 40 | node: 8 41 | } 42 | }]], 43 | plugins: ['@babel/plugin-proposal-class-properties', '@babel/plugin-transform-runtime'], 44 | sourceType: 'unambiguous' 45 | } 46 | } 47 | ] 48 | }, 49 | { 50 | test: /\.node$/, 51 | use: 'node-loader' 52 | } 53 | ] 54 | }, 55 | node: { 56 | __dirname: process.env.NODE_ENV !== 'production', 57 | __filename: process.env.NODE_ENV !== 'production' 58 | }, 59 | output: { 60 | filename: '[name].js', 61 | libraryTarget: 'commonjs2', 62 | path: path.join(__dirname, '../dist/electron') 63 | }, 64 | plugins: [ 65 | new webpack.NoEmitOnErrorsPlugin() 66 | ], 67 | resolve: { 68 | extensions: ['.js', '.json', '.node'] 69 | }, 70 | target: 'electron-main' 71 | } 72 | 73 | /** 74 | * Adjust mainConfig for development settings 75 | */ 76 | if (process.env.NODE_ENV !== 'production') { 77 | mainConfig.plugins.push( 78 | new webpack.DefinePlugin({ 79 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"` 80 | }) 81 | ) 82 | } 83 | 84 | /** 85 | * Adjust mainConfig for production settings 86 | */ 87 | if (process.env.NODE_ENV === 'production') { 88 | mainConfig.plugins.push( 89 | new MinifyPlugin(), 90 | new webpack.DefinePlugin({ 91 | 'process.env.NODE_ENV': '"production"' 92 | }) 93 | ) 94 | } 95 | 96 | module.exports = mainConfig 97 | -------------------------------------------------------------------------------- /.electron-vue/webpack.renderer.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'renderer' 4 | 5 | const path = require('path') 6 | const { dependencies } = require('../package.json') 7 | const webpack = require('webpack') 8 | 9 | const BabiliWebpackPlugin = require('babili-webpack-plugin') 10 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 11 | const HtmlWebpackPlugin = require('html-webpack-plugin') 12 | const { VueLoaderPlugin } = require('vue-loader') 13 | 14 | /** 15 | * List of node_modules to include in webpack bundle 16 | * 17 | * Required for specific packages like Vue UI libraries 18 | * that provide pure *.vue files that need compiling 19 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals 20 | */ 21 | let whiteListedModules = ['vue', 'vuetify'] 22 | 23 | let rendererConfig = { 24 | devtool: '#cheap-module-eval-source-map', 25 | entry: { 26 | renderer: path.join(__dirname, '..', 'src') 27 | }, 28 | externals: [ 29 | ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)) 30 | ], 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.(yml|yaml)$/, 35 | loader: 'yml-loader' 36 | }, 37 | { 38 | test: /\.(js|vue)$/, 39 | enforce: 'pre', 40 | exclude: /node_modules|output-filter|node-scheduler|object-detector/, 41 | use: { 42 | loader: 'eslint-loader', 43 | options: { 44 | formatter: require('eslint-friendly-formatter') 45 | } 46 | } 47 | }, 48 | { 49 | test: /\.scss$/, 50 | use: ['vue-style-loader', 'css-loader', 'sass-loader'] 51 | }, 52 | { 53 | test: /\.sass$/, 54 | use: ['vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax'] 55 | }, 56 | { 57 | test: /\.less$/, 58 | use: ['vue-style-loader', 'css-loader', 'less-loader'] 59 | }, 60 | { 61 | test: /\.css$/, 62 | use: ['vue-style-loader', 'css-loader'] 63 | }, 64 | { 65 | test: /\.html$/, 66 | use: 'vue-html-loader' 67 | }, 68 | { 69 | test: /\.js$/, 70 | exclude: /node_modules/, 71 | use: [ 72 | { 73 | loader: 'babel-loader', 74 | options: { 75 | presets: ['@babel/env'], 76 | plugins: ['@babel/plugin-proposal-class-properties', '@babel/plugin-transform-runtime'], 77 | sourceType: 'unambiguous' 78 | } 79 | } 80 | ] 81 | }, 82 | { 83 | test: /\.vue$/, 84 | use: { 85 | loader: 'vue-loader', 86 | options: { 87 | extractCSS: process.env.NODE_ENV === 'production', 88 | loaders: { 89 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', 90 | scss: 'vue-style-loader!css-loader!sass-loader', 91 | less: 'vue-style-loader!css-loader!less-loader' 92 | } 93 | } 94 | } 95 | }, 96 | { 97 | test: /\.node$/, 98 | use: 'node-loader' 99 | }, 100 | { 101 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 102 | use: { 103 | loader: 'url-loader', 104 | query: { 105 | limit: 10000, 106 | name: 'imgs/[name]--[folder].[ext]' 107 | } 108 | } 109 | }, 110 | { 111 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 112 | loader: 'url-loader', 113 | options: { 114 | limit: 10000, 115 | name: 'media/[name]--[folder].[ext]' 116 | } 117 | }, 118 | { 119 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 120 | use: { 121 | loader: 'url-loader', 122 | query: { 123 | limit: 10000, 124 | name: 'fonts/[name]--[folder].[ext]' 125 | } 126 | } 127 | } 128 | ] 129 | }, 130 | node: { 131 | __dirname: process.env.NODE_ENV !== 'production', 132 | __filename: process.env.NODE_ENV !== 'production' 133 | }, 134 | plugins: [ 135 | new VueLoaderPlugin(), 136 | new MiniCssExtractPlugin({filename: 'styles.css'}), 137 | new HtmlWebpackPlugin({ 138 | filename: 'index.html', 139 | template: path.resolve(__dirname, '../src/index.ejs'), 140 | inject: false, 141 | minify: { 142 | collapseWhitespace: true, 143 | removeAttributeQuotes: true, 144 | removeComments: true 145 | }, 146 | nodeModules: process.env.NODE_ENV !== 'production' 147 | ? path.resolve(__dirname, '../node_modules') 148 | : false 149 | }), 150 | new webpack.HotModuleReplacementPlugin(), 151 | new webpack.NoEmitOnErrorsPlugin() 152 | ], 153 | output: { 154 | filename: '[name].js', 155 | libraryTarget: 'commonjs2', 156 | path: path.join(__dirname, '../dist/electron') 157 | }, 158 | resolve: { 159 | alias: { 160 | '@': path.join(__dirname, '../src'), 161 | 'vue$': 'vue/dist/vue.esm.js' 162 | }, 163 | extensions: ['.js', '.vue', '.json', '.css', '.node'] 164 | }, 165 | target: 'electron-renderer' 166 | } 167 | 168 | /** 169 | * Adjust rendererConfig for development settings 170 | */ 171 | if (process.env.NODE_ENV !== 'production') { 172 | rendererConfig.plugins.push( 173 | new webpack.DefinePlugin({ 174 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"` 175 | }) 176 | ) 177 | } 178 | 179 | /** 180 | * Adjust rendererConfig for production settings 181 | */ 182 | if (process.env.NODE_ENV === 'production') { 183 | rendererConfig.devtool = '' 184 | 185 | rendererConfig.plugins.push( 186 | new BabiliWebpackPlugin(), 187 | new webpack.DefinePlugin({ 188 | 'process.env.NODE_ENV': '"production"' 189 | }), 190 | new webpack.LoaderOptionsPlugin({ 191 | minimize: true 192 | }) 193 | ) 194 | } 195 | 196 | module.exports = rendererConfig 197 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | browser: true, 9 | node: true 10 | }, 11 | extends: 'standard', 12 | globals: { 13 | __static: true 14 | }, 15 | plugins: [ 16 | 'html' 17 | ], 18 | 'rules': { 19 | // allow paren-less arrow functions 20 | 'arrow-parens': 0, 21 | // allow async-await 22 | 'generator-star-spacing': 0, 23 | // allow debugger during development 24 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 25 | 26 | 'semi': 0, 27 | 'no-trailing-spaces': 0, 28 | 'camelcase': 0 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | release 3 | dist 4 | build 5 | coverage 6 | node_modules 7 | npm-debug.log 8 | npm-debug.log.* 9 | thumbs.db 10 | !.gitkeep 11 | adbkey 12 | package-lock.json 13 | report.html 14 | output.xml 15 | log.html 16 | *.pyc 17 | release 18 | bin 19 | node-scheduler 20 | .vscode 21 | *.node 22 | *.dll 23 | sign.cer 24 | node_cache 25 | .third-party 26 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | electron_mirror=https://npm.taobao.org/mirrors/electron/ 2 | registry=https://registry.npm.taobao.org 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 R.J. Zhou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple video player using electron & ffplay (as node addon) 2 | 3 | ![avatar](./ffplay.png) 4 | 5 | # License 6 | * [FFMpeg] (https://ffmpeg.org/legal.html) 7 | * [cmake-js] (https://github.com/cmake-js/cmake-js) 8 | * [cmake-node-module] (https://github.com/mapbox/cmake-node-module/tree/master) 9 | 10 | # Requirements 11 | - Windows 10 x64 (not tested on other platforms) 12 | - CMake 3.9 or above 13 | - Visual Studio 2017/2019 14 | - nodejs 12.x.x or above 15 | 16 | - https://github.com/ShiftMediaProject/SDL/releases/download/release-2.0.14/libsdl_release-2.0.14_msvc15.zip 17 | - https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2021-01-14-13-18/ffmpeg-n4.3.1-29-g89daac5fe2-win64-lgpl-shared-4.3.zip 18 | 19 | uncompress libsdl2*.zip & ffmpeg*.zip to electron-ffplay/.third-party/prebuilt 20 | 21 | put .third_party/prebuilt/bin[/x64]/*.dll in electron-ffplay/ 22 | 23 | # build 24 | ```bash 25 | # get electron-ffplay source 26 | cd electron-ffplay 27 | 28 | # install dependencies 29 | npm i 30 | 31 | # build addon 32 | npm run configure:ffplay 33 | npm run build:ffplay 34 | 35 | # run 36 | npm run dev 37 | 38 | ``` 39 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Commented sections below can be used to run tests on the CI server 2 | # https://simulatedgreg.gitbooks.io/electron-vue/content/en/testing.html#on-the-subject-of-ci-testing 3 | version: 0.1.{build} 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | image: Visual Studio 2017 10 | platform: 11 | - x64 12 | 13 | cache: 14 | - node_modules 15 | - '%APPDATA%\npm-cache' 16 | - '%USERPROFILE%\.electron' 17 | - '%USERPROFILE%\AppData\Local\Yarn\cache' 18 | 19 | init: 20 | - git config --global core.autocrlf input 21 | 22 | install: 23 | - ps: Install-Product node 8 x64 24 | - git reset --hard HEAD 25 | - yarn 26 | - node --version 27 | 28 | build_script: 29 | #- yarn test 30 | - yarn build 31 | 32 | test: off 33 | -------------------------------------------------------------------------------- /cmake-tool/appCMakeJSConfig.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let path = require("path"); 3 | let _ = require("lodash"); 4 | 5 | function getConfig(lookPath, log) { 6 | let pjsonPath = path.join(lookPath, "package.json"); 7 | log.silly("CFG", "Looking for package.json in: '" + pjsonPath + "'."); 8 | try { 9 | let json = require(pjsonPath); 10 | log.silly("CFG", "Loaded:\n" + JSON.stringify(json)); 11 | if (_.isPlainObject(json) && _.isPlainObject(json["cmake-js"])) { 12 | log.silly("CFG", "Config found."); 13 | return json["cmake-js"]; 14 | } 15 | else { 16 | log.silly("CFG", "Config not found."); 17 | return null; 18 | } 19 | } 20 | catch (e) { 21 | log.silly("CFG", "'package.json' not found."); 22 | return null; 23 | } 24 | } 25 | 26 | module.exports = function (projectPath, log) { 27 | log.verbose("CFG", "Looking for application level CMake.js config in '" + projectPath + "."); 28 | let currPath = projectPath; 29 | let lastConfig = null; 30 | let currConfig; 31 | for (; ;) { 32 | currConfig = getConfig(currPath, log); 33 | if (currConfig) { 34 | lastConfig = currConfig; 35 | } 36 | try { 37 | log.silly("CFG", "Looking for parent path."); 38 | let lastPath = currPath; 39 | currPath = path.normalize(path.join(currPath, "..")); 40 | if (lastPath === currPath) { 41 | currPath = null; // root 42 | } 43 | if (currPath) { 44 | log.silly("CFG", "Parent path: '" + currPath + "'."); 45 | } 46 | } 47 | catch (e) { 48 | log.silly("CFG", "Exception:\n" + e.stack); 49 | break; 50 | } 51 | if (currPath === null) { 52 | log.silly("CFG", "Parent path with package.json file doesn't exists. Done."); 53 | break; 54 | } 55 | } 56 | if (lastConfig) { 57 | log.verbose("CFG", "Application level CMake.js config found:\n" + JSON.stringify(lastConfig)); 58 | } 59 | else { 60 | log.verbose("CFG", "Application level CMake.js config doesn't exists."); 61 | } 62 | return lastConfig; 63 | }; 64 | -------------------------------------------------------------------------------- /cmake-tool/buildSystem.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let CMake = require("./cMake"); 3 | let CMLog = require("./cmLog"); 4 | let appCMakeJSConfig = require("./appCMakeJSConfig"); 5 | let path = require("path"); 6 | let _ = require("lodash"); 7 | let Toolset = require("./toolset"); 8 | 9 | function BuildSystem(options) { 10 | this.options = options || {}; 11 | this.options.directory = path.resolve(this.options.directory || process.cwd()); 12 | this.log = new CMLog(this.options); 13 | let appConfig = appCMakeJSConfig(this.options.directory, this.log); 14 | if (_.isPlainObject(appConfig)) { 15 | if (_.keys(appConfig).length) { 16 | this.log.verbose("CFG", "Applying CMake.js config from root package.json:"); 17 | this.log.verbose("CFG", JSON.stringify(appConfig)); 18 | this.options.arch = this.options.arch || appConfig.arch; 19 | } 20 | } 21 | this.log.verbose("CFG", "Build system options:"); 22 | this.log.verbose("CFG", JSON.stringify(this.options)); 23 | this.cmake = new CMake(this.options); 24 | this.toolset = new Toolset(this.options); 25 | } 26 | 27 | BuildSystem.prototype._ensureInstalled = async function () { 28 | try { 29 | await this.toolset.initialize(true); 30 | } 31 | catch (e) { 32 | this._showError(e); 33 | throw e; 34 | } 35 | }; 36 | 37 | BuildSystem.prototype._showError = function (e) { 38 | if (this.log.level === "verbose" || this.log.level === "silly") { 39 | this.log.error("OMG", e.stack); 40 | } 41 | else { 42 | this.log.error("OMG", e.message); 43 | } 44 | }; 45 | 46 | BuildSystem.prototype._invokeCMake = async function (method) { 47 | try { 48 | await this._ensureInstalled(); 49 | return await this.cmake[method](); 50 | } 51 | catch (e) { 52 | this._showError(e); 53 | throw e; 54 | } 55 | }; 56 | 57 | BuildSystem.prototype.getConfigureCommand = function () { 58 | return this._invokeCMake("getConfigureCommand"); 59 | }; 60 | 61 | BuildSystem.prototype.configure = function () { 62 | return this._invokeCMake("configure"); 63 | }; 64 | 65 | BuildSystem.prototype.getBuildCommand = function () { 66 | return this._invokeCMake("getBuildCommand"); 67 | }; 68 | 69 | BuildSystem.prototype.build = function () { 70 | return this._invokeCMake("build"); 71 | }; 72 | 73 | BuildSystem.prototype.getCleanCommand = function () { 74 | return this._invokeCMake("getCleanCommand"); 75 | }; 76 | 77 | BuildSystem.prototype.clean = function () { 78 | return this._invokeCMake("clean"); 79 | }; 80 | 81 | BuildSystem.prototype.reconfigure = function () { 82 | return this._invokeCMake("reconfigure"); 83 | }; 84 | 85 | BuildSystem.prototype.rebuild = function () { 86 | return this._invokeCMake("rebuild"); 87 | }; 88 | 89 | BuildSystem.prototype.compile = function () { 90 | return this._invokeCMake("compile"); 91 | }; 92 | 93 | module.exports = BuildSystem; 94 | -------------------------------------------------------------------------------- /cmake-tool/cMake.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let which = require("which"); 3 | let fs = require("fs-extra"); 4 | let path = require("path"); 5 | let _ = require("lodash"); 6 | let environment = require("./environment"); 7 | let CMLog = require("./cmLog"); 8 | let TargetOptions = require("./targetOptions"); 9 | let processHelpers = require("./processHelpers"); 10 | let npmConfigData = require("rc")("npm"); 11 | let Toolset = require("./toolset"); 12 | const electron = require('electron') 13 | const {execFileSync} = require('child_process') 14 | 15 | const execResult = execFileSync(electron, [path.resolve(__dirname, 'electron-abi.js')]) 16 | const ABI = execResult.toString().trim() 17 | 18 | function CMake(options) { 19 | this.options = options || {}; 20 | this.log = new CMLog(this.options); 21 | this.projectRoot = path.resolve(this.options.directory || process.cwd()); 22 | this.workDir = path.resolve(this.options.out || path.join(this.projectRoot, "build")); 23 | this.config = this.options.debug ? "Debug" : "Release"; 24 | this.buildDir = path.join(this.workDir, this.config); 25 | this._isAvailable = null; 26 | this.targetOptions = new TargetOptions(this.options); 27 | this.toolset = new Toolset(this.options); 28 | this.cMakeOptions = this.options.cMakeOptions || {}; 29 | this.silent = !!options.silent; 30 | } 31 | 32 | Object.defineProperties(CMake.prototype, { 33 | path: { 34 | get: function () { 35 | return this.options.cmakePath || "cmake"; 36 | } 37 | }, 38 | isAvailable: { 39 | get: function () { 40 | if (this._isAvailable === null) { 41 | this._isAvailable = CMake.isAvailable(this.options); 42 | } 43 | return this._isAvailable; 44 | } 45 | } 46 | }); 47 | 48 | CMake.isAvailable = function (options) { 49 | options = options || {}; 50 | try { 51 | if (options.cmakePath) { 52 | let stat = fs.lstatSync(options.cmakePath); 53 | return !stat.isDirectory(); 54 | } 55 | else { 56 | which.sync("cmake"); 57 | return true; 58 | } 59 | } 60 | catch (e) { 61 | _.noop(e); 62 | } 63 | return false; 64 | }; 65 | 66 | CMake.getGenerators = async function (options) { 67 | let arch = " [arch]"; 68 | options = options || {}; 69 | let gens = []; 70 | if (CMake.isAvailable(options)) { 71 | // try parsing machine-readable capabilities (available since CMake 3.7) 72 | try { 73 | let stdout = await processHelpers.exec((options.cmakePath || "cmake") + " -E capabilities"); 74 | let capabilities = JSON.parse(stdout); 75 | return capabilities.generators.map(x => x.name); 76 | } 77 | catch (error) { 78 | this.log.verbose("TOOL", "Failed to query CMake capabilities (CMake is probably older than 3.7)"); 79 | } 80 | 81 | // fall back to parsing help text 82 | let stdout = await processHelpers.exec((options.cmakePath || "cmake") + " --help"); 83 | let hasCr = stdout.includes("\r\n"); 84 | let output = hasCr ? stdout.split("\r\n") : stdout.split("\n"); 85 | let on = false; 86 | output.forEach(function (line, i) { 87 | if (on) { 88 | let parts = line.split("="); 89 | if ((parts.length === 2 && parts[0].trim()) || 90 | (parts.length === 1 && i !== output.length - 1 && output[i + 1].trim()[0] === "=")) { 91 | let gen = parts[0].trim(); 92 | if (_.endsWith(gen, arch)) { 93 | gen = gen.substr(0, gen.length - arch.length); 94 | } 95 | gens.push(gen); 96 | } 97 | } 98 | if (line.trim() === "Generators") { 99 | on = true; 100 | } 101 | }); 102 | } 103 | else { 104 | throw new Error("CMake is not installed. Install CMake."); 105 | } 106 | return gens; 107 | }; 108 | 109 | CMake.prototype.getGenerators = function () { 110 | return CMake.getGenerators(this.options); 111 | }; 112 | 113 | CMake.prototype.verifyIfAvailable = function () { 114 | if (!this.isAvailable) { 115 | throw new Error("CMake executable is not found. Please use your system's package manager to install it, or you can get installers from there: http://cmake.org."); 116 | } 117 | }; 118 | 119 | CMake.prototype.getConfigureCommand = async function () { 120 | // Create command: 121 | let command = this.path; 122 | command += " \"" + this.projectRoot + "\" --no-warn-unused-cli"; 123 | 124 | let D = []; 125 | 126 | // CMake.js watermark 127 | D.push({"NODE_ABI": ABI}); 128 | 129 | // Build configuration: 130 | D.push({"CMAKE_BUILD_TYPE": this.config}); 131 | if (environment.isWin) { 132 | D.push({"CMAKE_RUNTIME_OUTPUT_DIRECTORY": this.workDir}); 133 | } 134 | else { 135 | D.push({"CMAKE_LIBRARY_OUTPUT_DIRECTORY": this.buildDir}); 136 | } 137 | 138 | // Runtime: 139 | D.push({"NODE_ARCH": this.targetOptions.arch}); 140 | 141 | // Custom options 142 | for (let k of _.keys(this.cMakeOptions)) { 143 | D.push({[k]: this.cMakeOptions[k]}); 144 | } 145 | 146 | // Sources: 147 | let srcPaths = []; 148 | if (environment.isWin) { 149 | let delayHook = path.normalize(path.join(__dirname, 'cpp', 'win_delay_load_hook.cc')); 150 | 151 | srcPaths.push(delayHook.replace(/\\/gm, '/')); 152 | } 153 | 154 | D.push({"DELAY_LOAD_SRC": srcPaths.join(";")}); 155 | 156 | // Toolset: 157 | await this.toolset.initialize(false); 158 | 159 | if (this.toolset.generator) { 160 | command += " -G\"" + this.toolset.generator + "\""; 161 | } 162 | if (this.toolset.platform) { 163 | command += " -A\"" + this.toolset.platform + "\""; 164 | } 165 | if (this.toolset.toolset) { 166 | command += " -T\"" + this.toolset.toolset + "\""; 167 | } 168 | if (this.toolset.cppCompilerPath) { 169 | D.push({"CMAKE_CXX_COMPILER": this.toolset.cppCompilerPath}); 170 | } 171 | if (this.toolset.cCompilerPath) { 172 | D.push({"CMAKE_C_COMPILER": this.toolset.cCompilerPath}); 173 | } 174 | if (this.toolset.compilerFlags.length) { 175 | D.push({"CMAKE_CXX_FLAGS": this.toolset.compilerFlags.join(" ")}); 176 | } 177 | if (this.toolset.makePath) { 178 | D.push({"CMAKE_MAKE_PROGRAM": this.toolset.makePath}); 179 | } 180 | 181 | // Load NPM config 182 | for (let key of _.keys(npmConfigData)) { 183 | if (_.startsWith(key, "cmake_")) { 184 | let s = {}; 185 | let sk = key.substr(6); 186 | if (sk) { 187 | s[sk] = npmConfigData[key]; 188 | if (s[sk]) { 189 | D.push(s); 190 | } 191 | } 192 | } 193 | } 194 | 195 | command += " " + 196 | D.map(function (p) { 197 | return "-D" + _.keys(p)[0] + "=\"" + _.values(p)[0] + "\""; 198 | }).join(" "); 199 | 200 | return command; 201 | }; 202 | 203 | CMake.prototype.configure = async function () { 204 | this.verifyIfAvailable(); 205 | 206 | this.log.info("CMD", "CONFIGURE"); 207 | let listPath = path.join(this.projectRoot, "CMakeLists.txt"); 208 | let command = await this.getConfigureCommand(); 209 | 210 | try { 211 | await fs.lstat(listPath); 212 | } 213 | catch (e) { 214 | throw new Error("'" + listPath + "' not found."); 215 | } 216 | 217 | try { 218 | await fs.ensureDir(this.workDir); 219 | } 220 | catch (e) { 221 | _.noop(e); 222 | } 223 | 224 | let cwd = process.cwd(); 225 | process.chdir(this.workDir); 226 | try { 227 | await this._run(command); 228 | } 229 | finally { 230 | process.chdir(cwd); 231 | } 232 | }; 233 | 234 | CMake.prototype.ensureConfigured = async function () { 235 | try { 236 | await fs.lstat(path.join(this.workDir, "CMakeCache.txt")); 237 | } 238 | catch (e) { 239 | _.noop(e); 240 | await this.configure(); 241 | } 242 | }; 243 | 244 | CMake.prototype.getBuildCommand = function () { 245 | var command = this.path + " --build \"" + this.workDir + "\" --config " + this.config; 246 | if (this.options.target) { 247 | command += " --target " + this.options.target; 248 | } 249 | return Promise.resolve(command); 250 | }; 251 | 252 | CMake.prototype.build = async function () { 253 | this.verifyIfAvailable(); 254 | 255 | await this.ensureConfigured(); 256 | let buildCommand = await this.getBuildCommand(); 257 | this.log.info("CMD", "BUILD"); 258 | await this._run(buildCommand); 259 | }; 260 | 261 | CMake.prototype.getCleanCommand = function () { 262 | return this.path + " -E remove_directory \"" + this.workDir + "\""; 263 | }; 264 | 265 | CMake.prototype.clean = function () { 266 | this.verifyIfAvailable(); 267 | 268 | this.log.info("CMD", "CLEAN"); 269 | return this._run(this.getCleanCommand()); 270 | }; 271 | 272 | CMake.prototype.reconfigure = async function () { 273 | await this.clean(); 274 | await this.configure(); 275 | }; 276 | 277 | CMake.prototype.rebuild = async function () { 278 | await this.clean(); 279 | await this.build(); 280 | }; 281 | 282 | CMake.prototype.compile = async function () { 283 | try { 284 | await this.build(); 285 | } 286 | catch (e) { 287 | _.noop(e); 288 | this.log.info("REP", "Build has been failed, trying to do a full rebuild."); 289 | await this.rebuild(); 290 | } 291 | }; 292 | 293 | CMake.prototype._run = function (command) { 294 | this.log.info("RUN", command); 295 | return processHelpers.run(command, {silent: this.silent}); 296 | }; 297 | 298 | module.exports = CMake; 299 | -------------------------------------------------------------------------------- /cmake-tool/cmLog.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let log = require("npmlog"); 3 | 4 | function CMLog(options) { 5 | this.options = options || {}; 6 | this.debug = require("debug")(this.options.logName || "cmake-js"); 7 | } 8 | 9 | Object.defineProperties(CMLog.prototype, { 10 | level: { 11 | get: function() { 12 | if (this.options.noLog) { 13 | return "silly"; 14 | } 15 | else { 16 | return log.level; 17 | } 18 | } 19 | } 20 | }); 21 | 22 | CMLog.prototype.silly = function(cat, msg) { 23 | if (this.options.noLog) { 24 | this.debug(cat + ": " + msg); 25 | } 26 | else { 27 | log.silly(cat, msg); 28 | } 29 | }; 30 | 31 | CMLog.prototype.verbose = function(cat, msg) { 32 | if (this.options.noLog) { 33 | this.debug(cat + ": " + msg); 34 | } 35 | else { 36 | log.verbose(cat, msg); 37 | } 38 | }; 39 | 40 | CMLog.prototype.info = function(cat, msg) { 41 | if (this.options.noLog) { 42 | this.debug(cat + ": " + msg); 43 | } 44 | else { 45 | log.info(cat, msg); 46 | } 47 | }; 48 | 49 | CMLog.prototype.warn = function(cat, msg) { 50 | if (this.options.noLog) { 51 | this.debug(cat + ": " + msg); 52 | } 53 | else { 54 | log.warn(cat, msg); 55 | } 56 | }; 57 | 58 | CMLog.prototype.http = function(cat, msg) { 59 | if (this.options.noLog) { 60 | this.debug(cat + ": " + msg); 61 | } 62 | else { 63 | log.http(cat, msg); 64 | } 65 | }; 66 | 67 | CMLog.prototype.error = function(cat, msg) { 68 | if (this.options.noLog) { 69 | this.debug(cat + ": " + msg); 70 | } 71 | else { 72 | log.error(cat, msg); 73 | } 74 | }; 75 | 76 | module.exports = CMLog; -------------------------------------------------------------------------------- /cmake-tool/cpp/win_delay_load_hook.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * When this file is linked to a DLL, it sets up a delay-load hook that 3 | * intervenes when the DLL is trying to load 'node.exe' or 'iojs.exe' 4 | * dynamically. Instead of trying to locate the .exe file it'll just return 5 | * a handle to the process image. 6 | * 7 | * This allows compiled addons to work when node.exe or iojs.exe is renamed. 8 | */ 9 | 10 | #ifdef _MSC_VER 11 | 12 | #ifndef WIN32_LEAN_AND_MEAN 13 | #define WIN32_LEAN_AND_MEAN 14 | #endif 15 | 16 | #include 17 | 18 | #include 19 | #include 20 | 21 | static FARPROC WINAPI load_exe_hook(unsigned int event, DelayLoadInfo* info) { 22 | HMODULE m; 23 | if (event != dliNotePreLoadLibrary) 24 | return NULL; 25 | 26 | if (_stricmp(info->szDll, "iojs.exe") != 0 && 27 | _stricmp(info->szDll, "node.exe") != 0) 28 | return NULL; 29 | 30 | m = GetModuleHandle(NULL); 31 | return (FARPROC) m; 32 | } 33 | 34 | decltype(__pfnDliNotifyHook2) __pfnDliNotifyHook2 = load_exe_hook; 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /cmake-tool/electron-abi.js: -------------------------------------------------------------------------------- 1 | const {app} = require('electron') 2 | const nodeMajor2Abis = { 3 | '8': 57, 4 | '9': 59, 5 | '10': 64, 6 | '11': 64, 7 | '12': 72, 8 | '13': 79, 9 | '14': 83, 10 | '15': 88 11 | } 12 | 13 | app.allowRendererProcessReuse = false; 14 | 15 | const ABI = nodeMajor2Abis[process.versions.node.split('.')[0]] 16 | console.log(ABI) 17 | app.quit() 18 | -------------------------------------------------------------------------------- /cmake-tool/environment.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let os = require("os"); 3 | let which = require("which"); 4 | let _ = require("lodash"); 5 | 6 | let environment = module.exports = { 7 | moduleVersion: require("../package.json").version, 8 | platform: os.platform(), 9 | isWin: os.platform() === "win32", 10 | isLinux: os.platform() === "linux", 11 | isOSX: os.platform() === "darwin", 12 | arch: os.arch(), 13 | isX86: os.arch() === "ia32", 14 | isX64: os.arch() === "x64", 15 | isArm: os.arch() === "arm", 16 | home: process.env[(os.platform() === "win32") ? "USERPROFILE" : "HOME"], 17 | EOL: os.EOL 18 | }; 19 | 20 | Object.defineProperties(environment, { 21 | isPosix: { 22 | get: function () { 23 | return !this.isWin; 24 | } 25 | }, 26 | _isNinjaAvailable: { 27 | value: null, 28 | writable: true 29 | }, 30 | isNinjaAvailable: { 31 | get: function() { 32 | if (this._isNinjaAvailable === null) { 33 | this._isNinjaAvailable = false; 34 | try { 35 | if (which.sync("ninja")) { 36 | this._isNinjaAvailable = true; 37 | } 38 | } 39 | catch (e) { 40 | _.noop(e); 41 | } 42 | } 43 | return this._isNinjaAvailable; 44 | } 45 | }, 46 | _isMakeAvailable: { 47 | value: null, 48 | writable: true 49 | }, 50 | isMakeAvailable: { 51 | get: function() { 52 | if (this._isMakeAvailable === null) { 53 | this._isMakeAvailable = false; 54 | try { 55 | if (which.sync("make")) { 56 | this._isMakeAvailable = true; 57 | } 58 | } 59 | catch (e) { 60 | _.noop(e); 61 | } 62 | } 63 | return this._isMakeAvailable; 64 | } 65 | }, 66 | _isGPPAvailable: { 67 | value: null, 68 | writable: true 69 | }, 70 | isGPPAvailable: { 71 | get: function() { 72 | if (this._isGPPAvailable === null) { 73 | this._isGPPAvailable = false; 74 | try { 75 | if (which.sync("g++")) { 76 | this._isGPPAvailable = true; 77 | } 78 | } 79 | catch (e) { 80 | _.noop(e); 81 | } 82 | } 83 | return this._isGPPAvailable; 84 | } 85 | }, 86 | _isClangAvailable: { 87 | value: null, 88 | writable: true 89 | }, 90 | isClangAvailable: { 91 | get: function() { 92 | if (this._isClangAvailable === null) { 93 | this._isClangAvailable = false; 94 | try { 95 | if (which.sync("clang++")) { 96 | this._isClangAvailable = true; 97 | } 98 | } 99 | catch (e) { 100 | _.noop(e); 101 | } 102 | } 103 | return this._isClangAvailable; 104 | } 105 | } 106 | }); -------------------------------------------------------------------------------- /cmake-tool/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var log = require("npmlog"); 3 | var _ = require("lodash"); 4 | var BuildSystem = require("./buildSystem"); 5 | var util = require("util"); 6 | var version = require("../package").version; 7 | var logLevels = ["silly", "verbose", "info", "http", "warn", "error"]; 8 | 9 | var yargs = require("yargs") 10 | .usage("CMake.js " + version + "\n\nUsage: $0 [] [options]") 11 | .command("configure", "Configure CMake project") 12 | .command("print-configure", "Print the configuration command") 13 | .command("build", "Build the project (will configure first if required)") 14 | .command("print-build", "Print the build command") 15 | .command("clean", "Clean the project directory") 16 | .command("print-clean", "Print the clean command") 17 | .command("reconfigure", "Clean the project directory then configure the project") 18 | .command("rebuild", "Clean the project directory then build the project") 19 | .command("compile", "Build the project, and if build fails, try a full rebuild") 20 | .options({ 21 | h: { 22 | alias: "help", 23 | demand: false, 24 | describe: "show this screen", 25 | type: "boolean" 26 | }, 27 | l: { 28 | alias: "log-level", 29 | demand: false, 30 | describe: "set log level (" + logLevels.join(", ") + "), default is info", 31 | type: "string" 32 | }, 33 | d: { 34 | alias: "directory", 35 | demand: false, 36 | describe: "specify CMake project's directory (where CMakeLists.txt located)", 37 | type: "string" 38 | }, 39 | D: { 40 | alias: "debug", 41 | demand: false, 42 | describe: "build debug configuration", 43 | type: "boolean" 44 | }, 45 | c: { 46 | alias: "cmake-path", 47 | demand: false, 48 | describe: "path of CMake executable", 49 | type: "string" 50 | }, 51 | m: { 52 | alias: "prefer-make", 53 | demand: false, 54 | describe: "use Unix Makefiles even if Ninja is available (Posix)", 55 | type: "boolean" 56 | }, 57 | x: { 58 | alias: "prefer-xcode", 59 | demand: false, 60 | describe: "use Xcode instead of Unix Makefiles", 61 | type: "boolean" 62 | }, 63 | g: { 64 | alias: "prefer-gnu", 65 | demand: false, 66 | describe: "use GNU compiler instead of default CMake compiler, if available (Posix)", 67 | type: "boolean" 68 | }, 69 | G: { 70 | alias: "generator", 71 | demand: false, 72 | describe: "use specified generator", 73 | type: "string" 74 | }, 75 | t: { 76 | alias: "toolset", 77 | demand: false, 78 | describe: "use specified toolset", 79 | type: "string" 80 | }, 81 | A: { 82 | alias: "platform", 83 | demand: false, 84 | describe: "use specified platform name", 85 | type: "string" 86 | }, 87 | T: { 88 | alias: "target", 89 | demand: false, 90 | describe: "only build the specified target", 91 | type: "string" 92 | }, 93 | C: { 94 | alias: "prefer-clang", 95 | demand: false, 96 | describe: "use Clang compiler instead of default CMake compiler, if available (Posix)", 97 | type: "boolean" 98 | }, 99 | cc: { 100 | demand: false, 101 | describe: "use the specified C compiler", 102 | type: "string" 103 | }, 104 | cxx: { 105 | demand: false, 106 | describe: "use the specified C++ compiler", 107 | type: "string" 108 | }, 109 | a: { 110 | alias: "arch", 111 | demand: false, 112 | describe: "the architecture to build in", 113 | type: "string" 114 | }, 115 | "CD": { 116 | demand: false, 117 | describe: "Custom argument passed to CMake in format: -D", 118 | type: "string" 119 | }, 120 | i: { 121 | alias: "silent", 122 | describe: "Prevents CMake.js to print to the stdio", 123 | type: "boolean" 124 | }, 125 | O: { 126 | alias: "out", 127 | describe: "Specify the output directory to compile to, default is projectRoot/build", 128 | type: "string" 129 | } 130 | }); 131 | var argv = yargs.argv; 132 | 133 | // If help, then print and exit: 134 | 135 | if (argv.h) { 136 | console.info(yargs.help()); 137 | process.exit(0); 138 | } 139 | 140 | // Setup log level: 141 | 142 | if (argv.l && _.includes(logLevels, argv.l)) { 143 | log.level = argv.l; 144 | log.resume(); 145 | } 146 | 147 | log.silly("CON", "argv:"); 148 | log.silly("CON", util.inspect(argv)); 149 | 150 | log.verbose("CON", "Parsing arguments"); 151 | 152 | // Extract custom cMake options 153 | var customOptions = {}; 154 | _.keys(argv).forEach(function (key) { 155 | if (argv[key] && _.startsWith(key, "CD")) { 156 | customOptions[key.substr(2)] = argv[key]; 157 | } 158 | }); 159 | 160 | var options = { 161 | directory: argv.directory || null, 162 | debug: argv.debug, 163 | cmakePath: argv.c || null, 164 | generator: argv.G, 165 | toolset: argv.t, 166 | platform: argv.A, 167 | target: argv.T, 168 | preferMake: argv.m, 169 | preferXcode: argv.x, 170 | preferGnu: argv.g, 171 | preferClang: argv.C, 172 | cCompilerPath: argv.cc, 173 | cppCompilerPath: argv.cxx, 174 | arch: argv.a, 175 | cMakeOptions: customOptions, 176 | silent: argv.i, 177 | out: argv.O 178 | }; 179 | 180 | log.verbose("CON", "options:"); 181 | log.verbose("CON", util.inspect(options)); 182 | 183 | var command = _.first(argv._) || "build"; 184 | 185 | log.verbose("CON", "Running command: " + command); 186 | 187 | var buildSystem = new BuildSystem(options); 188 | 189 | function ifCommand(c, f) { 190 | if (c === command) { 191 | f(); 192 | return true; 193 | } 194 | return false; 195 | } 196 | 197 | function exitOnError(promise) { 198 | promise.catch(function () { 199 | process.exit(1); 200 | }); 201 | } 202 | 203 | function configure() { 204 | exitOnError(buildSystem.configure()); 205 | } 206 | function printConfigure() { 207 | exitOnError(buildSystem.getConfigureCommand() 208 | .then(function (command) { 209 | console.info(command); 210 | })); 211 | } 212 | function build() { 213 | exitOnError(buildSystem.build()); 214 | } 215 | function printBuild() { 216 | exitOnError(buildSystem.getBuildCommand() 217 | .then(function (command) { 218 | console.info(command); 219 | })); 220 | } 221 | function clean() { 222 | exitOnError(buildSystem.clean()); 223 | } 224 | function printClean() { 225 | exitOnError(buildSystem.getCleanCommand() 226 | .then(function (command) { 227 | console.info(command); 228 | })); 229 | } 230 | function reconfigure() { 231 | exitOnError(buildSystem.reconfigure()); 232 | } 233 | function rebuild() { 234 | exitOnError(buildSystem.rebuild()); 235 | } 236 | function compile() { 237 | exitOnError(buildSystem.compile()); 238 | } 239 | 240 | var done 241 | done = done || ifCommand("configure", configure); 242 | done = done || ifCommand("print-configure", printConfigure); 243 | done = done || ifCommand("build", build); 244 | done = done || ifCommand("print-build", printBuild); 245 | done = done || ifCommand("clean", clean); 246 | done = done || ifCommand("print-clean", printClean); 247 | done = done || ifCommand("reconfigure", reconfigure); 248 | done = done || ifCommand("rebuild", rebuild); 249 | done = done || ifCommand("compile", compile); 250 | 251 | if (!done) { 252 | if (command) { 253 | log.error("COM", "Unknown command: " + command); 254 | process.exit(1); 255 | } 256 | else { 257 | build(); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /cmake-tool/node-addon.cmake: -------------------------------------------------------------------------------- 1 | if (NOT NODE_MODULE_CACHE_DIR) 2 | set(NODE_MODULE_CACHE_DIR "${CMAKE_BINARY_DIR}") 3 | endif() 4 | 5 | if (NOT NODE_ADDON_API) 6 | set(NODE_ADDON_API 1.7.1) 7 | endif() 8 | 9 | if (NOT NODE_NAN_API) 10 | set(NODE_NAN_API 2.14.1) 11 | endif() 12 | 13 | 14 | if (WIN32) 15 | if (CMAKE_CL_64) 16 | set(ARCHSUFFIX "win32-x64") 17 | else() 18 | set(ARCHSUFFIX "win32-x86") 19 | endif() 20 | else() 21 | set(ARCHSUFFIX "linux-x64") 22 | endif() 23 | 24 | function(_node_module_download _URL _FILE) 25 | get_filename_component(_DIR "${_FILE}" DIRECTORY) 26 | file(MAKE_DIRECTORY "${_DIR}") 27 | message(STATUS "[Node.js] Downloading ${_URL}...") 28 | file(DOWNLOAD "${_URL}" "${_FILE}" STATUS _STATUS TLS_VERIFY ON) 29 | list(GET _STATUS 0 _STATUS_CODE) 30 | if(NOT _STATUS_CODE EQUAL 0) 31 | list(GET _STATUS 1 _STATUS_MESSAGE) 32 | message(FATAL_ERROR "[Node.js] Failed to download ${_URL}: ${_STATUS_MESSAGE}") 33 | endif() 34 | endfunction() 35 | 36 | function(_node_module_unpack_tar_gz _URL _PATH _DEST) 37 | string(RANDOM LENGTH 32 _TMP) 38 | set(_TMP "${CMAKE_BINARY_DIR}/${_TMP}") 39 | _node_module_download("${_URL}" "${_TMP}.tar.gz") 40 | file(REMOVE_RECURSE "${_DEST}" "${_TMP}") 41 | file(MAKE_DIRECTORY "${_TMP}") 42 | execute_process(COMMAND ${CMAKE_COMMAND} -E tar xfz "${_TMP}.tar.gz" 43 | WORKING_DIRECTORY "${_TMP}" 44 | RESULT_VARIABLE _STATUS_CODE 45 | OUTPUT_VARIABLE _STATUS_MESSAGE 46 | ERROR_VARIABLE _STATUS_MESSAGE) 47 | if(NOT _STATUS_CODE EQUAL 0) 48 | message(FATAL_ERROR "[Node.js] Failed to unpack ${_URL}: ${_STATUS_MESSAGE}") 49 | endif() 50 | get_filename_component(_DIR "${_DEST}" DIRECTORY) 51 | file(MAKE_DIRECTORY "${_DIR}") 52 | file(RENAME "${_TMP}/${_PATH}" "${_DEST}") 53 | file(REMOVE_RECURSE "${_TMP}" "${_TMP}.tar.gz") 54 | endfunction() 55 | 56 | 57 | function(add_node_module NAME) 58 | cmake_parse_arguments("" "" "ADDON_VERSION;NAN_VERSION;CACHE_DIR;SUFFIX" "NODE_ABIS" ${ARGN}) 59 | if (NOT _CACHE_DIR) 60 | set(_CACHE_DIR "${NODE_MODULE_CACHE_DIR}") 61 | endif() 62 | if (NOT _ADDON_VERSION) 63 | set(_ADDON_VERSION "${NODE_ADDON_API}") 64 | endif() 65 | if (NOT _SUFFIX) 66 | if (WIN32) 67 | set(_SUFFIX ".dll") 68 | else() 69 | set(_SUFFIX ".so") 70 | endif() 71 | endif() 72 | 73 | if(NOT _NODE_ABIS) 74 | message(FATAL_ERROR "No ABIs specified") 75 | endif() 76 | 77 | # Create master target 78 | add_library(${NAME} INTERFACE) 79 | 80 | # Obtain a list of current Node versions and retrieves the latest version per ABI 81 | if(NOT EXISTS "${_CACHE_DIR}/node/index.tab") 82 | _node_module_download( 83 | "Node.js version list" 84 | "https://nodejs.org/dist/index.tab" 85 | "${_CACHE_DIR}/node/index.tab" 86 | ) 87 | endif() 88 | 89 | file(STRINGS "${_CACHE_DIR}/node/index.tab" _INDEX_FILE) 90 | list(REMOVE_AT _INDEX_FILE 0) 91 | 92 | set(_ABIS) 93 | foreach(_LINE IN LISTS _INDEX_FILE) 94 | string(REGEX MATCHALL "[^\t]*\t" _COLUMNS "${_LINE}") 95 | list(GET _COLUMNS 8 _ABI) 96 | string(STRIP "${_ABI}" _ABI) 97 | if(NOT DEFINED _NODE_ABI_${_ABI}_VERSION) 98 | list(APPEND _ABIS ${_ABI}) 99 | list(GET _COLUMNS 0 _VERSION) 100 | string(STRIP "${_VERSION}" _NODE_ABI_${_ABI}_VERSION) 101 | endif() 102 | endforeach() 103 | 104 | # Install Nan 105 | if(_NAN_VERSION AND NOT EXISTS "${_CACHE_DIR}/nan/${_NAN_VERSION}/nan.h") 106 | _node_module_unpack_tar_gz( 107 | "Nan ${_NAN_VERSION}" 108 | "https://registry.npmjs.org/nan/-/nan-${_NAN_VERSION}.tgz" 109 | "package" 110 | "${_CACHE_DIR}/nan/${_NAN_VERSION}" 111 | ) 112 | endif() 113 | 114 | # Install addon api 115 | if(_ADDON_VERSION AND NOT EXISTS "${_CACHE_DIR}/node-addon-api/${_ADDON_VERSION}/napi.h") 116 | _node_module_unpack_tar_gz( 117 | "https://registry.npmjs.org/node-addon-api/-/node-addon-api-${_ADDON_VERSION}.tgz" 118 | "package" 119 | "${_CACHE_DIR}/node-addon-api/${_ADDON_VERSION}" 120 | ) 121 | endif() 122 | 123 | # Generate a target for every ABI 124 | set(_TARGETS) 125 | foreach(_ABI IN LISTS _NODE_ABIS) 126 | set(_NODE_VERSION ${_NODE_ABI_${_ABI}_VERSION}) 127 | 128 | # Download the headers if we don't have them yet 129 | if(NOT EXISTS "${_CACHE_DIR}/node/${_NODE_VERSION}/node.h") 130 | _node_module_unpack_tar_gz( 131 | "headers for Node ${_NODE_VERSION}" 132 | "https://nodejs.org/download/release/${_NODE_VERSION}/node-${_NODE_VERSION}-headers.tar.gz" 133 | "node-${_NODE_VERSION}/include/node" 134 | "${_CACHE_DIR}/node/${_NODE_VERSION}" 135 | ) 136 | endif() 137 | 138 | if (WIN32) 139 | if (CMAKE_CL_64) 140 | set(LIB_DIST_URL "https://nodejs.org/dist/${_NODE_VERSION}/win-x64/node.lib") 141 | set(LIB_DIST "${_CACHE_DIR}/node/${_NODE_VERSION}/win-x64/node.lib") 142 | else() 143 | set(LIB_DIST_URL "https://nodejs.org/dist/${_NODE_VERSION}/win-x86/node.lib") 144 | set(LIB_DIST "${_CACHE_DIR}/node/${_NODE_VERSION}/win-x86/node.lib") 145 | endif() 146 | 147 | if(NOT EXISTS "${LIB_DIST}") 148 | _node_module_download( 149 | "${LIB_DIST_URL}" 150 | "${LIB_DIST}" 151 | ) 152 | endif() 153 | endif() 154 | 155 | # Generate the library 156 | set(_TARGET "${NAME}-${ARCHSUFFIX}.abi-${_ABI}") 157 | add_library(${_TARGET} SHARED "${_CACHE_DIR}/empty.cpp") 158 | list(APPEND _TARGETS "${_TARGET}") 159 | 160 | # C identifiers can only contain certain characters (e.g. no dashes) 161 | string(REGEX REPLACE "[^a-z0-9]+" "_" NAME_IDENTIFIER "${NAME}") 162 | 163 | set_target_properties(${_TARGET} PROPERTIES 164 | OUTPUT_NAME "${_TARGET}" 165 | SOURCES "${DELAY_LOAD_SRC}" # Removes the fake empty.cpp again 166 | PREFIX "" 167 | SUFFIX "${_SUFFIX}" 168 | MACOSX_RPATH ON 169 | C_VISIBILITY_PRESET hidden 170 | CXX_VISIBILITY_PRESET hidden 171 | POSITION_INDEPENDENT_CODE TRUE 172 | ) 173 | 174 | target_compile_definitions(${_TARGET} PRIVATE 175 | "MODULE_NAME=${NAME_IDENTIFIER}" 176 | "BUILDING_NODE_EXTENSION" 177 | "_LARGEFILE_SOURCE" 178 | "_FILE_OFFSET_BITS=64" 179 | ) 180 | 181 | target_include_directories(${_TARGET} SYSTEM PRIVATE 182 | "${_CACHE_DIR}/node/${_NODE_VERSION}" 183 | ) 184 | 185 | if(_NAN_VERSION) 186 | # target_compile_options(${_TARGET} PRIVATE -std=c++11) 187 | target_include_directories(${_TARGET} SYSTEM PRIVATE 188 | "${_CACHE_DIR}/nan/${_NAN_VERSION}" 189 | ) 190 | endif() 191 | 192 | if(_ADDON_VERSION) 193 | # target_compile_options(${_TARGET} PRIVATE -std=c++11) 194 | target_include_directories(${_TARGET} SYSTEM PRIVATE 195 | "${_CACHE_DIR}/node-addon-api/${_ADDON_VERSION}" 196 | ) 197 | endif() 198 | 199 | target_link_libraries(${_TARGET} PRIVATE ${NAME}) 200 | if (WIN32) 201 | target_link_libraries(${_TARGET} PRIVATE ${LIB_DIST}) 202 | endif() 203 | 204 | if(APPLE) 205 | # Ensures that linked symbols are loaded when the module is loaded instead of causing 206 | # unresolved symbol errors later during runtime. 207 | set_target_properties(${_TARGET} PROPERTIES 208 | LINK_FLAGS "-undefined dynamic_lookup -bind_at_load" 209 | ) 210 | target_compile_definitions(${_TARGET} PRIVATE 211 | "_DARWIN_USE_64_BIT_INODE=1" 212 | ) 213 | else() 214 | if (MSVC) 215 | set_target_properties(${_TARGET} PROPERTIES COMPILE_FLAGS "/EHsc") 216 | set_target_properties(${_TARGET} PROPERTIES LINK_FLAGS "/DELAYLOAD:node.exe /DELAYLOAD:iojs.exe") 217 | else() 218 | # Ensures that linked symbols are loaded when the module is loaded instead of causing 219 | # unresolved symbol errors later during runtime. 220 | set_target_properties(${_TARGET} PROPERTIES 221 | LINK_FLAGS "-z now" 222 | ) 223 | endif() 224 | endif() 225 | endforeach() 226 | 227 | # Add a target that builds all Node ABIs. 228 | add_custom_target("${NAME}.all") 229 | add_dependencies("${NAME}.all" ${_TARGETS}) 230 | 231 | # Add a variable that allows users to iterate over all of the generated/dependendent targets. 232 | set("${NAME}::abis" "${_ABIS}" PARENT_SCOPE) 233 | set("${NAME}::targets" "${_TARGETS}" PARENT_SCOPE) 234 | endfunction() 235 | -------------------------------------------------------------------------------- /cmake-tool/processHelpers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let Promise = require("bluebird"); 3 | let splitargs = require("splitargs"); 4 | let _ = require("lodash"); 5 | let spawn = require("child_process").spawn; 6 | let exec = require("child_process").exec; 7 | 8 | let processHelpers = { 9 | run: function (command, options) { 10 | options = _.defaults(options, {silent: false}); 11 | return new Promise(function (resolve, reject) { 12 | let args = splitargs(command); 13 | let name = args[0]; 14 | args.splice(0, 1); 15 | let child = spawn(name, args, {stdio: options.silent ? "ignore" : "inherit"}); 16 | let ended = false; 17 | child.on("error", function (e) { 18 | if (!ended) { 19 | reject(e); 20 | ended = true; 21 | } 22 | }); 23 | child.on("exit", function (code, signal) { 24 | if (!ended) { 25 | if (code === 0) { 26 | resolve(); 27 | } 28 | else { 29 | reject(new Error("Process terminated: " + code || signal)); 30 | } 31 | ended = true; 32 | } 33 | }); 34 | }); 35 | }, 36 | exec: function(command) { 37 | return new Promise(function (resolve, reject) { 38 | exec(command, function (err, stdout, stderr) { 39 | if (err) { 40 | reject(new Error(err.message + "\n" + (stdout || stderr))); 41 | } 42 | else { 43 | resolve(stdout); 44 | } 45 | }); 46 | }); 47 | } 48 | }; 49 | 50 | module.exports = processHelpers; 51 | -------------------------------------------------------------------------------- /cmake-tool/targetOptions.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let environment = require("./environment"); 4 | let _ = require("lodash"); 5 | 6 | function TargetOptions(options) { 7 | this.options = options || {}; 8 | } 9 | 10 | Object.defineProperties(TargetOptions.prototype, { 11 | arch: { 12 | get: function () { 13 | return this.options.arch || environment.arch; 14 | } 15 | }, 16 | isX86: { 17 | get: function () { 18 | return this.arch === "ia32"; 19 | } 20 | }, 21 | isX64: { 22 | get: function () { 23 | return this.arch === "x64"; 24 | } 25 | }, 26 | isArm: { 27 | get: function () { 28 | return this.arch === "arm"; 29 | } 30 | } 31 | }); 32 | 33 | module.exports = TargetOptions; -------------------------------------------------------------------------------- /cmake-tool/toolset.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let _ = require("lodash"); 3 | let TargetOptions = require("./targetOptions"); 4 | let environment = require("./environment"); 5 | let assert = require("assert"); 6 | let vsDetect = require("./vsDetect"); 7 | let path = require("path"); 8 | let CMLog = require("./cmLog"); 9 | let processHelpers = require("./processHelpers"); 10 | 11 | function Toolset(options) { 12 | this.options = options || {}; 13 | this.targetOptions = new TargetOptions(this.options); 14 | this.generator = options.generator; 15 | this.toolset = options.toolset; 16 | this.platform = options.platform; 17 | this.target = options.target; 18 | this.cCompilerPath = options.cCompilerPath; 19 | this.cppCompilerPath = options.cppCompilerPath; 20 | this.compilerFlags = []; 21 | this.linkerFlags = []; 22 | this.makePath = null; 23 | this.log = new CMLog(this.options); 24 | this._initialized = false; 25 | } 26 | 27 | Toolset.prototype.initialize = async function (install) { 28 | if (!this._initialized) { 29 | if (environment.isWin) { 30 | await this.initializeWin(install); 31 | } 32 | else { 33 | this.initializePosix(install); 34 | } 35 | this._initialized = true; 36 | } 37 | }; 38 | 39 | Toolset.prototype.initializePosix = function (install) { 40 | if (!this.cCompilerPath || !this.cppCompilerPath) { 41 | // 1: Compiler 42 | if (!environment.isGPPAvailable && !environment.isClangAvailable) { 43 | if (environment.isOSX) { 44 | throw new Error("C++ Compiler toolset is not available. Install Xcode Commandline Tools from Apple Dev Center, or install Clang with homebrew by invoking: 'brew install llvm --with-clang --with-asan'."); 45 | } 46 | else { 47 | throw new Error("C++ Compiler toolset is not available. Install proper compiler toolset with your package manager, eg. 'sudo apt-get install g++'."); 48 | } 49 | } 50 | 51 | if (this.options.preferClang && environment.isClangAvailable) { 52 | if (install) { 53 | this.log.info("TOOL", "Using clang++ compiler, because preferClang option is set, and clang++ is available."); 54 | } 55 | this.cppCompilerPath = this.cppCompilerPath || "clang++"; 56 | this.cCompilerPath = this.cCompilerPath || "clang"; 57 | } 58 | else if (this.options.preferGnu && environment.isGPPAvailable) { 59 | if (install) { 60 | this.log.info("TOOL", "Using g++ compiler, because preferGnu option is set, and g++ is available."); 61 | } 62 | this.cppCompilerPath = this.cppCompilerPath || "g++"; 63 | this.cCompilerPath = this.cCompilerPath || "gcc"; 64 | } 65 | } 66 | // if it's already set because of options... 67 | if (this.generator) { 68 | if (install) { 69 | this.log.info("TOOL", "Using " + this.options.generator + " generator, as specified from commandline."); 70 | } 71 | } 72 | // 2: Generator 73 | else if (environment.isOSX) { 74 | if (this.options.preferXcode) { 75 | if (install) { 76 | this.log.info("TOOL", "Using Xcode generator, because preferXcode option is set."); 77 | } 78 | this.generator = "Xcode"; 79 | } 80 | else if (this.options.preferMake && environment.isMakeAvailable) { 81 | if (install) { 82 | this.log.info("TOOL", "Using Unix Makefiles generator, because preferMake option is set, and make is available."); 83 | } 84 | this.generator = "Unix Makefiles"; 85 | } 86 | else if (environment.isNinjaAvailable) { 87 | if (install) { 88 | this.log.info("TOOL", "Using Ninja generator, because ninja is available."); 89 | } 90 | this.generator = "Ninja"; 91 | } 92 | else { 93 | if (install) { 94 | this.log.info("TOOL", "Using Unix Makefiles generator."); 95 | } 96 | this.generator = "Unix Makefiles"; 97 | } 98 | } 99 | else { 100 | if (this.options.preferMake && environment.isMakeAvailable) { 101 | if (install) { 102 | this.log.info("TOOL", "Using Unix Makefiles generator, because preferMake option is set, and make is available."); 103 | } 104 | this.generator = "Unix Makefiles"; 105 | } 106 | else if (environment.isNinjaAvailable) { 107 | if (install) { 108 | this.log.info("TOOL", "Using Ninja generator, because ninja is available."); 109 | } 110 | this.generator = "Ninja"; 111 | } 112 | else { 113 | if (install) { 114 | this.log.info("TOOL", "Using Unix Makefiles generator."); 115 | } 116 | this.generator = "Unix Makefiles"; 117 | } 118 | } 119 | 120 | // 3: Flags 121 | if (environment.isOSX) { 122 | if (install) { 123 | this.log.verbose("TOOL", "Setting default OSX compiler flags."); 124 | } 125 | 126 | this.compilerFlags.push("-D_DARWIN_USE_64_BIT_INODE=1"); 127 | this.compilerFlags.push("-D_LARGEFILE_SOURCE"); 128 | this.compilerFlags.push("-D_FILE_OFFSET_BITS=64"); 129 | this.compilerFlags.push("-DBUILDING_NODE_EXTENSION"); 130 | this.linkerFlags.push("-undefined dynamic_lookup"); 131 | } 132 | 133 | // 4: Build target 134 | if (this.options.target) { 135 | this.log.info("TOOL", "Building only the " + this.options.target + " target, as specified from the command line."); 136 | } 137 | }; 138 | 139 | Toolset.prototype.initializeWin = async function (install) { 140 | // Visual Studio: 141 | // if it's already set because of options... 142 | if (this.generator) { 143 | if (install) { 144 | this.log.info("TOOL", "Using " + this.options.generator + " generator, as specified from commandline."); 145 | } 146 | 147 | this.linkerFlags.push("/DELAYLOAD:NODE.EXE"); 148 | 149 | if (this.targetOptions.isX86) { 150 | if (install) { 151 | this.log.verbose("TOOL", "Setting SAFESEH:NO linker flag."); 152 | } 153 | this.linkerFlags.push("/SAFESEH:NO"); 154 | } 155 | return; 156 | } 157 | let topVS = await this._getTopSupportedVisualStudioGenerator(); 158 | if (topVS) { 159 | if (install) { 160 | this.log.info("TOOL", `Using ${topVS} generator.`); 161 | } 162 | this.generator = topVS; 163 | 164 | this.linkerFlags.push("/DELAYLOAD:NODE.EXE"); 165 | 166 | if (this.targetOptions.isX86) { 167 | if (install) { 168 | this.log.verbose("TOOL", "Setting SAFESEH:NO linker flag."); 169 | } 170 | this.linkerFlags.push("/SAFESEH:NO"); 171 | } 172 | 173 | // The CMake Visual Studio Generator does not support the Win64 or ARM suffix on 174 | // the generator name. Instead the generator platform must be set explicitly via 175 | // the platform parameter 176 | if (!this.platform && this.generator.startsWith("Visual Studio 16")) { 177 | switch(this.targetOptions.arch) { 178 | case "ia32": 179 | this.platform = "Win32"; 180 | break; 181 | case "x64": 182 | this.platform = "x64"; 183 | break; 184 | case "arm": 185 | this.platform = "ARM"; 186 | break; 187 | case "arm64": 188 | this.platform = "ARM64"; 189 | break; 190 | default: 191 | this.log.warn("TOOL", "Unknown NodeJS architecture: " + this.targetOptions.arch); 192 | break; 193 | } 194 | } 195 | } 196 | else { 197 | throw new Error("There is no Visual C++ compiler installed. Install Visual C++ Build Toolset or Visual Studio."); 198 | } 199 | }; 200 | 201 | Toolset.prototype._getTopSupportedVisualStudioGenerator = async function () { 202 | let CMake = require("./cMake"); 203 | assert(environment.isWin); 204 | 205 | let vswhereVersion = await this._getVersionFromVSWhere(); 206 | 207 | let list = await CMake.getGenerators(this.options); 208 | let maxVer = 0; 209 | let result = null; 210 | for (let gen of list) { 211 | let found = /^visual studio (\d+)/i.exec(gen); 212 | if (!found) { 213 | continue; 214 | } 215 | 216 | let ver = parseInt(found[1]); 217 | if (ver <= maxVer) { 218 | continue; 219 | } 220 | 221 | // unlike previous versions "Visual Studio 16 2019" doesn't end with arch name 222 | const isAboveVS16 = ver >= 16; 223 | if (!isAboveVS16) { 224 | const is64Bit = gen.endsWith("Win64"); 225 | if ((this.targetOptions.isX86 && is64Bit) || (this.targetOptions.isX64 && !is64Bit)) { 226 | continue; 227 | } 228 | } 229 | 230 | if (ver === vswhereVersion || (await vsDetect.isInstalled(ver + ".0"))) { 231 | result = gen; 232 | maxVer = ver; 233 | } 234 | } 235 | return result; 236 | }; 237 | 238 | Toolset.prototype._getVersionFromVSWhere = async function () { 239 | let programFilesPath = _.get(process.env, "ProgramFiles(x86)", _.get(process.env, "ProgramFiles")); 240 | let vswhereCommand = path.resolve(programFilesPath, "Microsoft Visual Studio", "Installer", "vswhere.exe"); 241 | let vswhereOutput = null; 242 | 243 | try { 244 | this.log.verbose("TOOL", `Looking for vswhere.exe at '${vswhereCommand}'.`); 245 | vswhereOutput = await processHelpers.exec(`"${vswhereCommand}" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationVersion`); 246 | } 247 | catch (e) { 248 | this.log.verbose("TOOL", "Could not find vswhere.exe (VS installation is probably older than 15.2)."); 249 | return null; 250 | } 251 | 252 | if (!vswhereOutput) { 253 | return null; 254 | } 255 | 256 | let version = vswhereOutput.trim(); 257 | version = version.substring(0, version.indexOf(".")); 258 | 259 | return Number(version); 260 | }; 261 | 262 | module.exports = Toolset; 263 | -------------------------------------------------------------------------------- /cmake-tool/vsDetect.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | let processHelpers = require("./processHelpers"); 3 | let _ = require("lodash"); 4 | let path = require("path"); 5 | 6 | let vsDetect = { 7 | isInstalled: async function (version) { 8 | let vsInstalled = await this._isVSInstalled(version); 9 | let vsvNextInstalled = await this._isVSvNextInstalled(version); 10 | let buildToolsInstalled = await this._isBuildToolsInstalled(version); 11 | let foundByVSWhere = await this._isFoundByVSWhere(version); 12 | 13 | return vsInstalled || vsvNextInstalled || buildToolsInstalled || foundByVSWhere; 14 | }, 15 | 16 | _isFoundByVSWhere: async function (version) { 17 | // TODO: with auto download 18 | /* 19 | let mainVer = version.split(".")[0]; 20 | let command = path.resolve("vswhere.exe"); 21 | try { 22 | let stdout = yield processHelpers.exec(command, ["-version", version]); 23 | return stdout && stdout.indexOf("installationVersion: " + mainVer) > 0; 24 | } 25 | catch (e) { 26 | _.noop(e); 27 | } 28 | */ 29 | return false; 30 | }, 31 | 32 | _isBuildToolsInstalled: async function (version) { 33 | let mainVer = version.split(".")[0]; 34 | let key; 35 | let testPhrase; 36 | if (Number(mainVer) >= 15) { 37 | key = "HKLM\\SOFTWARE\\Classes\\Installer\\Dependencies\\Microsoft.VS.windows_toolscore,v" + mainVer; 38 | testPhrase = "Version"; 39 | } 40 | else { 41 | key = "HKLM\\SOFTWARE\\Classes\\Installer\\Dependencies\\Microsoft.VS.VisualCppBuildTools_x86_enu,v" + mainVer; 42 | testPhrase = "Visual C++"; 43 | } 44 | let command = "reg query \"" + key + "\""; 45 | try { 46 | let stdout = await processHelpers.exec(command); 47 | return stdout && stdout.indexOf(testPhrase) > 0; 48 | } 49 | catch (e) { 50 | _.noop(e); 51 | } 52 | return false; 53 | }, 54 | 55 | _isVSInstalled: async function (version) { 56 | // On x64 this will look for x64 keys only, but if VS and compilers installed properly, 57 | // it will write it's keys to 64 bit registry as well. 58 | let command = "reg query \"HKLM\\Software\\Microsoft\\VisualStudio\\" + version + "\""; 59 | try { 60 | let stdout = await processHelpers.exec(command); 61 | if (stdout) { 62 | let lines = stdout.split("\r\n").filter(function (line) { 63 | return line.length > 10; 64 | }); 65 | if (lines.length >= 4) { 66 | return true; 67 | } 68 | } 69 | } 70 | catch (e) { 71 | _.noop(e); 72 | } 73 | return false; 74 | }, 75 | 76 | _isVSvNextInstalled: async function (version) { 77 | let mainVer = version.split(".")[0]; 78 | let command = "reg query \"HKLM\\SOFTWARE\\Classes\\Installer\\Dependencies\\Microsoft.VisualStudio.MinShell.Msi,v" + mainVer + "\""; 79 | try { 80 | let stdout = await processHelpers.exec(command); 81 | if (stdout) { 82 | let lines = stdout.split("\r\n").filter(function (line) { 83 | return line.length > 10; 84 | }); 85 | if (lines.length >= 3) { 86 | return true; 87 | } 88 | } 89 | } 90 | catch (e) { 91 | _.noop(e); 92 | } 93 | return false; 94 | } 95 | }; 96 | 97 | module.exports = vsDetect; 98 | -------------------------------------------------------------------------------- /ffplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevevista/electron-ffplay/b41cecdedbe047a35b3a4d09ef7b2bf09a21f53a/ffplay.png -------------------------------------------------------------------------------- /licenses/FFmpeg.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | Most files in FFmpeg are under the GNU Lesser General Public License version 2.1 4 | or later (LGPL v2.1+). Read the file `COPYING.LGPLv2.1` for details. Some other 5 | files have MIT/X11/BSD-style licenses. In combination the LGPL v2.1+ applies to 6 | FFmpeg. 7 | 8 | Some optional parts of FFmpeg are licensed under the GNU General Public License 9 | version 2 or later (GPL v2+). See the file `COPYING.GPLv2` for details. None of 10 | these parts are used by default, you have to explicitly pass `--enable-gpl` to 11 | configure to activate them. In this case, FFmpeg's license changes to GPL v2+. 12 | 13 | Specifically, the GPL parts of FFmpeg are: 14 | 15 | - libpostproc 16 | - optional x86 optimization in the files 17 | - `libavcodec/x86/flac_dsp_gpl.asm` 18 | - `libavcodec/x86/idct_mmx.c` 19 | - `libavfilter/x86/vf_removegrain.asm` 20 | - the following building and testing tools 21 | - `compat/solaris/make_sunver.pl` 22 | - `doc/t2h.pm` 23 | - `doc/texi2pod.pl` 24 | - `libswresample/tests/swresample.c` 25 | - `tests/checkasm/*` 26 | - `tests/tiny_ssim.c` 27 | - the following filters in libavfilter: 28 | - `signature_lookup.c` 29 | - `vf_blackframe.c` 30 | - `vf_boxblur.c` 31 | - `vf_colormatrix.c` 32 | - `vf_cover_rect.c` 33 | - `vf_cropdetect.c` 34 | - `vf_delogo.c` 35 | - `vf_eq.c` 36 | - `vf_find_rect.c` 37 | - `vf_fspp.c` 38 | - `vf_geq.c` 39 | - `vf_histeq.c` 40 | - `vf_hqdn3d.c` 41 | - `vf_kerndeint.c` 42 | - `vf_lensfun.c` (GPL version 3 or later) 43 | - `vf_mcdeint.c` 44 | - `vf_mpdecimate.c` 45 | - `vf_nnedi.c` 46 | - `vf_owdenoise.c` 47 | - `vf_perspective.c` 48 | - `vf_phase.c` 49 | - `vf_pp.c` 50 | - `vf_pp7.c` 51 | - `vf_pullup.c` 52 | - `vf_repeatfields.c` 53 | - `vf_sab.c` 54 | - `vf_signature.c` 55 | - `vf_smartblur.c` 56 | - `vf_spp.c` 57 | - `vf_stereo3d.c` 58 | - `vf_super2xsai.c` 59 | - `vf_tinterlace.c` 60 | - `vf_uspp.c` 61 | - `vf_vaguedenoiser.c` 62 | - `vsrc_mptestsrc.c` 63 | 64 | Should you, for whatever reason, prefer to use version 3 of the (L)GPL, then 65 | the configure parameter `--enable-version3` will activate this licensing option 66 | for you. Read the file `COPYING.LGPLv3` or, if you have enabled GPL parts, 67 | `COPYING.GPLv3` to learn the exact legal terms that apply in this case. 68 | 69 | There are a handful of files under other licensing terms, namely: 70 | 71 | * The files `libavcodec/jfdctfst.c`, `libavcodec/jfdctint_template.c` and 72 | `libavcodec/jrevdct.c` are taken from libjpeg, see the top of the files for 73 | licensing details. Specifically note that you must credit the IJG in the 74 | documentation accompanying your program if you only distribute executables. 75 | You must also indicate any changes including additions and deletions to 76 | those three files in the documentation. 77 | * `tests/reference.pnm` is under the expat license. 78 | 79 | 80 | ## External libraries 81 | 82 | FFmpeg can be combined with a number of external libraries, which sometimes 83 | affect the licensing of binaries resulting from the combination. 84 | 85 | ### Compatible libraries 86 | 87 | The following libraries are under GPL version 2: 88 | - avisynth 89 | - frei0r 90 | - libcdio 91 | - libdavs2 92 | - librubberband 93 | - libvidstab 94 | - libx264 95 | - libx265 96 | - libxavs 97 | - libxavs2 98 | - libxvid 99 | 100 | When combining them with FFmpeg, FFmpeg needs to be licensed as GPL as well by 101 | passing `--enable-gpl` to configure. 102 | 103 | The following libraries are under LGPL version 3: 104 | - gmp 105 | - libaribb24 106 | - liblensfun 107 | 108 | When combining them with FFmpeg, use the configure option `--enable-version3` to 109 | upgrade FFmpeg to the LGPL v3. 110 | 111 | The VMAF, mbedTLS, RK MPI, OpenCORE and VisualOn libraries are under the Apache License 112 | 2.0. That license is incompatible with the LGPL v2.1 and the GPL v2, but not with 113 | version 3 of those licenses. So to combine these libraries with FFmpeg, the 114 | license version needs to be upgraded by passing `--enable-version3` to configure. 115 | 116 | The smbclient library is under the GPL v3, to combine it with FFmpeg, 117 | the options `--enable-gpl` and `--enable-version3` have to be passed to 118 | configure to upgrade FFmpeg to the GPL v3. 119 | 120 | ### Incompatible libraries 121 | 122 | There are certain libraries you can combine with FFmpeg whose licenses are not 123 | compatible with the GPL and/or the LGPL. If you wish to enable these 124 | libraries, even in circumstances that their license may be incompatible, pass 125 | `--enable-nonfree` to configure. This will cause the resulting binary to be 126 | unredistributable. 127 | 128 | The Fraunhofer FDK AAC and OpenSSL libraries are under licenses which are 129 | incompatible with the GPLv2 and v3. To the best of our knowledge, they are 130 | compatible with the LGPL. 131 | -------------------------------------------------------------------------------- /node-ffplay/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | # cmake -G "Visual Studio 15" -A x64 .. -DFFMPEG_PREBUILD_DIR=... 3 | # cmake --build . --config Release 4 | 5 | cmake_minimum_required(VERSION 3.9) 6 | 7 | project(node-ffplay) 8 | 9 | set(THIRD_PARTY_PREBUILT ${THIRD_PARTY_DIR}/prebuilt) 10 | 11 | if (NOT FFMPEG_PREBUILD_DIR) 12 | set(FFMPEG_PREBUILD_DIR ${THIRD_PARTY_PREBUILT}) 13 | endif() 14 | 15 | set(FFMPEG_INCLUDE_PATH ${FFMPEG_PREBUILD_DIR}/include) 16 | set(FFMPEG_LIB_PATH ${FFMPEG_PREBUILD_DIR}/lib) 17 | 18 | set(THIRD_INC_PATH ${THIRD_PARTY_PREBUILT}/include) 19 | if (CMAKE_CL_64) 20 | set(THIRD_LIB_PATH ${THIRD_PARTY_PREBUILT}/lib/x64) 21 | else() 22 | set(THIRD_LIB_PATH ${THIRD_PARTY_PREBUILT}/lib/x86) 23 | endif() 24 | 25 | include(../cmake-tool/node-addon.cmake) 26 | 27 | # build nodejs module 28 | add_node_module(node-ffplay 29 | CACHE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../node_cache" 30 | SUFFIX ".dll" 31 | NODE_ABIS ${NODE_ABI}) 32 | 33 | list(GET node-ffplay::targets 0 _TARGET) 34 | 35 | target_sources(node-ffplay INTERFACE 36 | ${CMAKE_CURRENT_SOURCE_DIR}/src/wrap.cc 37 | ${CMAKE_CURRENT_SOURCE_DIR}/src/player.cc 38 | ) 39 | 40 | if(MSVC) 41 | target_compile_options(node-ffplay INTERFACE /W3 /WX- 42 | /wd"4005" 43 | /wd"4018" 44 | /wd"4047" 45 | /wd"4090" 46 | /wd"4101" 47 | /wd"4146" 48 | /wd"4229" 49 | /wd"4244" 50 | /wd"4267" 51 | /wd"4305" 52 | /wd"4334" 53 | /wd"4819" 54 | /wd"4996") 55 | endif() 56 | 57 | target_link_directories(node-ffplay INTERFACE 58 | ${FFMPEG_LIB_PATH} 59 | ${THIRD_LIB_PATH}) 60 | 61 | target_include_directories(node-ffplay INTERFACE 62 | ${FFMPEG_INCLUDE_PATH} 63 | ${THIRD_INC_PATH}) 64 | target_compile_definitions(node-ffplay INTERFACE BUILD_NODEJS WIN32 _WINDOWS _USE_MATH_DEFINES _CRT_SECURE_NO_WARNINGS _WIN32_WINNT=0x0600 NDEBUG) 65 | target_compile_definitions(node-ffplay INTERFACE NAPI_CPP_EXCEPTIONS) # NAPI_DISABLE_CPP_EXCEPTIONS 66 | #target_compile_definitions(node-ffplay INTERFACE BUILD_WITH_AUDIO_FILTER) # BUILD_WITH_VIDEO_FILTER 67 | 68 | target_link_libraries(node-ffplay INTERFACE 69 | avcodec 70 | avutil 71 | avformat 72 | swresample 73 | swscale 74 | avfilter 75 | sdl2) 76 | 77 | add_custom_target(CopyRuntimeFiles ALL 78 | VERBATIM 79 | COMMAND_EXPAND_LISTS 80 | COMMAND ${CMAKE_COMMAND} -E 81 | copy_if_different 82 | $ 83 | "${CMAKE_CURRENT_SOURCE_DIR}/../" 84 | ) 85 | -------------------------------------------------------------------------------- /node-ffplay/src/player.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #undef main 4 | 5 | extern "C" { 6 | #include "libavutil/opt.h" 7 | #include "libavcodec/avcodec.h" 8 | #include "libavformat/avformat.h" 9 | # include "libavfilter/avfilter.h" 10 | # include "libavfilter/buffersink.h" 11 | # include "libavfilter/buffersrc.h" 12 | } 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | using namespace std; 23 | 24 | #ifndef CONFIG_AVFILTER 25 | #undef BUILD_WITH_AUDIO_FILTER 26 | #undef BUILD_WITH_VIDEO_FILTER 27 | #endif 28 | 29 | /* no AV correction is done if too big error */ 30 | #define AV_NOSYNC_THRESHOLD 10.0 31 | 32 | void ff_init(); 33 | 34 | enum MediaStatus { 35 | MEDIA_STATUS_START = 1, 36 | MEDIA_STATUS_PAUSED, 37 | MEDIA_STATUS_RESUMED, 38 | MEDIA_STATUS_REWIND_END 39 | }; 40 | 41 | enum { 42 | AV_SYNC_AUDIO_MASTER, /* default choice */ 43 | AV_SYNC_VIDEO_MASTER, 44 | AV_SYNC_EXTERNAL_CLOCK, /* synchronize to an external clock */ 45 | }; 46 | 47 | #define VIDEO_PICTURE_QUEUE_SIZE 3 48 | #define SUBPICTURE_QUEUE_SIZE 16 49 | #define SAMPLE_QUEUE_SIZE 9 50 | #define FRAME_QUEUE_SIZE FFMAX(SAMPLE_QUEUE_SIZE, FFMAX(VIDEO_PICTURE_QUEUE_SIZE, SUBPICTURE_QUEUE_SIZE)) 51 | 52 | typedef struct AudioParams { 53 | int freq{0}; 54 | int channels{0}; 55 | int64_t channel_layout{0}; 56 | enum AVSampleFormat fmt{ AV_SAMPLE_FMT_NONE }; 57 | int frame_size{0}; 58 | int bytes_per_sec{0}; 59 | } AudioParams; 60 | 61 | 62 | struct Clock { 63 | double pts{0}; /* clock base */ 64 | double pts_drift{0}; /* clock base minus time at which we updated the clock */ 65 | double last_updated{0}; 66 | double speed{0}; 67 | int serial{0}; /* clock is based on a packet with this serial */ 68 | int paused{0}; 69 | int *queue_serial{0}; /* pointer to the current packet queue serial, used for obsolete clock detection */ 70 | 71 | Clock(int *queue_serial); 72 | double get_clock() const; 73 | double time_passed() const; 74 | void update(); 75 | void set_clock_at(double pts, int serial, double time); 76 | void set_clock(double pts, int serial); 77 | void sync_clock_to_slave(const Clock *slave, double threshold = AV_NOSYNC_THRESHOLD); 78 | void set_clock_speed(double speed); 79 | }; 80 | 81 | class PacketQueue { 82 | public: 83 | PacketQueue(int& serial); 84 | ~PacketQueue(); 85 | 86 | bool has_enough_packets(const AVRational& time_base) const; 87 | int size() const { return size_; } 88 | int packetsCount() const { return nb_packets; } 89 | bool empty() const { return nb_packets == 0; } 90 | 91 | int put(AVPacket *pkt, int specified_serial = -1); 92 | int put_nullpacket(int stream_index); 93 | int get(AVPacket *pkt, int *serial); 94 | void start(); 95 | void nextSerial(); 96 | void abort(); 97 | 98 | private: 99 | void flush(); 100 | int put_private(std::unique_lock& lk, AVPacket *pkt, int specified_serial); 101 | 102 | private: 103 | std::queue> pkts_; 104 | int nb_packets{0}; 105 | int size_{0}; 106 | int64_t duration_{0}; 107 | bool abort_request_{true}; 108 | std::mutex mtx; 109 | std::condition_variable cond; 110 | 111 | int& serial_; // referenced streaming serial 112 | }; 113 | 114 | /* Common struct for handling all types of decoded data and allocated render buffers. */ 115 | struct Frame { 116 | AVFrame *frame{nullptr}; 117 | AVSubtitle sub{0}; 118 | int serial{0}; 119 | double pts{0}; /* presentation timestamp for the frame */ 120 | double duration{0}; /* estimated duration of the frame */ 121 | int64_t pos{0}; /* byte position of the frame in the input file */ 122 | int width{0}; 123 | int height{0}; 124 | int format{0}; 125 | AVRational sar{0}; 126 | int uploaded{0}; 127 | }; 128 | 129 | struct SimpleFrame { 130 | SimpleFrame(AVFrame *src_frame, int serial, double pts, double duration) { 131 | frame = av_frame_alloc(); 132 | av_frame_move_ref(frame, src_frame); 133 | av_frame_unref(src_frame); 134 | this->serial = serial; 135 | 136 | this->pts = pts; 137 | this->duration = duration; 138 | } 139 | 140 | ~SimpleFrame() { 141 | av_frame_free(&frame); 142 | } 143 | 144 | SimpleFrame(const SimpleFrame&) = delete; 145 | SimpleFrame& operator=(const SimpleFrame&) = delete; 146 | 147 | SimpleFrame(SimpleFrame&& other) { 148 | this->frame = other.frame; 149 | this->serial = other.serial; 150 | this->pts = other.pts; 151 | this->duration = other.duration; 152 | other.frame = nullptr; 153 | } 154 | 155 | SimpleFrame& operator=(SimpleFrame&& other) { 156 | this->frame = other.frame; 157 | this->serial = other.serial; 158 | this->pts = other.pts; 159 | this->duration = other.duration; 160 | other.frame = nullptr; 161 | return *this; 162 | } 163 | 164 | AVFrame *frame{nullptr}; 165 | int serial{0}; 166 | double pts{0}; /* presentation timestamp for the frame */ 167 | double duration{0}; /* estimated duration of the frame */ 168 | }; 169 | 170 | struct FrameQueue { 171 | FrameQueue(const int& serial, int max_size, bool keep_last); 172 | ~FrameQueue(); 173 | void abort(); 174 | 175 | int64_t lastShownPosition() const; 176 | /* return the number of undisplayed frames in the queue */ 177 | int nb_remaining() const { 178 | return size - rindex_shown; 179 | } 180 | 181 | Frame *peek_readable(); 182 | Frame *peek_writable(); 183 | void next(); 184 | void push(); 185 | Frame *peek(); 186 | Frame *peek_next(); 187 | Frame *peek_last(); 188 | 189 | Frame queue[FRAME_QUEUE_SIZE]{0}; 190 | int rindex{0}; 191 | int windex{0}; 192 | int size{0}; 193 | int rindex_shown{0}; 194 | std::mutex mtx; 195 | std::condition_variable cond; 196 | 197 | private: 198 | const int& serial_; 199 | const int max_size_{0}; 200 | const bool keep_last_{false}; 201 | bool abort_request_{false}; 202 | }; 203 | 204 | using PacketGetter = std::function; 205 | 206 | class Decoder { 207 | public: 208 | Decoder(const int& serial); 209 | virtual ~Decoder(); 210 | 211 | void init(AVCodecContext *avctx); 212 | void destroy(); 213 | 214 | void abort(); 215 | bool finished() const; 216 | int decodeFrame(PacketGetter packet_getter, AVFrame *frame, AVSubtitle *sub, int& pkt_serial); 217 | 218 | template 219 | void start(LoopFunc&& f) { 220 | abort(); 221 | abort_request_ = false; 222 | tid_ = std::thread([this, f] { 223 | f(this, &this->finished_); 224 | }); 225 | } 226 | 227 | bool valid() const { return !!avctx_; } 228 | const AVCodecContext* context() const { return avctx_; } 229 | 230 | int64_t start_pts{0}; 231 | AVRational start_pts_tb{0}; 232 | int64_t next_pts{0}; 233 | AVRational next_pts_tb{0}; 234 | 235 | private: 236 | std::thread tid_; 237 | bool abort_request_{true}; 238 | 239 | int finished_{0}; 240 | const int& serial_; 241 | 242 | AVCodecContext *avctx_{nullptr}; 243 | AVPacket pending_pkt_{0}; 244 | bool packet_pending_{false}; 245 | }; 246 | 247 | 248 | // only for test 249 | class SyncDecoder : public Decoder { 250 | public: 251 | SyncDecoder(); 252 | ~SyncDecoder(); 253 | 254 | int decodeBuffer(const uint8_t* data, int size, AVFrame **frame_out); 255 | 256 | static SyncDecoder* open(const char* codec_name, const AVCodecParameters *par); 257 | private: 258 | int placeHodler_{1}; 259 | AVFrame *frame_{nullptr}; 260 | }; 261 | 262 | enum MediaCommand { 263 | MEDIA_CMD_QUIT = 1, 264 | MEDIA_CMD_PAUSE, 265 | MEDIA_CMD_VOLUMN, // arg0=0: mute arg0=1: up arg0=1-1: down 266 | MEDIA_CMD_NEXT_FRAME, 267 | MEDIA_CMD_PREV_FRAME, 268 | MEDIA_CMD_CHAPTER, 269 | MEDIA_CMD_SEEK, 270 | MEDIA_CMD_SPEED 271 | }; 272 | 273 | /* 274 | MEDIA_CMD_SEEK, arg0, 275 | arg0 = 0, seek by pts 276 | arg0 = 1, seek by pts relative, arg1 = delta 277 | arg0 = 2, seek by frmae id, arg1 = int64(id) 278 | 279 | */ 280 | 281 | enum SeekMethod { 282 | SEEK_METHOD_NONE, 283 | SEEK_METHOD_POS, 284 | SEEK_METHOD_BYTES, 285 | SEEK_METHOD_REWIND, 286 | SEEK_METHOD_REWIND_CONTINUE 287 | }; 288 | 289 | typedef struct MediaEvent { 290 | int event; 291 | int arg0; 292 | double arg1; 293 | double arg2; 294 | } MediaEvent; 295 | 296 | class EventQueue { 297 | public: 298 | void set(MediaEvent *evt); 299 | bool get(MediaEvent *evt); 300 | 301 | private: 302 | std::queue evts_; 303 | bool quit_{false}; 304 | std::mutex mtx; 305 | }; 306 | 307 | 308 | struct ConverterContext { 309 | ConverterContext(AVPixelFormat fmt); 310 | ~ConverterContext(); 311 | 312 | int convert(AVFrame *frame); 313 | int convert(int src_format, int src_width, int src_height, const uint8_t * const*pixels, int* pitch); 314 | 315 | struct SwsContext *convert_ctx{nullptr}; 316 | uint8_t* buffer{nullptr}; 317 | int buffer_size_{0}; 318 | 319 | AVFrame *frame_{nullptr}; 320 | const AVPixelFormat target_fmt{AV_PIX_FMT_NONE}; 321 | }; 322 | 323 | struct Detection_t; 324 | 325 | using OnStatus = std::function; 326 | using OnMetaInfo = std::function; 332 | using OnStatics = std::function; 333 | using OnClockUpdate = std::function; 334 | using OnIYUVDisplay = std::function; 335 | using OnAIData = std::function; 336 | using OnLog = std::function; 337 | 338 | class PlayBackContext { 339 | public: 340 | virtual ~PlayBackContext(); 341 | PlayBackContext(); 342 | 343 | void eventLoop(int argc, char **argv); 344 | void sendEvent(int event, int arg0, double arg1, double arg2); 345 | 346 | protected: 347 | const Clock& masterClock() const; 348 | int get_master_sync_type() const; 349 | double get_master_clock() const; 350 | void adjustExternalClockSpeed(); 351 | 352 | int64_t ptsToFrameId(double pts) const; 353 | double frameIdToPts(int64_t id) const; 354 | 355 | void streamOpen(); 356 | void streamClose(); 357 | void streamComponentOpen(int stream_index); 358 | void streamComponentClose(int stream_index); 359 | 360 | static int decode_interrupt_cb(void *ctx); 361 | 362 | void doReadInThread(); 363 | 364 | void audioOpen(int64_t wanted_channel_layout, int wanted_nb_channels, int wanted_sample_rate); 365 | static void sdl_audio_callback(void *opaque, Uint8 *stream, int len); 366 | int audio_decode_frame(); 367 | int synchronize_audio(int nb_samples); 368 | 369 | void configureAudioFilters(bool force_output_format); 370 | int configure_video_filters(AVFilterGraph *graph, const char *vfilters, AVFrame *frame); 371 | 372 | void onPacketDrained(); 373 | 374 | int onVideoFrameDecodedReversed(AVFrame *frame, int serial); 375 | 376 | void startVideoDecodeThread(); 377 | void startAudioDecodeThread(); 378 | void startSubtitleDecodeThread(); 379 | 380 | void startDataDecode(); 381 | void stopDataDecode(); 382 | int receiveDataPacket(AVPacket *pkt, int& pkt_serial); 383 | int dealWithDataPacket(const AVPacket *pkt, const int pkt_serial); 384 | 385 | int pushPacket(AVPacket* pkt, int specified_serial = -1); 386 | void newSerial(); 387 | 388 | int queuePicture(AVFrame *src_frame, double pts, double duration, int64_t pos, int serial); 389 | 390 | void refreshLoopWaitEvent(MediaEvent *event); 391 | int handleEvent(const MediaEvent& event, int& quit); 392 | 393 | int getVideoFrame(AVFrame *frame, int& pkt_serial); 394 | 395 | void video_refresh(double *remaining_time); 396 | void video_image_display(); 397 | 398 | void video_refresh_rewind(double *remaining_time); 399 | double computeVideoTargetDelayReversed(const Frame *lastvp, const Frame *vp) const; 400 | double vpDurationReversed(const Frame *vp, const Frame *nextvp) const; 401 | 402 | double vp_duration(const Frame *vp, const Frame *nextvp) const; 403 | double compute_target_delay(double delay) const; 404 | 405 | void stream_toggle_pause(); 406 | 407 | void videoRefreshShowStatus(int64_t& last_time) const; 408 | 409 | void sendSeekRequest(SeekMethod req, int64_t pos, int64_t rel = 0); 410 | 411 | void updateVolume(int sign, double step); 412 | void setVolume(int val); 413 | void setMute(bool v); 414 | void toggleMute(); 415 | int getVolume() const; 416 | bool isMuted() const; 417 | 418 | void change_speed(double speed); 419 | 420 | void step_to_next_frame(); 421 | void step_to_prev_frame(); 422 | void togglePause(); 423 | void seek_chapter(int incr); 424 | 425 | bool videoPacketIsAddonData(AVCodecID codec_id, const AVPacket *pkt) const; 426 | 427 | protected: 428 | //bool rewindMode() const { return speed_ < 0; } 429 | bool rewindMode() const { return rewind_; } 430 | 431 | protected: 432 | Clock audclk; 433 | Clock vidclk; 434 | Clock extclk; 435 | 436 | bool realtime_{false}; 437 | 438 | AVFormatContext *ic{nullptr}; 439 | 440 | // seeking & speed 441 | SeekMethod seekMethod_{SEEK_METHOD_NONE}; 442 | bool rewind_{false}; 443 | int64_t seek_pos{0}; 444 | int64_t seek_rel{0}; 445 | int read_pause_return{0}; 446 | double speed_{1.0}; 447 | double prev_speed_{0}; 448 | bool stepping_{false}; 449 | 450 | std::deque rewindBuffer_; 451 | int64_t frameRewindTarget_; 452 | int64_t rewindEofPts_{0}; 453 | int64_t syncVideoPts_{-1}; 454 | 455 | double max_frame_duration{0}; // maximum duration of a frame - above this, we consider the jump a timestamp discontinuity 456 | int last_video_stream{-1}; 457 | int last_audio_stream{-1}; 458 | int last_subtitle_stream{-1}; 459 | int last_data_stream{-1}; 460 | 461 | int audio_stream{-1}; 462 | AVStream *audio_st{nullptr}; 463 | PacketQueue audioPacketQueue_; 464 | FrameQueue sampleQueue_; 465 | Decoder audioDecoder_; 466 | 467 | int audio_hw_buf_size{0}; 468 | uint8_t *audio_buf{nullptr}; 469 | uint8_t *audio_buf1{nullptr}; 470 | unsigned int audio_buf_size{0}; /* in bytes */ 471 | unsigned int audio_buf1_size{0}; 472 | int audio_buf_index{0}; /* in bytes */ 473 | int audio_write_buf_size{0}; 474 | bool muted_{false}; 475 | struct AudioParams audio_src; 476 | 477 | struct SwrContext *swr_ctx{nullptr}; 478 | 479 | double audio_clock{0}; 480 | int audio_clock_serial{-1}; 481 | double audio_diff_cum{0}; /* used for AV difference average computation */ 482 | double audio_diff_avg_coef{0}; 483 | double audio_diff_threshold{0}; 484 | int audio_diff_avg_count{0}; 485 | 486 | int64_t audio_callback_time{0}; 487 | 488 | #if defined(BUILD_WITH_AUDIO_FILTER) || defined(BUILD_WITH_VIDEO_FILTER) 489 | struct AudioParams audio_filter_src; 490 | #endif 491 | struct AudioParams audio_tgt; 492 | 493 | int video_stream{-1}; 494 | AVStream *video_st{nullptr}; 495 | PacketQueue videoPacketQueue_; 496 | FrameQueue pictureQueue_; 497 | Decoder videoDecoder_; 498 | 499 | int subtitle_stream{-1}; 500 | AVStream *subtitle_st{nullptr}; 501 | PacketQueue subtitlePacketQueue_; 502 | FrameQueue subtitleQueue_; 503 | Decoder subtitleDecoder_; 504 | 505 | int data_stream{-1}; 506 | AVStream *data_st{nullptr}; 507 | PacketQueue dataPacketQueue_; 508 | std::thread data_tid_; 509 | 510 | double frame_last_returned_time{0}; 511 | double frame_last_filter_delay{0}; 512 | 513 | int paused{0}; 514 | int last_paused{0}; 515 | int queue_attachments_req{0}; 516 | 517 | SDL_AudioDeviceID audio_dev{0}; 518 | 519 | #if defined(BUILD_WITH_AUDIO_FILTER) || defined(BUILD_WITH_VIDEO_FILTER) 520 | int vfilter_idx{0}; 521 | AVFilterContext *in_video_filter{nullptr}; // the first filter in the video chain 522 | AVFilterContext *out_video_filter{nullptr}; // the last filter in the video chain 523 | AVFilterContext *in_audio_filter{nullptr}; // the first filter in the audio chain 524 | AVFilterContext *out_audio_filter{nullptr}; // the last filter in the audio chain 525 | AVFilterGraph *agraph{nullptr}; // audio filter graph 526 | #endif 527 | 528 | int64_t duration_{AV_NOPTS_VALUE}; 529 | int64_t start_time_{0}; 530 | 531 | int videoSerial_{0}; 532 | int audioSerial_{0}; 533 | int dataSerial_{0}; 534 | int subtitleSerial_{0}; 535 | 536 | bool abort_reading_{false}; 537 | bool eof_{false}; 538 | std::thread read_tid_; 539 | std::condition_variable continue_read_thread_; 540 | std::mutex wait_mtx; 541 | 542 | EventQueue evq_; 543 | 544 | // video 545 | int frame_drops_early{0}; 546 | int frame_drops_late{0}; 547 | 548 | bool drop_frame_mode{false}; 549 | 550 | AVRational data_time_base_{1, AV_TIME_BASE}; 551 | AVRational video_time_base_{1, AV_TIME_BASE}; 552 | AVRational video_frame_rate_{0}; 553 | double frame_duration_{0}; 554 | 555 | bool force_refresh_{false}; 556 | 557 | double frame_timer_{0}; 558 | 559 | ConverterContext yuv_ctx_; 560 | ConverterContext sub_yuv_ctx_; 561 | 562 | public: 563 | OnStatus onStatus; 564 | OnMetaInfo onMetaInfo; 565 | OnStatics onStatics; 566 | OnClockUpdate onClockUpdate; 567 | OnIYUVDisplay onIYUVDisplay; 568 | OnAIData onAIData; 569 | OnLog onLog; 570 | 571 | public: 572 | AVDictionary *swr_opts{nullptr}; 573 | AVDictionary *sws_dict{nullptr}; 574 | AVDictionary *format_opts{nullptr}; 575 | AVDictionary *codec_opts{nullptr}; 576 | 577 | bool audio_disable{false}; 578 | bool subtitle_disable{false}; 579 | bool data_disable{false}; 580 | 581 | string wanted_stream_spec[AVMEDIA_TYPE_NB]; 582 | 583 | int64_t start_time{AV_NOPTS_VALUE}; 584 | int64_t duration{AV_NOPTS_VALUE}; 585 | int seek_by_bytes{-1}; 586 | float seek_interval{10}; 587 | int audio_volume{100}; 588 | 589 | AVInputFormat *iformat{nullptr}; 590 | string filename; 591 | 592 | bool fast{false}; 593 | bool genpts{false}; 594 | int lowres{0}; 595 | 596 | int decoder_reorder_pts{-1}; 597 | int av_sync_type{AV_SYNC_AUDIO_MASTER}; 598 | int framedrop{-1}; 599 | int infinite_buffer{-1}; 600 | 601 | #if defined(BUILD_WITH_AUDIO_FILTER) || defined(BUILD_WITH_VIDEO_FILTER) 602 | vector vfilters_list; 603 | string afilters; 604 | int filter_nbthreads{0}; 605 | #endif 606 | 607 | string audio_codec_name; 608 | string subtitle_codec_name; 609 | string video_codec_name; 610 | 611 | bool showStatus{false}; 612 | }; 613 | 614 | // 615 | // 616 | struct Detection_t { 617 | double pts; 618 | }; 619 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-ffplay", 3 | "version": "1.0.1", 4 | "author": "R.J.", 5 | "description": "An electron-vue project", 6 | "license": "MIT", 7 | "main": "dist/electron/main.js", 8 | "scripts": { 9 | "build:pack": "electron-builder", 10 | "build:dir": "electron-builder --dir", 11 | "build:dist": "node --max_old_space_size=32768 .electron-vue/build.js", 12 | "build": "node --max_old_space_size=32768 .electron-vue/build.js && electron-builder", 13 | "build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js", 14 | "dev": "node .electron-vue/dev-runner.js", 15 | "e2e": "npm run pack && mocha test/e2e", 16 | "lint": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter src test", 17 | "lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter --fix src test", 18 | "configure:ffplay": "node cmake-tool configure -d node-ffplay --CDTHIRD_PARTY_DIR=../.third-party", 19 | "build:ffplay": "node cmake-tool build -d node-ffplay", 20 | "test": "npm run unit && npm run e2e", 21 | "unit": "karma start test/unit/karma.conf.js" 22 | }, 23 | "build": { 24 | "asar": true, 25 | "productName": "electron-ffplay", 26 | "appId": "com.steveista.app", 27 | "directories": { 28 | "output": "release" 29 | }, 30 | "files": [ 31 | "dist/electron/**/*" 32 | ], 33 | "extraFiles": [ 34 | "licenses/**/*", 35 | "*.dll" 36 | ], 37 | "dmg": { 38 | "contents": [ 39 | { 40 | "x": 410, 41 | "y": 150, 42 | "type": "link", 43 | "path": "/Applications" 44 | }, 45 | { 46 | "x": 130, 47 | "y": 150, 48 | "type": "file" 49 | } 50 | ] 51 | }, 52 | "mac": { 53 | "icon": "resources/icons/icon.icns" 54 | }, 55 | "win": { 56 | "icon": "resources/icons/icon.ico", 57 | "sign": ".electron-vue/sign.js" 58 | }, 59 | "linux": { 60 | "icon": "resources/icons" 61 | } 62 | }, 63 | "devDependencies": { 64 | "@babel/core": "^7.5.5", 65 | "@babel/plugin-proposal-class-properties": "^7.5.5", 66 | "@babel/plugin-transform-runtime": "^7.5.5", 67 | "@babel/preset-env": "^7.7.7", 68 | "@babel/runtime": "^7.5.5", 69 | "@mdi/font": "^4.1.95", 70 | "ajv": "^6.5.0", 71 | "babel-eslint": "^10.0.2", 72 | "babel-loader": "^8.0.6", 73 | "babel-minify-webpack-plugin": "^0.3.1", 74 | "babel-plugin-istanbul": "^4.1.6", 75 | "babel-plugin-transform-runtime": "^6.23.0", 76 | "babili-webpack-plugin": "^0.1.2", 77 | "balloon-css": "^1.0.4", 78 | "big-integer": "^1.6.48", 79 | "bluebird": "^3.7.2", 80 | "brace": "^0.11.1", 81 | "chai": "^4.1.2", 82 | "chalk": "^2.4.1", 83 | "cross-env": "^5.1.6", 84 | "css-loader": "^0.28.11", 85 | "del": "^3.0.0", 86 | "devtron": "^1.4.0", 87 | "electron": "^8.2.2", 88 | "electron-builder": "^20.43.0", 89 | "electron-debug": "^3.0.0", 90 | "electron-devtools-installer": "^2.2.4", 91 | "electron-log": "^4.0.3", 92 | "eslint": "^4.19.1", 93 | "eslint-config-standard": "^11.0.0", 94 | "eslint-friendly-formatter": "^4.0.1", 95 | "eslint-loader": "^2.0.0", 96 | "eslint-plugin-html": "^4.0.3", 97 | "eslint-plugin-import": "^2.12.0", 98 | "eslint-plugin-node": "^6.0.1", 99 | "eslint-plugin-promise": "^3.8.0", 100 | "eslint-plugin-standard": "^3.1.0", 101 | "file-loader": "^1.1.11", 102 | "fs-extra": "^9.1.0", 103 | "html-webpack-plugin": "^3.2.0", 104 | "inject-loader": "^4.0.1", 105 | "karma": "^2.0.2", 106 | "karma-chai": "^0.1.0", 107 | "karma-coverage": "^1.1.2", 108 | "karma-electron": "^6.0.0", 109 | "karma-mocha": "^1.3.0", 110 | "karma-sourcemap-loader": "^0.3.7", 111 | "karma-spec-reporter": "^0.0.32", 112 | "karma-webpack": "^3.0.0", 113 | "lodash": "^4.17.20", 114 | "mini-css-extract-plugin": "0.4.0", 115 | "mocha": "^5.2.0", 116 | "moment": "^2.24.0", 117 | "multispinner": "^0.2.1", 118 | "node-loader": "^0.6.0", 119 | "node-sass": "^4.9.2", 120 | "npmlog": "^4.1.2", 121 | "raw-loader": "^4.0.0", 122 | "rc": "^1.2.8", 123 | "require-dir": "^1.0.0", 124 | "sass-loader": "^7.0.3", 125 | "selectn": "^1.1.2", 126 | "spectron": "^3.8.0", 127 | "splitargs": "0.0.7", 128 | "style-loader": "^0.21.0", 129 | "url-loader": "^1.0.1", 130 | "vue": "^2.6.10", 131 | "vue-devtools": "^5.1.3", 132 | "vue-html-loader": "^1.2.4", 133 | "vue-i18n": "^8.11.2", 134 | "vue-loader": "^15.2.4", 135 | "vue-style-loader": "^4.1.0", 136 | "vue-template-compiler": "^2.5.16", 137 | "vuetify": "^2.1.15", 138 | "vuex": "^3.1.1", 139 | "webpack": "^4.15.1", 140 | "webpack-cli": "^3.0.8", 141 | "webpack-dev-server": "^3.1.4", 142 | "webpack-hot-middleware": "^2.22.2", 143 | "webpack-merge": "^4.1.3", 144 | "which": "^2.0.2", 145 | "xml2js": "^0.4.22", 146 | "yml-loader": "^2.1.0" 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /resources/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevevista/electron-ffplay/b41cecdedbe047a35b3a4d09ef7b2bf09a21f53a/resources/icons/256x256.png -------------------------------------------------------------------------------- /resources/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevevista/electron-ffplay/b41cecdedbe047a35b3a4d09ef7b2bf09a21f53a/resources/icons/icon.icns -------------------------------------------------------------------------------- /resources/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevevista/electron-ffplay/b41cecdedbe047a35b3a4d09ef7b2bf09a21f53a/resources/icons/icon.ico -------------------------------------------------------------------------------- /src/components/app-bar.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 79 | 80 | 85 | 86 | 117 | -------------------------------------------------------------------------------- /src/components/app.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 39 | 40 | 75 | 76 | 82 | -------------------------------------------------------------------------------- /src/components/dialogs.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 136 | -------------------------------------------------------------------------------- /src/components/xplayer/controller.vue: -------------------------------------------------------------------------------- 1 | 98 | 99 | 349 | 350 | 603 | -------------------------------------------------------------------------------- /src/components/xplayer/detection-canvas.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export function drawDetectionBox (ctx, type, rect, class_name) { 3 | // ctx.lineWidth = 5; 4 | ctx.strokeStyle = type === 0 ? 'green' : (type === 1 ? 'red' : 'blue'); 5 | ctx.strokeRect(rect[0], rect[1], rect[2] - rect[0], rect[3] - rect[1]); 6 | ctx.fillStyle = 'red'; 7 | if (class_name) { 8 | ctx.font = '16px Georgia'; 9 | ctx.fillText(class_name, rect[0], rect[1]); 10 | } 11 | } 12 | 13 | export function drawLandmark (ctx, landmarks) { 14 | ctx.strokeStyle = "green"; 15 | ctx.strokeWidth = 1; 16 | for (let i = 0; i < landmarks.length / 2; i++) { 17 | const x = landmarks[i * 2] 18 | const y = landmarks[i * 2 + 1] 19 | ctx.beginPath(); 20 | ctx.arc(x, y, 1, 0, 2 * Math.PI, true); 21 | ctx.stroke(); 22 | } 23 | } 24 | 25 | export function drawDetection (ctx, det) { 26 | drawDetectionBox(ctx, det.type, det.rect, det.labelString) 27 | if (det.landmarks && det.landmarks.length) { 28 | drawLandmark(ctx, det.landmarks); 29 | } 30 | } 31 | 32 | export function h264ProfileToCodec (profile) { 33 | switch (profile) { 34 | case 'Main': 35 | return 'avc1.4d0028'; 36 | case 'Baseline': 37 | return 'avc1.42001e'; 38 | case 'Constrained Baseline': 39 | return 'avc1.42001f'; 40 | case 'Extended': 41 | return 'avc1.580028' 42 | case 'High': 43 | return 'avc1.640028' 44 | case 'High 10': 45 | case 'High 10 Intra': 46 | return 'avc1.6e0028' 47 | case 'High 4:2:2': 48 | case 'High 4:2:2 Intra': 49 | return 'avc1.7a0028' 50 | case 'High 4:4:4': 51 | case 'High 4:4:4 Predictive': 52 | case 'High 4:4:4 Intra': 53 | return 'avc1.f40028' 54 | default: 55 | return 'avc1.42001e'; 56 | } 57 | } 58 | 59 | export class DetectionCanvas { 60 | constructor (canvas) { 61 | this.canvas = canvas 62 | this.ctx = canvas.getContext('2d'); 63 | } 64 | 65 | drawBox (type, rect, class_name) { 66 | drawDetectionBox(this.ctx, type, rect, class_name) 67 | } 68 | 69 | drawLandmark (landmarks) { 70 | drawLandmark(this.ctx, landmarks); 71 | } 72 | 73 | drawDetection (det) { 74 | drawDetection(this.ctx, det); 75 | } 76 | 77 | clear () { 78 | this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/components/xplayer/info-panel.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 133 | 134 | 175 | -------------------------------------------------------------------------------- /src/components/xplayer/loading.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 78 | -------------------------------------------------------------------------------- /src/components/xplayer/play-list.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 109 | 110 | 309 | -------------------------------------------------------------------------------- /src/components/xplayer/settings.vue: -------------------------------------------------------------------------------- 1 | 110 | 111 | 124 | -------------------------------------------------------------------------------- /src/components/xplayer/test_decode.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | import {Decoder} from '../../ffplay' 3 | import {EventEmitter} from 'events'; 4 | import fs from 'fs' 5 | 6 | let frameSize = [ 7 | 11727, 8 | 4582, 9 | 1456, 10 | 664, 11 | 776, 12 | 3797, 13 | 1434, 14 | 703, 15 | 677, 16 | 4446, 17 | 1476, 18 | 702, 19 | 743, 20 | 5640, 21 | 1655, 22 | 1037, 23 | 830, 24 | 4764, 25 | 1016, 26 | 697, 27 | 485, 28 | 4279, 29 | 957, 30 | 451, 31 | 541, 32 | 3538, 33 | 707, 34 | 508, 35 | 298, 36 | 2636, 37 | 430, 38 | 421, 39 | 3896, 40 | 1081, 41 | 622, 42 | 571, 43 | 4058, 44 | 1022, 45 | 611, 46 | 477, 47 | 3526, 48 | 917, 49 | 499, 50 | 505, 51 | 3473, 52 | 1204, 53 | 561, 54 | 625, 55 | 4322, 56 | 1211, 57 | 819, 58 | 511, 59 | 4564, 60 | 1222, 61 | 593, 62 | 760, 63 | 3768, 64 | 1167, 65 | 670, 66 | 538, 67 | 3671, 68 | 1015, 69 | 589, 70 | 645, 71 | 4105, 72 | 1265, 73 | 653, 74 | 710, 75 | 3837, 76 | 1332, 77 | 628, 78 | 712, 79 | 3769, 80 | 1162, 81 | 760, 82 | 630, 83 | 3636, 84 | 969, 85 | 897, 86 | 4692, 87 | 1448, 88 | 848, 89 | 729, 90 | 4932, 91 | 1589, 92 | 800, 93 | 836, 94 | 3728, 95 | 1570, 96 | 696, 97 | 1156, 98 | 5333, 99 | 2190, 100 | 1128, 101 | 1240, 102 | 4944, 103 | 1119, 104 | 760, 105 | 530, 106 | 3421, 107 | 648, 108 | 427, 109 | 440, 110 | 3225, 111 | 683, 112 | 440, 113 | 324, 114 | 2844, 115 | 534, 116 | 322, 117 | 379, 118 | 2770, 119 | 667, 120 | 414, 121 | 329, 122 | 2568, 123 | 702, 124 | 363, 125 | 397, 126 | 3351, 127 | 929, 128 | 438, 129 | 456, 130 | 3965, 131 | 1184, 132 | 566, 133 | 740, 134 | 5103, 135 | 1449, 136 | 662, 137 | 622, 138 | 4300, 139 | 1167, 140 | 614, 141 | 590, 142 | 3681, 143 | 1348, 144 | 675, 145 | 851, 146 | 4469, 147 | 1874, 148 | 1072, 149 | 1003, 150 | 4192, 151 | 1828, 152 | 974, 153 | 869, 154 | 3521, 155 | 1534, 156 | 829, 157 | 720, 158 | 4906, 159 | 2117, 160 | 993, 161 | 1649, 162 | 6121, 163 | 2386, 164 | 1728, 165 | 1229, 166 | 3624, 167 | 1129, 168 | 603, 169 | 575, 170 | 2698, 171 | 764, 172 | 515, 173 | 461, 174 | 3283, 175 | 1213, 176 | 600, 177 | 936, 178 | 5888, 179 | 2247, 180 | 1412, 181 | 1203, 182 | 3826, 183 | 991, 184 | 3312, 185 | 1011, 186 | 2710, 187 | 2465, 188 | 2398, 189 | 2363, 190 | 2847, 191 | 2830, 192 | 2723, 193 | 4034, 194 | 1224, 195 | 2328, 196 | 2556, 197 | 2747, 198 | 2931, 199 | 3095, 200 | 2934, 201 | 3007, 202 | 6070, 203 | 2441, 204 | 1198, 205 | 1103, 206 | 1816, 207 | 2331, 208 | 2820, 209 | 2778, 210 | 2397, 211 | 2459, 212 | 2464, 213 | 2581, 214 | 2380, 215 | 2184, 216 | 2476, 217 | 3283, 218 | 4155, 219 | 4041, 220 | 4012, 221 | 3768, 222 | 15280, 223 | 841, 224 | 628, 225 | 11795, 226 | 1044, 227 | 550, 228 | 494, 229 | 5967, 230 | 827, 231 | 522, 232 | 404, 233 | 8338, 234 | 653, 235 | 371, 236 | 327, 237 | 9169, 238 | 964, 239 | 393, 240 | 440, 241 | 6349, 242 | 513, 243 | 313, 244 | 487, 245 | 7677, 246 | 1069, 247 | 505, 248 | 520, 249 | 7883, 250 | 901, 251 | 619, 252 | 544, 253 | 7344, 254 | 807, 255 | 502, 256 | 314, 257 | 33123, 258 | 5625, 259 | 467, 260 | 278, 261 | 165, 262 | 4590, 263 | 604, 264 | 419, 265 | 67, 266 | 5746, 267 | 266, 268 | 214, 269 | 451, 270 | 5018, 271 | 507, 272 | 208, 273 | 283, 274 | 5388, 275 | 845, 276 | 317, 277 | 139, 278 | 5015, 279 | 387, 280 | 132, 281 | 212, 282 | 5159, 283 | 458, 284 | 174, 285 | 158, 286 | 5264, 287 | 228, 288 | 160, 289 | 224, 290 | 4882, 291 | 615, 292 | 307, 293 | 368, 294 | 3745, 295 | 413, 296 | 380, 297 | 351, 298 | 2650, 299 | 617, 300 | 285, 301 | 168, 302 | 964, 303 | 811, 304 | 445, 305 | 324, 306 | 312 307 | ]; 308 | 309 | const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) 310 | 311 | async function loopPacket (fd, ff) { 312 | let decoer = new Decoder('h264') 313 | 314 | for (const size of frameSize) { 315 | const buffer = await readFile(fd, size) 316 | 317 | // decode (buffer[, size, offset, type, callback]) 318 | // type = 0 padding YUV [width, height, ystride, ustride, vstride, Y, U, V] 319 | // type = 1 compat YUV [width, height, data] 320 | // type = 2 RGBA [width, height, data] 321 | let result = decoer.decode(buffer, size) 322 | // console.log('--------->', result) 323 | if (result) { 324 | let frame = { 325 | width: result[0], 326 | height: result[1], 327 | format: { 328 | cropLeft: 0, 329 | cropTop: 0, 330 | cropWidth: result[0], 331 | cropHeight: result[1] 332 | }, 333 | y: { bytes: result[5], stride: result[2] }, 334 | u: { bytes: result[6], stride: result[3] }, 335 | v: { bytes: result[7], stride: result[4] } 336 | } 337 | 338 | ff.emit('yuv', frame) 339 | await delay(50); 340 | } 341 | } 342 | 343 | ff.emit('end') 344 | } 345 | 346 | function openFile (url) { 347 | return new Promise((resolve, reject) => { 348 | fs.open(url, 'r', (err, fd) => { 349 | if (err) reject(err) 350 | else resolve(fd) 351 | }) 352 | }) 353 | } 354 | 355 | function readFile (fd, size) { 356 | let buffer = Buffer.alloc(size) 357 | return new Promise((resolve, reject) => { 358 | fs.read(fd, buffer, 0, size, null, (err) => { 359 | if (err) reject(err) 360 | else resolve(buffer) 361 | }); 362 | }) 363 | } 364 | 365 | export function DecodeSync (vargs) { 366 | let url = vargs[vargs.length - 1] // 'C:\\Users\\rjzhou\\Documents\\foreman_352x288_30fps.h264' 367 | let emit = new EventEmitter() 368 | 369 | openFile(url).then(fd => loopPacket(fd, emit)) 370 | 371 | emit.quit = () => {} 372 | emit.toogle_pause = () => {} 373 | emit.toogle_mute = () => {} 374 | emit.volume_up = () => {} 375 | emit.volume_down = () => {} 376 | emit.volume = (v) => {} 377 | emit.seek = (v) => {} 378 | emit.seek_to = (v) => {} 379 | 380 | return emit 381 | } 382 | 383 | export async function DecodeInPlayer (ff, url) { 384 | ff.open_video_decode('h264') 385 | 386 | const fd = await openFile(url) 387 | 388 | for (const size of frameSize) { 389 | const buffer = await readFile(fd, size) 390 | ff.send_video_data(buffer) 391 | await delay(50); 392 | } 393 | ff.quit() 394 | } 395 | -------------------------------------------------------------------------------- /src/components/xplayer/video.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 495 | 496 | 521 | -------------------------------------------------------------------------------- /src/components/xplayer/yuv-canvas.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2014-2016 Brion Vibber 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | MPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | /* eslint-disable*/ 22 | "use strict"; 23 | 24 | const shaders = { 25 | vertex: ` 26 | precision lowp float; 27 | 28 | attribute vec2 aPosition; 29 | attribute vec2 aLumaPosition; 30 | attribute vec2 aChromaPosition; 31 | varying vec2 vLumaPosition; 32 | varying vec2 vChromaPosition; 33 | 34 | void main() { 35 | gl_Position = vec4(aPosition, 0, 1); 36 | vLumaPosition = aLumaPosition; 37 | vChromaPosition = aChromaPosition; 38 | } 39 | `, 40 | // inspired by https://github.com/mbebenita/Broadway/blob/master/Player/canvas.js 41 | fragment: ` 42 | precision lowp float; 43 | 44 | uniform sampler2D uTextureY; 45 | uniform sampler2D uTextureCb; 46 | uniform sampler2D uTextureCr; 47 | varying vec2 vLumaPosition; 48 | varying vec2 vChromaPosition; 49 | 50 | void main() { 51 | // Y, Cb, and Cr planes are uploaded as LUMINANCE textures. 52 | float fY = texture2D(uTextureY, vLumaPosition).x; 53 | float fCb = texture2D(uTextureCb, vChromaPosition).x; 54 | float fCr = texture2D(uTextureCr, vChromaPosition).x; 55 | 56 | // Premultipy the Y... 57 | float fYmul = fY * 1.1643828125; 58 | 59 | // And convert that to RGB! 60 | gl_FragColor = vec4(fYmul + 1.59602734375 * fCr - 0.87078515625, fYmul - 0.39176171875 * fCb - 0.81296875 * fCr + 0.52959375, fYmul + 2.017234375 * fCb - 1.081390625, 1); 61 | } 62 | `, 63 | vertexStripe: ` 64 | precision lowp float; 65 | 66 | attribute vec2 aPosition; 67 | attribute vec2 aTexturePosition; 68 | varying vec2 vTexturePosition; 69 | 70 | void main() { 71 | gl_Position = vec4(aPosition, 0, 1); 72 | vTexturePosition = aTexturePosition; 73 | } 74 | `, 75 | // extra 'stripe' texture fiddling to work around IE 11's poor performance on gl.LUMINANCE and gl.ALPHA textures 76 | // Y, Cb, and Cr planes are mapped into a pseudo-RGBA texture 77 | // so we can upload them without expanding the bytes on IE 11 78 | // which doesn't allow LUMINANCE or ALPHA textures 79 | // The stripe textures mark which channel to keep for each pixel. 80 | // Each texture extraction will contain the relevant value in one 81 | // channel only. 82 | fragmentStripe: ` 83 | precision lowp float; 84 | 85 | uniform sampler2D uStripe; 86 | uniform sampler2D uTexture; 87 | varying vec2 vTexturePosition; 88 | 89 | void main() { 90 | float fLuminance = dot(texture2D(uStripe, vTexturePosition), texture2D(uTexture, vTexturePosition)); 91 | gl_FragColor = vec4(fLuminance, fLuminance, fLuminance, 1); 92 | } 93 | ` 94 | }; 95 | 96 | /** 97 | * Warning: canvas must not have been used for 2d drawing prior! 98 | * 99 | * @param {HTMLCanvasElement} canvas - HTML canvas element to attach to 100 | * @constructor 101 | */ 102 | function WebGLFrameSink(canvas, owner) { 103 | var self = this; 104 | const options = { 105 | // Don't trigger discrete GPU in multi-GPU systems 106 | preferLowPowerToHighPerformance: true, 107 | powerPreference: 'low-power', 108 | // Don't try to use software GL rendering! 109 | failIfMajorPerformanceCaveat: true, 110 | // In case we need to capture the resulting output. 111 | preserveDrawingBuffer: true 112 | }; 113 | 114 | const gl = canvas.getContext('webgl2', options); 115 | 116 | if (gl === null) { 117 | throw new Error('WebGL unavailable'); 118 | } 119 | 120 | function compileShader(type, source) { 121 | var shader = gl.createShader(type); 122 | gl.shaderSource(shader, source); 123 | gl.compileShader(shader); 124 | 125 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { 126 | var err = gl.getShaderInfoLog(shader); 127 | gl.deleteShader(shader); 128 | throw new Error('GL shader compilation for ' + type + ' failed: ' + err); 129 | } 130 | 131 | return shader; 132 | } 133 | 134 | 135 | var program, 136 | unpackProgram, 137 | err; 138 | 139 | // In the world of GL there are no rectangles. 140 | // There are only triangles. 141 | // THERE IS NO SPOON. 142 | var rectangle = new Float32Array([ 143 | // First triangle (top left, clockwise) 144 | -1.0, -1.0, 145 | +1.0, -1.0, 146 | -1.0, +1.0, 147 | 148 | // Second triangle (bottom right, clockwise) 149 | -1.0, +1.0, 150 | +1.0, -1.0, 151 | +1.0, +1.0 152 | ]); 153 | 154 | var textures = {}; 155 | var framebuffers = {}; 156 | var stripes = {}; 157 | var buf, positionLocation, unpackPositionLocation; 158 | var unpackTexturePositionBuffer, unpackTexturePositionLocation; 159 | var stripeLocation, unpackTextureLocation; 160 | var lumaPositionBuffer, lumaPositionLocation; 161 | var chromaPositionBuffer, chromaPositionLocation; 162 | 163 | function createOrReuseTexture(name) { 164 | if (!textures[name]) { 165 | textures[name] = gl.createTexture(); 166 | } 167 | return textures[name]; 168 | } 169 | 170 | function uploadTexture(name, width, height, data) { 171 | var texture = createOrReuseTexture(name); 172 | gl.activeTexture(gl.TEXTURE0); 173 | 174 | if (WebGLFrameSink.stripe) { 175 | var uploadTemp = !textures[name + '_temp']; 176 | var tempTexture = createOrReuseTexture(name + '_temp'); 177 | gl.bindTexture(gl.TEXTURE_2D, tempTexture); 178 | if (uploadTemp) { 179 | // new texture 180 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 181 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 182 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 183 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 184 | gl.texImage2D( 185 | gl.TEXTURE_2D, 186 | 0, // mip level 187 | gl.RGBA, // internal format 188 | width / 4, 189 | height, 190 | 0, // border 191 | gl.RGBA, // format 192 | gl.UNSIGNED_BYTE, // type 193 | data // data! 194 | ); 195 | } else { 196 | // update texture 197 | gl.texSubImage2D( 198 | gl.TEXTURE_2D, 199 | 0, // mip level 200 | 0, // x offset 201 | 0, // y offset 202 | width / 4, 203 | height, 204 | gl.RGBA, // format 205 | gl.UNSIGNED_BYTE, // type 206 | data // data! 207 | ); 208 | } 209 | 210 | var stripeTexture = textures[name + '_stripe']; 211 | var uploadStripe = !stripeTexture; 212 | if (uploadStripe) { 213 | stripeTexture = createOrReuseTexture(name + '_stripe'); 214 | } 215 | gl.bindTexture(gl.TEXTURE_2D, stripeTexture); 216 | if (uploadStripe) { 217 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 218 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 219 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 220 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 221 | gl.texImage2D( 222 | gl.TEXTURE_2D, 223 | 0, // mip level 224 | gl.RGBA, // internal format 225 | width, 226 | 1, 227 | 0, // border 228 | gl.RGBA, // format 229 | gl.UNSIGNED_BYTE, //type 230 | buildStripe(width, 1) // data! 231 | ); 232 | } 233 | 234 | } else { 235 | gl.bindTexture(gl.TEXTURE_2D, texture); 236 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 237 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 238 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 239 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 240 | gl.texImage2D( 241 | gl.TEXTURE_2D, 242 | 0, // mip level 243 | gl.LUMINANCE, // internal format 244 | width, 245 | height, 246 | 0, // border 247 | gl.LUMINANCE, // format 248 | gl.UNSIGNED_BYTE, //type 249 | data // data! 250 | ); 251 | } 252 | } 253 | 254 | function unpackTexture(name, width, height) { 255 | var texture = textures[name]; 256 | 257 | // Upload to a temporary RGBA texture, then unpack it. 258 | // This is faster than CPU-side swizzling in ANGLE on Windows. 259 | gl.useProgram(unpackProgram); 260 | 261 | var fb = framebuffers[name]; 262 | if (!fb) { 263 | // Create a framebuffer and an empty target size 264 | gl.activeTexture(gl.TEXTURE0); 265 | gl.bindTexture(gl.TEXTURE_2D, texture); 266 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 267 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 268 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 269 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 270 | gl.texImage2D( 271 | gl.TEXTURE_2D, 272 | 0, // mip level 273 | gl.RGBA, // internal format 274 | width, 275 | height, 276 | 0, // border 277 | gl.RGBA, // format 278 | gl.UNSIGNED_BYTE, //type 279 | null // data! 280 | ); 281 | 282 | fb = framebuffers[name] = gl.createFramebuffer(); 283 | } 284 | 285 | gl.bindFramebuffer(gl.FRAMEBUFFER, fb); 286 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); 287 | 288 | var tempTexture = textures[name + '_temp']; 289 | gl.activeTexture(gl.TEXTURE1); 290 | gl.bindTexture(gl.TEXTURE_2D, tempTexture); 291 | gl.uniform1i(unpackTextureLocation, 1); 292 | 293 | var stripeTexture = textures[name + '_stripe']; 294 | gl.activeTexture(gl.TEXTURE2); 295 | gl.bindTexture(gl.TEXTURE_2D, stripeTexture); 296 | gl.uniform1i(stripeLocation, 2); 297 | 298 | // Rectangle geometry 299 | gl.bindBuffer(gl.ARRAY_BUFFER, buf); 300 | gl.enableVertexAttribArray(positionLocation); 301 | gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); 302 | 303 | // Set up the texture geometry... 304 | gl.bindBuffer(gl.ARRAY_BUFFER, unpackTexturePositionBuffer); 305 | gl.enableVertexAttribArray(unpackTexturePositionLocation); 306 | gl.vertexAttribPointer(unpackTexturePositionLocation, 2, gl.FLOAT, false, 0, 0); 307 | 308 | // Draw into the target texture... 309 | gl.viewport(0, 0, width, height); 310 | 311 | gl.drawArrays(gl.TRIANGLES, 0, rectangle.length / 2); 312 | 313 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 314 | 315 | } 316 | 317 | function attachTexture(name, register, index) { 318 | gl.activeTexture(register); 319 | gl.bindTexture(gl.TEXTURE_2D, textures[name]); 320 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 321 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 322 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 323 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 324 | 325 | gl.uniform1i(gl.getUniformLocation(program, name), index); 326 | } 327 | 328 | function buildStripe(width) { 329 | if (stripes[width]) { 330 | return stripes[width]; 331 | } 332 | var len = width, 333 | out = new Uint32Array(len); 334 | for (var i = 0; i < len; i += 4) { 335 | out[i ] = 0x000000ff; 336 | out[i + 1] = 0x0000ff00; 337 | out[i + 2] = 0x00ff0000; 338 | out[i + 3] = 0xff000000; 339 | } 340 | return stripes[width] = new Uint8Array(out.buffer); 341 | } 342 | 343 | function initProgram(vertexShaderSource, fragmentShaderSource) { 344 | var vertexShader = compileShader(gl.VERTEX_SHADER, vertexShaderSource); 345 | var fragmentShader = compileShader(gl.FRAGMENT_SHADER, fragmentShaderSource); 346 | 347 | var program = gl.createProgram(); 348 | gl.attachShader(program, vertexShader); 349 | gl.attachShader(program, fragmentShader); 350 | 351 | gl.linkProgram(program); 352 | if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { 353 | var err = gl.getProgramInfoLog(program); 354 | gl.deleteProgram(program); 355 | throw new Error('GL program linking failed: ' + err); 356 | } 357 | 358 | return program; 359 | } 360 | 361 | function init() { 362 | if (WebGLFrameSink.stripe) { 363 | unpackProgram = initProgram(shaders.vertexStripe, shaders.fragmentStripe); 364 | unpackPositionLocation = gl.getAttribLocation(unpackProgram, 'aPosition'); 365 | 366 | unpackTexturePositionBuffer = gl.createBuffer(); 367 | var textureRectangle = new Float32Array([ 368 | 0, 0, 369 | 1, 0, 370 | 0, 1, 371 | 0, 1, 372 | 1, 0, 373 | 1, 1 374 | ]); 375 | gl.bindBuffer(gl.ARRAY_BUFFER, unpackTexturePositionBuffer); 376 | gl.bufferData(gl.ARRAY_BUFFER, textureRectangle, gl.STATIC_DRAW); 377 | 378 | unpackTexturePositionLocation = gl.getAttribLocation(unpackProgram, 'aTexturePosition'); 379 | stripeLocation = gl.getUniformLocation(unpackProgram, 'uStripe'); 380 | unpackTextureLocation = gl.getUniformLocation(unpackProgram, 'uTexture'); 381 | } 382 | program = initProgram(shaders.vertex, shaders.fragment); 383 | 384 | buf = gl.createBuffer(); 385 | gl.bindBuffer(gl.ARRAY_BUFFER, buf); 386 | gl.bufferData(gl.ARRAY_BUFFER, rectangle, gl.STATIC_DRAW); 387 | 388 | positionLocation = gl.getAttribLocation(program, 'aPosition'); 389 | lumaPositionBuffer = gl.createBuffer(); 390 | lumaPositionLocation = gl.getAttribLocation(program, 'aLumaPosition'); 391 | chromaPositionBuffer = gl.createBuffer(); 392 | chromaPositionLocation = gl.getAttribLocation(program, 'aChromaPosition'); 393 | } 394 | 395 | /** 396 | * Actually draw a frame. 397 | * @param {YUVFrame} buffer - YUV frame buffer object 398 | */ 399 | self.drawFrame = function(buffer) { 400 | const {width, height, cropLeft, cropTop, cropWidth, cropHeight} = buffer; 401 | 402 | var formatUpdate = (!program || canvas.width !== width || canvas.height !== height); 403 | if (formatUpdate) { 404 | // Keep the canvas at the right size... 405 | canvas.width = buffer.width; 406 | canvas.height = buffer.height; 407 | if (owner) { 408 | owner.changeWindowSize(width, height); 409 | } 410 | self.clear(); 411 | } 412 | 413 | if (!program) { 414 | init(); 415 | } 416 | 417 | if (formatUpdate) { 418 | // clear cache 419 | textures = {}; 420 | framebuffers = {}; 421 | stripes = {}; 422 | 423 | var setupTexturePosition = function(buffer, location, texWidth) { 424 | // Warning: assumes that the stride for Cb and Cr is the same size in output pixels 425 | var textureX0 = cropLeft / texWidth; 426 | var textureX1 = (cropLeft + cropWidth) / texWidth; 427 | var textureY0 = (cropTop + cropHeight) / height; 428 | var textureY1 = cropTop / height; 429 | var textureRectangle = new Float32Array([ 430 | textureX0, textureY0, 431 | textureX1, textureY0, 432 | textureX0, textureY1, 433 | textureX0, textureY1, 434 | textureX1, textureY0, 435 | textureX1, textureY1 436 | ]); 437 | 438 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 439 | gl.bufferData(gl.ARRAY_BUFFER, textureRectangle, gl.STATIC_DRAW); 440 | }; 441 | setupTexturePosition( 442 | lumaPositionBuffer, 443 | lumaPositionLocation, 444 | buffer.y.stride); 445 | setupTexturePosition( 446 | chromaPositionBuffer, 447 | chromaPositionLocation, 448 | buffer.u.stride * 2); 449 | } 450 | 451 | // Create or update the textures... 452 | uploadTexture('uTextureY', buffer.y.stride, buffer.height, buffer.y.bytes); 453 | uploadTexture('uTextureCb', buffer.u.stride, buffer.height / 2, buffer.u.bytes); 454 | uploadTexture('uTextureCr', buffer.v.stride, buffer.height / 2, buffer.v.bytes); 455 | 456 | if (WebGLFrameSink.stripe) { 457 | // Unpack the textures after upload to avoid blocking on GPU 458 | unpackTexture('uTextureY', buffer.y.stride, buffer.height); 459 | unpackTexture('uTextureCb', buffer.u.stride, buffer.height / 2); 460 | unpackTexture('uTextureCr', buffer.v.stride, buffer.height / 2); 461 | } 462 | 463 | // Set up the rectangle and draw it 464 | gl.useProgram(program); 465 | gl.viewport(0, 0, canvas.width, canvas.height); 466 | 467 | attachTexture('uTextureY', gl.TEXTURE0, 0); 468 | attachTexture('uTextureCb', gl.TEXTURE1, 1); 469 | attachTexture('uTextureCr', gl.TEXTURE2, 2); 470 | 471 | // Set up geometry 472 | gl.bindBuffer(gl.ARRAY_BUFFER, buf); 473 | gl.enableVertexAttribArray(positionLocation); 474 | gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); 475 | 476 | // Set up the texture geometry... 477 | gl.bindBuffer(gl.ARRAY_BUFFER, lumaPositionBuffer); 478 | gl.enableVertexAttribArray(lumaPositionLocation); 479 | gl.vertexAttribPointer(lumaPositionLocation, 2, gl.FLOAT, false, 0, 0); 480 | 481 | gl.bindBuffer(gl.ARRAY_BUFFER, chromaPositionBuffer); 482 | gl.enableVertexAttribArray(chromaPositionLocation); 483 | gl.vertexAttribPointer(chromaPositionLocation, 2, gl.FLOAT, false, 0, 0); 484 | 485 | // Aaaaand draw stuff. 486 | gl.drawArrays(gl.TRIANGLES, 0, rectangle.length / 2); 487 | }; 488 | 489 | self.clear = function() { 490 | gl.viewport(0, 0, canvas.width, canvas.height); 491 | gl.clearColor(0.0, 0.0, 0.0, 0.0); 492 | gl.clear(gl.COLOR_BUFFER_BIT); 493 | }; 494 | 495 | self.cropImage = function (cropCanvas, rect) { 496 | const width = rect[2] - rect[0]; 497 | const height = rect[3] - rect[1] 498 | const y = canvas.height - rect[3] 499 | 500 | const ctx = cropCanvas.getContext('2d') 501 | const img = ctx.createImageData(width, height) 502 | 503 | gl.readPixels(rect[0], y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, img.data); 504 | // console.log(img.data); // Uint8Array 505 | cropCanvas.width = width; 506 | cropCanvas.height = height; 507 | 508 | ctx.putImageData(img, 0, 0) 509 | } 510 | 511 | self.clear(); 512 | 513 | return self; 514 | } 515 | 516 | // For Windows; luminance and alpha textures are ssllooww to upload, 517 | // so we pack into RGBA and unpack in the shaders. 518 | // 519 | // This seems to affect all browsers on Windows, probably due to fun 520 | // mismatches between GL and D3D. 521 | WebGLFrameSink.stripe = (function() { 522 | if (navigator.userAgent.indexOf('Windows') !== -1) { 523 | return true; 524 | } 525 | return false; 526 | })(); 527 | 528 | module.exports = WebGLFrameSink; 529 | 530 | -------------------------------------------------------------------------------- /src/ffplay.js: -------------------------------------------------------------------------------- 1 | import {requireAddon} from './import-dll' 2 | 3 | const binding = requireAddon('node-ffplay'); 4 | const {EventEmitter} = require('events'); 5 | const {inherits} = require('util'); 6 | const {PlayBack, Decoder} = binding 7 | 8 | inherits(PlayBack, EventEmitter) 9 | 10 | PlayBack.prototype.command = function (...args) { 11 | this.send(...args) 12 | } 13 | 14 | PlayBack.prototype.quit = function (reason) { 15 | this.send('quit') 16 | } 17 | 18 | PlayBack.prototype.toogle_pause = function () { 19 | this.send('pause') 20 | } 21 | 22 | PlayBack.prototype.toogle_mute = function () { 23 | this.send('mute') 24 | } 25 | 26 | PlayBack.prototype.volume_up = function () { 27 | this.send('volume', 1) 28 | } 29 | 30 | PlayBack.prototype.volume_down = function () { 31 | this.send('volume', -1) 32 | } 33 | 34 | PlayBack.prototype.volume = function (v) { 35 | this.send('volume', 2, v) 36 | } 37 | 38 | PlayBack.prototype.seek = function (...args) { 39 | this.send('seek', ...args) 40 | } 41 | 42 | PlayBack.prototype.seek_to = function (v) { 43 | this.send('seek', 0, v) 44 | } 45 | 46 | PlayBack.prototype.speed = function (v) { 47 | this.send('speed', 0, v) 48 | } 49 | 50 | export { 51 | PlayBack, 52 | Decoder 53 | } 54 | -------------------------------------------------------------------------------- /src/i18n/en.yml: -------------------------------------------------------------------------------- 1 | video: 2 | Loop: Loop 3 | Speed: Speed 4 | Normal: Normal 5 | load_fail: Load fail 6 | switching_to: Switching_to 7 | switched_to: Switched to 8 | quality: Quality 9 | FF: FF 10 | REW: REW 11 | Setting: Setting 12 | Full_screen: Full screen 13 | s: s 14 | Show_subtitle: Show subtitle 15 | Hide_subtitle: Hide subtitle 16 | Volume: Volume 17 | Live: Live 18 | Video_info: Video info 19 | prev_frame: Previous Frame 20 | next_frame: Next Frame 21 | detection: AI Detection 22 | in_screen: In Picture 23 | speed: Speed 24 | autoHideControl: Auto hide controller 25 | snapshot: Snapshot 26 | openUrl: Open URL 27 | openDir: Open Directory 28 | playControl: Play Control 29 | settings: 30 | snapshot_format: Snapshot saving format 31 | auto_next: Auto play next 32 | play_list: 33 | remove: Remove 34 | removeAll: Remove All 35 | add: Add 36 | -------------------------------------------------------------------------------- /src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | 4 | Vue.use(VueI18n) 5 | 6 | const files = require.context('.', false, /\.yml$/) 7 | const messages = {} 8 | 9 | files.keys().forEach(key => { 10 | messages[key.replace(/(\.\/|\.yml)/g, '')] = files(key) 11 | }) 12 | 13 | export const localeDisplay = { 14 | 'zh-cn': '中文', 15 | 'en': 'English' 16 | }; 17 | 18 | export function detectLocale () { 19 | let locale = (navigator.language || navigator.browserLangugae).toLowerCase(); 20 | switch (true) { 21 | case /^en.*/i.test(locale): 22 | locale = 'en' 23 | break 24 | case /^it.*/i.test(locale): 25 | locale = 'it' 26 | break 27 | case /^fr.*/i.test(locale): 28 | locale = 'fr' 29 | break 30 | case /^pt.*/i.test(locale): 31 | locale = 'pt' 32 | break 33 | case /^pt-BR.*/i.test(locale): 34 | locale = 'pt-br' 35 | break 36 | case /^ja.*/i.test(locale): 37 | locale = 'ja' 38 | break 39 | case /^zh-CN/i.test(locale): 40 | locale = 'zh-cn' 41 | break 42 | case /^zh-TW/i.test(locale): 43 | locale = 'zh-tw' 44 | break 45 | case /^zh.*/i.test(locale): 46 | locale = 'zh-cn' 47 | break 48 | case /^es.*/i.test(locale): 49 | locale = 'es' 50 | break 51 | case /^de.*/i.test(locale): 52 | locale = 'de' 53 | break 54 | case /^ru.*/i.test(locale): 55 | locale = 'ru' 56 | break 57 | case /^pl.*/i.test(locale): 58 | locale = 'pl' 59 | break 60 | default: 61 | locale = 'en' 62 | } 63 | 64 | return locale 65 | } 66 | 67 | const i18n = new VueI18n({ 68 | locale: detectLocale(), 69 | messages 70 | }); 71 | 72 | i18n.localeDisplay = localeDisplay; 73 | 74 | export default i18n; 75 | -------------------------------------------------------------------------------- /src/i18n/zh-cn.yml: -------------------------------------------------------------------------------- 1 | video: 2 | Loop: 循环 3 | Speed: 速度 4 | Normal: 正常 5 | load_fail: 视频加载失败 6 | switching_to: 正在切换至 7 | switched_to: 已经切换至 8 | quality: 画质 9 | FF: 快进 10 | REW: 快退 11 | Setting: 设置 12 | Full_screen: 全屏 13 | s: 秒 14 | Show_subtitle: 显示字幕 15 | Hide_subtitle: 隐藏字幕 16 | Volume: 音量 17 | Live: 直播 18 | Video_info: 视频统计信息 19 | prev_frame: 上一帧 20 | next_frame: 下一帧 21 | detection: AI 侦测 22 | in_screen: 画中画 23 | speed: 播放速度 24 | autoHideControl: 自动隐藏控制栏 25 | snapshot: 截图 26 | openUrl: 打开流媒体 27 | openDir: 打开播放目录 28 | playControl: 播放控制 29 | settings: 30 | snapshot_format: 截图保存格式 31 | auto_next: 自动播放下一个 32 | play_list: 33 | remove: 移除 34 | removeAll: 清空 35 | add: 添加 36 | -------------------------------------------------------------------------------- /src/import-dll.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | const PEPPER = 49 4 | 5 | const nodeMajor2Abis = { 6 | '8': 57, 7 | '9': 59, 8 | '10': 64, 9 | '11': 64, 10 | '12': 72, 11 | '13': 79, 12 | '14': 83, 13 | '15': 88 14 | } 15 | 16 | const ABI = nodeMajor2Abis[process.versions.node.split('.')[0]] 17 | const EXT = process.platform === 'win32' ? '.dll' : '.so' 18 | const archName = process.platform + '-' + process.arch 19 | 20 | /* eslint-disable no-undef */ 21 | 22 | // build module directly to dll 23 | // rel: D:\dev\nivm-next\dist\win-unpacked\resources\app.asar\build 24 | // devl: D:\dev\nivm-next\build 25 | const rootPath = process.env.NODE_ENV === 'development' ? '..' : '../../..' 26 | 27 | __non_webpack_require__.extensions[EXT] = __non_webpack_require__.extensions['.node']; 28 | 29 | export function resolvePepperPath (name) { 30 | return path.resolve(__dirname, rootPath, `${name}-${archName}-pepper_${PEPPER}${EXT}`) 31 | } 32 | 33 | export function resolveAddonPath (name) { 34 | return path.resolve(__dirname, rootPath, `${name}-${archName}.abi-${ABI}${EXT}`) 35 | } 36 | 37 | export function requireAddon (modname) { 38 | return __non_webpack_require__(resolveAddonPath(modname)) 39 | } 40 | -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NIVM 6 | <% if (htmlWebpackPlugin.options.nodeModules) { %> 7 | 8 | 11 | <% } %> 12 | 13 | 14 |
15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import '@mdi/font/css/materialdesignicons.css' 2 | import 'vuetify/dist/vuetify.min.css' 3 | import 'balloon-css'; 4 | import Vue from 'vue' 5 | import Vuetify from 'vuetify' 6 | 7 | import store from './store' 8 | import i18n from './i18n' 9 | import App from './components/app' 10 | import zhHans from 'vuetify/es5/locale/zh-Hans' 11 | import electron from 'electron' 12 | 13 | /* eslint-disable no-new */ 14 | Vue.config.productionTip = false 15 | Vue.prototype.$electron = electron 16 | 17 | Vue.use(Vuetify) 18 | 19 | // load default settings 20 | const darkStr = localStorage.getItem('settings.theme.dark') || 'true'; 21 | const themeDark = darkStr === 'true'; 22 | 23 | const localeStr = localStorage.getItem('settings.locale'); 24 | if (localeStr) { 25 | i18n.locale = localeStr; 26 | } 27 | 28 | const vm = new Vue({ 29 | components: { App }, 30 | store, 31 | i18n, 32 | vuetify: new Vuetify({ 33 | lang: { 34 | locales: { 'zh-cn': zhHans }, 35 | current: i18n.locale 36 | }, 37 | theme: { 38 | dark: themeDark 39 | } 40 | }), 41 | template: '' 42 | }).$mount('#app') 43 | 44 | window.addEventListener('beforeunload', e => { 45 | // options are restored in app.mounted 46 | localStorage.setItem('settings.theme.dark', vm.$vuetify.theme.dark); 47 | localStorage.setItem('settings.locale', vm.$i18n.locale); 48 | store.commit('video/saveAllSettings') 49 | }); 50 | -------------------------------------------------------------------------------- /src/main/index.dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is used specifically and only for development. It installs 3 | * `electron-debug` & `vue-devtools`. There shouldn't be any need to 4 | * modify this file, but it can be used to extend your development 5 | * environment. 6 | */ 7 | 8 | /* eslint-disable */ 9 | 10 | // Install `electron-debug` with `devtron` 11 | require('electron-debug')({ showDevTools: true, devToolsMode: 'detach' }) 12 | 13 | // Install `vue-devtools` 14 | require('electron').app.on('ready', () => { 15 | require('electron').BrowserWindow.addDevToolsExtension('node_modules/vue-devtools/vender') 16 | /* 17 | let installExtension = require('electron-devtools-installer') 18 | installExtension.default(installExtension.VUEJS_DEVTOOLS) 19 | .then(() => {}) 20 | .catch(err => { 21 | console.log('Unable to install `vue-devtools`: \n', err) 22 | }) 23 | */ 24 | }) 25 | 26 | // Require `main` process to boot app 27 | require('./index') 28 | -------------------------------------------------------------------------------- /src/main/index.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, globalShortcut } from 'electron' 2 | 3 | app.commandLine.appendSwitch('disable-site-isolation-trials'); 4 | app.allowRendererProcessReuse = false; 5 | 6 | let mainWindow 7 | // let splashScreen 8 | 9 | const rootURL = process.env.NODE_ENV === 'development' 10 | ? `http://localhost:9080` 11 | : `file://${__dirname}` 12 | 13 | // app.commandLine.appendSwitch("ignore-gpu-blacklist"); 14 | // app.commandLine.appendSwitch("register-pepper-plugins", 'ppapi_mpv.dll;application/x-mpvjs'); 15 | 16 | function createWindow () { 17 | /** 18 | * Initial window options 19 | */ 20 | mainWindow = new BrowserWindow({ 21 | height: 698, 22 | useContentSize: true, 23 | width: 1200, 24 | show: false, 25 | webPreferences: { 26 | nodeIntegration: true, 27 | webviewTag: true, 28 | webSecurity: false, 29 | nativeWindowOpen: true 30 | // plugins: true 31 | }, 32 | frame: false, 33 | autoHideMenuBar: true 34 | }) 35 | 36 | mainWindow.once('ready-to-show', () => { 37 | // if (splashScreen) { 38 | // splashScreen.isDestroyed() || splashScreen.close(); 39 | // splashScreen = null; 40 | // } 41 | mainWindow.show() 42 | }) 43 | 44 | mainWindow.loadURL(rootURL + '/index.html') 45 | 46 | mainWindow.on('closed', () => { 47 | mainWindow = null 48 | }) 49 | } 50 | 51 | app.on('ready', () => { 52 | globalShortcut.register('CommandOrControl+Shift+I', () => { 53 | mainWindow.webContents.toggleDevTools() 54 | }) 55 | 56 | const gotTheLock = app.requestSingleInstanceLock() 57 | 58 | if (!gotTheLock) { 59 | app.quit() 60 | } else { 61 | app.on('second-instance', (event, commandLine, workingDirectory) => { 62 | // 当运行第二个实例时,将会聚焦到myWindow这个窗口 63 | if (mainWindow) { 64 | if (mainWindow.isMinimized()) mainWindow.restore() 65 | mainWindow.focus() 66 | } 67 | }) 68 | } 69 | 70 | createWindow() 71 | /* 72 | splashScreen = new BrowserWindow({ 73 | height: 600, 74 | width: 800, 75 | show: false, 76 | frame: false, 77 | autoHideMenuBar: true, 78 | center: true, 79 | webPreferences: { 80 | webSecurity: false 81 | } 82 | }) 83 | 84 | splashScreen.once('ready-to-show', () => { 85 | splashScreen.show() 86 | }) 87 | 88 | splashScreen.loadURL(process.env.NODE_ENV === 'development' ? `file://${__dirname}/../splash.html` : `file://${__dirname}/splash.html`) 89 | */ 90 | }) 91 | 92 | app.on('will-quit', function () { 93 | // Unregister all shortcuts. 94 | globalShortcut.unregisterAll() 95 | }) 96 | 97 | app.on('window-all-closed', () => { 98 | if (process.platform !== 'darwin') { 99 | app.quit() 100 | } 101 | }) 102 | 103 | app.on('activate', () => { 104 | if (mainWindow === null) { 105 | createWindow() 106 | } 107 | }) 108 | 109 | /** 110 | * Auto Updater 111 | * 112 | * Uncomment the following code below and install `electron-updater` to 113 | * support auto updating. Code Signing with a valid certificate is required. 114 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating 115 | */ 116 | 117 | /* 118 | import { autoUpdater } from 'electron-updater' 119 | 120 | autoUpdater.on('update-downloaded', () => { 121 | autoUpdater.quitAndInstall() 122 | }) 123 | 124 | app.on('ready', () => { 125 | if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates() 126 | }) 127 | */ 128 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | /** 5 | * The file enables `@/store/index.js` to import all vuex modules 6 | * in a one-shot manner. There should not be any reason to edit this file. 7 | */ 8 | 9 | const files = require.context('.', false, /\.js$/) 10 | const modules = {} 11 | 12 | files.keys().forEach(key => { 13 | if (key === './index.js') return 14 | modules[key.replace(/(\.\/|\.js)/g, '')] = files(key).default 15 | }) 16 | 17 | Vue.use(Vuex) 18 | 19 | export default new Vuex.Store({ 20 | modules, 21 | strict: process.env.NODE_ENV === 'development' 22 | }) 23 | -------------------------------------------------------------------------------- /src/store/video.js: -------------------------------------------------------------------------------- 1 | const streamingTransport = localStorage.getItem('settings.video.streamingTransport') || 'udp'; 2 | 3 | let autoPlayNext = localStorage.getItem('settings.video.autoPlayNext'); 4 | autoPlayNext = autoPlayNext === null || autoPlayNext === 'true' 5 | 6 | const snapshotFormat = localStorage.getItem('settings.video.snapshotFormat') || 'png'; 7 | 8 | const state = { 9 | currentVideo: null, 10 | streamingTransport, 11 | autoPlayNext, 12 | snapshotFormat, 13 | usePcInference: false, 14 | inferenceFeatures: ['faces', 'age', 'emotions', 'vehicles'] 15 | } 16 | 17 | const getters = { 18 | currentVideo (state) { 19 | return state.currentVideo 20 | } 21 | } 22 | 23 | const mutations = { 24 | setStreamingTransport (state, v) { 25 | state.streamingTransport = v 26 | }, 27 | setAutoPlayNext (state, v) { 28 | state.autoPlayNext = v 29 | }, 30 | setSnapshotFormat (state, v) { 31 | state.snapshotFormat = v 32 | }, 33 | saveAllSettings (state) { 34 | localStorage.setItem('settings.video.streamingTransport', state.streamingTransport); 35 | localStorage.setItem('settings.video.autoPlayNext', state.autoPlayNext); 36 | }, 37 | setPcInference (state, v) { 38 | state.usePcInference = v 39 | }, 40 | setInferenceFeatures (state, v) { 41 | state.inferenceFeatures = v 42 | } 43 | } 44 | 45 | export default { 46 | namespaced: true, 47 | state, 48 | getters, 49 | mutations 50 | } 51 | -------------------------------------------------------------------------------- /test/e2e/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Set BABEL_ENV to use proper env config 4 | process.env.BABEL_ENV = 'test' 5 | 6 | // Enable use of ES6+ on required files 7 | require('babel-register')({ 8 | ignore: /node_modules/ 9 | }) 10 | 11 | // Attach Chai APIs to global scope 12 | const { expect, should, assert } = require('chai') 13 | global.expect = expect 14 | global.should = should 15 | global.assert = assert 16 | 17 | // Require all JS files in `./specs` for Mocha to consume 18 | require('require-dir')('./specs') 19 | -------------------------------------------------------------------------------- /test/e2e/specs/Launch.spec.js: -------------------------------------------------------------------------------- 1 | import utils from '../utils' 2 | 3 | describe('Launch', function () { 4 | beforeEach(utils.beforeEach) 5 | afterEach(utils.afterEach) 6 | 7 | it('shows the proper application title', function () { 8 | return this.app.client.getTitle() 9 | .then(title => { 10 | expect(title).to.equal('my-project') 11 | }) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /test/e2e/utils.js: -------------------------------------------------------------------------------- 1 | import electron from 'electron' 2 | import { Application } from 'spectron' 3 | 4 | export default { 5 | afterEach () { 6 | this.timeout(10000) 7 | 8 | if (this.app && this.app.isRunning()) { 9 | return this.app.stop() 10 | } 11 | }, 12 | beforeEach () { 13 | this.timeout(10000) 14 | this.app = new Application({ 15 | path: electron, 16 | args: ['src/main.js'], 17 | startTimeout: 10000, 18 | waitTimeout: 10000 19 | }) 20 | 21 | return this.app.start() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | Vue.config.devtools = false 3 | Vue.config.productionTip = false 4 | 5 | // require all test files (files that ends with .spec.js) 6 | const testsContext = require.context('./specs', true, /\.spec$/) 7 | testsContext.keys().forEach(testsContext) 8 | 9 | // require all src files except main.js for coverage. 10 | // you can also change this to match only the subset of files that 11 | // you want coverage for. 12 | const srcContext = require.context('../../src/renderer', true, /^\.\/(?!main(\.js)?$)/) 13 | srcContext.keys().forEach(srcContext) 14 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const merge = require('webpack-merge') 5 | const webpack = require('webpack') 6 | 7 | const baseConfig = require('../../.electron-vue/webpack.renderer.config') 8 | const projectRoot = path.resolve(__dirname, '../../src/renderer') 9 | 10 | // Set BABEL_ENV to use proper preset config 11 | process.env.BABEL_ENV = 'test' 12 | 13 | let webpackConfig = merge(baseConfig, { 14 | devtool: '#inline-source-map', 15 | plugins: [ 16 | new webpack.DefinePlugin({ 17 | 'process.env.NODE_ENV': '"testing"' 18 | }) 19 | ] 20 | }) 21 | 22 | // don't treat dependencies as externals 23 | delete webpackConfig.entry 24 | delete webpackConfig.externals 25 | delete webpackConfig.output.libraryTarget 26 | 27 | // apply vue option to apply isparta-loader on js 28 | webpackConfig.module.rules 29 | .find(rule => rule.use.loader === 'vue-loader').use.options.loaders.js = 'babel-loader' 30 | 31 | module.exports = config => { 32 | config.set({ 33 | browsers: ['visibleElectron'], 34 | client: { 35 | useIframe: false 36 | }, 37 | coverageReporter: { 38 | dir: './coverage', 39 | reporters: [ 40 | { type: 'lcov', subdir: '.' }, 41 | { type: 'text-summary' } 42 | ] 43 | }, 44 | customLaunchers: { 45 | 'visibleElectron': { 46 | base: 'Electron', 47 | flags: ['--show'] 48 | } 49 | }, 50 | frameworks: ['mocha', 'chai'], 51 | files: ['./index.js'], 52 | preprocessors: { 53 | './index.js': ['webpack', 'sourcemap'] 54 | }, 55 | reporters: ['spec', 'coverage'], 56 | singleRun: true, 57 | webpack: webpackConfig, 58 | webpackMiddleware: { 59 | noInfo: true 60 | } 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /test/unit/specs/LandingPage.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import LandingPage from '@/components/LandingPage' 3 | 4 | describe('LandingPage.vue', () => { 5 | it('should render correct contents', () => { 6 | const vm = new Vue({ 7 | el: document.createElement('div'), 8 | render: h => h(LandingPage) 9 | }).$mount() 10 | 11 | expect(vm.$el.querySelector('.title').textContent).to.contain('Welcome to your new project!') 12 | }) 13 | }) 14 | --------------------------------------------------------------------------------