├── .babelrc ├── .electron-vue ├── build.js ├── dev-client.js ├── dev-runner.js ├── webpack.main.config.js ├── webpack.renderer.config.js └── webpack.web.config.js ├── .eslintrc ├── .gitignore ├── .npmrc ├── .prettierrc ├── .travis.yml ├── LICENSE ├── Note.md ├── README.md ├── appveyor.yml ├── build └── icons │ ├── 256x256.png │ ├── icon.icns │ └── icon.ico ├── config.yaml ├── danmaku-scroll ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ └── Danmaku.vue │ ├── main.ts │ ├── service │ │ ├── api.ts │ │ ├── promise-queue.ts │ │ └── util.ts │ └── shims-vue.d.ts ├── tsconfig.json └── vue.config.js ├── danmaku ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ ├── Danmaku.vue │ │ ├── FanMedal.vue │ │ ├── GiftCard.vue │ │ ├── GiftCardMini.vue │ │ ├── GiftTag.vue │ │ ├── GiftTagExpand.vue │ │ └── SimilarCommentBadge.vue │ ├── main.js │ └── service │ │ ├── api.js │ │ ├── const.js │ │ ├── promise-queue.js │ │ └── util.js └── vue.config.js ├── er.drawio ├── jsconfig.json ├── package.json ├── pnpm-lock.yaml ├── src ├── index.ejs ├── main │ ├── index.dev.js │ └── index.js ├── renderer │ ├── App.vue │ ├── assets │ │ ├── .gitkeep │ │ ├── logo.png │ │ └── tip-01.png │ ├── components │ │ ├── ASR.vue │ │ ├── ASRWindow.vue │ │ ├── AutoReply.vue │ │ ├── Command.vue │ │ ├── Config.vue │ │ ├── Danmaku-Scroll.vue │ │ ├── FanMedal.vue │ │ ├── GiftCard.vue │ │ ├── GiftCardMini.vue │ │ ├── Help.vue │ │ ├── Home.vue │ │ ├── Introduction.vue │ │ ├── Live.vue │ │ ├── LiveWindow.vue │ │ ├── Lottery.vue │ │ ├── Message.vue │ │ ├── NotFound.vue │ │ ├── SpeechToDanmaku.vue │ │ ├── Statistic.vue │ │ ├── StyleEditor.vue │ │ ├── StyleSetting.vue │ │ ├── TagContent.vue │ │ └── Vote.vue │ ├── main.js │ ├── router │ │ └── index.ts │ └── store │ │ ├── index.ts │ │ └── modules │ │ └── Config.ts └── service │ ├── api.ts │ ├── bilibili-bridge.ts │ ├── config-loader.ts │ ├── const.ts │ ├── event.js │ ├── global.js │ ├── processor.worklet.js │ ├── promise-queue.js │ ├── util.js │ ├── vad.js │ └── ws.js ├── static ├── .gitkeep ├── er.png ├── intro1.png ├── intro2.png ├── intro3.png ├── intro4.png └── intro5.png └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "comments": false, 3 | "presets": ["@babel/preset-typescript"], 4 | "env": { 5 | "main": { 6 | "presets": [ 7 | [ 8 | "@babel/preset-env", 9 | { 10 | "targets": { 11 | "node": "16" 12 | } 13 | } 14 | ] 15 | ] 16 | }, 17 | "renderer": { 18 | "presets": [ 19 | [ 20 | "@babel/preset-env", 21 | { 22 | "modules": false 23 | } 24 | ] 25 | ] 26 | }, 27 | "web": { 28 | "presets": [ 29 | [ 30 | "@babel/preset-env", 31 | { 32 | "modules": false 33 | } 34 | ] 35 | ] 36 | } 37 | }, 38 | "plugins": [ 39 | // Stage 0 40 | "@babel/plugin-proposal-function-bind", 41 | 42 | // Stage 1 43 | "@babel/plugin-proposal-export-default-from", 44 | ["@babel/plugin-proposal-nullish-coalescing-operator", { "loose": false }], 45 | 46 | // Stage 2 47 | "@babel/plugin-proposal-export-namespace-from", 48 | "@babel/plugin-proposal-throw-expressions", 49 | 50 | // Stage 3 51 | "@babel/plugin-syntax-dynamic-import", 52 | "@babel/plugin-syntax-import-meta", 53 | "@babel/plugin-proposal-json-strings", 54 | 55 | "@babel/plugin-transform-runtime" 56 | ] 57 | } -------------------------------------------------------------------------------- /.electron-vue/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | const { say } = require('cfonts') 6 | // const chalk = require('chalk') 7 | const del = require('del') 8 | const { spawn } = require('child_process') 9 | const webpack = require('webpack') 10 | const Multispinner = require('multispinner') 11 | 12 | 13 | const mainConfig = require('./webpack.main.config') 14 | const rendererConfig = require('./webpack.renderer.config') 15 | const webConfig = require('./webpack.web.config') 16 | 17 | // const doneLog = chalk.bgGreen.white(' DONE ') + ' ' 18 | // const errorLog = chalk.bgRed.white(' ERROR ') + ' ' 19 | // const okayLog = chalk.bgBlue.white(' OKAY ') + ' ' 20 | const isCI = process.env.CI || false 21 | 22 | if (process.env.BUILD_TARGET === 'clean') clean() 23 | else if (process.env.BUILD_TARGET === 'web') web() 24 | else build() 25 | 26 | function clean() { 27 | del.sync(['build/*', '!build/icons', '!build/icons/icon.*']) 28 | // console.log(`\n${doneLog}\n`) 29 | process.exit() 30 | } 31 | 32 | function build() { 33 | greeting() 34 | 35 | del.sync(['dist/electron/*', '!.gitkeep']) 36 | 37 | const tasks = ['main', 'renderer'] 38 | const m = new Multispinner(tasks, { 39 | preText: 'building', 40 | postText: 'process' 41 | }) 42 | 43 | let results = '' 44 | 45 | m.on('success', () => { 46 | process.stdout.write('\x1B[2J\x1B[0f') 47 | console.log(`\n\n${results}`) 48 | // console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`) 49 | process.exit() 50 | }) 51 | 52 | pack(mainConfig).then(result => { 53 | results += result + '\n\n' 54 | m.success('main') 55 | }).catch(err => { 56 | m.error('main') 57 | // console.log(`\n ${errorLog}failed to build main process`) 58 | console.error(`\n${err}\n`) 59 | process.exit(1) 60 | }) 61 | 62 | pack(rendererConfig).then(result => { 63 | results += result + '\n\n' 64 | m.success('renderer') 65 | }).catch(err => { 66 | m.error('renderer') 67 | // console.log(`\n ${errorLog}failed to build renderer process`) 68 | console.error(`\n${err}\n`) 69 | process.exit(1) 70 | }) 71 | } 72 | 73 | function pack(config) { 74 | return new Promise((resolve, reject) => { 75 | config.mode = 'production' 76 | webpack(config, (err, stats) => { 77 | if (err) reject(err.stack || err) 78 | else if (stats.hasErrors()) { 79 | let err = '' 80 | 81 | stats.toString({ 82 | chunks: false, 83 | colors: true 84 | }) 85 | .split(/\r?\n/) 86 | .forEach(line => { 87 | err += ` ${line}\n` 88 | }) 89 | 90 | reject(err) 91 | } else { 92 | resolve(stats.toString({ 93 | chunks: false, 94 | colors: true 95 | })) 96 | } 97 | }) 98 | }) 99 | } 100 | 101 | function web() { 102 | del.sync(['dist/web/*', '!.gitkeep']) 103 | webConfig.mode = 'production' 104 | webpack(webConfig, (err, stats) => { 105 | if (err || stats.hasErrors()) console.log(err) 106 | 107 | console.log(stats.toString({ 108 | chunks: false, 109 | colors: true 110 | })) 111 | 112 | process.exit() 113 | }) 114 | } 115 | 116 | function greeting() { 117 | const cols = process.stdout.columns 118 | let text = '' 119 | 120 | if (cols > 85) text = 'lets-build' 121 | else if (cols > 60) text = 'lets-|build' 122 | else text = false 123 | 124 | if (text && !isCI) { 125 | say(text, { 126 | colors: ['yellow'], 127 | font: 'simple3d', 128 | space: false 129 | }) 130 | } else { 131 | // console.log(chalk.yellow.bold('\n lets-build')) 132 | } 133 | console.log() 134 | } -------------------------------------------------------------------------------- /.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-runner.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // import chalk from 'chalk' 4 | const electron = require('electron') 5 | const path = require('path') 6 | const { say } = require('cfonts') 7 | const { spawn } = require('child_process') 8 | const webpack = require('webpack') 9 | const WebpackDevServer = require('webpack-dev-server') 10 | // const webpackHotMiddleware = require('webpack-hot-middleware') 11 | 12 | const mainConfig = require('./webpack.main.config') 13 | const rendererConfig = require('./webpack.renderer.config') 14 | 15 | let electronProcess = null 16 | let manualRestart = false 17 | // let hotMiddleware 18 | 19 | function logStats(proc, data) { 20 | let log = '' 21 | 22 | // log += chalk.yellow.bold(`┏ ${proc} Process ${new Array((19 - proc.length) + 1).join('-')}`) 23 | log += '\n\n' 24 | 25 | if (typeof data === 'object') { 26 | data.toString({ 27 | colors: true, 28 | chunks: false 29 | }).split(/\r?\n/).forEach(line => { 30 | log += ' ' + line + '\n' 31 | }) 32 | } else { 33 | log += ` ${data}\n` 34 | } 35 | 36 | // log += '\n' + chalk.yellow.bold(`┗ ${new Array(28 + 1).join('-')}`) + '\n' 37 | 38 | console.log(log) 39 | } 40 | 41 | function startRenderer() { 42 | return new Promise((resolve, reject) => { 43 | rendererConfig.entry.renderer = [ 44 | // path.join(__dirname, 'dev-client') 45 | ].concat(rendererConfig.entry.renderer) 46 | rendererConfig.mode = 'development' 47 | const compiler = webpack(rendererConfig) 48 | // hotMiddleware = webpackHotMiddleware(compiler, { 49 | // log: false, 50 | // heartbeat: 2500 51 | // }) 52 | 53 | compiler.hooks.compilation.tap('compilation', compilation => { 54 | // compilation.hooks.htmlWebpackPluginAfterEmit.tapAsync('html-webpack-plugin-after-emit', (data, cb) => { 55 | // // hotMiddleware.publish({ action: 'reload' }) 56 | // cb() 57 | // }) 58 | }) 59 | 60 | compiler.hooks.done.tap('done', stats => { 61 | logStats('Renderer', stats) 62 | }) 63 | 64 | const server = new WebpackDevServer( 65 | compiler, 66 | { 67 | static: { 68 | directory: path.resolve(__dirname, "static"), 69 | }, 70 | hot: true, 71 | onBeforeSetupMiddleware: function (devServer) { 72 | devServer.middleware.waitUntilValid(() => { 73 | resolve() 74 | }) 75 | }, 76 | } 77 | ) 78 | 79 | server.listen(9080) 80 | }) 81 | } 82 | 83 | function startMain() { 84 | return new Promise((resolve, reject) => { 85 | mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main) 86 | mainConfig.mode = 'development' 87 | const compiler = webpack(mainConfig) 88 | 89 | compiler.hooks.watchRun.tapAsync('watch-run', (compilation, done) => { 90 | // logStats('Main', chalk.white.bold('compiling...')) 91 | // hotMiddleware.publish({ action: 'compiling' }) 92 | done() 93 | }) 94 | 95 | compiler.watch({}, (err, stats) => { 96 | if (err) { 97 | console.log(err) 98 | return 99 | } 100 | 101 | logStats('Main', stats) 102 | 103 | if (electronProcess && electronProcess.kill) { 104 | manualRestart = true 105 | process.kill(electronProcess.pid) 106 | electronProcess = null 107 | startElectron() 108 | 109 | setTimeout(() => { 110 | manualRestart = false 111 | }, 5000) 112 | } 113 | 114 | resolve() 115 | }) 116 | }) 117 | } 118 | 119 | function startElectron() { 120 | var args = [ 121 | '--inspect=5858', 122 | path.join(__dirname, '../dist/electron/main.js') 123 | ] 124 | 125 | // detect yarn or npm and process commandline args accordingly 126 | if (process.env.npm_execpath.endsWith('yarn.js')) { 127 | args = args.concat(process.argv.slice(3)) 128 | } else if (process.env.npm_execpath.endsWith('npm-cli.js')) { 129 | args = args.concat(process.argv.slice(2)) 130 | } 131 | 132 | electronProcess = spawn(electron, args) 133 | 134 | electronProcess.stdout.on('data', data => { 135 | electronLog(data, 'blue') 136 | }) 137 | electronProcess.stderr.on('data', data => { 138 | electronLog(data, 'red') 139 | }) 140 | 141 | electronProcess.on('close', () => { 142 | if (!manualRestart) process.exit() 143 | }) 144 | } 145 | 146 | function electronLog(data, color) { 147 | let log = '' 148 | data = data.toString().split(/\r?\n/) 149 | data.forEach(line => { 150 | log += ` ${line}\n` 151 | }) 152 | if (/[0-9A-z]+/.test(log)) { 153 | console.log( 154 | // chalk[color].bold('┏ Electron -------------------') + 155 | '\n\n' + 156 | log + 157 | // chalk[color].bold('┗ ----------------------------') + 158 | '\n' 159 | ) 160 | } 161 | } 162 | 163 | function greeting() { 164 | const cols = process.stdout.columns 165 | let text = '' 166 | 167 | if (cols > 104) text = 'electron-vue' 168 | else if (cols > 76) text = 'electron-|vue' 169 | else text = false 170 | 171 | if (text) { 172 | say(text, { 173 | colors: ['yellow'], 174 | font: 'simple3d', 175 | space: false 176 | }) 177 | } else { 178 | // console.log(chalk.yellow.bold('\n electron-vue')) 179 | } 180 | // console.log(chalk.blue(' getting ready...') + '\n') 181 | } 182 | 183 | function init() { 184 | greeting() 185 | 186 | Promise.all([startRenderer(), startMain()]) 187 | .then(() => { 188 | startElectron() 189 | }) 190 | .catch(err => { 191 | console.error(err) 192 | }) 193 | } 194 | 195 | init() 196 | -------------------------------------------------------------------------------- /.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 TerserPlugin = require("terser-webpack-plugin"); 10 | 11 | let mainConfig = { 12 | entry: { 13 | main: path.join(__dirname, '../src/main/index.js') 14 | }, 15 | optimization: { 16 | minimize: true, 17 | minimizer: [(compiler) => { 18 | new TerserPlugin({ 19 | terserOptions: { 20 | compress: {}, 21 | } 22 | }).apply(compiler); 23 | }], 24 | }, 25 | externals: [ 26 | ...Object.keys(dependencies || {}) 27 | ], 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.tsx?$/, 32 | use: { 33 | loader: 'babel-loader', 34 | options: { 35 | presets: [ 36 | '@babel/preset-env', 37 | '@babel/preset-typescript' 38 | ] 39 | } 40 | }, 41 | exclude: /node_modules/ 42 | }, 43 | { 44 | test: /\.js$/, 45 | use: 'babel-loader', 46 | exclude: /node_modules/ 47 | }, 48 | { 49 | test: /\.node$/, 50 | use: 'node-loader' 51 | } 52 | ] 53 | }, 54 | node: { 55 | __dirname: process.env.NODE_ENV !== 'production', 56 | __filename: process.env.NODE_ENV !== 'production' 57 | }, 58 | output: { 59 | filename: '[name].js', 60 | libraryTarget: 'commonjs2', 61 | path: path.join(__dirname, '../dist/electron') 62 | }, 63 | plugins: [ 64 | new webpack.NoEmitOnErrorsPlugin() 65 | ], 66 | resolve: { 67 | extensions: ['.js', '.json', '.node', '.vue', '.tsx', '.ts'], 68 | alias: { 69 | 'vue': '@vue/runtime-dom', 70 | 'Vue': 'vue/dist/vue.esm-bundler.js', 71 | } 72 | }, 73 | target: 'electron-main' 74 | } 75 | 76 | /** 77 | * Adjust mainConfig for development settings 78 | */ 79 | if (process.env.NODE_ENV !== 'production') { 80 | mainConfig.plugins.push( 81 | new webpack.DefinePlugin({ 82 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"` 83 | }) 84 | ) 85 | } 86 | 87 | /** 88 | * Adjust mainConfig for production settings 89 | */ 90 | if (process.env.NODE_ENV === 'production') { 91 | mainConfig.plugins.push( 92 | new webpack.DefinePlugin({ 93 | 'process.env.NODE_ENV': '"production"' 94 | }) 95 | ) 96 | } 97 | 98 | module.exports = mainConfig 99 | -------------------------------------------------------------------------------- /.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 TerserPlugin = require("terser-webpack-plugin"); 10 | const CopyWebpackPlugin = require('copy-webpack-plugin') 11 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 12 | const HtmlWebpackPlugin = require('html-webpack-plugin') 13 | const { VueLoaderPlugin } = require('vue-loader') 14 | 15 | /** 16 | * List of node_modules to include in webpack bundle 17 | * 18 | * Required for specific packages like Vue UI libraries 19 | * that provide pure *.vue files that need compiling 20 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals 21 | */ 22 | let whiteListedModules = [] 23 | 24 | let rendererConfig = { 25 | devtool: 'source-map', 26 | optimization: { 27 | minimize: true, 28 | minimizer: [(compiler) => { 29 | new TerserPlugin({ 30 | terserOptions: { 31 | compress: {}, 32 | } 33 | }).apply(compiler); 34 | }], 35 | }, 36 | 37 | entry: { 38 | renderer: path.join(__dirname, '../src/renderer/main.js') 39 | }, 40 | externals: [ 41 | ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)) 42 | ], 43 | module: { 44 | rules: [ 45 | { 46 | test: /\.tsx?$/, 47 | use: { 48 | loader: 'babel-loader', 49 | options: { 50 | presets: [ 51 | '@babel/preset-env', 52 | '@babel/preset-typescript' 53 | ] 54 | } 55 | }, 56 | exclude: /node_modules/ 57 | }, 58 | { 59 | test: /\.vue$/, 60 | use: { 61 | loader: 'vue-loader', 62 | options: { 63 | extractCSS: process.env.NODE_ENV === 'production', 64 | loaders: { 65 | // sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', 66 | // scss: 'vue-style-loader!css-loader!sass-loader', 67 | // less: 'vue-style-loader!css-loader!less-loader' 68 | } 69 | } 70 | }, 71 | }, 72 | // { 73 | // test: /\.scss$/, 74 | // use: ['vue-style-loader', 'css-loader', 'sass-loader'] 75 | // }, 76 | // { 77 | // test: /\.sass$/, 78 | // use: ['vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax'] 79 | // }, 80 | { 81 | test: /\.less$/, 82 | use: ['vue-style-loader', 'css-loader', 'less-loader'] 83 | }, 84 | { 85 | test: /\.css$/, 86 | use: ['vue-style-loader', 'css-loader'] 87 | }, 88 | { 89 | test: /\.html$/, 90 | use: 'vue-html-loader' 91 | }, 92 | { 93 | test: /\.js$/, 94 | use: 'babel-loader', 95 | exclude: /node_modules/ 96 | }, 97 | { 98 | test: /\.node$/, 99 | use: 'node-loader' 100 | }, 101 | { 102 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 103 | use: { 104 | loader: 'url-loader', 105 | options: { 106 | limit: 10000, 107 | name: 'imgs/[name]--[folder].[ext]' 108 | } 109 | } 110 | }, 111 | { 112 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 113 | loader: 'url-loader', 114 | options: { 115 | limit: 10000, 116 | name: 'media/[name]--[folder].[ext]' 117 | } 118 | }, 119 | { 120 | test: /\.woff2?$/i, 121 | type: 'asset/resource', 122 | dependency: { not: ['url'] }, 123 | }, 124 | // { 125 | // test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 126 | // use: { 127 | // loader: 'url-loader', 128 | // options: { 129 | // limit: 10000, 130 | // name: 'fonts/[name]--[folder].[ext]' 131 | // } 132 | // } 133 | // } 134 | ], 135 | }, 136 | node: { 137 | __dirname: process.env.NODE_ENV !== 'production', 138 | __filename: process.env.NODE_ENV !== 'production' 139 | }, 140 | plugins: [ 141 | new VueLoaderPlugin(), 142 | new MiniCssExtractPlugin({ filename: 'styles.css' }), 143 | new HtmlWebpackPlugin({ 144 | filename: 'index.html', 145 | template: path.resolve(__dirname, '../src/index.ejs'), 146 | minify: { 147 | collapseWhitespace: true, 148 | removeAttributeQuotes: true, 149 | removeComments: true 150 | }, 151 | nodeModules: process.env.NODE_ENV !== 'production' 152 | ? path.resolve(__dirname, '../node_modules') 153 | : false 154 | }), 155 | new webpack.HotModuleReplacementPlugin(), 156 | new webpack.NoEmitOnErrorsPlugin(), 157 | // new CopyWebpackPlugin({ 158 | // patterns: [ 159 | // // ... 160 | // { 161 | // from: "node_modules/@ricky0123/vad/dist/*.worklet.js", 162 | // to: "[name][ext]", 163 | // }, 164 | // { 165 | // from: "node_modules/@ricky0123/vad/dist/*.onnx", 166 | // to: "[name][ext]", 167 | // }, 168 | // { from: "node_modules/onnxruntime-web/dist/*.wasm", to: "[name][ext]" }, 169 | // ], 170 | // }), 171 | ], 172 | output: { 173 | filename: '[name].js', 174 | libraryTarget: 'commonjs2', 175 | path: path.join(__dirname, '../dist/electron') 176 | }, 177 | resolve: { 178 | alias: { 179 | '@': path.join(__dirname, '../src/renderer'), 180 | 'vue$': 'vue/dist/vue.esm.js' 181 | }, 182 | extensions: ['.js', '.vue', '.json', '.css', '.node', '.ts'] 183 | }, 184 | target: 'electron-renderer' 185 | } 186 | 187 | /** 188 | * Adjust rendererConfig for development settings 189 | */ 190 | if (process.env.NODE_ENV !== 'production') { 191 | rendererConfig.plugins.push( 192 | new webpack.DefinePlugin({ 193 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"` 194 | }) 195 | ) 196 | } 197 | 198 | /** 199 | * Adjust rendererConfig for production settings 200 | */ 201 | if (process.env.NODE_ENV === 'production') { 202 | rendererConfig.devtool = 'source-map' 203 | 204 | rendererConfig.plugins.push( 205 | new CopyWebpackPlugin({ 206 | patterns: [{ 207 | from: path.join(__dirname, '../static'), 208 | to: path.join(__dirname, '../dist/electron/static'), 209 | globOptions: { 210 | ignore: ['.*'] 211 | } 212 | }, { 213 | from: path.join(__dirname, '../danmaku-dist'), 214 | to: path.join(__dirname, '../dist/electron/danmaku'), 215 | }] 216 | }), 217 | new webpack.DefinePlugin({ 218 | 'process.env.NODE_ENV': '"production"' 219 | }), 220 | new webpack.LoaderOptionsPlugin({ 221 | minimize: true 222 | }) 223 | ) 224 | } 225 | 226 | module.exports = rendererConfig 227 | -------------------------------------------------------------------------------- /.electron-vue/webpack.web.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'web' 4 | 5 | const path = require('path') 6 | const webpack = require('webpack') 7 | 8 | const TerserPlugin = require("terser-webpack-plugin"); 9 | const CopyWebpackPlugin = require('copy-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 | let webConfig = { 15 | devtool: 'source-map', 16 | optimization: { 17 | minimize: true, 18 | minimizer: [(compiler) => { 19 | new TerserPlugin({ 20 | terserOptions: { 21 | compress: {}, 22 | } 23 | }).apply(compiler); 24 | }], 25 | }, 26 | entry: { 27 | web: path.join(__dirname, '../src/renderer/main.js') 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.tsx?$/, 33 | use: { 34 | loader: 'babel-loader', 35 | options: { 36 | presets: [ 37 | '@babel/preset-env', 38 | '@babel/preset-typescript' 39 | ] 40 | } 41 | }, 42 | exclude: /node_modules/ 43 | }, 44 | // { 45 | // test: /\.scss$/, 46 | // use: ['vue-style-loader', 'css-loader', 'sass-loader'] 47 | // }, 48 | // { 49 | // test: /\.sass$/, 50 | // use: ['vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax'] 51 | // }, 52 | // { 53 | // test: /\.less$/, 54 | // use: ['vue-style-loader', 'css-loader', 'less-loader'] 55 | // }, 56 | { 57 | test: /\.css$/, 58 | use: ['vue-style-loader', 'css-loader'] 59 | }, 60 | { 61 | test: /\.html$/, 62 | use: 'vue-html-loader' 63 | }, 64 | { 65 | test: /\.js$/, 66 | use: 'babel-loader', 67 | include: [path.resolve(__dirname, '../src/renderer')], 68 | exclude: /node_modules/ 69 | }, 70 | { 71 | test: /\.vue$/, 72 | use: { 73 | loader: 'vue-loader', 74 | options: { 75 | extractCSS: true, 76 | loaders: { 77 | // sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', 78 | // scss: 'vue-style-loader!css-loader!sass-loader', 79 | // less: 'vue-style-loader!css-loader!less-loader' 80 | }, 81 | }, 82 | } 83 | }, 84 | { 85 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 86 | use: { 87 | loader: 'url-loader', 88 | query: { 89 | limit: 10000, 90 | name: 'imgs/[name].[ext]' 91 | } 92 | } 93 | }, 94 | { 95 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 96 | use: { 97 | loader: 'url-loader', 98 | query: { 99 | limit: 10000, 100 | name: 'fonts/[name].[ext]' 101 | } 102 | } 103 | }, 104 | ], 105 | }, 106 | plugins: [ 107 | new VueLoaderPlugin(), 108 | new MiniCssExtractPlugin({ filename: 'styles.css' }), 109 | new HtmlWebpackPlugin({ 110 | filename: 'index.html', 111 | template: path.resolve(__dirname, '../src/index.ejs'), 112 | minify: { 113 | collapseWhitespace: true, 114 | removeAttributeQuotes: true, 115 | removeComments: true 116 | }, 117 | nodeModules: false 118 | }), 119 | new webpack.DefinePlugin({ 120 | 'process.env.IS_WEB': 'true' 121 | }), 122 | new webpack.HotModuleReplacementPlugin(), 123 | new webpack.NoEmitOnErrorsPlugin(), 124 | ], 125 | output: { 126 | filename: '[name].js', 127 | path: path.join(__dirname, '../dist/web') 128 | }, 129 | resolve: { 130 | alias: { 131 | '@': path.join(__dirname, '../src/renderer'), 132 | 'vue$': 'vue/dist/vue.esm.js' 133 | }, 134 | extensions: ['.js', '.vue', '.json', '.css', '.ts'] 135 | }, 136 | target: 'web' 137 | } 138 | 139 | /** 140 | * Adjust webConfig for production settings 141 | */ 142 | if (process.env.NODE_ENV === 'production') { 143 | webConfig.devtool = 'source-map' 144 | 145 | webConfig.plugins.push( 146 | new CopyWebpackPlugin({ 147 | patterns: [{ 148 | from: path.join(__dirname, '../static'), 149 | to: path.join(__dirname, '../dist/web/static'), 150 | globOptions: { 151 | ignore: ['.*'] 152 | } 153 | }] 154 | }), 155 | new webpack.DefinePlugin({ 156 | 'process.env.NODE_ENV': '"production"' 157 | }), 158 | new webpack.LoaderOptionsPlugin({ 159 | minimize: true 160 | }) 161 | ) 162 | } 163 | 164 | module.exports = webConfig 165 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:vue/vue3-essential", 4 | "plugin:vue/vue3-recommended" 5 | ], 6 | "plugins": [ 7 | "@typescript-eslint" 8 | ], 9 | "rules": { 10 | "no-console": "off", 11 | "no-debugger": "off", 12 | "no-unused-vars": "off", 13 | "semi": "off", 14 | // "vue/multiline-html-element-content-newline": "off", 15 | "vue/singleline-html-element-content-newline": "off", 16 | "vue/max-attributes-per-line": "off", 17 | "vue/multi-word-component-names": "off", 18 | "vue/require-prop-types": "off" 19 | } 20 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/* 3 | build/* 4 | !build/icons 5 | node_modules/ 6 | npm-debug.log 7 | npm-debug.log.* 8 | thumbs.db 9 | !.gitkeep 10 | record/ 11 | data/ 12 | tmp/ 13 | *.log 14 | electron-vite-vue/ 15 | danmaku-dist/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | electron_mirror=https://registry.npmmirror.com/-/binary/electron/ 2 | # registry=https://registry.npmmirror.com 3 | node-linker=hoisted 4 | public-hoist-pattern=* 5 | shamefully-hoist=true 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 200, 3 | "tabWidth": 2, 4 | "tabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "quoteProps": "as-needed", 8 | "tailingComma": "all", 9 | "bracketSpacing": true, 10 | "jsxBracketSameLine": false 11 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode8.3 2 | sudo: required 3 | dist: trusty 4 | language: c 5 | matrix: 6 | include: 7 | - os: osx 8 | - os: linux 9 | env: CC=clang CXX=clang++ npm_config_clang=1 10 | compiler: clang 11 | cache: 12 | directories: 13 | - node_modules 14 | - "$HOME/.electron" 15 | - "$HOME/.cache" 16 | addons: 17 | apt: 18 | packages: 19 | - libgnome-keyring-dev 20 | - icnsutils 21 | before_install: 22 | - mkdir -p /tmp/git-lfs && curl -L https://github.com/github/git-lfs/releases/download/v1.2.1/git-lfs-$([ 23 | "$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || echo "darwin")-amd64-1.2.1.tar.gz 24 | | tar -xz -C /tmp/git-lfs --strip-components 1 && /tmp/git-lfs/git-lfs pull 25 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils; fi 26 | install: 27 | - nvm install 10 28 | - curl -o- -L https://yarnpkg.com/install.sh | bash 29 | - source ~/.bashrc 30 | - npm install -g xvfb-maybe 31 | - yarn 32 | script: 33 | - yarn run build 34 | branches: 35 | only: 36 | - master 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 其妙 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 | -------------------------------------------------------------------------------- /Note.md: -------------------------------------------------------------------------------- 1 | ### vue-cli 2 | vue ui 3 | 4 | ### iview 5 | Switch -> i-switch 6 | Circle -> i-circle 7 | Col -> i-col 8 | 9 | 元素包裹性 10 | 包裹性就是父元素的宽度会收缩到和内部元素宽度一样。 11 | 12 | ### vue 13 | 单文件组件设置body style 需要在生命周期beforecreate时设置,透明窗体需要提前设置背景透明 14 | v-deep 作用于 scope 中更深层的子元素 15 | 16 | ### vuex-electron 17 | 默认使用 electron-store 存储 18 | ``` 19 | import Store from "electron-store"; 20 | const store = new Store({ name: "vuex" }); 21 | store.clear(); 22 | ``` 23 | 24 | ### 关闭windowshadow避免resize产生重影 25 | 26 | ### production debug: --remote-debugging-port=8315, http://localhost:8315/ 27 | 28 | ### vuex 29 | 计算属性需要在storage里初始化才能reactive 30 | 31 | ### 32 | less other | grep -v "COMBO_SEND" | grep -v "ONLINE_RANK_COUNT" | grep -v "ENTRY_EFFECT" | grep -v "SUPER_CHAT_MESSAGE_JPN" | grep -v "SUPER_CHAT_MESSAGE" | grep -v "NOTICE_MSG" | grep -v "ONLINE_RANK_V2" | grep -v "ONLINE_RANK_TOP3" | grep -v "HOT_RANK_CHANGED" | grep -v "GUARD_BUY" | grep -v "HOT_RANK_SETTLEMENT" | grep -v "WELCOME_GUARD" | grep -v "ACTIVITY_BANNER_UPDATE_V2" | grep -v "ROOM_BANNER" | grep -v "PK_BATTLE_END" | grep -v "PANEL" | grep -v "PK_BATTLE_SETTLE_V2" | grep -v "PK_BATTLE_START_NEW" | grep -v "USER_TOAST_MSG" | grep -v "PK_BATTLE_PROCESS" | grep -v "WIDGET_BANNER" | grep -v "PK_BATTLE_PRE" | grep -v "ONLINERANK" | grep -v "PK_BATTLE_SETTLE" | grep -v "ROOM_RANK" | grep -v "WELCOME" | grep -v "PK_BATTLE_START" | grep -v "ROOM_BLOCK_MSG" | grep -v "ANCHOR_LOT_CHECKSTATUS" | grep -v "ANCHOR_LOT_START" | grep -v "ANCHOR_LOT_END" | grep -v "ANCHOR_LOT_AWARD" | grep -v "GUARD_ACHIEVEMENT_ROOM" | grep -v "PK_LOTTERY_START" | grep -v "ROOM_CHANGE" | grep -v "ROOM_SKIN_MSG" | grep -v "SPECIAL_GIFT" | grep -v "room_admin_entrance" | grep -v "ROOM_ADMINS" | grep -v "TRADING_SCORE" | grep -v "PK_BATTLE_ENTRANCE" | grep -v "SYS_MSG" | grep -v "MATCH_ROOM_CONF" | grep -v "WARNING" | grep -v "SUPER_CHAT_ENTRANCE" | grep -v "CUT_OFF" | grep -v "PREPARING" | grep -v "LIVE" 33 | 34 | {"cmd":"PREPARING","roomid":"22632424","_id":"ovMFjeyeInIFs8yG"} 35 | {"cmd":"LIVE","live_key":"142000930295522209","sub_session_key":"142000930295522209sub_time:1616591278","live_platform":"pc","live_model":0,"roomid":664481,"_id":"stfb6ahOWmAU3kgf"} 36 | 37 | { 38 | "cmd":"ANCHOR_LOT_START", 39 | "data":{ 40 | "asset_icon":"https://i0.hdslb.com/bfs/live/992c2ccf88d3ea99620fb3a75e672e0abe850e9c.png", 41 | "award_image":"", 42 | "award_name":"优菈一只或1000元红包", 43 | "award_num":1, 44 | "cur_gift_num":0, 45 | "current_time":1621350047, 46 | "danmu":"好耶!", 47 | "gift_id":30572, 48 | "gift_name":"大闸蟹", 49 | "gift_num":1, 50 | "gift_price":2000, 51 | "goaway_time":180, 52 | "goods_id":15, 53 | "id":1217192, 54 | "is_broadcast":1, 55 | "join_type":1, 56 | "lot_status":0, 57 | "max_time":600, 58 | "require_text":"无", 59 | "require_type":0, 60 | "require_value":0, 61 | "room_id":732602, 62 | "send_gift_ensure":0, 63 | "show_panel":1, 64 | "status":1, 65 | "time":599, 66 | "url":"https://live.bilibili.com/p/html/live-lottery/anchor-join.html?is_live_half_webview=1&hybrid_biz=live-lottery-anchor&hybrid_half_ui=1,5,100p,100p,000000,0,30,0,0,1;2,5,100p,100p,000000,0,30,0,0,1;3,5,100p,100p,000000,0,30,0,0,1;4,5,100p,100p,000000,0,30,0,0,1;5,5,100p,100p,000000,0,30,0,0,1;6,5,100p,100p,000000,0,30,0,0,1;7,5,100p,100p,000000,0,30,0,0,1;8,5,100p,100p,000000,0,30,0,0,1", 67 | "web_url":"https://live.bilibili.com/p/html/live-lottery/anchor-join.html" 68 | }, 69 | "_id":"rzPhXsmhGyyZhvTq" 70 | } 71 | 72 | { 73 | "cmd":"ANCHOR_LOT_AWARD", 74 | "data":{ 75 | "award_image":"", 76 | "award_name":"优菈一只或1000元红包", 77 | "award_num":1, 78 | "award_users":[ 79 | { 80 | "uid":272853351, 81 | "uname":"天之悠净", 82 | "face":"http://i0.hdslb.com/bfs/face/3084517bd2f7438cb2706f1c959ba3d4b750cdaf.jpg", 83 | "level":2, 84 | "color":9868950 85 | } 86 | ], 87 | "id":1217574, 88 | "lot_status":2, 89 | "url":"https://live.bilibili.com/p/html/live-lottery/anchor-join.html?is_live_half_webview=1&hybrid_biz=live-lottery-anchor&hybrid_half_ui=1,5,100p,100p,000000,0,30,0,0,1;2,5,100p,100p,000000,0,30,0,0,1;3,5,100p,100p,000000,0,30,0,0,1;4,5,100p,100p,000000,0,30,0,0,1;5,5,100p,100p,000000,0,30,0,0,1;6,5,100p,100p,000000,0,30,0,0,1;7,5,100p,100p,000000,0,30,0,0,1;8,5,100p,100p,000000,0,30,0,0,1", 90 | "web_url":"https://live.bilibili.com/p/html/live-lottery/anchor-join.html" 91 | }, 92 | "_id":"PtQC7AP1mPQ01fHz" 93 | } 94 | 95 | {"cmd":"ANCHOR_LOT_END","data":{"id":1217574},"_id":"M0zRZUq3C7wKsPGZ"} 96 | 97 | 98 | [ 99 | 0, 100 | 1, 101 | 25, 102 | 16777215, 103 | 1627829012878, 104 | 791172865, 105 | 0, 106 | '4be2ca3d', 107 | 0, 108 | 0, 109 | 0, 110 | '', 111 | 2, 112 | '{}', 113 | { 114 | voice_url: 'https%3A%2F%2Fboss.hdslb.com%2Flive-dm-voice%2Fc67ca588adb026ef4a4232da6c9dd4b31627829012.wav%3FX-Amz-Algorithm%3DAWS4-HMAC-SHA256%26X-Amz-Credential%3D2663ba902868f12f%252F20210801%252Fshjd%252Fs3%252Faws4_request%26X-Amz-Date%3D20210801T144332Z%26X-Amz-Expires%3D600000%26X-Amz-SignedHeaders%3Dhost%26X-Amz-Signature%3Dcecf439309b565588eba54758b9e4dbcf43ff4a2d4ca1bc057afa138f21185f0', 115 | file_format: 'wav', 116 | text: '那女孩对我说,戴佳伟是个大帅哥。', 117 | file_duration: 5 118 | } 119 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bilibili-danmaku 2 | 3 | 这是一个桌面端哔哩哔哩的直播弹幕应用(electron-vue) 4 | 5 | 目前支持直播间数据统计、多样化自定义弹幕展示、弹幕投票、自动语音/文字回复等功能 6 | 7 | ### 下载 8 | https://github.com/usagiring/bilibili-live-danmaku/releases 9 | 10 | ### 示例 11 | 弹幕窗及设置页面: 12 | ![intro1](https://github.com/usagiring/bilibili-live-danmaku/blob/master/static/intro1.png) 13 | 14 | 弹幕投票: 15 | ![intro2](https://github.com/usagiring/bilibili-live-danmaku/blob/master/static/intro2.png) 16 | 17 | 数据统计: 18 | ![intro3](https://github.com/usagiring/bilibili-live-danmaku/blob/master/static/intro3.png) 19 | 20 | 自动回复: 21 | ![intro4](https://github.com/usagiring/bilibili-live-danmaku/blob/master/static/intro4.png) 22 | 23 | 观看直播: 24 | ![intro5](https://github.com/usagiring/bilibili-live-danmaku/blob/master/static/intro5.png) 25 | 26 | ### 说明 27 | - 本应用数据基于本地收集到的数据,未被应用统计到的数据不进行统计。 28 | - 十分内互动人数,由进入直播间、发送弹幕、送礼组成。 29 | 30 | ### for developer 31 | 32 | 架构图: 33 | ![er](https://github.com/usagiring/bilibili-live-danmaku/blob/master/static/er.png) 34 | 35 | ``` bash 36 | # install dependencies 37 | npm install 38 | 39 | # serve with hot reload at localhost:9080 40 | npm run dev 41 | 42 | # build electron application for production 43 | npm run build 44 | ``` -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 0.1.{build} 2 | 3 | branches: 4 | only: 5 | - master 6 | 7 | image: Visual Studio 2017 8 | platform: 9 | - x64 10 | 11 | cache: 12 | - node_modules 13 | - '%APPDATA%\npm-cache' 14 | - '%USERPROFILE%\.electron' 15 | - '%USERPROFILE%\AppData\Local\Yarn\cache' 16 | 17 | init: 18 | - git config --global core.autocrlf input 19 | 20 | install: 21 | - ps: Install-Product node 8 x64 22 | - git reset --hard HEAD 23 | - yarn 24 | - node --version 25 | 26 | build_script: 27 | - yarn build 28 | 29 | test: off 30 | -------------------------------------------------------------------------------- /build/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagiring/bilibili-live-danmaku/4de4134eb479f2d727a14d672e485b651e2007e0/build/icons/256x256.png -------------------------------------------------------------------------------- /build/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagiring/bilibili-live-danmaku/4de4134eb479f2d727a14d672e485b651e2007e0/build/icons/icon.icns -------------------------------------------------------------------------------- /build/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagiring/bilibili-live-danmaku/4de4134eb479f2d727a14d672e485b651e2007e0/build/icons/icon.ico -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | # 启动端口 2 | PORT: 8081 3 | # 最多记录历史房间号 4 | MAX_HISTORY_ROOM: 15 5 | # 弹幕窗渲染页面地址 6 | DANMAKU_RENDER_PATH: '' 7 | SAVE_ALL_BILI_MESSAGE: false 8 | # 礼物色彩区分, 1~6 分别对应B站 6档SC价格 9 | PRICE_PROPERTIES: 10 | 1: 11 | backgroundColor: "#EDF5FF" 12 | backgroundPriceColor: "#7497CD" 13 | backgroundBottomColor: "#2A60B2" 14 | time: 60000 15 | 2: 16 | backgroundColor: "#DBFFFD" 17 | backgroundPriceColor: "#7DA4BD" 18 | backgroundBottomColor: "#427D9E" 19 | time: 120000 20 | 3: 21 | backgroundColor: "#FFF1C5" 22 | backgroundPriceColor: "gold" 23 | backgroundBottomColor: "#E2B52B" 24 | time: 300000 25 | 4: 26 | backgroundColor: "rgb(255,234,210)" 27 | backgroundPriceColor: "rgb(255,234,210)" 28 | backgroundBottomColor: "rgb(244,148,67)" 29 | time: 1800000 30 | 5: 31 | backgroundColor: "rgb(255,231,228)" 32 | backgroundPriceColor: "rgb(255,231,228)" 33 | backgroundBottomColor: "rgb(229,77,77)" 34 | time: 3600000 35 | 6: 36 | backgroundColor: "rgb(255,216,216)" 37 | backgroundPriceColor: "rgb(255,216,216)" 38 | backgroundBottomColor: "rgb(171,26,50)" 39 | time: 7200000 -------------------------------------------------------------------------------- /danmaku-scroll/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /danmaku-scroll/README.md: -------------------------------------------------------------------------------- 1 | # danmaku-scroll 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Customize configuration 19 | See [Configuration Reference](https://cli.vuejs.org/config/). 20 | -------------------------------------------------------------------------------- /danmaku-scroll/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /danmaku-scroll/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "danmaku-scroll", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "npm run serve", 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "build:monorepo": "npm run build && rm -rf ../danmaku-dist/danmaku-scroll && mv dist ../danmaku-dist/danmaku-scroll" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.27.2", 13 | "core-js": "^3.25.0", 14 | "lodash": "^4.17.21", 15 | "vue": "^3.2.38" 16 | }, 17 | "devDependencies": { 18 | "@types/lodash": "^4.14.184", 19 | "@vue/cli-plugin-babel": "~5.0.8", 20 | "@vue/cli-plugin-typescript": "~5.0.8", 21 | "@vue/cli-service": "~5.0.8", 22 | "typescript": "~4.8.2" 23 | }, 24 | "browserslist": [ 25 | "> 1%", 26 | "last 2 versions", 27 | "not dead", 28 | "not ie 11" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /danmaku-scroll/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagiring/bilibili-live-danmaku/4de4134eb479f2d727a14d672e485b651e2007e0/danmaku-scroll/public/favicon.ico -------------------------------------------------------------------------------- /danmaku-scroll/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /danmaku-scroll/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | 17 | 26 | -------------------------------------------------------------------------------- /danmaku-scroll/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagiring/bilibili-live-danmaku/4de4134eb479f2d727a14d672e485b651e2007e0/danmaku-scroll/src/assets/logo.png -------------------------------------------------------------------------------- /danmaku-scroll/src/components/Danmaku.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 409 | 410 | 411 | 433 | -------------------------------------------------------------------------------- /danmaku-scroll/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /danmaku-scroll/src/service/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | let BASE_URL = '' 4 | 5 | export function init({ port }: { port: string }) { 6 | BASE_URL = `http://127.0.0.1:${port}` 7 | } 8 | 9 | export async function getSetting() { 10 | const res = await axios.get(`${BASE_URL}/api/setting`) 11 | return res.data 12 | } 13 | 14 | // export async function getGiftConfig(roomId) { 15 | // const res = await axios.get(`${BASE_URL}/api/room/${roomId}/gift/map`) 16 | // return res.data 17 | // } -------------------------------------------------------------------------------- /danmaku-scroll/src/service/promise-queue.ts: -------------------------------------------------------------------------------- 1 | class PromiseQueue { 2 | queue: any = [] 3 | limit = 0 4 | highWaterMark = 0 5 | current = 0 6 | highWaterMarkResolve: any = null 7 | waitAllResolve: any = null 8 | pipeFn: any = null 9 | catchFn: any = null 10 | 11 | constructor(options: any = {}) { 12 | const { limit, highWaterMark } = options 13 | this.limit = limit || 32 14 | this.highWaterMark = highWaterMark || 0 15 | this.queue = [] 16 | this.current = 0 17 | this.highWaterMarkResolve = null 18 | this.waitAllResolve = null 19 | } 20 | 21 | async push(fn, ...params) { 22 | this.queue.push({ fn, params }) 23 | this.broker() 24 | 25 | if (this.highWaterMark && this.queue.length > this.highWaterMark) { 26 | return new Promise((resolve, reject) => { 27 | this.highWaterMarkResolve = resolve 28 | }) 29 | } else { 30 | return { message: 'ok' } 31 | } 32 | } 33 | 34 | broker() { 35 | if (this.current === this.limit) { 36 | return 37 | } 38 | 39 | const item = this.queue.shift() 40 | if (!item) { 41 | return 42 | } 43 | 44 | this.current++ 45 | this.run({ fn: item.fn, params: item.params }) 46 | .then((result) => { 47 | if (this.pipeFn) { 48 | this.pipeFn(result) 49 | } 50 | }, (error) => { 51 | if (this.catchFn) { 52 | this.catchFn(error) 53 | } else { 54 | console.error(error) 55 | } 56 | }) 57 | .catch(error => { 58 | console.error(error) 59 | }) 60 | .finally(() => { 61 | this.processed() 62 | this.broker() 63 | }) 64 | } 65 | 66 | run({ fn, params }) { 67 | return fn(...params) 68 | } 69 | 70 | processed() { 71 | // release channel 72 | this.current-- 73 | 74 | if (this.highWaterMarkResolve && this.queue.length <= this.limit) { 75 | this.highWaterMarkResolve({ message: 'ok' }) 76 | } 77 | 78 | if (this.waitAllResolve) { 79 | const hasPending = this.queue.length !== 0 80 | const hasRuning = this.current > 0 81 | if (!hasPending && !hasRuning) { 82 | this.waitAllResolve() 83 | } 84 | } 85 | } 86 | 87 | pipe(callback) { 88 | this.pipeFn = callback 89 | } 90 | 91 | catch(catchFn) { 92 | this.catchFn = catchFn 93 | } 94 | 95 | async waitAll() { 96 | return new Promise((resolve, reject) => { 97 | this.waitAllResolve = resolve 98 | }) 99 | } 100 | } 101 | 102 | export default PromiseQueue 103 | -------------------------------------------------------------------------------- /danmaku-scroll/src/service/util.ts: -------------------------------------------------------------------------------- 1 | export async function wait(ms = 1000) { 2 | await new Promise(resolve => setTimeout(resolve, ms)) 3 | } -------------------------------------------------------------------------------- /danmaku-scroll/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /danmaku-scroll/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "moduleResolution": "node", 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "useDefineForClassFields": true, 13 | "noImplicitAny": false, 14 | "sourceMap": true, 15 | "baseUrl": ".", 16 | "types": [ 17 | "webpack-env" 18 | ], 19 | "paths": { 20 | "@/*": [ 21 | "src/*" 22 | ] 23 | }, 24 | "lib": [ 25 | "esnext", 26 | "dom", 27 | "dom.iterable", 28 | "scripthost" 29 | ] 30 | }, 31 | "include": [ 32 | "src/**/*.ts", 33 | "src/**/*.tsx", 34 | "src/**/*.vue", 35 | "tests/**/*.ts", 36 | "tests/**/*.tsx" 37 | ], 38 | "exclude": [ 39 | "node_modules" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /danmaku-scroll/vue.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('@vue/cli-service') 2 | module.exports = defineConfig({ 3 | transpileDependencies: true, 4 | publicPath: './danmaku-scroll' 5 | }) 6 | -------------------------------------------------------------------------------- /danmaku/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /danmaku/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 其妙 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 | -------------------------------------------------------------------------------- /danmaku/README.md: -------------------------------------------------------------------------------- 1 | # bilibili-danmaku-page 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | -------------------------------------------------------------------------------- /danmaku/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /danmaku/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tokine/bilibili-danmaku-page", 3 | "version": "0.1.9", 4 | "author": "其妙 ", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "npm run serve", 8 | "serve": "vue-cli-service serve", 9 | "build": "vue-cli-service build", 10 | "build:monorepo": "npm run build && rm -rf ../danmaku-dist &&mv dist ../danmaku-dist", 11 | "lint": "vue-cli-service lint", 12 | "prepublish": "npm run build", 13 | "release": "npm run build && cp package.json dist && cd dist && npm publish", 14 | "link": "npm run build && cp package.json dist && cd dist && npm link" 15 | }, 16 | "dependencies": { 17 | "axios": "^1.7.2", 18 | "core-js": "^3.25.0", 19 | "lodash": "^4.17.21", 20 | "moment": "^2.29.4", 21 | "view-ui-plus": "^1.3.1", 22 | "vue": "^3.2.38" 23 | }, 24 | "devDependencies": { 25 | "@typescript-eslint/eslint-plugin": "^7.12.0", 26 | "@vue/cli-plugin-babel": "^5.0.8", 27 | "@vue/cli-plugin-eslint": "^5.0.8", 28 | "@vue/cli-service": "^5.0.8", 29 | "babel-eslint": "^10.1.0", 30 | "eslint": "^8.23.0", 31 | "eslint-plugin-vue": "^9.4.0", 32 | "vue-template-compiler": "^2.7.10" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /danmaku/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /danmaku/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagiring/bilibili-live-danmaku/4de4134eb479f2d727a14d672e485b651e2007e0/danmaku/public/favicon.ico -------------------------------------------------------------------------------- /danmaku/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | bilibili-danmaku-page 11 | 12 | 13 | 14 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /danmaku/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 23 | -------------------------------------------------------------------------------- /danmaku/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagiring/bilibili-live-danmaku/4de4134eb479f2d727a14d672e485b651e2007e0/danmaku/src/assets/logo.png -------------------------------------------------------------------------------- /danmaku/src/components/FanMedal.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 41 | 42 | 50 | -------------------------------------------------------------------------------- /danmaku/src/components/GiftCard.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 40 | 41 | 49 | -------------------------------------------------------------------------------- /danmaku/src/components/GiftCardMini.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 35 | 36 | 60 | -------------------------------------------------------------------------------- /danmaku/src/components/GiftTag.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 62 | -------------------------------------------------------------------------------- /danmaku/src/components/GiftTagExpand.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 38 | 39 | 80 | -------------------------------------------------------------------------------- /danmaku/src/components/SimilarCommentBadge.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 36 | 37 | 51 | -------------------------------------------------------------------------------- /danmaku/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import ViewUIPlus from 'view-ui-plus' 4 | import 'view-ui-plus/dist/styles/viewuiplus.css' 5 | 6 | // Vue.use(ViewUI); 7 | // Vue.config.productionTip = false 8 | 9 | createApp(App) 10 | .use(ViewUIPlus) 11 | .mount('#app') 12 | -------------------------------------------------------------------------------- /danmaku/src/service/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | let BASE_URL = '' 4 | export function init({ port }) { 5 | BASE_URL = `http://127.0.0.1:${port}` 6 | } 7 | 8 | export async function getSetting() { 9 | const res = await axios.get(`${BASE_URL}/api/setting`) 10 | return res.data 11 | } 12 | 13 | export async function getGiftConfig(roomId) { 14 | const res = await axios.get(`${BASE_URL}/api/room/${roomId}/gift/map`) 15 | return res.data 16 | } -------------------------------------------------------------------------------- /danmaku/src/service/const.js: -------------------------------------------------------------------------------- 1 | export const DEFAULT_AVATAR = 'https://static.hdslb.com/images/member/noface.gif' 2 | 3 | export const PRICE_PROPERTIES = { 4 | 1: { 5 | backgroundColor: "#EDF5FF", 6 | backgroundPriceColor: "#7497CD", 7 | backgroundBottomColor: "#2A60B2", 8 | time: 60000, 9 | }, 10 | 2: { 11 | backgroundColor: "#DBFFFD", 12 | backgroundPriceColor: "#7DA4BD", 13 | backgroundBottomColor: "#427D9E", 14 | time: 120000, 15 | }, 16 | 3: { 17 | backgroundColor: "#FFF1C5", 18 | backgroundPriceColor: "gold", 19 | backgroundBottomColor: "#E2B52B", 20 | time: 300000, 21 | }, 22 | 4: { 23 | backgroundColor: "rgb(255,234,210)", 24 | backgroundPriceColor: "rgb(255,234,210)", 25 | backgroundBottomColor: "rgb(244,148,67)", 26 | time: 1800000, 27 | }, 28 | 5: { 29 | backgroundColor: "rgb(255,231,228)", 30 | backgroundPriceColor: "rgb(255,231,228)", 31 | backgroundBottomColor: "rgb(229,77,77)", 32 | time: 3600000, 33 | }, 34 | 6: { 35 | backgroundColor: "rgb(255,216,216)", 36 | backgroundPriceColor: "rgb(255,216,216)", 37 | backgroundBottomColor: "rgb(171,26,50)", 38 | time: 7200000, 39 | }, 40 | }; 41 | 42 | export const GUARD_ICON_1 = "https://i0.hdslb.com/bfs/activity-plat/static/20200716/1d0c5a1b042efb59f46d4ba1286c6727/icon-guard1.png@44w_44h.webp" 43 | export const GUARD_ICON_2 = "https://i0.hdslb.com/bfs/activity-plat/static/20200716/1d0c5a1b042efb59f46d4ba1286c6727/icon-guard2.png@44w_44h.webp" 44 | export const GUARD_ICON_3 = "https://i0.hdslb.com/bfs/activity-plat/static/20200716/1d0c5a1b042efb59f46d4ba1286c6727/icon-guard3.png@44w_44h.webp" 45 | 46 | export const GUARD_ICON_MAP = { 47 | 1: GUARD_ICON_1, 48 | 2: GUARD_ICON_2, 49 | 3: GUARD_ICON_3, 50 | } 51 | 52 | export const GUARD_LEVEL_MAP = { 53 | 0: "normal", 54 | 1: "governor", 55 | 2: "admiral", 56 | 3: "captain", 57 | }; 58 | 59 | export const INTERACT_TYPE = { 60 | 1: '进入', 61 | 2: '关注', 62 | 3: '分享' 63 | } 64 | 65 | export const ENTER_ROOM_TYPE = { 66 | 1: '进入', 67 | 2: '光临' 68 | } 69 | 70 | export const COLORS = ['crimson', 'darkorange', 'moccasin', 'forestgreen', 'darkcyan', 'dodgerblue', 'violet'] 71 | export const MAX_MESSAGE = 150 72 | // export const GIFT_CONFIG_MAP = JSON.parse(fs.readFileSync(`gift_config`, 'utf8')) 73 | -------------------------------------------------------------------------------- /danmaku/src/service/promise-queue.js: -------------------------------------------------------------------------------- 1 | class PromiseQueue { 2 | // queue = [] 3 | // limit = 0 4 | // highWaterMark = 0 5 | // current = 0 6 | // highWaterMarkResolve = null 7 | // waitAllResolve = null 8 | 9 | constructor(options = {}) { 10 | const { limit, highWaterMark } = options 11 | this.limit = limit || 32 12 | this.highWaterMark = highWaterMark || 0 13 | this.queue = [] 14 | this.current = 0 15 | this.highWaterMarkResolve = null 16 | this.waitAllResolve = null 17 | } 18 | 19 | async push(fn, ...params) { 20 | this.queue.push({ fn, params }) 21 | this.broker() 22 | 23 | if (this.highWaterMark && this.queue.length > this.highWaterMark) { 24 | return new Promise((resolve, reject) => { 25 | this.highWaterMarkResolve = resolve 26 | }) 27 | } else { 28 | return { message: 'ok' } 29 | } 30 | } 31 | 32 | broker(channel) { 33 | if (this.current === this.limit) { 34 | return 35 | } 36 | 37 | const item = this.queue.shift() 38 | if (!item) { 39 | return 40 | } 41 | 42 | this.current++ 43 | this.run({ fn: item.fn, params: item.params }) 44 | .then((result) => { 45 | if (this.pipeFn) { 46 | this.pipeFn(result) 47 | } 48 | }, (error) => { 49 | if (this.catchFn) { 50 | this.catchFn(error) 51 | } else { 52 | console.error(error) 53 | } 54 | }) 55 | .catch(error => { 56 | console.error(error) 57 | }) 58 | .finally(() => { 59 | this.processed(channel) 60 | this.broker(channel) 61 | }) 62 | } 63 | 64 | run({ fn, params }) { 65 | return fn(...params) 66 | } 67 | 68 | processed() { 69 | // release channel 70 | this.current-- 71 | 72 | if (this.highWaterMarkResolve && this.queue.length <= this.limit) { 73 | this.highWaterMarkResolve({ message: 'ok' }) 74 | } 75 | 76 | if (this.waitAllResolve) { 77 | const hasPending = this.queue.length !== 0 78 | const hasRuning = this.current > 0 79 | if (!hasPending && !hasRuning) { 80 | this.waitAllResolve() 81 | } 82 | } 83 | } 84 | 85 | pipe(callback) { 86 | this.pipeFn = callback 87 | } 88 | 89 | catch(catchFn) { 90 | this.catchFn = catchFn 91 | } 92 | 93 | async waitAll() { 94 | return new Promise((resolve, reject) => { 95 | this.waitAllResolve = resolve 96 | }) 97 | } 98 | } 99 | 100 | export default PromiseQueue 101 | -------------------------------------------------------------------------------- /danmaku/src/service/util.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import { PRICE_PROPERTIES } from './const' 3 | 4 | export function dateFormat(date, formatter = "YYYY-MM-DD HH:mm:ss") { 5 | return moment(date).format(formatter); 6 | } 7 | 8 | export function getPriceProperties(price) { 9 | if (price < 50) { 10 | return PRICE_PROPERTIES["1"]; 11 | } 12 | if (price >= 50 && price < 100) { 13 | return PRICE_PROPERTIES["2"]; 14 | } 15 | if (price >= 100 && price < 500) { 16 | return PRICE_PROPERTIES["3"]; 17 | } 18 | if (price >= 500 && price < 1000) { 19 | return PRICE_PROPERTIES["4"]; 20 | } 21 | if (price >= 1000 && price < 2000) { 22 | return PRICE_PROPERTIES["5"]; 23 | } 24 | if (price >= 2000) { 25 | return PRICE_PROPERTIES["6"]; 26 | } 27 | } 28 | 29 | export async function wait(ms = 1000) { 30 | await new Promise(resolve => setTimeout(resolve, ms)) 31 | } -------------------------------------------------------------------------------- /danmaku/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lintOnSave: true 3 | } 4 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*" 4 | ] 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bilibili-danmaku", 3 | "version": "0.5.7", 4 | "author": "其妙 ", 5 | "description": "这是一个哔哩哔哩的直播弹幕应用", 6 | "license": "MIT", 7 | "main": "dist/electron/main.js", 8 | "scripts": { 9 | "build": "node .electron-vue/build.js && electron-builder", 10 | "build:all": "npm run build:danmaku && npm run build:danmaku-scroll && node .electron-vue/build.js && electron-builder", 11 | "build:dir": "node .electron-vue/build.js && electron-builder --dir", 12 | "build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js", 13 | "build:web": "cross-env BUILD_TARGET=web node .electron-vue/build.js", 14 | "dev": "node .electron-vue/dev-runner.js", 15 | "pack": "npm run pack:main && npm run pack:renderer", 16 | "pack:main": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.main.config.js", 17 | "pack:renderer": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.renderer.config.js", 18 | "postinstall": "", 19 | "ui": "vue ui", 20 | "release": "npm run build -- -p always", 21 | "cnpmi": "npm --registry https://registry.npm.taobao.org install", 22 | "build:danmaku": "cd danmaku && npm run build:monorepo", 23 | "build:danmaku-scroll": "cd danmaku-scroll && npm run build:monorepo" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/usagiring/bilibili-live-danmaku.git" 28 | }, 29 | "build": { 30 | "productName": "bilibili-danmaku", 31 | "appId": "com.electron.bilibili-danmaku", 32 | "directories": { 33 | "output": "build" 34 | }, 35 | "files": [ 36 | "dist/electron/**/*" 37 | ], 38 | "extraFiles": [ 39 | "config.yaml" 40 | ], 41 | "dmg": { 42 | "contents": [ 43 | { 44 | "x": 410, 45 | "y": 150, 46 | "type": "link", 47 | "path": "/Applications" 48 | }, 49 | { 50 | "x": 130, 51 | "y": 150, 52 | "type": "file" 53 | } 54 | ] 55 | }, 56 | "mac": { 57 | "icon": "build/icons/icon.icns" 58 | }, 59 | "win": { 60 | "icon": "build/icons/icon.ico", 61 | "target": [ 62 | { 63 | "target": "nsis", 64 | "arch": [ 65 | "x64" 66 | ] 67 | } 68 | ] 69 | }, 70 | "linux": { 71 | "icon": "build/icons" 72 | }, 73 | "nsis": { 74 | "oneClick": false, 75 | "allowElevation": true, 76 | "allowToChangeInstallationDirectory": true, 77 | "createDesktopShortcut": true, 78 | "createStartMenuShortcut": true 79 | }, 80 | "publish": [ 81 | { 82 | "provider": "github" 83 | } 84 | ] 85 | }, 86 | "overrides": { 87 | "webpack": "5.74.0" 88 | }, 89 | "dependencies": { 90 | "@electron/remote": "^2.0.8", 91 | "@tokine/bilibili-bridge": "0.2.24", 92 | "axios": "^1.7.2", 93 | "echarts": "^5.3.3", 94 | "echarts-wordcloud": "^2.0.0", 95 | "electron-store": "^8.1.0", 96 | "electron-updater": "^5.2.1", 97 | "flv.js": "^1.6.2", 98 | "follow-redirects": "^1.15.6", 99 | "font-list": "^1.4.5", 100 | "lodash": "^4.17.21", 101 | "moment": "^2.29.4", 102 | "qrcode": "^1.5.4", 103 | "view-ui-plus": "^1.3.1", 104 | "vue": "^3.2.38", 105 | "vue-router": "^4.1.5", 106 | "vue3-smooth-dnd": "^0.0.2", 107 | "vuex": "^4.0.2", 108 | "vuex-electron": "github:usagiring/vuex-electron", 109 | "yaml": "^2.4.4" 110 | }, 111 | "devDependencies": { 112 | "@babel/core": "^7.18.13", 113 | "@babel/plugin-proposal-export-default-from": "^7.18.10", 114 | "@babel/plugin-proposal-export-namespace-from": "^7.18.9", 115 | "@babel/plugin-proposal-function-bind": "^7.18.9", 116 | "@babel/plugin-proposal-json-strings": "^7.18.6", 117 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", 118 | "@babel/plugin-proposal-optional-chaining": "^7.18.9", 119 | "@babel/plugin-proposal-throw-expressions": "^7.18.6", 120 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 121 | "@babel/plugin-syntax-import-meta": "^7.10.4", 122 | "@babel/plugin-transform-runtime": "^7.18.10", 123 | "@babel/preset-env": "^7.18.10", 124 | "@babel/preset-typescript": "^7.24.7", 125 | "@babel/register": "^7.18.9", 126 | "@babel/runtime": "^7.24.7", 127 | "@types/node": "^20.4.5", 128 | "@typescript-eslint/eslint-plugin": "^7.12.0", 129 | "babel-loader": "^8.2.5", 130 | "cfonts": "^3.1.1", 131 | "copy-webpack-plugin": "^11.0.0", 132 | "cross-env": "^7.0.3", 133 | "css-loader": "^6.7.1", 134 | "del": "^3.0.0", 135 | "devtron": "^1.4.0", 136 | "electron": "^32.1.2", 137 | "electron-builder": "^25.0.5", 138 | "electron-debug": "^3.2.0", 139 | "electron-devtools-installer": "^3.2.0", 140 | "eslint": "^8.46.0", 141 | "eslint-plugin-vue": "^9.4.0", 142 | "file-loader": "^6.2.0", 143 | "html-webpack-plugin": "^5.5.0", 144 | "mini-css-extract-plugin": "2.6.1", 145 | "multispinner": "^0.2.1", 146 | "node-loader": "^2.0.0", 147 | "prettier": "^2.7.1", 148 | "style-loader": "^3.3.1", 149 | "terser-webpack-plugin": "^5.3.10", 150 | "typescript": "^4.9.5", 151 | "url-loader": "^4.1.1", 152 | "vue-html-loader": "^1.2.4", 153 | "vue-loader": "^17.0.0", 154 | "vue-style-loader": "^4.1.3", 155 | "vue-template-compiler": "^2.7.10", 156 | "vue-tsc": "^0.40.1", 157 | "webpack": "5.74.0", 158 | "webpack-dev-server": "^4.15.2", 159 | "webpack-hot-middleware": "^2.25.2", 160 | "worklet-loader": "^2.0.0" 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | bilibili-danmaku 9 | <% if (htmlWebpackPlugin.options.nodeModules) { %> 10 | 11 | 14 | <% } %> 15 | 16 | 17 | 18 |
19 | 20 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /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: false }) 12 | 13 | // Install `vue-devtools` 14 | require('electron').app.on('ready', () => { 15 | let installExtension = require('electron-devtools-installer') 16 | installExtension.default(installExtension.VUEJS_DEVTOOLS) 17 | .then(() => {}) 18 | .catch(err => { 19 | console.log('Unable to install `vue-devtools`: \n', err) 20 | }) 21 | }) 22 | 23 | // Require `main` process to boot app 24 | require('./index') -------------------------------------------------------------------------------- /src/main/index.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, ipcMain, nativeImage, session, Menu, Tray } from 'electron' 2 | import path from 'path' 3 | import { autoUpdater } from 'electron-updater' 4 | import { IPC_CHECK_FOR_UPDATE, IPC_DOWNLOAD_UPDATE, IPC_ENABLE_WEB_CONTENTS, IPC_UPDATE_AVAILABLE, IPC_DOWNLOAD_PROGRESS, IPC_LIVE_WINDOW_PLAY, IPC_LIVE_WINDOW_CLOSE, IPC_GET_VERSION, IPC_GET_EXE_PATH, IPC_GET_USER_PATH, IPC_LIVE_WINDOW_ON_TOP } from '../service/const' 5 | import '../renderer/store' 6 | import bilibiliBridge from '../service/bilibili-bridge' 7 | import { initialize, enable } from '@electron/remote/main' 8 | import { PORT, SAVE_ALL_BILI_MESSAGE, DANMAKU_RENDER_PATH } from '../service/config-loader' 9 | 10 | initialize() 11 | 12 | process.on('uncaughtException', (error) => { 13 | console.log('uncaughtException'); 14 | console.error(error); 15 | }); 16 | 17 | bilibiliBridge({ 18 | USER_DATA_PATH: app.getPath('userData'), 19 | PORT, 20 | SAVE_ALL_BILI_MESSAGE, 21 | HTML_PATH: process.env.NODE_ENV === 'development' ? path.join(__dirname, '../../danmaku-dist') : DANMAKU_RENDER_PATH || path.join(__dirname, 'danmaku'), 22 | }) 23 | 24 | /** 25 | * Set `__static` path to static files in production 26 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-static-assets.html 27 | */ 28 | if (process.env.NODE_ENV !== 'development') { 29 | global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\') 30 | } 31 | 32 | let mainWindow 33 | const winURL = process.env.NODE_ENV === 'development' 34 | ? `http://localhost:9080` 35 | : `file://${__dirname}/index.html` 36 | 37 | function createWindow() { 38 | /** 39 | * Initial window options 40 | */ 41 | mainWindow = new BrowserWindow({ 42 | height: 900, 43 | useContentSize: true, 44 | width: 1200, 45 | titleBarStyle: 'hidden', 46 | webPreferences: { 47 | nodeIntegration: true, 48 | contextIsolation: false, 49 | }, 50 | // icon: path.join(__dirname, '../../build/icons/icon.ico') 51 | }) 52 | mainWindow.setIcon(nativeImage.createFromPath(path.join(__dirname, '../../build/icons/icon.ico'))) 53 | mainWindow.setMenuBarVisibility(false) 54 | 55 | mainWindow.loadURL(winURL) 56 | 57 | mainWindow.on('closed', () => { 58 | mainWindow = null 59 | app.quit() 60 | }) 61 | 62 | enable(mainWindow.webContents) 63 | 64 | // mainWindow.on('close', (e) => { 65 | // app.quit() 66 | // }) 67 | 68 | } 69 | 70 | app.on('ready', () => { 71 | 72 | // 视频流需要加上referer 73 | // Modify the user agent for all requests to the following urls. 74 | const filter = { 75 | urls: ['https://*.bilivideo.com/*'] 76 | } 77 | 78 | session.defaultSession.webRequest.onBeforeSendHeaders(filter, (details, callback) => { 79 | details.requestHeaders['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36' 80 | details.requestHeaders['Referer'] = 'https://api.live.bilibili.com/' 81 | callback({ requestHeaders: details.requestHeaders }) 82 | }) 83 | 84 | createWindow() 85 | 86 | 87 | // const tray = new Tray(path.join(__dirname, '../renderer/assets/logo.png')) 88 | // const contextMenu = Menu.buildFromTemplate([ 89 | // { label: 'Item1', type: 'radio' }, 90 | // { label: 'Item2', type: 'radio' }, 91 | // { label: 'Item3', type: 'radio', checked: true }, 92 | // { label: 'Item4', type: 'radio' } 93 | // ]) 94 | // tray.setToolTip('bilibili-danmaku') 95 | // tray.setContextMenu(contextMenu) 96 | // tray.on('click', async () => { 97 | // mainWindow.show() 98 | // }) 99 | 100 | ipcMain.handle(IPC_GET_USER_PATH, async () => { 101 | return app.getPath('userData'); 102 | }) 103 | 104 | ipcMain.handle(IPC_GET_EXE_PATH, async () => { 105 | return app.getPath('exe'); 106 | }) 107 | 108 | ipcMain.handle(IPC_GET_VERSION, async () => { 109 | return app.getVersion() 110 | }) 111 | 112 | ipcMain.handle(IPC_ENABLE_WEB_CONTENTS, async (event, data) => { 113 | if (data.windowId) { 114 | enable(BrowserWindow.fromId(data.windowId).webContents) 115 | } 116 | }) 117 | 118 | ipcMain.on(IPC_LIVE_WINDOW_PLAY, (event, data) => { 119 | if (data.windowId) { 120 | BrowserWindow.fromId(data.windowId).webContents.send(IPC_LIVE_WINDOW_PLAY, data) 121 | } 122 | }) 123 | 124 | ipcMain.on(IPC_LIVE_WINDOW_ON_TOP, (event, data) => { 125 | if (data.windowId) { 126 | BrowserWindow.fromId(data.windowId).webContents.send(IPC_LIVE_WINDOW_ON_TOP, data) 127 | } 128 | }) 129 | 130 | ipcMain.on(IPC_LIVE_WINDOW_CLOSE, (event, data) => { 131 | if (data.windowId) { 132 | BrowserWindow.fromId(data.windowId).webContents.send(IPC_LIVE_WINDOW_CLOSE, data) 133 | } else { 134 | mainWindow.webContents.send(IPC_LIVE_WINDOW_CLOSE, data) 135 | } 136 | }) 137 | 138 | /** 139 | * Auto Updater 140 | * 141 | * Uncomment the following code below and install `electron-updater` to 142 | * support auto updating. Code Signing with a valid certificate is required. 143 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating 144 | */ 145 | if (process.env.NODE_ENV === 'production') { 146 | autoUpdater.autoDownload = false 147 | autoUpdater.autoInstallOnAppQuit = false 148 | // autoUpdater.checkForUpdatesAndNotify() 149 | 150 | ipcMain.on(IPC_CHECK_FOR_UPDATE, async (event) => { 151 | autoUpdater.checkForUpdates() 152 | // event.sender.send(IPC_UPDATE_AVAILABLE) 153 | }) 154 | 155 | ipcMain.on(IPC_DOWNLOAD_UPDATE, () => { 156 | autoUpdater.downloadUpdate() 157 | }) 158 | 159 | autoUpdater.on('update-available', () => { 160 | mainWindow.webContents.send(IPC_UPDATE_AVAILABLE) 161 | }) 162 | 163 | autoUpdater.on('download-progress', (progress, bytesPerSecond, percent, total, transferred) => { 164 | mainWindow.webContents.send(IPC_DOWNLOAD_PROGRESS, { 165 | progress, 166 | bytesPerSecond, 167 | percent, 168 | total 169 | }) 170 | 171 | // NOTE: 完成load之后再 172 | // mainWindow.webContents.on('did-finish-load', function () { 173 | // mainWindow.webContents.send('channelCanBeAnything', 'message'); 174 | // }); 175 | }) 176 | 177 | autoUpdater.on('update-downloaded', () => { 178 | autoUpdater.quitAndInstall() 179 | }) 180 | 181 | autoUpdater.on('error', (error) => { 182 | console.error(`AutoUpdate: ${error === null ? "unknown" : (error.stack || error).toString()}`) 183 | }) 184 | 185 | autoUpdater.on('update-not-available', () => { 186 | console.log('AutoUpdate: update-not-available') 187 | }) 188 | } 189 | }) 190 | 191 | 192 | app.on('window-all-closed', () => { 193 | if (process.platform !== 'darwin') { 194 | app.quit() 195 | } 196 | }) 197 | 198 | app.on('activate', () => { 199 | if (mainWindow === null) { 200 | createWindow() 201 | } 202 | }) 203 | -------------------------------------------------------------------------------- /src/renderer/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 39 | 40 | 83 | 84 | 96 | -------------------------------------------------------------------------------- /src/renderer/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagiring/bilibili-live-danmaku/4de4134eb479f2d727a14d672e485b651e2007e0/src/renderer/assets/.gitkeep -------------------------------------------------------------------------------- /src/renderer/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagiring/bilibili-live-danmaku/4de4134eb479f2d727a14d672e485b651e2007e0/src/renderer/assets/logo.png -------------------------------------------------------------------------------- /src/renderer/assets/tip-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagiring/bilibili-live-danmaku/4de4134eb479f2d727a14d672e485b651e2007e0/src/renderer/assets/tip-01.png -------------------------------------------------------------------------------- /src/renderer/components/ASRWindow.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 87 | 88 | 117 | -------------------------------------------------------------------------------- /src/renderer/components/Command.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 133 | 134 | 145 | -------------------------------------------------------------------------------- /src/renderer/components/Danmaku-Scroll.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 312 | 313 | 322 | -------------------------------------------------------------------------------- /src/renderer/components/FanMedal.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 41 | 42 | 50 | 51 | 62 | -------------------------------------------------------------------------------- /src/renderer/components/GiftCard.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 40 | 41 | 49 | -------------------------------------------------------------------------------- /src/renderer/components/GiftCardMini.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 43 | 44 | 68 | -------------------------------------------------------------------------------- /src/renderer/components/Help.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 37 | 38 | 48 | -------------------------------------------------------------------------------- /src/renderer/components/Introduction.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 34 | 35 | 47 | -------------------------------------------------------------------------------- /src/renderer/components/LiveWindow.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 231 | 232 | 261 | -------------------------------------------------------------------------------- /src/renderer/components/Lottery.vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 331 | 332 | 357 | -------------------------------------------------------------------------------- /src/renderer/components/NotFound.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /src/renderer/components/SpeechToDanmaku.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 285 | 286 | 291 | -------------------------------------------------------------------------------- /src/renderer/components/Statistic.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 377 | 378 | 421 | -------------------------------------------------------------------------------- /src/renderer/components/StyleEditor.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 63 | 64 | 70 | -------------------------------------------------------------------------------- /src/renderer/components/TagContent.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 68 | 69 | 88 | -------------------------------------------------------------------------------- /src/renderer/components/Vote.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 395 | 396 | 447 | -------------------------------------------------------------------------------- /src/renderer/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import ViewUIPlus from 'view-ui-plus' 6 | import 'view-ui-plus/dist/styles/viewuiplus.css' 7 | import global from '../service/global' 8 | 9 | const app = createApp(App) 10 | .use(router) 11 | .use(store) 12 | .use(ViewUIPlus) 13 | 14 | app.config.globalProperties.$global = global 15 | app.provide('globalValue', global) 16 | 17 | app.mount('#app') 18 | -------------------------------------------------------------------------------- /src/renderer/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | 3 | const routes = [ 4 | { 5 | path: '/', 6 | name: 'Home', 7 | component: () => import('../components/Home.vue'), 8 | children: [ 9 | { 10 | path: 'style', 11 | component: () => import('../components/StyleSetting.vue') 12 | }, 13 | { 14 | path: 'message', 15 | component: () => import('../components/Message.vue') 16 | }, 17 | { 18 | path: 'vote', 19 | component: () => import('../components/Vote.vue') 20 | }, 21 | { 22 | path: 'lottery', 23 | component: () => import('../components/Lottery.vue') 24 | }, 25 | { 26 | path: 'statistic', 27 | component: () => import('../components/Statistic.vue') 28 | }, 29 | { 30 | path: 'live', 31 | component: () => import('../components/Live.vue') 32 | }, 33 | { 34 | path: 'help', 35 | component: () => import('../components/Help.vue') 36 | }, 37 | { 38 | path: 'auto-reply', 39 | component: () => import('../components/AutoReply.vue') 40 | }, 41 | { 42 | path: 'command', 43 | component: () => import('../components/Command.vue') 44 | }, 45 | { 46 | path: 'config', 47 | component: () => import('../components/Config.vue') 48 | }, 49 | { 50 | path: 'danmaku-scroll', 51 | component: () => import('../components/Danmaku-Scroll.vue') 52 | }, 53 | { 54 | path: 'asr', 55 | component: () => import('../components/ASR.vue') 56 | }, 57 | { 58 | path: '', 59 | component: () => import('../components/Introduction.vue') 60 | } 61 | ] 62 | }, 63 | { 64 | path: '/asr-window', 65 | component: () => import('../components/ASRWindow.vue') 66 | }, 67 | { 68 | path: '/speech-to-danmaku', 69 | component: () => import('../components/SpeechToDanmaku.vue') 70 | }, 71 | { 72 | path: '/live-window', 73 | component: () => import('../components/LiveWindow.vue') 74 | }, 75 | { 76 | path: '/:pathMatch(.*)*', 77 | redirect: () => import('../components/NotFound.vue') 78 | } 79 | ] 80 | 81 | export default createRouter({ 82 | // 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。 83 | history: createWebHashHistory(), 84 | routes, // `routes: routes` 的缩写 85 | }) 86 | -------------------------------------------------------------------------------- /src/renderer/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | const { createPersistedState, createSharedMutations } = require('vuex-electron') 3 | import Config from './modules/Config' 4 | 5 | // const { context, modules } = loadModules() 6 | 7 | const store = createStore({ 8 | modules: { 9 | Config: Config 10 | }, 11 | plugins: [ 12 | createPersistedState({ 13 | whitelist: [ 14 | 'UPDATE_STYLE', 15 | 'UPDATE_CONFIG', 16 | ], 17 | }), 18 | createSharedMutations() // vuex-electron 引入了一个用于多进程间共享 Vuex Store 的状态的插件。如果没有多进程交互的需求,完全可以不引入这个插件。 19 | ], 20 | strict: process.env.NODE_ENV !== 'production' 21 | }) 22 | 23 | export default store 24 | 25 | // if (module.hot) { 26 | // // 在任何模块发生改变时进行热重载。 27 | // module.hot.accept(context.id, () => { 28 | // const { modules } = loadModules() 29 | 30 | // store.hotUpdate({ 31 | // modules 32 | // }) 33 | // }) 34 | // } 35 | -------------------------------------------------------------------------------- /src/renderer/store/modules/Config.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_CONFIG, DEFAULT_STYLE } from '../../../service/const' 2 | // const state = DEFAULT_CONFIG 3 | const state = () => ({ 4 | ...DEFAULT_CONFIG, 5 | ...DEFAULT_STYLE, 6 | }) 7 | 8 | const mutations = { 9 | UPDATE_STYLE(state, payload) { 10 | const objKey = `${payload.prop}_lv${payload.role}` 11 | state[objKey] = { ...state[objKey], ...payload.style } 12 | }, 13 | 14 | UPDATE_CONFIG(state, payload) { 15 | for (const key in payload) { 16 | state[key] = payload[key] 17 | } 18 | }, 19 | 20 | CLEAR_TEXT_STROKE_VERSION_0_4_8(state, payload) { 21 | const array = [ 22 | { 23 | prop: 'name', 24 | role: '0', 25 | }, 26 | { 27 | prop: 'comment', 28 | role: '0', 29 | }, 30 | { 31 | prop: 'name', 32 | role: '1', 33 | }, 34 | { 35 | prop: 'comment', 36 | role: '1', 37 | }, 38 | { 39 | prop: 'name', 40 | role: '2', 41 | }, 42 | { 43 | prop: 'comment', 44 | role: '2', 45 | }, 46 | { 47 | prop: 'name', 48 | role: '3', 49 | }, 50 | { 51 | prop: 'comment', 52 | role: '3', 53 | }, 54 | { 55 | prop: 'name', 56 | role: 'admin', 57 | }, 58 | { 59 | prop: 'comment', 60 | role: 'admin', 61 | } 62 | ] 63 | array.forEach(i => { 64 | const objKey = `${i.prop}_lv${i.role}` 65 | const newData = { ...state[objKey] } 66 | delete newData['-webkit-text-stroke-width'] 67 | delete newData['-webkit-text-stroke-color'] 68 | state[objKey] = newData 69 | }) 70 | } 71 | } 72 | 73 | const actions = { 74 | async UPDATE_STYLE({ commit }, payload) { 75 | commit('UPDATE_STYLE', payload) 76 | }, 77 | 78 | async UPDATE_CONFIG({ commit }, payload) { 79 | commit('UPDATE_CONFIG', payload) 80 | }, 81 | 82 | async CLEAR_TEXT_STROKE_VERSION_0_4_8({ commit }) { 83 | commit('CLEAR_TEXT_STROKE_VERSION_0_4_8') 84 | } 85 | } 86 | 87 | export default { 88 | state, 89 | mutations, 90 | actions 91 | } 92 | -------------------------------------------------------------------------------- /src/service/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { BASE_URL } from '../service/config-loader' 3 | 4 | export async function connect({ roomId, uid }) { 5 | const res = await axios.post(`${BASE_URL}/api/room/${roomId}/connect`, { 6 | roomId, 7 | uid, 8 | }) 9 | return res.data 10 | } 11 | 12 | export async function disconnect({ roomId }) { 13 | const res = await axios.post(`${BASE_URL}/api/room/${roomId}/disconnect`) 14 | return res.data 15 | } 16 | 17 | export async function touch() { 18 | const res = await axios.get(`${BASE_URL}/api/touch`) 19 | return res.data 20 | } 21 | 22 | export async function getRealTimeViewersCount({ roomId }) { 23 | const res = await axios.get(`${BASE_URL}/api/room/${roomId}/real-time/viewer/count`) 24 | return res.data 25 | } 26 | 27 | export async function getRoomStatus({ roomId }) { 28 | const res = await axios.get(`${BASE_URL}/api/room/${roomId}/status`) 29 | return res.data 30 | } 31 | 32 | export async function clearDB({ names }) { 33 | const res = await axios.post(`${BASE_URL}/api/db/clear`, { 34 | names 35 | }) 36 | return res.data 37 | } 38 | 39 | export async function backupDB({ names }) { 40 | const res = await axios.post(`${BASE_URL}/api/db/backup`, { 41 | names 42 | }) 43 | return res.data 44 | } 45 | 46 | export async function updateSetting(settings) { 47 | const res = await axios.put(`${BASE_URL}/api/setting`, { 48 | upsert: settings 49 | }) 50 | return res.data 51 | } 52 | 53 | export async function clearMessage() { 54 | const res = await axios.post(`${BASE_URL}/api/message/clear`) 55 | return res.data 56 | } 57 | 58 | export async function queryGifts(body) { 59 | const res = await axios.post(`${BASE_URL}/api/gift/query`, body) 60 | return res.data 61 | } 62 | 63 | export async function queryInteracts(body) { 64 | const res = await axios.post(`${BASE_URL}/api/interact/query`, body) 65 | return res.data 66 | } 67 | 68 | export async function queryComments(body) { 69 | const res = await axios.post(`${BASE_URL}/api/comment/query`, body) 70 | return res.data 71 | } 72 | 73 | export async function queryLotteryHistories(body) { 74 | const res = await axios.post(`${BASE_URL}/api/lottery/history/query`, body) 75 | return res.data 76 | } 77 | 78 | export async function deleteLotteryHistories(body) { 79 | const res = await axios.delete(`${BASE_URL}/api/lottery/history`, body) 80 | return res.data 81 | } 82 | 83 | export async function addLotteryHistory(body) { 84 | const res = await axios.post(`${BASE_URL}/api/lottery/history`, body) 85 | return res.data 86 | } 87 | 88 | export async function countComments(body) { 89 | const res = await axios.post(`${BASE_URL}/api/comment/count`, body) 90 | return res.data 91 | } 92 | 93 | export async function countInteracts(body) { 94 | const res = await axios.post(`${BASE_URL}/api/interact/count`, body) 95 | return res.data 96 | } 97 | 98 | export async function countGifts(body) { 99 | const res = await axios.post(`${BASE_URL}/api/gift/count`, body) 100 | return res.data 101 | } 102 | 103 | export async function sendMessages(body) { 104 | const res = await axios.post(`${BASE_URL}/api/message/send`, body) 105 | return res.data 106 | } 107 | 108 | // export async function sendExampleMessages(body) { 109 | // const res = await axios.post(`${BASE_URL}/api/messages/examples/send`, body) 110 | // return res.data 111 | // } 112 | 113 | export async function getGiftConfig(roomId: string) { 114 | const res = await axios.get(`${BASE_URL}/api/room/${roomId}/gift/map`) 115 | return res.data 116 | } 117 | 118 | // export async function getVoices() { 119 | // const res = await axios.get(`${BASE_URL}/api/voices`) 120 | // return res.data 121 | // } 122 | 123 | // export async function speak(body) { 124 | // const res = await axios.post(`${BASE_URL}/api/speak`, body) 125 | // return res.data 126 | // } 127 | 128 | export async function statistic(body) { 129 | const res = await axios.post(`${BASE_URL}/api/statistic`, body) 130 | return res.data 131 | } 132 | 133 | export async function commentWordExtract(body) { 134 | const res = await axios.post(`${BASE_URL}/api/statistic/comment/keyword-extract`, body) 135 | return res.data 136 | } 137 | 138 | export async function exportFile(body) { 139 | const res = await axios.post(`${BASE_URL}/api/statistic/gift/export`, body, { 140 | // responseType: 'stream', // ????? 141 | // decompress: false, 142 | transitional: { 143 | forcedJSONParsing: false, 144 | } 145 | }) 146 | return res.data 147 | } 148 | 149 | export async function initialASR(body) { 150 | const res = await axios.post(`${BASE_URL}/api/asr/initial`, body) 151 | return res.data 152 | } 153 | 154 | export async function startLiveStreamASR(body) { 155 | const res = await axios.post(`${BASE_URL}/api/asr/live/start`, body) 156 | return res.data 157 | } 158 | 159 | export async function closeLiveStreamASR(body) { 160 | const res = await axios.post(`${BASE_URL}/api/asr/live/close`, body) 161 | return res.data 162 | } 163 | 164 | export async function closeASR(body) { 165 | const res = await axios.post(`${BASE_URL}/api/asr/close`, body) 166 | return res.data 167 | } 168 | 169 | export async function getASRStatus() { 170 | const res = await axios.get(`${BASE_URL}/api/asr/status`) 171 | return res.data 172 | } 173 | 174 | export async function translateSentence(body) { 175 | const res = await axios.post(`${BASE_URL}/api/translate/sentence`, body) 176 | return res.data 177 | } 178 | 179 | export async function translateOpen(body) { 180 | const res = await axios.post(`${BASE_URL}/api/translate/open`, body) 181 | return res.data 182 | } 183 | 184 | export async function translateClose(body) { 185 | const res = await axios.post(`${BASE_URL}/api/translate/close`, body) 186 | return res.data 187 | } 188 | 189 | export async function getTranslateStatus() { 190 | const res = await axios.get(`${BASE_URL}/api/translate/status`) 191 | return res.data 192 | } 193 | 194 | export async function needRefreshCookie() { 195 | const res = await axios.get(`${BASE_URL}/api/cookie/refresh/check`) 196 | return res.data 197 | } 198 | 199 | export async function refreshCookie(body: { refreshToken: string }) { 200 | const res = await axios.post(`${BASE_URL}/api/cookie/refresh`, body) 201 | return res.data 202 | } 203 | 204 | export async function initialSpeechRegcognition(body) { 205 | const res = await axios.post(`${BASE_URL}/api/speech-recognition/initial`, body) 206 | return res.data 207 | } 208 | 209 | export async function speechToText(body) { 210 | const res = await axios.post(`${BASE_URL}/api/speech-recognition/speech-to-text`, body) 211 | return res.data 212 | } 213 | 214 | export async function sendComment({ message, roomId }) { 215 | const res = await axios.post(`${BASE_URL}/api/bilibili/room/${roomId}/comment/send`, { 216 | comment: message 217 | }) 218 | return res.data 219 | } 220 | 221 | export async function getMedalList({ page, pageSize }) { 222 | const res = await axios.get(`${BASE_URL}/api/bilibili/medal/list?page=${page}&pageSize=${pageSize}`) 223 | return res.data 224 | } 225 | 226 | export async function getRoomInfoV2(roomId) { 227 | const res = await axios.get(`${BASE_URL}/api/bilibili/room/${roomId}/info`) 228 | return res.data 229 | } 230 | 231 | export async function getRoomInfoByIds(roomIds) { 232 | const res = await axios.post(`${BASE_URL}/api/bilibili/room/info`, { 233 | roomIds: roomIds 234 | }) 235 | return res.data 236 | } 237 | 238 | export async function getGuardInfo(roomId, uid) { 239 | const res = await axios.get(`${BASE_URL}/api/bilibili/room/${roomId}/guard?uid=${uid}`) 240 | return res.data 241 | } 242 | 243 | export async function getUserInfoV2(uid) { 244 | const res = await axios.get(`${BASE_URL}/api/bilibili/user/${uid}/info`) 245 | return res.data 246 | } 247 | 248 | export async function getUserInfoInRoom(roomId) { 249 | const res = await axios.get(`${BASE_URL}/api/bilibili/room/${roomId}/user/info`) 250 | return res.data 251 | } 252 | 253 | export async function wearMedal(medalId) { 254 | const res = await axios.post(`${BASE_URL}/api/bilibili/medal/wear`, { 255 | medalId 256 | }) 257 | return res.data 258 | } 259 | 260 | export async function getRandomPlayUrl({ 261 | roomId, 262 | qn, 263 | withCookie = false, 264 | }) { 265 | const res = await axios.get(`${BASE_URL}/api/bilibili/room/${roomId}/playurl?qn=${qn}&withCookie=${withCookie}`) 266 | return res.data 267 | } 268 | 269 | export async function record({ 270 | roomId, 271 | output, 272 | qn, 273 | platform, 274 | withCookie 275 | }) { 276 | const res = await axios.post(`${BASE_URL}/api/room/${roomId}/record/start`, { 277 | output, 278 | qn, 279 | platform, 280 | withCookie 281 | }) 282 | return res.data 283 | } 284 | 285 | export async function cancelRecord({ 286 | roomId, 287 | recordId, 288 | }) { 289 | const res = await axios.post(`${BASE_URL}/api/room/${roomId}/record/cancel`, { 290 | recordId 291 | }) 292 | return res.data 293 | } 294 | 295 | export async function getRecordState({ 296 | roomId 297 | }) { 298 | const res = await axios.get(`${BASE_URL}/api/room/${roomId}/record/status`) 299 | return res.data 300 | } 301 | 302 | export async function getQrCode() { 303 | const res = await axios.get(`${BASE_URL}/api/login/qr-code/generate`) 304 | return res.data 305 | } 306 | 307 | export async function loginFromQrCode(qrCodeKey) { 308 | const res = await axios.get(`${BASE_URL}/api/login/qr-code/poll?qrCodeKey=${qrCodeKey}`) 309 | return res.data 310 | } 311 | 312 | export async function addLike({ 313 | roomId, 314 | ruid, 315 | count 316 | }) { 317 | const res = await axios.post(`${BASE_URL}/api/bilibili/room/${roomId}/like`, { 318 | ruid, 319 | count, 320 | }) 321 | return res.data 322 | } -------------------------------------------------------------------------------- /src/service/bilibili-bridge.ts: -------------------------------------------------------------------------------- 1 | import bridge from '@tokine/bilibili-bridge' 2 | import store from '../renderer/store' 3 | import { DEFAULT_STYLE } from './const' 4 | 5 | const defaultOptions = { 6 | ...DEFAULT_STYLE, 7 | // @ts-ignore 8 | ...store.state.Config, 9 | } 10 | 11 | export default function init(options) { 12 | bridge(Object.assign({}, defaultOptions, options)) 13 | } -------------------------------------------------------------------------------- /src/service/config-loader.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import YAML from 'yaml' 3 | 4 | const config = fs.readFileSync(`config.yaml`, 'utf8') 5 | const OPTION_CONFIG = YAML.parse(config) 6 | 7 | export const PORT = OPTION_CONFIG.PORT || 8081 8 | export const BASE_URL = `http://127.0.0.1:${PORT}` 9 | export const BASE_WS_URL = `ws://127.0.0.1:${PORT}` 10 | export const SAVE_ALL_BILI_MESSAGE = OPTION_CONFIG.SAVE_ALL_BILI_MESSAGE || false 11 | export const DANMAKU_RENDER_PATH = OPTION_CONFIG.DANMAKU_RENDER_PATH -------------------------------------------------------------------------------- /src/service/event.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | class Emitter extends EventEmitter { } 3 | export default new Emitter() -------------------------------------------------------------------------------- /src/service/global.js: -------------------------------------------------------------------------------- 1 | // 全局变量 2 | const globalVar = { 3 | name: 'bilibili-live-danmaku', 4 | voices: [] 5 | }; 6 | 7 | export default globalVar -------------------------------------------------------------------------------- /src/service/processor.worklet.js: -------------------------------------------------------------------------------- 1 | class WorkletProcessor extends AudioWorkletProcessor { 2 | constructor() { 3 | super() 4 | console.log(`sampleRate: ${sampleRate}`) 5 | } 6 | 7 | process(inputs, outputs, parameters) { 8 | const input = inputs[0][0] // 单声道采样数据, default 128 frame/times 9 | const pcmData = new Int16Array(input.length) 10 | 11 | // 32bit to 16bit 12 | for (let i = 0; i < input.length; i++) { 13 | let s = Math.max(-1, Math.min(1, input[i])) 14 | s = s < 0 ? s * 0x8000 : s * 0x7FFF 15 | pcmData[i] = s 16 | } 17 | 18 | // TODO 19 | const pcmStr = JSON.stringify(Array.from(pcmData)) 20 | this.port.postMessage(pcmStr) 21 | 22 | return true 23 | } 24 | } 25 | 26 | registerProcessor("worklet-processor", WorkletProcessor); -------------------------------------------------------------------------------- /src/service/promise-queue.js: -------------------------------------------------------------------------------- 1 | class PromiseQueue { 2 | queue = [] 3 | limit = 0 4 | highWaterMark = 0 5 | current = 0 6 | highWaterMarkResolve = null 7 | waitAllResolve = null 8 | 9 | constructor (options = {}) { 10 | const { limit, highWaterMark } = options 11 | this.limit = limit || 32 12 | this.highWaterMark = highWaterMark || 0 13 | } 14 | 15 | async push (fn, ...params) { 16 | this.queue.push({ fn, params }) 17 | this.broker() 18 | 19 | if (this.highWaterMark && this.queue.length > this.highWaterMark) { 20 | return new Promise((resolve, reject) => { 21 | this.highWaterMarkResolve = resolve 22 | }) 23 | } else { 24 | return { message: 'ok' } 25 | } 26 | } 27 | 28 | broker (channel) { 29 | if (this.current === this.limit) { 30 | return 31 | } 32 | 33 | const item = this.queue.shift() 34 | if (!item) { 35 | return 36 | } 37 | 38 | this.current++ 39 | this.run({ fn: item.fn, params: item.params }) 40 | .then((result) => { 41 | if (this.pipeFn) { 42 | this.pipeFn(result) 43 | } 44 | }, (error) => { 45 | if (this.catchFn) { 46 | this.catchFn(error) 47 | } else { 48 | console.error(error) 49 | } 50 | }) 51 | .catch(error => { 52 | console.error(error) 53 | }) 54 | .finally(() => { 55 | this.processed(channel) 56 | this.broker(channel) 57 | }) 58 | } 59 | 60 | run ({ fn, params }) { 61 | return fn(...params) 62 | } 63 | 64 | processed () { 65 | // release channel 66 | this.current-- 67 | 68 | if (this.highWaterMarkResolve && this.queue.length <= this.limit) { 69 | this.highWaterMarkResolve({ message: 'ok' }) 70 | } 71 | 72 | if (this.waitAllResolve) { 73 | const hasPending = this.queue.length !== 0 74 | const hasRuning = this.current > 0 75 | if (!hasPending && !hasRuning) { 76 | this.waitAllResolve() 77 | } 78 | } 79 | } 80 | 81 | pipe (callback) { 82 | this.pipeFn = callback 83 | } 84 | 85 | catch (catchFn) { 86 | this.catchFn = catchFn 87 | } 88 | 89 | async waitAll () { 90 | return new Promise((resolve, reject) => { 91 | this.waitAllResolve = resolve 92 | }) 93 | } 94 | } 95 | 96 | export default PromiseQueue 97 | -------------------------------------------------------------------------------- /src/service/util.js: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import { PRICE_PROPERTIES, GUARD_ICON_MAP, INTERACT_TYPE } from './const' 3 | 4 | // TODO 设置一些更小的粒度? < 1 5 | export function getPriceProperties(price) { 6 | if (price < 50) { 7 | return PRICE_PROPERTIES["1"]; 8 | } 9 | if (price >= 50 && price < 100) { 10 | return PRICE_PROPERTIES["2"]; 11 | } 12 | if (price >= 100 && price < 500) { 13 | return PRICE_PROPERTIES["3"]; 14 | } 15 | if (price >= 500 && price < 1000) { 16 | return PRICE_PROPERTIES["4"]; 17 | } 18 | if (price >= 1000 && price < 2000) { 19 | return PRICE_PROPERTIES["5"]; 20 | } 21 | if (price >= 2000) { 22 | return PRICE_PROPERTIES["6"]; 23 | } 24 | } 25 | 26 | export function getGuardIcon(level) { 27 | return GUARD_ICON_MAP[level] 28 | } 29 | 30 | export function getInteractType(type) { 31 | return INTERACT_TYPE[type] 32 | } 33 | 34 | const units = ['B/s', 'KB/s', 'MB/s'] 35 | export function parseDownloadRate(value, unitIndex = 0) { 36 | if (value < 1024) return `${value} ${units[unitIndex]}` 37 | value = (value / 1024).toFixed(1) 38 | return parseDownloadRate(value, ++unitIndex) 39 | } 40 | 41 | export function parseHexColor(colorNumber) { 42 | return `#${colorNumber.toString(16).padStart(6, '0')}` 43 | } 44 | 45 | export function dateFormat(date, formatter = "YYYY-MM-DD HH:mm:ss") { 46 | return moment(date).format(formatter); 47 | } 48 | 49 | export async function wait(ms = 1000) { 50 | await new Promise(resolve => setTimeout(resolve, ms)) 51 | } 52 | 53 | // export function setGiftConfigMap(gifts) { 54 | // const giftConfigMap = gifts.reduce((map, gift) => { 55 | // return Object.assign(map, { 56 | // [gift.id]: { 57 | // webp: gift.webp, 58 | // name: gift.name, 59 | // price: gift.price 60 | // } 61 | // }) 62 | // }, {}) 63 | // fs.writeFileSync('gift_config', JSON.stringify(giftConfigMap)) 64 | // } 65 | 66 | // [{ ... , probability: number }] 67 | export function getRandomItem(items) { 68 | const total = items.reduce((total, item) => { 69 | return total + item.probability 70 | }, 0) 71 | const factor = Math.random() * total 72 | let threshold = 0 73 | 74 | for (let i = 0; i < items.length; i++) { 75 | threshold += items[i].probability 76 | if (threshold > factor) { 77 | return items[i] 78 | } 79 | } 80 | 81 | return null 82 | } 83 | -------------------------------------------------------------------------------- /src/service/vad.js: -------------------------------------------------------------------------------- 1 | const VAD = function (options) { 2 | // Default options 3 | this.options = { 4 | fftSize: 512, 5 | bufferLen: 512, 6 | voice_stop: function () { }, 7 | voice_start: function () { }, 8 | smoothingTimeConstant: 0.99, 9 | energy_offset: 1e-8, // The initial offset. 10 | energy_threshold_ratio_pos: 2, // Signal must be twice the offset 11 | energy_threshold_ratio_neg: 0.5, // Signal must be half the offset 12 | energy_integration: 1, // Size of integration change compared to the signal per second. 13 | filter: [ 14 | { f: 200, v: 0 }, // 0 -> 200 is 0 15 | { f: 2000, v: 1 } // 200 -> 2k is 1 16 | ], 17 | source: null, 18 | context: null 19 | }; 20 | 21 | // User options 22 | for (var option in options) { 23 | if (options.hasOwnProperty(option)) { 24 | this.options[option] = options[option]; 25 | } 26 | } 27 | 28 | // Require source 29 | if (!this.options.source) 30 | throw new Error("The options must specify a MediaStreamAudioSourceNode."); 31 | 32 | // Set this.options.context 33 | this.options.context = this.options.source.context; 34 | 35 | // Calculate time relationships 36 | this.hertzPerBin = this.options.context.sampleRate / this.options.fftSize; 37 | this.iterationFrequency = this.options.context.sampleRate / this.options.bufferLen; 38 | this.iterationPeriod = 1 / this.iterationFrequency; 39 | 40 | var DEBUG = true; 41 | if (DEBUG) console.log( 42 | 'Vad' + 43 | ' | sampleRate: ' + this.options.context.sampleRate + 44 | ' | hertzPerBin: ' + this.hertzPerBin + 45 | ' | iterationFrequency: ' + this.iterationFrequency + 46 | ' | iterationPeriod: ' + this.iterationPeriod 47 | ); 48 | 49 | this.setFilter = function (shape) { 50 | this.filter = []; 51 | for (var i = 0, iLen = this.options.fftSize / 2; i < iLen; i++) { 52 | this.filter[i] = 0; 53 | for (var j = 0, jLen = shape.length; j < jLen; j++) { 54 | if (i * this.hertzPerBin < shape[j].f) { 55 | this.filter[i] = shape[j].v; 56 | break; // Exit j loop 57 | } 58 | } 59 | } 60 | } 61 | 62 | this.setFilter(this.options.filter); 63 | 64 | this.ready = {}; 65 | this.vadState = false; // True when Voice Activity Detected 66 | 67 | // Energy detector props 68 | this.energy_offset = this.options.energy_offset; 69 | this.energy_threshold_pos = this.energy_offset * this.options.energy_threshold_ratio_pos; 70 | this.energy_threshold_neg = this.energy_offset * this.options.energy_threshold_ratio_neg; 71 | 72 | this.voiceTrend = 0; 73 | this.voiceTrendMax = 10; 74 | this.voiceTrendMin = -10; 75 | this.voiceTrendStart = 0; 76 | this.voiceTrendEnd = -0; 77 | 78 | // Create analyser 79 | this.analyser = this.options.context.createAnalyser(); 80 | this.analyser.smoothingTimeConstant = this.options.smoothingTimeConstant; // 0.99; 81 | this.analyser.fftSize = this.options.fftSize; 82 | 83 | this.floatFrequencyData = new Float32Array(this.analyser.frequencyBinCount); 84 | 85 | // Setup local storage of the Linear FFT data 86 | this.floatFrequencyDataLinear = new Float32Array(this.floatFrequencyData.length); 87 | 88 | // Connect this.analyser 89 | this.options.source.connect(this.analyser); 90 | 91 | // Create ScriptProcessorNode 92 | this.scriptProcessorNode = this.options.context.createScriptProcessor(this.options.bufferLen, 1, 1); 93 | 94 | // Connect scriptProcessorNode (Theretically, not required) 95 | this.scriptProcessorNode.connect(this.options.context.destination); 96 | 97 | // Create callback to update/analyze floatFrequencyData 98 | var self = this; 99 | this.scriptProcessorNode.onaudioprocess = function (event) { 100 | self.analyser.getFloatFrequencyData(self.floatFrequencyData); 101 | self.update(); 102 | self.monitor(); 103 | }; 104 | 105 | // Connect scriptProcessorNode 106 | this.options.source.connect(this.scriptProcessorNode); 107 | 108 | // log stuff 109 | this.logging = false; 110 | this.log_i = 0; 111 | this.log_limit = 100; 112 | 113 | this.triggerLog = function (limit) { 114 | this.logging = true; 115 | this.log_i = 0; 116 | this.log_limit = typeof limit === 'number' ? limit : this.log_limit; 117 | } 118 | 119 | this.log = function (msg) { 120 | if (this.logging && this.log_i < this.log_limit) { 121 | this.log_i++; 122 | console.log(msg); 123 | } else { 124 | this.logging = false; 125 | } 126 | } 127 | 128 | this.update = function () { 129 | // Update the local version of the Linear FFT 130 | var fft = this.floatFrequencyData; 131 | for (var i = 0, iLen = fft.length; i < iLen; i++) { 132 | this.floatFrequencyDataLinear[i] = Math.pow(10, fft[i] / 10); 133 | } 134 | this.ready = {}; 135 | } 136 | 137 | this.getEnergy = function () { 138 | if (this.ready.energy) { 139 | return this.energy; 140 | } 141 | 142 | var energy = 0; 143 | var fft = this.floatFrequencyDataLinear; 144 | 145 | for (var i = 0, iLen = fft.length; i < iLen; i++) { 146 | energy += this.filter[i] * fft[i] * fft[i]; 147 | } 148 | 149 | this.energy = energy; 150 | this.ready.energy = true; 151 | 152 | return energy; 153 | } 154 | 155 | this.monitor = function () { 156 | var energy = this.getEnergy(); 157 | var signal = energy - this.energy_offset; 158 | 159 | if (signal > this.energy_threshold_pos) { 160 | this.voiceTrend = (this.voiceTrend + 1 > this.voiceTrendMax) ? this.voiceTrendMax : this.voiceTrend + 1; 161 | } else if (signal < -this.energy_threshold_neg) { 162 | this.voiceTrend = (this.voiceTrend - 1 < this.voiceTrendMin) ? this.voiceTrendMin : this.voiceTrend - 1; 163 | } else { 164 | // voiceTrend gets smaller 165 | if (this.voiceTrend > 0) { 166 | this.voiceTrend--; 167 | } else if (this.voiceTrend < 0) { 168 | this.voiceTrend++; 169 | } 170 | } 171 | 172 | var start = false, end = false; 173 | if (this.voiceTrend > this.voiceTrendStart) { 174 | // Start of speech detected 175 | start = true; 176 | } else if (this.voiceTrend < this.voiceTrendEnd) { 177 | // End of speech detected 178 | end = true; 179 | } 180 | 181 | // Integration brings in the real-time aspect through the relationship with the frequency this functions is called. 182 | var integration = signal * this.iterationPeriod * this.options.energy_integration; 183 | 184 | // Idea?: The integration is affected by the voiceTrend magnitude? - Not sure. Not doing atm. 185 | 186 | // The !end limits the offset delta boost till after the end is detected. 187 | if (integration > 0 || !end) { 188 | this.energy_offset += integration; 189 | } else { 190 | this.energy_offset += integration * 10; 191 | } 192 | this.energy_offset = this.energy_offset < 0 ? 0 : this.energy_offset; 193 | this.energy_threshold_pos = this.energy_offset * this.options.energy_threshold_ratio_pos; 194 | this.energy_threshold_neg = this.energy_offset * this.options.energy_threshold_ratio_neg; 195 | 196 | // Broadcast the messages 197 | if (start && !this.vadState) { 198 | this.vadState = true; 199 | this.options.voice_start(); 200 | } 201 | if (end && this.vadState) { 202 | this.vadState = false; 203 | this.options.voice_stop(); 204 | } 205 | 206 | this.log( 207 | 'e: ' + energy + 208 | ' | e_of: ' + this.energy_offset + 209 | ' | e+_th: ' + this.energy_threshold_pos + 210 | ' | e-_th: ' + this.energy_threshold_neg + 211 | ' | signal: ' + signal + 212 | ' | int: ' + integration + 213 | ' | voiceTrend: ' + this.voiceTrend + 214 | ' | start: ' + start + 215 | ' | end: ' + end 216 | ); 217 | 218 | return signal; 219 | } 220 | }; 221 | 222 | export default VAD -------------------------------------------------------------------------------- /src/service/ws.js: -------------------------------------------------------------------------------- 1 | import { BASE_WS_URL } from '../service/config-loader' 2 | 3 | const ws = new WebSocket(BASE_WS_URL) 4 | ws.onopen = () => { } 5 | 6 | ws.onclose = (code) => { 7 | console.log('ws close: ', code) 8 | } 9 | 10 | ws.onerror = (err) => { 11 | console.error(err) 12 | } 13 | 14 | export default ws 15 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagiring/bilibili-live-danmaku/4de4134eb479f2d727a14d672e485b651e2007e0/static/.gitkeep -------------------------------------------------------------------------------- /static/er.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagiring/bilibili-live-danmaku/4de4134eb479f2d727a14d672e485b651e2007e0/static/er.png -------------------------------------------------------------------------------- /static/intro1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagiring/bilibili-live-danmaku/4de4134eb479f2d727a14d672e485b651e2007e0/static/intro1.png -------------------------------------------------------------------------------- /static/intro2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagiring/bilibili-live-danmaku/4de4134eb479f2d727a14d672e485b651e2007e0/static/intro2.png -------------------------------------------------------------------------------- /static/intro3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagiring/bilibili-live-danmaku/4de4134eb479f2d727a14d672e485b651e2007e0/static/intro3.png -------------------------------------------------------------------------------- /static/intro4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagiring/bilibili-live-danmaku/4de4134eb479f2d727a14d672e485b651e2007e0/static/intro4.png -------------------------------------------------------------------------------- /static/intro5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usagiring/bilibili-live-danmaku/4de4134eb479f2d727a14d672e485b651e2007e0/static/intro5.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*" 4 | ], 5 | "compilerOptions": { 6 | // "module": "es2015", 7 | // "moduleResolution": "node", 8 | // "target": "es5", 9 | // "sourceMap": true, 10 | // "allowJs": true, 11 | "outDir": "./dist/", 12 | "noImplicitAny": false, 13 | "module": "nodenext", 14 | // "target": "es5", 15 | "jsx": "react", 16 | "allowJs": true, 17 | // "moduleResolution": "node", 18 | "allowSyntheticDefaultImports": true, 19 | "esModuleInterop": true 20 | } 21 | } --------------------------------------------------------------------------------