├── releases ├── app │ └── background@2x.png └── build_dmg.sh ├── app ├── assets │ └── TrayIconTemplate@2x.png ├── preload.js ├── video-manager.js ├── index.html ├── auto-update.js ├── settings.js ├── main.js ├── tray-menu.js └── index.js ├── .gitignore ├── .editorconfig ├── .eslintrc.json ├── .babelrc ├── entitlements.mac.plist ├── readme.md └── package.json /releases/app/background@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaru/mewcam/HEAD/releases/app/background@2x.png -------------------------------------------------------------------------------- /app/assets/TrayIconTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaru/mewcam/HEAD/app/assets/TrayIconTemplate@2x.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist-bodypix-app/ 2 | node_modules/ 3 | .cache/ 4 | 5 | releases/**/*.app 6 | releases/**/*.dmg 7 | releases/build/ 8 | -------------------------------------------------------------------------------- /app/preload.js: -------------------------------------------------------------------------------- 1 | window.remote = require('electron').remote; 2 | window.ipcRenderer = require('electron').ipcRenderer; 3 | window.__dirname = __dirname; 4 | window.os = require('os'); 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | # editorconfig-tools is unable to ignore longs strings or urls 11 | max_line_length = off -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true 6 | }, 7 | "parserOptions": { 8 | // Required for certain syntax usages 9 | //"ecmaVersion": 6 10 | "ecmaVersion": 2017 11 | }, 12 | "extends": ["eslint:recommended", "google"] 13 | } 14 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "esmodules": false, 7 | "targets": { 8 | "browsers": [ 9 | "> 3%" 10 | ] 11 | } 12 | } 13 | ] 14 | ], 15 | "plugins": [ 16 | ["transform-runtime", {"polyfill": false}] 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /app/video-manager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * VideoManager class 3 | */ 4 | module.exports = class VideoManager { 5 | /** 6 | * constructor 7 | */ 8 | constructor() { 9 | } 10 | 11 | /** 12 | * Get video list 13 | * @return {Promise} 14 | */ 15 | getVideoList() { 16 | return navigator.mediaDevices.enumerateDevices().then((info) => { 17 | return info.filter((device) => { 18 | return device.kind === 'videoinput'; 19 | }); 20 | }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.allow-dyld-environment-variables 10 | 11 | com.apple.security.device.camera 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /releases/build_dmg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf $1 4 | rm -rf ./app/mewcam.app 5 | cp -R ./build/mac/mewcam.app ./app 6 | 7 | test -f mewcam.dmg && rm mewcam.dmg 8 | test -f rw.mewcam.dmg && rm rw.mewcam.dmg 9 | create-dmg \ 10 | --volname "mewcam" \ 11 | --background "./app/background@2x.png" \ 12 | --window-pos 400 200 \ 13 | --window-size 400 400 \ 14 | --icon-size 128 \ 15 | --icon "mewcam.app" 50 190 \ 16 | --app-drop-link 240 190 \ 17 | --hide-extension "mewcam.app" \ 18 | "mewcam.dmg" \ 19 | "./app/" 20 | 21 | mkdir $1 22 | mv mewcam.dmg $1 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # mewcam 2 | 3 | mewcam is an app that allows you to display a camera with a background dropped on the front of your desktop. 4 | 5 | mewcam 6 | 7 | mewcam demo 8 | 9 | 10 | see https://twitter.com/zaru/status/1259870621581754370 11 | 12 | ## Download 13 | 14 | - [v1.3.0 for macOS](https://github.com/zaru/mewcam/releases/download/v1.3.0/mewcam_mac_v1.3.0.dmg) 15 | - [v1.3.0 for Windows](https://github.com/zaru/mewcam/releases/download/v1.3.0/mewcam_win_v1.3.0.exe) 16 | 17 | ## ToDo 18 | 19 | - [x] Windows builds 20 | - [x] Support for multiple cameras 21 | - [x] Support for multiple transparency qualities 22 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 40 | 41 | 42 |
43 | 44 | 45 | 46 |
47 |
48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/auto-update.js: -------------------------------------------------------------------------------- 1 | const {app} = require('electron'); 2 | const log = require('electron-log'); 3 | const {autoUpdater} = require('electron-updater'); 4 | 5 | autoUpdater.logger = log; 6 | autoUpdater.logger.transports.file.level = 'info'; 7 | log.info('App starting...'); 8 | 9 | function sendStatusToWindow(text) { 10 | log.info(text); 11 | } 12 | autoUpdater.on('checking-for-update', () => { 13 | sendStatusToWindow('Checking for update...'); 14 | }); 15 | autoUpdater.on('update-available', (info) => { 16 | sendStatusToWindow('Update available.'); 17 | }); 18 | autoUpdater.on('update-not-available', (info) => { 19 | sendStatusToWindow('Update not available.'); 20 | }); 21 | autoUpdater.on('error', (err) => { 22 | sendStatusToWindow('Error in auto-updater. ' + err); 23 | }); 24 | autoUpdater.on('download-progress', (progressObj) => { 25 | let message = 'Download speed: ' + progressObj.bytesPerSecond; 26 | message = message + ' - Downloaded ' + progressObj.percent + '%'; 27 | message = message + ' (' + progressObj.transferred + '/' + progressObj.total + ')'; 28 | sendStatusToWindow(message); 29 | }); 30 | autoUpdater.on('update-downloaded', (info) => { 31 | sendStatusToWindow('Update downloaded'); 32 | }); 33 | app.on('ready', async () => { 34 | console.log('load auto-update'); 35 | autoUpdater.checkForUpdatesAndNotify(); 36 | }); 37 | -------------------------------------------------------------------------------- /app/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Settings class 3 | */ 4 | module.exports = class Settings { 5 | /** 6 | * constructor 7 | */ 8 | constructor() { 9 | } 10 | 11 | /** 12 | * Set deviceId 13 | * @param {string} deviceId 14 | */ 15 | setDeviceId(deviceId) { 16 | localStorage.setItem('deviceId', deviceId); 17 | } 18 | 19 | /** 20 | * Get deviceId 21 | * @return {string} 22 | */ 23 | getDeviceId() { 24 | return localStorage.getItem('deviceId'); 25 | } 26 | 27 | /** 28 | * Set quality 29 | * @param {string} key 30 | */ 31 | setBodyPixModel(key) { 32 | localStorage.setItem('bodyPixModel', key); 33 | } 34 | 35 | /** 36 | * Get quality 37 | * @return {string} 38 | */ 39 | getBodyPixModel() { 40 | return localStorage.getItem('bodyPixModel') || 'Middle'; 41 | } 42 | 43 | /** 44 | * Get BodyPix model param 45 | * @return {string} 46 | */ 47 | getBodyPixModelParam() { 48 | const key = this.getBodyPixModel(); 49 | return this._bodyPixModelList()[key]; 50 | } 51 | 52 | /** 53 | * BodyPix model list 54 | * @return {{high: object, low: object}} 55 | * @private 56 | */ 57 | _bodyPixModelList() { 58 | return { 59 | Low: { 60 | architecture: 'MobileNetV1', 61 | outputStride: 16, 62 | multiplier: 0.75, 63 | quantBytes: 2, 64 | }, 65 | Middle: { 66 | architecture: 'MobileNetV1', 67 | outputStride: 8, 68 | multiplier: 1.0, 69 | quantBytes: 4, 70 | }, 71 | High: { 72 | architecture: 'ResNet50', 73 | outputStride: 16, 74 | multiplier: 1.0, 75 | quantBytes: 4, 76 | }, 77 | }; 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | const {app, BrowserWindow, ipcMain} = require('electron'); 2 | const isDev = require('electron-is-dev'); 3 | const os = require('os'); 4 | const path = require('path'); 5 | require('./auto-update'); 6 | 7 | if (isDev) { 8 | process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = true; 9 | } 10 | 11 | 12 | /** 13 | * @type {BrowserWindow} 14 | */ 15 | let win = null; 16 | 17 | /** 18 | * MainWindow 19 | */ 20 | function createWindow() { 21 | win = new BrowserWindow({ 22 | width: 320, 23 | height: 240, 24 | hasShadow: false, 25 | transparent: true, 26 | frame: false, 27 | resizable: true, 28 | webPreferences: { 29 | nodeIntegration: false, 30 | preload: path.join(__dirname, '/preload.js'), 31 | }, 32 | }); 33 | 34 | const osName = os.platform(); 35 | // MagickCode 36 | if (osName === 'darwin') { 37 | app.dock.hide(); 38 | win.setAlwaysOnTop(true, 'floating'); 39 | win.setVisibleOnAllWorkspaces(true); 40 | win.setFullScreenable(false); 41 | app.dock.show(); 42 | } else if (osName === 'win32' || osName === 'cygwin') { 43 | win.setAlwaysOnTop(true); 44 | } 45 | 46 | win.addListener('resize', () => { 47 | win.setOpacity(0.5); 48 | setTimeout(() => { 49 | win.setOpacity(1.0); 50 | }, 250); 51 | }); 52 | 53 | win.loadURL(isDev ? 'http://localhost:1234' : `file://${__dirname}/../dist-bodypix-app/index.html`); 54 | 55 | // win.webContents.openDevTools(); 56 | } 57 | 58 | app.whenReady().then(createWindow); 59 | 60 | app.on('window-all-closed', () => { 61 | if (process.platform !== 'darwin') { 62 | app.quit(); 63 | } 64 | }); 65 | 66 | app.on('activate', () => { 67 | if (BrowserWindow.getAllWindows().length === 0) { 68 | createWindow(); 69 | } 70 | }); 71 | 72 | ipcMain.on('windowResize', (event, arg) => { 73 | let size = [320, 240]; 74 | if (arg === 'Small') { 75 | size = [160, 120]; 76 | } else if (arg === 'Big') { 77 | size = [640, 480]; 78 | } 79 | win.setSize(...size); 80 | }); 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mewcam", 3 | "version": "1.3.0", 4 | "description": "", 5 | "main": "app/main.js", 6 | "scripts": { 7 | "start": "concurrently \"cross-env NODE_ENV=development parcel ./app/index.html --no-hmr\" \"wait-on http://localhost:1234 && electron . \"", 8 | "build": "cross-env NODE_ENV=production parcel build ./app/index.html --public-url ./ --out-dir ./dist-bodypix-app", 9 | "pack:mac": "electron-builder --mac --x64", 10 | "pack:win": "electron-builder --win --x64", 11 | "pack:all": "npm run build && electron-builder --mac --win --x64", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@tensorflow-models/body-pix": "^2.0.5", 18 | "@tensorflow-models/posenet": "^2.2.1", 19 | "@tensorflow/tfjs-converter": "^1.7.4", 20 | "@tensorflow/tfjs-core": "^1.7.4", 21 | "electron-is-dev": "^1.2.0", 22 | "electron-log": "^4.1.2", 23 | "electron-updater": "^4.3.1" 24 | }, 25 | "devDependencies": { 26 | "babel-core": "^6.26.3", 27 | "babel-plugin-transform-runtime": "^6.23.0", 28 | "babel-polyfill": "^6.26.0", 29 | "babel-preset-env": "^1.7.0", 30 | "clang-format": "^1.4.0", 31 | "concurrently": "^5.2.0", 32 | "cross-env": "^7.0.2", 33 | "electron": "^8.2.5", 34 | "electron-builder": "^22.6.0", 35 | "eslint": "^7.0.0", 36 | "eslint-config-google": "^0.14.0", 37 | "parcel-bundler": "^1.12.4", 38 | "wait-on": "^5.0.0" 39 | }, 40 | "build": { 41 | "appId": "org.tofu-kun.mewcam", 42 | "directories": { 43 | "output": "releases/build" 44 | }, 45 | "files": [ 46 | "app", 47 | "build", 48 | "dist-bodypix-app", 49 | "package.json", 50 | "package-lock.json" 51 | ], 52 | "publish": [{ 53 | "provider": "github", 54 | "owner": "zaru", 55 | "repo": "mewcam" 56 | }], 57 | "mac": { 58 | "target": "dir", 59 | "entitlements": "entitlements.mac.plist" 60 | }, 61 | "win": { 62 | "target": "nsis" 63 | }, 64 | "nsis": { 65 | "oneClick": false, 66 | "allowToChangeInstallationDirectory": true 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/tray-menu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TrayMenu class 3 | */ 4 | module.exports = class TrayMenu { 5 | /** 6 | * Constractor 7 | * @param {Window} window 8 | */ 9 | constructor(window) { 10 | this.window = window; 11 | 12 | this._deviceId = ''; 13 | this._videoList = []; 14 | this._quality = ''; 15 | 16 | this.tray = null; 17 | this.trayMenu = null; 18 | this.callbackVideoMenu = null; 19 | this.callbackQualityMenu = null; 20 | } 21 | 22 | /** 23 | * Set deviceId 24 | * @param {string} deviceId 25 | */ 26 | set deviceId(deviceId) { 27 | this._deviceId = deviceId; 28 | } 29 | 30 | /** 31 | * Set video list 32 | * @param {MediaDeviceInfo[]} videoList 33 | */ 34 | set videoList(videoList) { 35 | this._videoList = videoList; 36 | } 37 | 38 | /** 39 | * Set quality 40 | * @param {string} quality 41 | */ 42 | set quality(quality) { 43 | this._quality = quality; 44 | } 45 | 46 | /** 47 | * Add event listener to video menu 48 | * @param {function} callback 49 | */ 50 | addEventListenerToVideoMenu(callback) { 51 | this.callbackVideoMenu = callback; 52 | } 53 | 54 | /** 55 | * Add event listener to quality menu 56 | * @param {function} callback 57 | */ 58 | addEventListenerToQualityMenu(callback) { 59 | this.callbackQualityMenu = callback; 60 | } 61 | 62 | /** 63 | * Launch tray menu 64 | */ 65 | launch() { 66 | this._buildTrayMenu(); 67 | } 68 | 69 | /** 70 | * Build video sub menu 71 | * @return {[]} 72 | * @private 73 | */ 74 | _buildVideoSubMenu() { 75 | const videoMenu = []; 76 | this._videoList.forEach((device) => { 77 | videoMenu.push({ 78 | label: device.label, 79 | click: () => { 80 | if (this.callbackVideoMenu) { 81 | this.callbackVideoMenu(device.deviceId); 82 | } 83 | }, 84 | type: 'radio', 85 | checked: this._deviceId === device.deviceId, 86 | }); 87 | }); 88 | return videoMenu; 89 | } 90 | 91 | /** 92 | * Build window size sub menu 93 | * @return {{checked: boolean, label: string, type: string, click(): void}[]} 94 | * @private 95 | */ 96 | _buildSizeSubMenu() { 97 | const menu = []; 98 | ['Big', 'Middle', 'Small'].forEach((size) => { 99 | menu.push({ 100 | label: size, 101 | click() { 102 | this.window.ipcRenderer.send('windowResize', size); 103 | }, 104 | type: 'radio', 105 | checked: size === 'Middle', 106 | }); 107 | }); 108 | return menu; 109 | } 110 | 111 | /** 112 | * Build quality sub menu 113 | * @return {{checked: boolean, label: string, type: string, click(): void}[]} 114 | * @private 115 | */ 116 | _buildQualitySubMenu() { 117 | const menu = []; 118 | ['High', 'Middle', 'Low'].forEach((size) => { 119 | menu.push({ 120 | label: size, 121 | click: () => { 122 | if (this.callbackQualityMenu) { 123 | this.callbackQualityMenu(size); 124 | } 125 | }, 126 | type: 'radio', 127 | checked: this._quality === size, 128 | }); 129 | }); 130 | return menu; 131 | } 132 | 133 | /** 134 | * Build video sub menu 135 | */ 136 | _buildTrayMenu() { 137 | const remote = this.window.remote; 138 | const {Tray, Menu} = remote; 139 | const icon = this.window.os.platform() === 'darwin' ? 140 | 'TrayIconTemplate.png' : 'TrayIconTemplate@2x.png'; 141 | this.tray = new Tray(this.window.__dirname + `/assets/${icon}`); 142 | 143 | const menu = Menu.buildFromTemplate([ 144 | { 145 | label: 'Select video', 146 | submenu: this._buildVideoSubMenu(), 147 | }, 148 | { 149 | label: 'Select size', 150 | submenu: this._buildSizeSubMenu(), 151 | }, 152 | { 153 | label: 'Select quality', 154 | submenu: this._buildQualitySubMenu(), 155 | }, 156 | ]); 157 | 158 | this.tray.setContextMenu(menu); 159 | } 160 | }; 161 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | const bodyPix = require('@tensorflow-models/body-pix'); 2 | const VideoManager = require('./video-manager'); 3 | const TrayMenu = require('./tray-menu'); 4 | const Settings = require('./settings'); 5 | const settings = new Settings; 6 | 7 | const state = { 8 | deviceId: null, 9 | video: null, 10 | videoWidth: 0, 11 | videoHeight: 0, 12 | changingVideo: false, 13 | ratio() { 14 | return this.videoHeight / this.videoWidth; 15 | }, 16 | tray: null, 17 | trayMenu: null, 18 | net: null, 19 | changingQuality: false, 20 | }; 21 | 22 | /** 23 | * Main 24 | * @param {string} deviceId 25 | * @return {Promise} 26 | */ 27 | async function workload(deviceId) { 28 | _setupResizeGuide(); 29 | await _loadVideo(deviceId); 30 | 31 | _resizeElement(window.innerWidth, window.innerHeight); 32 | 33 | state.net = await bodyPix.load(settings.getBodyPixModelParam()); 34 | 35 | const video = document.getElementById('video'); 36 | const canvas = document.getElementById('canvas'); 37 | const originalCanvas = document.getElementById('original-canvas'); 38 | 39 | async function segmentationFrame() { 40 | if (!state.changingVideo || !state.changingQuality) { 41 | const segmentation = await state.net.segmentPerson(state.video); 42 | 43 | const originalCtx = originalCanvas.getContext('2d'); 44 | const scale = originalCanvas.width / video.videoWidth; 45 | originalCtx.setTransform(scale, 0, 0, scale, 0, 0); 46 | originalCtx.drawImage(state.video, 0, 0); 47 | const imageData = originalCtx.getImageData( 48 | 0, 0, originalCanvas.width, originalCanvas.height, 49 | ); 50 | _drawToCanvas(canvas, segmentation, imageData); 51 | } 52 | 53 | requestAnimationFrame(segmentationFrame); 54 | } 55 | segmentationFrame(); 56 | } 57 | 58 | /** 59 | * Switch video 60 | * @param {string} deviceId 61 | */ 62 | function switchVideo(deviceId) { 63 | state.changingVideo = true; 64 | stopExistingVideoCapture(); 65 | _loadVideo(deviceId).then(() => { 66 | state.changingVideo = false; 67 | }); 68 | 69 | settings.setDeviceId(deviceId); 70 | } 71 | 72 | /** 73 | * Switch BodyPix quality 74 | * @param {string} quality 75 | * @return {Promise} 76 | */ 77 | async function switchQuality(quality) { 78 | state.changingQuality = true; 79 | settings.setBodyPixModel(quality); 80 | state.net = await bodyPix.load(settings.getBodyPixModelParam()); 81 | state.changingQuality = false; 82 | } 83 | 84 | function stopExistingVideoCapture() { 85 | if (state.video && state.video.srcObject) { 86 | state.video.srcObject.getTracks().forEach((track) => { 87 | track.stop(); 88 | }); 89 | state.video.srcObject = null; 90 | } 91 | } 92 | 93 | /** 94 | * Set up video stream 95 | * @return {Promise} 96 | * @private 97 | */ 98 | async function _setupStream() { 99 | const video = document.getElementById('video'); 100 | video.srcObject = await _getStream(); 101 | 102 | return new Promise((resolve) => { 103 | video.onloadedmetadata = () => { 104 | resolve(video); 105 | }; 106 | }); 107 | } 108 | 109 | /** 110 | * Get video stream 111 | * @return {Promise} 112 | * @private 113 | */ 114 | function _getStream() { 115 | const config = { 116 | video: { 117 | deviceId: state.deviceId, 118 | audio: false, 119 | facingMode: 'user', 120 | }, 121 | }; 122 | return navigator.mediaDevices.getUserMedia(config); 123 | } 124 | 125 | /** 126 | * Load video stream 127 | * @param {string} deviceId 128 | * @return {Promise} 129 | * @private 130 | */ 131 | async function _loadVideo(deviceId) { 132 | state.deviceId = deviceId; 133 | state.video = await _setupStream(); 134 | state.videoWidth = state.video.videoWidth; 135 | state.videoHeight = state.video.videoHeight; 136 | state.video.play(); 137 | } 138 | 139 | /** 140 | * Draw to canvas from body-pix segmentation video 141 | * @param {HTMLCanvasElement} canvas 142 | * @param {SemanticPersonSegmentation} segmentation 143 | * @param {ImageData} originalImage 144 | * @private 145 | */ 146 | function _drawToCanvas(canvas, segmentation, originalImage) { 147 | const ctx = canvas.getContext('2d'); 148 | const width = canvas.width; 149 | const height = canvas.height; 150 | ctx.clearRect(0, 0, width, height); 151 | const imageData = ctx.getImageData(0, 0, width, height); 152 | const pixels = imageData.data; 153 | for (let y = 0; y < height; y++) { 154 | for (let x = 0; x < width; x++) { 155 | const index = (y * width + x) * 4; 156 | const segmentIndex = y * width + x; 157 | if (segmentation.data[segmentIndex] === 1) { 158 | pixels[index] = originalImage.data[index]; 159 | pixels[index + 1] = originalImage.data[index + 1]; 160 | pixels[index + 2] = originalImage.data[index + 2]; 161 | pixels[index + 3] = originalImage.data[index + 3]; 162 | } 163 | } 164 | } 165 | ctx.putImageData(imageData, 0, 0); 166 | } 167 | 168 | /** 169 | * Resize element 170 | * @param {Number} width 171 | * @param {Number} height 172 | * @private 173 | */ 174 | function _resizeElement(width = 640, height = 480) { 175 | const borderBox = document.querySelector('.border-box'); 176 | borderBox.style.width = `${width}px`; 177 | borderBox.style.height = `${height}px`; 178 | 179 | ['canvas', 'original-canvas', 'video'].forEach((id) => { 180 | const element = document.getElementById(id); 181 | 182 | const windowRatio = height / width; 183 | if (windowRatio > state.ratio()) { 184 | element.width = width; 185 | element.height = width * state.ratio(); 186 | } else { 187 | element.width = height / state.ratio(); 188 | element.height = height; 189 | } 190 | }); 191 | } 192 | 193 | /** 194 | * Set up resize guide element 195 | * @private 196 | */ 197 | function _setupResizeGuide() { 198 | const wrap = document.querySelector('.wrap'); 199 | const borderBox = document.querySelector('.border-box'); 200 | wrap.addEventListener('mouseover', () => { 201 | borderBox.style.display = 'block'; 202 | }); 203 | wrap.addEventListener('mouseleave', () => { 204 | borderBox.style.display = 'none'; 205 | }); 206 | } 207 | 208 | window.addEventListener('resize', () => { 209 | const width = window.innerWidth; 210 | const height = window.innerHeight; 211 | _resizeElement(width, height); 212 | 213 | const borderBox = document.querySelector('.border-box'); 214 | borderBox.style.display = 'block'; 215 | const body = document.querySelector('body'); 216 | body.classList.add('resizing'); 217 | setTimeout(() => { 218 | body.classList.remove('resizing'); 219 | borderBox.style.display = 'none'; 220 | }, 500); 221 | }); 222 | 223 | const videoManager = new VideoManager; 224 | const trayMenu = new TrayMenu(window); 225 | videoManager.getVideoList().then((list) => { 226 | state.deviceId = settings.getDeviceId() || list[0].deviceId; 227 | trayMenu.deviceId = state.deviceId; 228 | trayMenu.quality = settings.getBodyPixModel(); 229 | trayMenu.videoList = list; 230 | trayMenu.addEventListenerToVideoMenu(switchVideo); 231 | trayMenu.addEventListenerToQualityMenu(switchQuality); 232 | trayMenu.launch(); 233 | workload(state.deviceId); 234 | }); 235 | --------------------------------------------------------------------------------