├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── FUNDING.yml ├── LICENSE ├── README.md ├── electron-builder.yml ├── package.json ├── public ├── favicon.ico └── index.html ├── readme_assets ├── app-overview_010.png ├── app-overview_041.png ├── html-view-circular.png ├── html-view-linear.png └── html-view.png ├── src ├── App.vue ├── assets │ ├── logo.png │ └── logo.svg ├── background.ts ├── components │ └── HelloWorld.vue ├── main.ts ├── plugins │ └── vuetify.ts ├── shims-tsx.d.ts ├── shims-vue.d.ts ├── store │ ├── actions.ts │ ├── index.ts │ ├── mutations.ts │ └── state.ts ├── types │ ├── title-mode.ts │ └── vmix-input.ts ├── utility │ ├── paths.ts │ └── time.ts ├── view-templates │ ├── overview.ts │ └── progress.ts ├── views │ ├── DelayCompensationSlider.vue │ ├── HtmlViews.vue │ ├── Layout │ │ └── AppBar.vue │ └── TitleModeSettings │ │ ├── CurrentPositionField.vue │ │ ├── CurrentPositionFields.vue │ │ ├── Index.vue │ │ ├── RemainingField.vue │ │ ├── RemainingFields.vue │ │ ├── WidthField.vue │ │ └── WidthFields.vue ├── web-frontend │ ├── README.md │ ├── package.json │ ├── src │ │ ├── Circular.vue │ │ ├── Linear.vue │ │ ├── Main.vue │ │ ├── app.js │ │ ├── mixin.js │ │ └── styles │ │ │ ├── app.sass │ │ │ ├── circular.sass │ │ │ ├── dark.sass │ │ │ └── linear.sass │ ├── webpack.mix.js │ └── yarn.lock └── webserver.ts ├── tsconfig.json ├── vue.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | 4 | env: { 5 | node: true 6 | }, 7 | 8 | extends: ['plugin:vue/essential', '@vue/prettier', '@vue/typescript'], 9 | 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'off' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | semicolon: 'off', 14 | semi: [2, 'never'] 15 | }, 16 | 17 | parserOptions: { 18 | parser: '@typescript-eslint/parser' 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | 23 | # Electron-builder output 24 | /dist_electron 25 | 26 | # Ignore files in dist public directory 27 | src/web-frontend/dist/public -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "printWidth": 100, 4 | "semi": false, 5 | "singleQuote": true 6 | } -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://www.paypal.me/stigaard"] 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jens Grønhøj Stigaard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vinproma 2 | 3 | [![vMix Input Progress Monitor App](https://img.shields.io/github/v/release/jensstigaard/vinproma.svg)](../../releases) 4 | [![vMix Input Progress Monitor App](https://img.shields.io/github/downloads/jensstigaard/vinproma/total.svg)](../../releases) 5 | [![Sponsor me - vMix Input Progress Monitor Electron](https://img.shields.io/badge/paypal-donate-brightgreen.svg)](https://paypal.me/stigaard) 6 | 7 | **v**Mix **In**put **Pro**gress **M**onitor **A**pp (shortened: *vinproma*) built with [ElectronJS](https://electronjs.org). ElectronJS is a cross-platform framework allowing the app to be built for each Windows, Mac or Linux. 8 | 9 | The app allows simple monitoring of realtime progress of the current playing video or audio track. The input in preview can also be to monitored in the HTML view. 10 | 11 | You are free to clone the repository to develop your own app based in this code. 12 | 13 | ## Feature summary 14 | - Read progress of current input in program 15 | - HTML view: View progress as a HTML page 16 | - Linear progress or circular 17 | - Light or dark mode 18 | - vMix title mode: Send progress to a vMix title input of your choice 19 | - Multiple types of data to send 20 | - Ability to setup multiple "destinations" 21 | - Ability to enable Delay compensation 22 | 23 | ## Downloads 24 | 25 | See the [Releases](../../releases) tab for a direct download of the app for Mac and Windows. 26 | 27 | 28 | ## App overview 29 | ![vMix Input Progress Monitor App](./readme_assets/app-overview_041.png "Application overview") 30 | 31 | ## HTML view (Linear or circular) 32 | ![vMix Input Progress Monitor App - HTML view Linear](./readme_assets/html-view-linear.png "HTML view linear") 33 | 34 | ![vMix Input Progress Monitor App - HTML view Circular](./readme_assets/html-view-circular.png "HTML view circular") 35 | 36 | 37 | ## Project architecture 38 | The project consists of several components. The app is built with Electron, meaning that the app can be compiled for Windows, Mac or Linux. 39 | 40 | The Electron frontend (also called "Renderer") serves the purpose of fetching info from a vMix instance, and letting the user see the state of the vMix instance connection and other settings. 41 | 42 | A web server lives on the backend side of the Electron app, serving requests from the users on port 8095. The web server includes a web socket server, serving flowing real time TCP/IP data to the connected clients/browsers. 43 | 44 | The web server communicates with the Electron frontend app, where practically realtime data from the vMix instances is passed as following: 45 | 46 | Electron renderer process → (via IPC) → Electron main process → Web server → (via Web Socket) → Web clients 47 | 48 | ## Known issues 49 | When running in development mode you can experience loss of connection to the vMix instance after some minutes. 50 | 51 | ## Roadmap 52 | 53 | - [ ] Add XAML template 54 | 55 | 56 | ## Project setup 57 | ### Install dependencies (based on package.json) 58 | ``` 59 | yarn 60 | ``` 61 | 62 | ### Compiles and hot-reloads for development 63 | ``` 64 | yarn electron:serve 65 | ``` 66 | 67 | ### Compiles and minifies for production 68 | ``` 69 | yarn electron:build 70 | ``` 71 | 72 | ### Lints and fixes files 73 | ``` 74 | yarn lint 75 | ``` 76 | 77 | ### Web frontend assets 78 | Note that in **src/web-frontend** there is also source code for the compiled assets for the web frontend. 79 | You should also install the dependencies for this by running 80 | ``` 81 | cd src/web-frontend 82 | yarn 83 | ``` 84 | 85 | Compiles and hot-reloads for development 86 | ``` 87 | yarn watch 88 | ``` 89 | 90 | There is also a alias for this in the main package.json: 91 | ``` 92 | yarn web-assets-watch 93 | ``` 94 | And also before building the Electron app, a command to build web frontend assets for production can be run: 95 | ``` 96 | yarn web-assets-prod 97 | ``` 98 | 99 | ### Customize configuration 100 | See [Configuration Reference](https://cli.vuejs.org/config/). 101 | -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | extraResources: 2 | - { from: "src/web-frontend/dist", to: "web-frontend/dist" } 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vinproma", 3 | "productName": "vinproma", 4 | "version": "0.4.1", 5 | "private": true, 6 | "author": { 7 | "name": "Jens Grønhøj Stigaard", 8 | "email": "jens@stigaard.info" 9 | }, 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/jensstigaard/vinproma.git" 14 | }, 15 | "scripts": { 16 | "serve": "vue-cli-service serve", 17 | "build": "vue-cli-service build", 18 | "lint": "vue-cli-service lint", 19 | "electron:build": "vue-cli-service electron:build", 20 | "electron:serve": "vue-cli-service electron:serve", 21 | "postinstall": "electron-builder install-app-deps", 22 | "postuninstall": "electron-builder install-app-deps", 23 | "web-assets-prod": "cd src/web-frontend && yarn prod", 24 | "web-assets-watch": "cd src/web-frontend && yarn watch" 25 | }, 26 | "main": "background.js", 27 | "dependencies": { 28 | "@fortawesome/fontawesome-free": "^5.15", 29 | "electron": "^13.6.6", 30 | "electron-window-state": "^5", 31 | "express": "^4.17", 32 | "ip": "^1.1", 33 | "vmix-js-utils": "^3", 34 | "vue": "^2.6", 35 | "vue-class-component": "^7.2", 36 | "vue-debounce-decorator": "^1", 37 | "vue-directive-long-press": "^1", 38 | "vue-vmix-conn-plugin": "^1", 39 | "vuetify": "^2", 40 | "vuex": "^3.6", 41 | "vuex-electron": "^1", 42 | "ws": "^7" 43 | }, 44 | "devDependencies": { 45 | "@types/cookie-parser": "^1.4", 46 | "@types/electron-devtools-installer": "^2.2.0", 47 | "@types/express": "^4.17", 48 | "@types/ip": "^1.1.0", 49 | "@types/node": "12", 50 | "@types/ws": "^7.4.0", 51 | "@typescript-eslint/eslint-plugin": "^4.11.1", 52 | "@typescript-eslint/parser": "4.4.0", 53 | "@vue/cli-plugin-eslint": "^4.5", 54 | "@vue/cli-plugin-typescript": "^4.5", 55 | "@vue/cli-plugin-vuex": "^4.5", 56 | "@vue/cli-service": "^4.5", 57 | "@vue/eslint-config-prettier": "^6.0", 58 | "@vue/eslint-config-typescript": "^7.0", 59 | "body-parser": "^1.19", 60 | "bufferutil": "^4.0", 61 | "cookie-parser": "^1.4", 62 | "deepmerge": "^4.2", 63 | "electron-devtools-installer": "^3.1.1", 64 | "eslint": "^6", 65 | "eslint-plugin-prettier": "^3.3.0", 66 | "eslint-plugin-vue": "^7.4.0", 67 | "fibers": "^5.0.0", 68 | "lint-staged": "^10.5", 69 | "prettier": "^2.2.1", 70 | "pug": "^3.0.3", 71 | "pug-plain-loader": "^1.1", 72 | "sass": "^1", 73 | "sass-loader": "^10", 74 | "typescript": "~4.0.5", 75 | "utf-8-validate": "^5", 76 | "vue-cli-plugin-electron-builder": "^2.0", 77 | "vue-cli-plugin-pug": "^2.0", 78 | "vue-cli-plugin-vuetify": "^2.0", 79 | "vue-property-decorator": "^9", 80 | "vue-template-compiler": "^2.6", 81 | "vuetify-loader": "^1.3.0" 82 | }, 83 | "gitHooks": { 84 | "pre-commit": "lint-staged" 85 | }, 86 | "lint-staged": { 87 | "*.{js,vue,ts}": [ 88 | "vue-cli-service lint", 89 | "git add" 90 | ] 91 | } 92 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jensstigaard/vinproma/cfe0c91e72118c39b2371bdb304c3efb7a1083ba/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vinproma – vMix Input Progress Monitor 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /readme_assets/app-overview_010.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jensstigaard/vinproma/cfe0c91e72118c39b2371bdb304c3efb7a1083ba/readme_assets/app-overview_010.png -------------------------------------------------------------------------------- /readme_assets/app-overview_041.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jensstigaard/vinproma/cfe0c91e72118c39b2371bdb304c3efb7a1083ba/readme_assets/app-overview_041.png -------------------------------------------------------------------------------- /readme_assets/html-view-circular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jensstigaard/vinproma/cfe0c91e72118c39b2371bdb304c3efb7a1083ba/readme_assets/html-view-circular.png -------------------------------------------------------------------------------- /readme_assets/html-view-linear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jensstigaard/vinproma/cfe0c91e72118c39b2371bdb304c3efb7a1083ba/readme_assets/html-view-linear.png -------------------------------------------------------------------------------- /readme_assets/html-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jensstigaard/vinproma/cfe0c91e72118c39b2371bdb304c3efb7a1083ba/readme_assets/html-view.png -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 327 | 328 | 335 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jensstigaard/vinproma/cfe0c91e72118c39b2371bdb304c3efb7a1083ba/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 46 2 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { app, protocol, BrowserWindow, ipcMain } from 'electron' 4 | import { createProtocol } from 'vue-cli-plugin-electron-builder/lib' 5 | import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer' 6 | import windowStateKeeper from 'electron-window-state' 7 | 8 | // Web socket and web socket server 9 | import WebSocket from 'ws' 10 | import webserver from './webserver' 11 | 12 | // VueX store 13 | // import store from './store' 14 | import './store' 15 | 16 | const isDevelopment = process.env.NODE_ENV !== 'production' 17 | 18 | // Keep a global reference of the window object, if you don't, the window will 19 | // be closed automatically when the JavaScript object is garbage collected. 20 | let win: BrowserWindow | null 21 | 22 | // Scheme must be registered before the app is ready 23 | protocol.registerSchemesAsPrivileged([ 24 | { scheme: 'app', privileges: { secure: true, standard: true } }, 25 | ]) 26 | 27 | function createWindow() { 28 | const mainWindowState = windowStateKeeper({ 29 | defaultWidth: 1000, 30 | defaultHeight: 600, 31 | }) 32 | 33 | // Create the browser window. 34 | win = new BrowserWindow({ 35 | width: mainWindowState.width, 36 | height: mainWindowState.height, 37 | x: mainWindowState.x, 38 | y: mainWindowState.y, 39 | 40 | webPreferences: { 41 | nodeIntegration: true, 42 | enableRemoteModule: true, 43 | }, 44 | }) 45 | 46 | // Manage window state (position and size) 47 | // and remember it for every time the application is opened 48 | mainWindowState.manage(win) 49 | 50 | if (process.env.WEBPACK_DEV_SERVER_URL) { 51 | // Load the url of the dev server if in development mode 52 | win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string) 53 | if (!process.env.IS_TEST) win.webContents.openDevTools() 54 | } else { 55 | createProtocol('app') 56 | // Load the index.html when not in development 57 | win.loadURL('app://./index.html') 58 | } 59 | 60 | win.on('closed', () => { 61 | win = null 62 | }) 63 | } 64 | 65 | // Quit when all windows are closed. 66 | app.on('window-all-closed', () => { 67 | // On macOS it is common for applications and their menu bar 68 | // to stay active until the user quits explicitly with Cmd + Q 69 | if (process.platform !== 'darwin') { 70 | app.quit() 71 | } 72 | }) 73 | 74 | app.on('activate', () => { 75 | // On macOS it's common to re-create a window in the app when the 76 | // dock icon is clicked and there are no other windows open. 77 | if (win === null) { 78 | createWindow() 79 | } 80 | }) 81 | 82 | // This method will be called when Electron has finished 83 | // initialization and is ready to create browser windows. 84 | // Some APIs can only be used after this event occurs. 85 | app.on('ready', async () => { 86 | if (isDevelopment && !process.env.IS_TEST) { 87 | // Install Vue Devtools 88 | // Devtools extensions are broken in Electron 6.0.0 and greater 89 | // See https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/378 for more info 90 | // Electron will not launch with Devtools extensions installed on Windows 10 with dark mode 91 | // If you are not using Windows 10 dark mode, you may uncomment these lines 92 | // In addition, if the linked issue is closed, you can upgrade electron and uncomment these lines 93 | try { 94 | await installExtension(VUEJS_DEVTOOLS) 95 | } catch (e) { 96 | console.error('Vue Devtools failed to install:', e.toString()) 97 | } 98 | } 99 | createWindow() 100 | }) 101 | 102 | // Exit cleanly on request from parent process in development mode. 103 | if (isDevelopment) { 104 | if (process.platform === 'win32') { 105 | process.on('message', (data) => { 106 | if (data === 'graceful-exit') { 107 | app.quit() 108 | } 109 | }) 110 | } else { 111 | process.on('SIGTERM', () => { 112 | app.quit() 113 | }) 114 | } 115 | } 116 | 117 | // this.conn.on('disconnect', console.error) 118 | 119 | // const isMac = process.platform === 'darwin' 120 | 121 | // const template = [ 122 | // // { role: 'appMenu' } 123 | // ...(isMac 124 | // ? [ 125 | // { 126 | // label: app.name, 127 | // submenu: [ 128 | // { role: 'about' }, 129 | // { type: 'separator' }, 130 | // { role: 'services' }, 131 | // { type: 'separator' }, 132 | // { role: 'hide' }, 133 | // { role: 'hideothers' }, 134 | // { role: 'unhide' }, 135 | // { type: 'separator' }, 136 | // { role: 'quit' } 137 | // ] 138 | // } 139 | // ] 140 | // : []), 141 | // { 142 | // label: 'View', 143 | // submenu: [ 144 | // // { 145 | // // label: 'Swap Preview/Program rows', 146 | // // accelerator: process.platform === 'darwin' ? 'Alt+Cmd+P' : 'Ctrl+Shift+P', 147 | // // click: async () => { 148 | // // store.dispatch('swapPreviewProgramRows') 149 | // // } 150 | // // }, 151 | // { type: 'separator' }, 152 | // { role: 'reload' }, 153 | // { role: 'forcereload' }, 154 | // { role: 'toggledevtools' }, 155 | // { type: 'separator' }, 156 | // { role: 'resetzoom' }, 157 | // { role: 'zoomin' }, 158 | // { role: 'zoomout' }, 159 | // { type: 'separator' }, 160 | // { role: 'togglefullscreen' } 161 | // ] 162 | // } 163 | // ] 164 | 165 | // // @ts-ignore 166 | // Menu.setApplicationMenu(Menu.buildFromTemplate(template)) 167 | 168 | let currentvMixData: string = '' 169 | 170 | function apiRoute(req: any, res: any) { 171 | if (currentvMixData === '') { 172 | return res.send({ message: 'No data...' }) 173 | } 174 | // console.log('Theme', theme) 175 | return res.send(`[${currentvMixData}]`) 176 | } 177 | 178 | // Initialize webserver 179 | const { wss } = webserver(apiRoute) 180 | 181 | // Upon new ws connection - send current vMix data 182 | wss.on('connection', (ws: WebSocket) => { 183 | // Guard if current vMix data is empty... 184 | if (currentvMixData.length === 0) { 185 | return 186 | } 187 | 188 | ws.send( 189 | JSON.stringify({ 190 | type: 'input', 191 | data: currentvMixData, 192 | }) 193 | ) 194 | }) 195 | 196 | // Listen for vMix info data from renderer thread 197 | ipcMain.on('vMixInfo', (_event, data: any) => { 198 | currentvMixData = data 199 | // console.log('IPC Event', event) 200 | // console.log(data) 201 | 202 | // Broadcast to all open ws clients 203 | wss.clients.forEach(function each(client: WebSocket) { 204 | // Check if ws client is open 205 | if (client.readyState === WebSocket.OPEN) { 206 | client.send( 207 | JSON.stringify({ 208 | type: 'input', 209 | data, 210 | }) 211 | ) 212 | } 213 | }) 214 | }) 215 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 137 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import store from './store' 4 | import vuetify from './plugins/vuetify' 5 | 6 | import { vMixConnectionPlugin, vMixConnectionPluginStore } from 'vue-vmix-conn-plugin' 7 | 8 | Vue.config.productionTip = false 9 | 10 | Vue.use(vMixConnectionPlugin, new vMixConnectionPluginStore()) 11 | 12 | new Vue({ 13 | store, 14 | vuetify, 15 | render: h => h(App) 16 | }).$mount('#app') 17 | -------------------------------------------------------------------------------- /src/plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify' 3 | 4 | import 'vuetify/dist/vuetify.min.css' 5 | import '@fortawesome/fontawesome-free/css/all.css' 6 | 7 | Vue.use(Vuetify) 8 | 9 | export default new Vuetify({ 10 | // Use Font Awesome icons 11 | icons: { 12 | iconfont: 'fa' 13 | }, 14 | 15 | // Defaults to light theme 16 | theme: { 17 | dark: false 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /src/store/actions.ts: -------------------------------------------------------------------------------- 1 | // 2 | export default { 3 | setHost({ commit }: { commit: Function }, newHost: string) { 4 | commit('setHost', newHost) 5 | }, 6 | 7 | addHostToPreviousConnectedVmixHosts({ state, commit }: { commit: Function; state: any }) { 8 | const host: string = state.vMixConnection.host 9 | 10 | if (state.previousVmixConnectionHosts.includes(host)) { 11 | return 12 | } 13 | 14 | commit('addHostToPreviousConnectedVmixHosts', state.vMixConnection.host) 15 | }, 16 | 17 | setDelayCompensation({ commit }: { commit: Function }, newValue: number) { 18 | commit('setDelayCompensation', newValue) 19 | }, 20 | 21 | toggleTitleMode({ commit }: { commit: Function }) { 22 | commit('toggleTitleMode') 23 | }, 24 | 25 | saveTitleModeSettings({ commit }: { commit: Function }, data: { [key: string]: any }) { 26 | commit('saveTitleModeSettings', data) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | // @ts-ignore - this is necessary since VS Code cannot find declarations of the types of vuex-electron 5 | import { createPersistedState, createSharedMutations } from 'vuex-electron' 6 | 7 | Vue.use(Vuex) 8 | 9 | import state from './state' 10 | import actions from './actions' 11 | import mutations from './mutations' 12 | 13 | export default new Vuex.Store({ 14 | state, 15 | 16 | actions, 17 | 18 | mutations, 19 | 20 | modules: {}, 21 | 22 | plugins: [createPersistedState(), createSharedMutations()], 23 | strict: process.env.NODE_ENV !== 'production' 24 | }) 25 | -------------------------------------------------------------------------------- /src/store/mutations.ts: -------------------------------------------------------------------------------- 1 | import { TitleModeSettings } from '@/types/title-mode' 2 | 3 | export default { 4 | /** 5 | * Set vMix connection host 6 | * @param state 7 | * @param newHost 8 | */ 9 | setHost(state: any, newHost: string) { 10 | state.vMixConnection.host = newHost 11 | }, 12 | 13 | /** 14 | * Add host to previous vMix connection hosts 15 | * @param state 16 | * @param host 17 | */ 18 | addHostToPreviousConnectedVmixHosts(state: any, host: string) { 19 | state.previousVmixConnectionHosts.push(host) 20 | }, 21 | 22 | /** 23 | * Set delay compensation 24 | * 25 | * @param state 26 | * @param newValue 27 | */ 28 | setDelayCompensation(state: any, newValue: number) { 29 | state.delayCompensation = newValue 30 | }, 31 | 32 | /** 33 | * Toggle title mode on/off 34 | * @param state 35 | */ 36 | toggleTitleMode(state: any) { 37 | state.titleMode.enabled = !state.titleMode.enabled 38 | }, 39 | 40 | /** 41 | * Save title mode settings 42 | * @param state 43 | */ 44 | saveTitleModeSettings(state: any, data: TitleModeSettings) { 45 | state.titleMode.enabled = data.enabled 46 | state.titleMode.sameInputForAllFields = data.sameInputForAllFields 47 | 48 | state.titleMode.fields.currentPosition = data.fields.currentPosition 49 | state.titleMode.fields.remaining = data.fields.remaining 50 | state.titleMode.fields.width = data.fields.width 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/store/state.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | vMixConnection: { 3 | host: '127.0.0.1', 4 | debug: false 5 | }, 6 | 7 | previousVmixConnectionHosts: [], 8 | 9 | // Delay compensation 10 | // How many milliseconds shall the presentation act as it is before in time 11 | delayCompensation: 0, 12 | 13 | titleMode: { 14 | enabled: false, 15 | sameInputForAllFields: true, 16 | fields: { 17 | currentPosition: [], 18 | remaining: [], 19 | width: [] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/types/title-mode.ts: -------------------------------------------------------------------------------- 1 | // Title Mode settings type 2 | export type TitleModeSettings = { 3 | enabled: boolean 4 | sameInputForAllFields: boolean 5 | fields: { 6 | currentPosition: TitleModeCurrentPositionField[] 7 | remaining: TitleModeRemainingField[] 8 | width: TitleModeWidthField[] 9 | } 10 | } 11 | 12 | type TitleModeField = { 13 | inputKey: string 14 | selectedName: string 15 | } 16 | 17 | export type TitleModeCurrentPositionField = TitleModeField & { includeRemaining: boolean } 18 | 19 | export type TitleModeRemainingField = TitleModeField 20 | 21 | export type TitleModeWidthField = TitleModeField & { totalWidth: number } 22 | -------------------------------------------------------------------------------- /src/types/vmix-input.ts: -------------------------------------------------------------------------------- 1 | export type VmixInput = { 2 | duration: number 3 | position: number 4 | } 5 | -------------------------------------------------------------------------------- /src/utility/paths.ts: -------------------------------------------------------------------------------- 1 | import electron from 'electron' 2 | import path from 'path' 3 | 4 | const app = (electron.app || electron.remote.app) 5 | 6 | // https://www.electron.build/configuration/contents#extraresources 7 | const map: { [key: string]: string } = { 8 | 9 | darwin: path.resolve( 10 | // Mac Resources path (with library executables) is under Resources directory, 11 | // which is in same root as MacOS folder, 12 | // which is the directory with the main electron app executable 13 | app.getPath('exe'), 14 | '../../Resources' 15 | ), 16 | 17 | win32: path.resolve( 18 | // app.getPath('exe'), 19 | 'resources' 20 | ), 21 | 22 | linux: path.resolve( 23 | // app.getPath('exe'), 24 | 'resources' 25 | ), 26 | } 27 | 28 | /** 29 | * 30 | * @param {string} resource 31 | * @returns {string} 32 | */ 33 | export function getResourcePath(resource: string): string { 34 | // If dev enviroment - use global executable 35 | if (process.env.NODE_ENV !== "production") { 36 | return path.resolve('src', resource) 37 | } 38 | 39 | return path.resolve(getResourcesDirectory(), resource) 40 | } 41 | 42 | export function getResourcesDirectory(): string { 43 | const platform: string = process.platform 44 | 45 | if (!(platform in map)) { 46 | throw new Error('Invalid platform') 47 | } 48 | 49 | return map[platform] 50 | } 51 | 52 | export default { 53 | getResourcePath 54 | } -------------------------------------------------------------------------------- /src/utility/time.ts: -------------------------------------------------------------------------------- 1 | import { VmixInput } from '@/types/vmix-input' 2 | 3 | /** 4 | * Utility method: Duration nice 5 | * 6 | * Input is a number of a duration 7 | * Output will then be niceified duration 8 | * Examples: 9 | * 90 -> 1:30 10 | * 1000 -> 16:40 11 | * 12 | * @param {number} duration 13 | * @returns {string} 14 | */ 15 | export function durationNice(duration: number): string { 16 | const minutes = Math.floor(duration / 60) 17 | 18 | const sec = duration % 60 19 | const seconds = `${sec < 10 ? '0' : ''}${sec}` 20 | 21 | return `${minutes}:${seconds}` 22 | } 23 | 24 | export function completeString(input: VmixInput, delayCompensation: number): string { 25 | return '' 26 | } 27 | -------------------------------------------------------------------------------- /src/view-templates/overview.ts: -------------------------------------------------------------------------------- 1 | export default ` 2 | 3 | 4 | 5 | 6 | 7 | vinproma – vMix Input Progress Monitor 8 | 9 | 10 | 11 | 12 | 23 | 24 | 25 | 26 | ` 27 | -------------------------------------------------------------------------------- /src/view-templates/progress.ts: -------------------------------------------------------------------------------- 1 | export default (type: string, theme: string = 'light') => { 2 | const parts = [ 3 | ` 4 | 5 | 6 | 7 | 8 | 9 | vinproma – vMix Input Progress Monitor 10 | 11 | `, 12 | // Inject CSS for dark theme if dark theme desired 13 | theme === 'dark' ? '' : '', 14 | ` 15 | 16 | 17 | 18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 | ` 27 | ] 28 | 29 | return parts.join('') 30 | } 31 | -------------------------------------------------------------------------------- /src/views/DelayCompensationSlider.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 52 | -------------------------------------------------------------------------------- /src/views/HtmlViews.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 31 | -------------------------------------------------------------------------------- /src/views/Layout/AppBar.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 54 | -------------------------------------------------------------------------------- /src/views/TitleModeSettings/CurrentPositionField.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 75 | -------------------------------------------------------------------------------- /src/views/TitleModeSettings/CurrentPositionFields.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 50 | -------------------------------------------------------------------------------- /src/views/TitleModeSettings/Index.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 289 | -------------------------------------------------------------------------------- /src/views/TitleModeSettings/RemainingField.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 72 | -------------------------------------------------------------------------------- /src/views/TitleModeSettings/RemainingFields.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 51 | -------------------------------------------------------------------------------- /src/views/TitleModeSettings/WidthField.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 76 | -------------------------------------------------------------------------------- /src/views/TitleModeSettings/WidthFields.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 50 | -------------------------------------------------------------------------------- /src/web-frontend/README.md: -------------------------------------------------------------------------------- 1 | Compile web frontend assets by running 2 | ```npx mix``` 3 | 4 | And for production 5 | ```npx mix --production``` -------------------------------------------------------------------------------- /src/web-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vinproma-web-frontend", 3 | "version": "1.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "author": "Jens Stigaard", 7 | "license": "MIT", 8 | "dependencies": { 9 | "font-awesome": "^4.7.0", 10 | "moment": "^2.29.4", 11 | "pug": "^3.0.3", 12 | "tailwindcss": "^2.0.2", 13 | "vue": "^2.6.12", 14 | "vue-circle-counter": "^3.0.0", 15 | "vue-native-websocket": "^2.0.14", 16 | "vue-timeago": "^5.1.3" 17 | }, 18 | "devDependencies": { 19 | "jsonfile": "^6.1", 20 | "laravel-mix": "^6", 21 | "mix-tailwindcss": "^1.3.0", 22 | "postcss": "^8.4.31", 23 | "pug-plain-loader": "^1.1.0", 24 | "resolve-url-loader": "^3.1.2", 25 | "vue-loader": "^15.9.6" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/web-frontend/src/Circular.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 65 | -------------------------------------------------------------------------------- /src/web-frontend/src/Linear.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 41 | -------------------------------------------------------------------------------- /src/web-frontend/src/Main.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 96 | -------------------------------------------------------------------------------- /src/web-frontend/src/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueNativeSock from 'vue-native-websocket' 3 | 4 | // Import App view 5 | import Main from './Main.vue' 6 | 7 | const host = location.host.split(':')[0] 8 | // Vue use native web socket client 9 | // To communicate with backend which fetches data from vMix instance 10 | Vue.use(VueNativeSock, `ws://${host}:8096/`, { 11 | reconnection: true, // (Boolean) whether to reconnect automatically (false) 12 | reconnectionAttempts: 5, // (Number) number of reconnection attempts before giving up (Infinity), 13 | reconnectionDelay: 3000, // (Number) how long to initially wait before attempting a new (1000) 14 | format: 'json', 15 | }) 16 | 17 | const app = new Vue({ 18 | render: (h) => h(Main), 19 | }).$mount('#app') 20 | -------------------------------------------------------------------------------- /src/web-frontend/src/mixin.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | // Helper to nicely present durations as readable text 4 | function durationNice(duration) { 5 | const minutes = Math.floor(duration / 60) 6 | 7 | const sec = duration % 60 8 | const seconds = `${sec < 10 ? '0' : ''}${sec}` 9 | 10 | return `${minutes}:${seconds}` 11 | } 12 | 13 | // Base Mixin 14 | export default Vue.extend({ 15 | // Helpers 16 | methods: { 17 | roundedSeconds(ms) { 18 | return Math.floor(ms / 1000) 19 | }, 20 | 21 | positionSeconds(input) { 22 | return this.roundedSeconds(input.position) 23 | }, 24 | 25 | durationSeconds(input) { 26 | return this.roundedSeconds(input.duration) 27 | }, 28 | 29 | hasDuration(input) { 30 | return input.hasOwnProperty('duration') && this.durationSeconds(input) > 0 31 | }, 32 | 33 | positionText(input) { 34 | return durationNice(this.positionSeconds(input)) 35 | }, 36 | 37 | durationText(input) { 38 | return durationNice(this.durationSeconds(input)) 39 | }, 40 | 41 | remainingText(input) { 42 | const diffMs = input.duration - input.position 43 | return durationNice(Math.ceil(diffMs / 1000)) 44 | }, 45 | 46 | positionPercentage(input) { 47 | return Math.round((input.position / input.duration) * 100) 48 | }, 49 | 50 | counterText(input) { 51 | if (!this.hasDuration(input)) { 52 | return `${input.type || '-'}` 53 | } 54 | // console.log(input) 55 | 56 | const pos = this.positionText(input) 57 | const dur = this.durationText(input) 58 | const remaining = this.remainingText(input) 59 | 60 | return `${pos} / ${dur} / ${remaining}` 61 | }, 62 | 63 | remainingWarning(input) { 64 | if (input.loop === 'True') { 65 | return false 66 | } 67 | 68 | // Remaining time in milliseconds 69 | const remainingTime = input.duration - input.position 70 | 71 | // If longer than 120 seconds then threshold is 10 sec - otherwise 5 seconds 72 | const warningThreshold = this.durationSeconds(input) > 120 ? 10000 : 5000 73 | 74 | return remainingTime <= warningThreshold 75 | }, 76 | 77 | isRunning(input) { 78 | return input.state === 'Running' 79 | }, 80 | 81 | backgroundColor(input, isProgram = true) { 82 | // Is paused? 83 | if (!this.isRunning(input)) { 84 | // Show blue-ish color when paused 85 | return 'rgba(100, 150, 255, 0.6)' 86 | } 87 | 88 | return this.remainingWarning(input) // Below "warning" threshold? 89 | ? 'rgba(255, 100, 100, 0.8)' // Show red (warning color) 90 | : 'rgba(100, 200, 100, 0.8)' // Otherwise green 91 | }, 92 | 93 | positionStyle(input) { 94 | const p = this.positionPercentage(input) 95 | const percentage = p < 2 ? 0 : p 96 | 97 | const background = this.backgroundColor(input) 98 | 99 | return { 100 | width: `${percentage}%`, 101 | background, 102 | } 103 | }, 104 | }, 105 | }) 106 | -------------------------------------------------------------------------------- /src/web-frontend/src/styles/app.sass: -------------------------------------------------------------------------------- 1 | // Fonts 2 | @import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons|Raleway:300,400,600") 3 | @import url("https://use.fontawesome.com/releases/v5.1.0/css/all.css") 4 | 5 | // Tailwind 6 | @tailwind base 7 | @tailwind components 8 | @tailwind utilities 9 | 10 | // Variables 11 | 12 | 13 | html, body 14 | margin: 0 15 | padding: 0 16 | 17 | height: 100% 18 | 19 | font-family: Lato, Raleway, sans-serif 20 | font-weight: normal 21 | font-size: 14pt 22 | 23 | overflow: hidden 24 | 25 | a 26 | text-decoration: none 27 | 28 | #overview-links 29 | text-align: center 30 | margin-top: 20px 31 | 32 | > div 33 | a 34 | display: inline-block 35 | width: 40% 36 | height: 150px 37 | 38 | padding: 50px 39 | 40 | font-size: 30pt 41 | text-align: center 42 | 43 | border: 1px solid #CCC 44 | background: #EEE 45 | 46 | color: #333 47 | 48 | &:nth-child(2) 49 | background: #222 50 | color: #EEE 51 | 52 | #app 53 | @import "circular" 54 | @import "linear" 55 | 56 | .fade-enter-active, .fade-leave-active 57 | transition: opacity .3s 58 | 59 | .fade-enter, .fade-leave-to 60 | // /* .fade-leave-active below version 2.1.8 */ { 61 | opacity: 0 62 | -------------------------------------------------------------------------------- /src/web-frontend/src/styles/circular.sass: -------------------------------------------------------------------------------- 1 | 2 | #circular-progress-bars 3 | margin: 10px 15px 4 | text-shadow: 2px 2px 4px rgba(0,0,0,0.5) 5 | 6 | .title 7 | font-size: 18pt 8 | 9 | svg 10 | margin: 0 auto 11 | 12 | // Shared for both program and preview linear bars 13 | .program-inputs, .preview-inputs 14 | .item 15 | 16 | &:not(:last-child) 17 | // border-bottom: 1px dotted #CCC 18 | -------------------------------------------------------------------------------- /src/web-frontend/src/styles/dark.sass: -------------------------------------------------------------------------------- 1 | body 2 | color: white -------------------------------------------------------------------------------- /src/web-frontend/src/styles/linear.sass: -------------------------------------------------------------------------------- 1 | 2 | #linear-progress-bars 3 | position: absolute 4 | 5 | bottom: 2% 6 | left: 2% 7 | right: 2% 8 | 9 | text-align: center 10 | 11 | .program-inputs .item 12 | height: 110px 13 | 14 | .text-overlays 15 | padding: 10px 16 | 17 | .preview-inputs .item 18 | height: 30px 19 | margin-bottom: 1px 20 | 21 | .text-overlays 22 | padding: 2px 23 | 24 | // Shared for both program and preview linear bars 25 | .program-inputs, .preview-inputs 26 | .item 27 | background: rgba(0, 0, 0, 0.2) 28 | position: relative 29 | 30 | .duration 31 | font-size: 30pt 32 | 33 | .text-overlays 34 | position: absolute 35 | height: 100% 36 | width: 100% 37 | 38 | z-index: 10 39 | 40 | text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3) 41 | 42 | .title 43 | // padding x: 5px 44 | 45 | .position-bar 46 | position: absolute 47 | left: 0 48 | top: 0 49 | bottom: 0 50 | // Variable width 51 | 52 | z-index: 0 53 | 54 | transition: width ease 0.7s, background ease 0.3s 55 | -------------------------------------------------------------------------------- /src/web-frontend/webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix') 2 | 3 | require('mix-tailwindcss') 4 | 5 | /* 6 | |-------------------------------------------------------------------------- 7 | | Mix Asset Management 8 | |-------------------------------------------------------------------------- 9 | | 10 | | Mix provides a clean, fluent API for defining some Webpack build steps 11 | | for your Laravel application. By default, we are compiling the Sass 12 | | file for your application, as well as bundling up your JS files. 13 | | 14 | */ 15 | 16 | mix.webpackConfig({ 17 | module: { 18 | rules: [ 19 | // https://vue-loader.vuejs.org/guide/pre-processors.html#pug 20 | { 21 | test: /\.pug$/, 22 | oneOf: [ 23 | // this applies to `