├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── README.md ├── assets ├── StoreImages │ ├── bbc.png │ ├── change.png │ └── select.png └── bbc.gif ├── package-lock.json ├── package.json ├── source ├── assets │ └── icons │ │ ├── logo-128.png │ │ ├── logo-16.png │ │ ├── logo-32.png │ │ └── logo-48.png ├── manifest.json ├── options.html ├── popup.html ├── scripts │ ├── WebAccessibleRecources │ │ └── mediaSourceSwap.js │ ├── background.js │ ├── contentScript.js │ ├── options.js │ └── popup.js └── styles │ ├── base │ ├── _components.scss │ ├── _fonts.scss │ ├── _reset.scss │ └── _variables.scss │ ├── options.scss │ └── popup.scss ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | // Latest stable ECMAScript features 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "chrome": "49", 9 | "firefox": "52", 10 | "opera": "36", 11 | "edge": "79" 12 | } 13 | } 14 | ] 15 | ], 16 | "plugins": [ 17 | // Some transforms (such as object-rest-spread) 18 | // don't work without it: https://github.com/babel/babel/issues/7215 19 | ["@babel/plugin-transform-destructuring", { "useBuiltIns": true }], 20 | ["@babel/plugin-proposal-object-rest-spread", { "useBuiltIns": true }], 21 | [ 22 | // Polyfills the runtime needed for async/await and generators 23 | "@babel/plugin-transform-runtime", 24 | { 25 | "helpers": false, 26 | "regenerator": true 27 | } 28 | ] 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | extension/ 4 | .yarn/ 5 | .pnp.js -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "webextensions": true 5 | }, 6 | "extends": [ 7 | "google" 8 | ], 9 | "rules": {} 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore haters 2 | haters/ 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional REPL history 61 | .node_repl_history 62 | 63 | # Output of 'npm pack' 64 | *.tgz 65 | 66 | # Yarn Integrity file 67 | .yarn-integrity 68 | 69 | # dotenv environment variables file 70 | .env 71 | .env.test 72 | 73 | # parcel-bundler cache (https://parceljs.org/) 74 | .cache 75 | 76 | # next.js build output 77 | .next 78 | 79 | # nuxt.js build output 80 | .nuxt 81 | 82 | # react / gatsby 83 | public/ 84 | 85 | # vuepress build output 86 | .vuepress/dist 87 | 88 | # Serverless directories 89 | .serverless/ 90 | 91 | # FuseBox cache 92 | .fusebox/ 93 | 94 | # DynamoDB Local files 95 | .dynamodb/ 96 | 97 | ### Sass ### 98 | .sass-cache/ 99 | *.css.map 100 | *.sass.map 101 | *.scss.map 102 | 103 | ## Build directory 104 | extension 105 | dist/ 106 | .awcache 107 | 108 | # yarn 2 109 | # https://github.com/yarnpkg/berry/issues/454#issuecomment-530312089 110 | .yarn/* 111 | !.yarn/releases 112 | !.yarn/plugins 113 | .pnp.* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Virtual backgrounds for Google Meet

2 | 3 | Allows to use images as a background during Google Meet calls 4 | 5 | ![](https://github.com/Coderantine/VirtualBackgroundsForWeb/blob/master/assets/bbc.gif) 6 | 7 | ### Development 8 | 9 | Ensure you have 10 | 11 | - [Node.js](https://nodejs.org) 10 or later installed 12 | - [Yarn](https://yarnpkg.com) v1 or v2 installed 13 | 14 | Then run the following: 15 | 16 | - `yarn install` to install dependencies. 17 | - `yarn run dev:chrome` to start the development server for chrome extension 18 | - `yarn run build:chrome` to build chrome extension 19 | 20 | - **Load extension in browser** 21 | - Go to the browser address bar and type `chrome://extensions` 22 | - Check the `Developer Mode` button to enable it. 23 | - Click on the `Load Unpacked Extension…` button. 24 | - Select your extension’s extracted directory. 25 | -------------------------------------------------------------------------------- /assets/StoreImages/bbc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coderantine/VirtualBackgroundsForWeb/95039dcb8c90a1083b156b04ce693e6cc00cedeb/assets/StoreImages/bbc.png -------------------------------------------------------------------------------- /assets/StoreImages/change.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coderantine/VirtualBackgroundsForWeb/95039dcb8c90a1083b156b04ce693e6cc00cedeb/assets/StoreImages/change.png -------------------------------------------------------------------------------- /assets/StoreImages/select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coderantine/VirtualBackgroundsForWeb/95039dcb8c90a1083b156b04ce693e6cc00cedeb/assets/StoreImages/select.png -------------------------------------------------------------------------------- /assets/bbc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coderantine/VirtualBackgroundsForWeb/95039dcb8c90a1083b156b04ce693e6cc00cedeb/assets/bbc.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "virtual-backgrounds-for-web", 3 | "version": "1.0.0", 4 | "description": "VirtualBackgroundsForWeb", 5 | "repository": "https://github.com/babgev/VirtualBackgroundsForWeb.git", 6 | "engines": { 7 | "node": ">=10.0.0", 8 | "yarn": ">=1.0.0" 9 | }, 10 | "main": "source/scripts/background.js", 11 | "scripts": { 12 | "dev:chrome": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=chrome webpack --watch", 13 | "dev:firefox": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=firefox webpack --watch", 14 | "dev:opera": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=opera webpack --watch", 15 | "build:chrome": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=chrome webpack", 16 | "build:firefox": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=firefox webpack", 17 | "build:opera": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=opera webpack", 18 | "build": "yarn run build:chrome && yarn run build:firefox && yarn run build:opera", 19 | "lint": "eslint . --ext .js", 20 | "lint:fix": "eslint . --ext .js --fix" 21 | }, 22 | "dependencies": { 23 | "@babel/runtime": "^7.9.2", 24 | "@material/button": "^6.0.0", 25 | "@material/ripple": "^6.0.0", 26 | "@tensorflow-models/body-pix": "^2.0.5", 27 | "@tensorflow/tfjs-converter": "^1.7.3", 28 | "@tensorflow/tfjs-core": "^1.7.3", 29 | "advanced-css-reset": "^1.1.0", 30 | "webext-base-css": "^1.0.0", 31 | "webextension-polyfill": "^0.6.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.9.0", 35 | "@babel/plugin-proposal-object-rest-spread": "^7.9.0", 36 | "@babel/plugin-transform-destructuring": "^7.8.8", 37 | "@babel/plugin-transform-runtime": "^7.9.0", 38 | "@babel/preset-env": "^7.9.0", 39 | "@typescript-eslint/eslint-plugin": "^2.28.0", 40 | "@typescript-eslint/parser": "^2.28.0", 41 | "autoprefixer": "^9.7.5", 42 | "babel-eslint": "^10.1.0", 43 | "babel-loader": "^8.1.0", 44 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 45 | "clean-webpack-plugin": "^3.0.0", 46 | "copy-webpack-plugin": "^5.1.1", 47 | "cross-env": "^7.0.2", 48 | "css-loader": "^3.4.2", 49 | "eslint": "^6.8.0", 50 | "eslint-config-airbnb": "^18.1.0", 51 | "eslint-config-google": "^0.14.0", 52 | "eslint-config-prettier": "^6.10.1", 53 | "eslint-plugin-import": "^2.20.2", 54 | "eslint-plugin-jsx-a11y": "^6.2.3", 55 | "eslint-plugin-prettier": "^3.1.3", 56 | "eslint-plugin-react": "^7.19.0", 57 | "eslint-plugin-react-hooks": "^2.5.1", 58 | "extract-loader": "^5.0.1", 59 | "file-loader": "^6.0.0", 60 | "html-webpack-plugin": "^4.0.3", 61 | "node-sass": "^4.13.1", 62 | "optimize-css-assets-webpack-plugin": "^5.0.3", 63 | "postcss-loader": "^3.0.0", 64 | "prettier": "^2.0.4", 65 | "resolve-url-loader": "^3.1.1", 66 | "sass-loader": "^8.0.2", 67 | "terser-webpack-plugin": "^2.3.5", 68 | "webpack": "^4.42.1", 69 | "webpack-cli": "^3.3.11", 70 | "webpack-extension-reloader": "^1.1.4", 71 | "webpack-fix-style-only-entries": "^0.4.0", 72 | "wext-manifest-loader": "^1.1.2", 73 | "wext-manifest-webpack-plugin": "^1.0.3", 74 | "zip-webpack-plugin": "^3.0.0" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /source/assets/icons/logo-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coderantine/VirtualBackgroundsForWeb/95039dcb8c90a1083b156b04ce693e6cc00cedeb/source/assets/icons/logo-128.png -------------------------------------------------------------------------------- /source/assets/icons/logo-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coderantine/VirtualBackgroundsForWeb/95039dcb8c90a1083b156b04ce693e6cc00cedeb/source/assets/icons/logo-16.png -------------------------------------------------------------------------------- /source/assets/icons/logo-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coderantine/VirtualBackgroundsForWeb/95039dcb8c90a1083b156b04ce693e6cc00cedeb/source/assets/icons/logo-32.png -------------------------------------------------------------------------------- /source/assets/icons/logo-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coderantine/VirtualBackgroundsForWeb/95039dcb8c90a1083b156b04ce693e6cc00cedeb/source/assets/icons/logo-48.png -------------------------------------------------------------------------------- /source/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Virtual backgrounds for Google Meet", 4 | "version": "0.0.1", 5 | "description": "Virtual backgrounds for Google Meet", 6 | "homepage_url": "https://github.com/babgev/VirtualBackgroundsForWeb", 7 | "short_name": "VirtualBackgroundsForMeet", 8 | "permissions": [ 9 | "storage", 10 | "unlimitedStorage", 11 | "https://meet.google.com/" 12 | ], 13 | "__chrome|firefox__author": "Coderantine", 14 | "__opera__developer": { 15 | "name": "Coderantine" 16 | }, 17 | "__firefox__applications": { 18 | "gecko": { 19 | "id": "{754FB1AD-CC3B-4856-B6A0-7786F8CA9D17}" 20 | } 21 | }, 22 | "__chrome__minimum_chrome_version": "49", 23 | "__opera__minimum_opera_version": "36", 24 | "browser_action": { 25 | "default_popup": "popup.html", 26 | "default_icon": { 27 | "16": "assets/icons/logo-16.png", 28 | "32": "assets/icons/logo-32.png", 29 | "48": "assets/icons/logo-48.png", 30 | "128": "assets/icons/logo-128.png" 31 | }, 32 | "default_title": "Virtual backgrounds for Google Meet", 33 | "__chrome|opera__chrome_style": false, 34 | "__firefox__browser_style": false 35 | }, 36 | "background": { 37 | "scripts": [ 38 | "js/background.bundle.js" 39 | ], 40 | "__chrome|opera__persistent": false 41 | }, 42 | "content_scripts": [{ 43 | "run_at": "document_start", 44 | "matches": [ 45 | "*://meet.google.com/*" 46 | ], 47 | "js": [ 48 | "js/contentScript.bundle.js" 49 | ] 50 | }], 51 | "web_accessible_resources": ["js/mediaSourceSwap.js"], 52 | "content_security_policy": "script-src 'self' https://unpkg.com; object-src 'self';" 53 | } -------------------------------------------------------------------------------- /source/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Options 8 | 9 | 10 | 11 | 12 |
13 |

14 |
15 | 16 |

17 |

18 | 22 |

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /source/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Popup 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 |
25 | 29 | 30 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /source/scripts/WebAccessibleRecources/mediaSourceSwap.js: -------------------------------------------------------------------------------- 1 | var realUserMediaCall = window.navigator.mediaDevices.getUserMedia; 2 | 3 | window.navigator.mediaDevices.getUserMedia = async function (constraints) { 4 | if (constraints.video.deviceId) { 5 | var canvas = document.getElementById("sourceCanvas"); 6 | var stream = await realUserMediaCall.call( 7 | navigator.mediaDevices, 8 | constraints 9 | ); 10 | var res = canvas.captureStream(60); 11 | var videoTrack = res.getVideoTracks()[0]; 12 | var videoTrackStop = videoTrack.stop; 13 | videoTrack.stop = function () { 14 | stream.getVideoTracks()[0].stop(); 15 | videoTrackStop.call(videoTrack); 16 | }; 17 | 18 | var videoElement = tryGetVideoElement(); 19 | videoElement.height = 400; 20 | videoElement.width = 400; 21 | videoElement.srcObject = stream; 22 | 23 | return res; 24 | } else { 25 | return await realUserMediaCall.call(navigator.mediaDevices, constraints); 26 | } 27 | }; 28 | 29 | function tryGetVideoElement() { 30 | var existingElement = document.getElementById("realVideo"); 31 | if (existingElement) { 32 | return existingElement; 33 | } 34 | var realVideo = document.createElement("video"); 35 | realVideo.setAttribute("id", "realVideo"); 36 | realVideo.setAttribute("style", "display:none"); 37 | document.documentElement.appendChild(realVideo); 38 | return realVideo; 39 | } 40 | -------------------------------------------------------------------------------- /source/scripts/background.js: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill'; 2 | 3 | const photos = [ 4 | { 5 | src: "https://images.unsplash.com/photo-1524758631624-e2822e304c36?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&&w=400&fit=crop", 6 | }, 7 | { 8 | src: "https://images.unsplash.com/photo-1484101403633-562f891dc89a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&w=400&fit=crop", 9 | }, 10 | { 11 | src: "https://images.unsplash.com/photo-1553503995-b6aefccad354?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&w=400&fit=crop", 12 | }, 13 | { 14 | src: "https://images.unsplash.com/photo-1488409688217-e6053b1e8f42?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&w=400&fit=crop", 15 | }, 16 | { 17 | src: "https://images.unsplash.com/photo-1520106392146-ef585c111254?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&w=400&fit=crop", 18 | }, 19 | { 20 | src: "https://images.unsplash.com/photo-1521334884684-d80222895322?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&w=400&fit=crop", 21 | }, 22 | { 23 | src: "https://images.unsplash.com/photo-1548154049-18dfc3fcb18b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&w=400&fit=crop", 24 | }, 25 | { 26 | src: "https://images.unsplash.com/photo-1531616918159-0c11198cd033?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&w=400&fit=crop", 27 | }, 28 | { 29 | src: "https://images.unsplash.com/photo-1549488344-1f9b8d2bd1f3?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&w=400&fit=crop", 30 | }, 31 | { 32 | src: "https://images.unsplash.com/photo-1465865523598-a834aac5d3fa?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&w=400&fit=crop", 33 | }, 34 | ]; 35 | 36 | 37 | browser.runtime.onInstalled.addListener(async () => { 38 | browser.storage.local.set({'photos': photos}); 39 | browser.storage.sync.set({'gameIsOn': true}); 40 | browser.storage.local.set({'backgroundSrc': photos[0]}); 41 | }); 42 | -------------------------------------------------------------------------------- /source/scripts/contentScript.js: -------------------------------------------------------------------------------- 1 | import * as bodyPix from "@tensorflow-models/body-pix"; 2 | import browser from "webextension-polyfill"; 3 | 4 | const state = { 5 | video: null, 6 | image: null, 7 | net: null, 8 | canvas: null, 9 | backgroundImage: null, 10 | backgroundSrc: null, 11 | gameIsOn: true, 12 | maskingFrameCounter: 0, 13 | maskCache: null, 14 | }; 15 | 16 | async function overrideGetUserMedia() { 17 | var canvas = document.createElement("canvas"); 18 | canvas.setAttribute("id", "sourceCanvas"); 19 | canvas.setAttribute("style", "display:none"); 20 | document.documentElement.appendChild(canvas); 21 | state.canvas = canvas; 22 | 23 | injectMediaSourceSwap(); 24 | 25 | // set up the mutation observer 26 | var observer = new MutationObserver(function (mutations, me) { 27 | // `mutations` is an array of mutations that occurred 28 | // `me` is the MutationObserver instance 29 | var canvas = document.getElementById("realVideo"); 30 | if (canvas) { 31 | realVideoAdded(canvas); 32 | me.disconnect(); // stop observing 33 | return; 34 | } 35 | }); 36 | 37 | // start observing 38 | observer.observe(document, { 39 | childList: true, 40 | subtree: true, 41 | }); 42 | } 43 | 44 | function toMask(personOrPartSegmentation) { 45 | if ( 46 | Array.isArray(personOrPartSegmentation) && 47 | personOrPartSegmentation.length === 0 48 | ) { 49 | return null; 50 | } 51 | var multiPersonOrPartSegmentation; 52 | if (!Array.isArray(personOrPartSegmentation)) { 53 | multiPersonOrPartSegmentation = [personOrPartSegmentation]; 54 | } else { 55 | multiPersonOrPartSegmentation = personOrPartSegmentation; 56 | } 57 | var width = multiPersonOrPartSegmentation[0].width; 58 | var height = multiPersonOrPartSegmentation[0].height; 59 | var bytes = new Uint8ClampedArray(state.image.data); 60 | for (var i = 0; i < height; i += 1) { 61 | for (var j = 0; j < width; j += 1) { 62 | var n = i * width + j; 63 | for (var k = 0; k < multiPersonOrPartSegmentation.length; k++) { 64 | if (multiPersonOrPartSegmentation[k].data[n] == 1) { 65 | bytes[4 * n] = 0; 66 | bytes[4 * n + 1] = 0; 67 | bytes[4 * n + 2] = 0; 68 | bytes[4 * n + 3] = 0; 69 | } 70 | } 71 | } 72 | } 73 | return new ImageData(bytes, width, height); 74 | } 75 | 76 | function realVideoAdded(video) { 77 | state.video = video; 78 | 79 | video.onloadedmetadata = function () { 80 | var background = new Image(); 81 | state.backgroundImage = background; 82 | background.setAttribute("style", "object-fit: cover"); 83 | state.video.width = state.video.videoWidth; 84 | state.video.height = state.video.videoHeight; 85 | state.canvas.width = state.video.width; 86 | state.canvas.height = state.video.height; 87 | background.height = state.video.height; 88 | background.width = state.video.width; 89 | background.crossOrigin = "Anonymous"; 90 | 91 | function outputsize() { 92 | state.backgroundImage.width = state.video.width; 93 | state.backgroundImage.height = state.video.height; 94 | } 95 | new ResizeObserver(outputsize).observe(state.video); 96 | 97 | background.onload = function () { 98 | var imageCanvas = document.createElement("canvas"); 99 | imageCanvas.width = background.width; 100 | imageCanvas.height = background.height; 101 | var ctx = imageCanvas.getContext("2d"); 102 | ctx.drawImage(background, 0, 0, background.width, background.height); 103 | 104 | var imgWidth = background.width || background.naturalWidth; 105 | var imgHeight = background.height || background.naturalHeight; 106 | var imageData = ctx.getImageData(0, 0, imgWidth, imgHeight); 107 | state.image = imageData; 108 | 109 | state.video.play(); 110 | segmentBodyInRealTime(); 111 | }; 112 | 113 | if (state.backgroundSrc.includes("unsplash")){ 114 | state.backgroundSrc = 115 | state.backgroundSrc.split("&w=")[0] + 116 | "&fit=crop&w=" + 117 | state.video.width; 118 | } 119 | 120 | background.src = state.backgroundSrc; 121 | }; 122 | } 123 | 124 | async function start() { 125 | await loadState(); 126 | overrideGetUserMedia(); 127 | await loadBodyPix(); 128 | } 129 | 130 | async function loadState() { 131 | state.gameIsOn = (await browser.storage.sync.get(["gameIsOn"])).gameIsOn; 132 | debugger; 133 | let backImage = (await browser.storage.local.get(["backgroundSrc"])) 134 | .backgroundSrc; 135 | state.backgroundSrc = backImage.src; 136 | } 137 | 138 | function injectMediaSourceSwap() { 139 | // from https://stackoverflow.com/questions/9515704/insert-code-into-the-page-context-using-a-content-script 140 | var script = document.createElement("script"); 141 | script.src = browser.runtime.getURL("js/mediaSourceSwap.js"); 142 | script.onload = function () { 143 | script.remove(); 144 | }; 145 | (document.head || document.documentElement).appendChild(script); 146 | } 147 | 148 | function segmentBodyInRealTime() { 149 | async function bodySegmentationFrame() { 150 | if (state.gameIsOn) { 151 | if (state.maskingFrameCounter == 0) { 152 | debugger; 153 | var multiPersonSegmentation = await estimateSegmentation(); 154 | state.maskCache = toMask(multiPersonSegmentation); 155 | } 156 | bodyPix.drawMask(state.canvas, state.video, state.maskCache, 1, 0, false); 157 | state.maskingFrameCounter++; 158 | if (state.maskingFrameCounter == 40) { 159 | state.maskingFrameCounter = 0; 160 | } 161 | } else { 162 | var ctx = state.canvas.getContext("2d"); 163 | ctx.drawImage(state.video, 0, 0); 164 | } 165 | 166 | requestAnimationFrame(bodySegmentationFrame); 167 | } 168 | 169 | bodySegmentationFrame(); 170 | } 171 | 172 | async function loadBodyPix() { 173 | state.net = await bodyPix.load({ 174 | architecture: "MobileNetV1", 175 | outputStride: 16, 176 | multiplier: 1, 177 | quantBytes: 2, 178 | }); 179 | } 180 | async function estimateSegmentation() { 181 | return await state.net?.segmentPerson(state.video, { 182 | internalResolution: "low", 183 | segmentationThreshold: 0.8, 184 | maxDetections: 1, 185 | scoreThreshold: 0.3, 186 | nmsRadius: 20, 187 | }); 188 | } 189 | 190 | browser.storage.onChanged.addListener(function (changes) { 191 | if (changes["backgroundSrc"]) { 192 | debugger; 193 | var backgroundImg = changes["backgroundSrc"].newValue; 194 | var backgroundImgSource = backgroundImg.src; 195 | if (!backgroundImg.isCustom) { 196 | backgroundImgSource = 197 | backgroundImgSource.split("&w=")[0] + 198 | "&fit=crop&w=" + 199 | state.video.width; 200 | } 201 | 202 | state.backgroundSrc = backgroundImgSource; 203 | state.backgroundImage.src = backgroundImgSource; 204 | state.backgroundImage.height = state.video.height; 205 | state.backgroundImage.width = state.video.width; 206 | } 207 | if (changes["gameIsOn"]) { 208 | state.gameIsOn = changes["gameIsOn"].newValue; 209 | } 210 | }); 211 | start(); 212 | -------------------------------------------------------------------------------- /source/scripts/options.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /source/scripts/popup.js: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill'; 2 | 3 | var photos = null; 4 | const image_container = document.querySelector(".images") 5 | 6 | const createImageGallery = images => { 7 | photos = images; 8 | let output = "" 9 | for (var i = 0; i< images.length; i++){ 10 | output += `
` 11 | } 12 | 13 | image_container.innerHTML = output 14 | } 15 | 16 | const changeImage = e => { 17 | if (e.target.src) { 18 | clearExistingSelection(); 19 | e.target.parentElement.classList.add("selected_image"); 20 | browser.storage.local.set({"backgroundSrc": {"src":e.target.src, "isCustom":e.target.getAttribute("isCustom")}}); 21 | } 22 | } 23 | 24 | function loadPhotos(){ 25 | browser.storage.local.get(['photos']) 26 | .then(result => createImageGallery(result.photos)); 27 | browser.storage.local.get(['backgroundSrc']) 28 | .then(res =>{ 29 | clearExistingSelection(); 30 | debugger; 31 | function getChildImage(element){ 32 | for (var i = 0; i < element.childNodes.length; i++) { 33 | if (element.childNodes[i].tagName == "IMG") { 34 | return element.childNodes[i]; 35 | } 36 | } 37 | } 38 | var existingImages = document.getElementsByClassName("image_item"); 39 | debugger; 40 | 41 | for (var i = 0; i < existingImages.length; i++) { 42 | var childImage = getChildImage(existingImages[i]); 43 | 44 | if (childImage.src.includes(res.backgroundSrc.src)){ 45 | existingImages[i].classList.add("selected_image"); 46 | return; 47 | } 48 | } 49 | }) 50 | } 51 | 52 | function clearExistingSelection(){ 53 | var existingImages = document.getElementsByClassName("image_item"); 54 | for (var i = 0; i < existingImages.length; i++) { 55 | existingImages[i].classList.remove("selected_image"); 56 | } 57 | } 58 | 59 | image_container.addEventListener("click", changeImage) 60 | const switchControl = new mdc.switchControl.MDCSwitch.attachTo(document.querySelector('#main_switch')); 61 | browser.storage.sync 62 | .get(["gameIsOn"]) 63 | .then((result) => switchControl.checked = result.gameIsOn); 64 | switchControl.listen('change', ()=>{ 65 | browser.storage.sync.set({"gameIsOn": switchControl.checked}); 66 | }) 67 | loadPhotos(); 68 | 69 | window.onload = function() { 70 | let imageInput = document.getElementById('image_input'); 71 | document.getElementById('image_input_button').addEventListener('click', openDialog); 72 | imageInput.addEventListener("change", imageUploaded); 73 | function openDialog() { 74 | imageInput.click(); 75 | } 76 | 77 | function imageUploaded(){ 78 | if (imageInput.files.length != 0){ 79 | for (let i = 0; i < imageInput.files.length; i++) { 80 | debugger; 81 | const img = imageInput.files[i]; 82 | var reader = new FileReader(); 83 | reader.onload = function(){ 84 | var dataURL = reader.result; 85 | addToPhotos(dataURL); 86 | }; 87 | reader.readAsDataURL(img); 88 | } 89 | } 90 | } 91 | 92 | function addToPhotos(url){ 93 | browser.storage.local.get(['photos']) 94 | .then(result => { 95 | var array = result["photos"]?result["photos"]:[]; 96 | debugger; 97 | var newItem = {"src":url, "isCustom":true}; 98 | array.unshift(newItem); 99 | browser.storage.local.set({'photos':array}); 100 | browser.storage.local.set({'backgroundSrc':newItem}); 101 | loadPhotos(); 102 | }); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /source/styles/base/_components.scss: -------------------------------------------------------------------------------- 1 | .d-none { 2 | display: none !important; 3 | } 4 | 5 | .v-none { 6 | visibility: hidden !important; 7 | } 8 | 9 | .text-center { 10 | text-align: center; 11 | } 12 | 13 | .mt-3 { 14 | margin-top: 3em; 15 | } 16 | 17 | .mb-2 { 18 | margin-bottom: 2em; 19 | } 20 | 21 | .my-2 { 22 | margin-top: 1em; 23 | margin-bottom: 2em; 24 | } 25 | 26 | .py-2 { 27 | padding: 1em 24px; 28 | } 29 | -------------------------------------------------------------------------------- /source/styles/base/_fonts.scss: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Nunito:400,600"); 2 | -------------------------------------------------------------------------------- /source/styles/base/_reset.scss: -------------------------------------------------------------------------------- 1 | @import '~advanced-css-reset/dist/reset.css'; 2 | -------------------------------------------------------------------------------- /source/styles/base/_variables.scss: -------------------------------------------------------------------------------- 1 | // colors 2 | $black: #0d0d0d; 3 | $greyWhite: #f3f3f3; 4 | $skyBlue: #8892b0; 5 | 6 | // fonts 7 | $nunito: "Nunito", sans-serif; 8 | 9 | // font weights 10 | $thin: 100; 11 | $exlight: 200; 12 | $light: 300; 13 | $regular: 400; 14 | $medium: 500; 15 | $semibold: 600; 16 | $bold: 700; 17 | $exbold: 800; 18 | $exblack: 900; 19 | 20 | // other 21 | -------------------------------------------------------------------------------- /source/styles/options.scss: -------------------------------------------------------------------------------- 1 | @import "base/fonts"; 2 | @import "base/reset"; 3 | @import "base/variables"; 4 | 5 | @import "~webext-base-css/webext-base.css"; 6 | -------------------------------------------------------------------------------- /source/styles/popup.scss: -------------------------------------------------------------------------------- 1 | @import "base/fonts"; 2 | @import "base/variables"; 3 | 4 | body{ 5 | font-family: 'Roboto', sans-serif; 6 | margin: 0; 7 | } 8 | 9 | .heading{ 10 | height: 60px; 11 | text-align: center; 12 | padding: 0; 13 | border-bottom: 4px solid #00796b; 14 | width: 100%; 15 | } 16 | 17 | #switch_label{ 18 | color: #00796b; 19 | font-size: 18px; 20 | } 21 | 22 | #main_switch{ 23 | margin-top: 21px; 24 | margin-left: 5px; 25 | } 26 | 27 | #popup { 28 | 29 | min-width: 300px; 30 | padding: 20px; 31 | 32 | 33 | h2 { 34 | font-size: 25px; 35 | text-align: center; 36 | } 37 | 38 | .images { 39 | grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); 40 | justify-content: center; 41 | align-items: center; 42 | } 43 | 44 | .image_item { 45 | border-radius: 8px; 46 | box-shadow: 0 0 5px #333; 47 | width: 100%; 48 | height: 10rem; 49 | display: block; 50 | margin: auto; 51 | margin-bottom: 1rem; 52 | cursor: pointer; 53 | } 54 | 55 | .image_item_inner{ 56 | height: 10rem; 57 | width: 100%; 58 | border-radius: 8px; 59 | object-fit: cover; 60 | } 61 | 62 | .image_item:hover{ 63 | box-shadow: 0 0 10px #333; 64 | transform: scale(1.025); 65 | } 66 | 67 | .selected_image{ 68 | border: 3px solid #00796b; 69 | .image_item_inner{ 70 | border-radius: 5px; 71 | } 72 | } 73 | } 74 | 75 | .mdc-fab--extended { 76 | position: fixed; 77 | bottom: 1rem; 78 | right: 1rem; 79 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const ZipPlugin = require('zip-webpack-plugin'); 4 | const TerserPlugin = require('terser-webpack-plugin'); 5 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const {CleanWebpackPlugin} = require('clean-webpack-plugin'); 8 | const ExtensionReloader = require('webpack-extension-reloader'); 9 | const WextManifestWebpackPlugin = require('wext-manifest-webpack-plugin'); 10 | const FixStyleOnlyEntriesPlugin = require('webpack-fix-style-only-entries'); 11 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 12 | 13 | const nodeEnv = process.env.NODE_ENV || 'development'; 14 | const targetBrowser = process.env.TARGET_BROWSER; 15 | 16 | const extensionReloaderPlugin = 17 | nodeEnv === 'development' 18 | ? new ExtensionReloader({ 19 | port: 9090, 20 | reloadPage: true, 21 | entries: { 22 | // TODO: reload manifest on update 23 | contentScript: 'contentScript', 24 | background: 'background', 25 | extensionPage: ['popup', 'options'], 26 | }, 27 | }) 28 | : () => { 29 | this.apply = () => {}; 30 | }; 31 | 32 | const getExtensionFileType = (browser) => { 33 | if (browser === 'opera') { 34 | return 'crx'; 35 | } 36 | if (browser === 'firefox') { 37 | return 'xpi'; 38 | } 39 | 40 | return 'zip'; 41 | }; 42 | 43 | module.exports = { 44 | devtool: false, // https://github.com/webpack/webpack/issues/1194#issuecomment-560382342 45 | 46 | mode: nodeEnv, 47 | 48 | stats: { 49 | all: false, 50 | builtAt: true, 51 | errors: true, 52 | hash: true, 53 | }, 54 | 55 | entry: { 56 | manifest: './source/manifest.json', 57 | background: './source/scripts/background.js', 58 | contentScript: './source/scripts/contentScript.js', 59 | popup: './source/scripts/popup.js', 60 | options: './source/scripts/options.js', 61 | styles: ['./source/styles/popup.scss', './source/styles/options.scss'], 62 | }, 63 | 64 | output: { 65 | path: path.resolve(__dirname, 'extension', targetBrowser), 66 | filename: 'js/[name].bundle.js', 67 | }, 68 | 69 | module: { 70 | rules: [ 71 | { 72 | type: 'javascript/auto', // prevent webpack handling json with its own loaders, 73 | test: /manifest\.json$/, 74 | use: { 75 | loader: 'wext-manifest-loader', 76 | options: { 77 | usePackageJSONVersion: true, // set to false to not use package.json version for manifest 78 | }, 79 | }, 80 | }, 81 | { 82 | test: /.(js|jsx)$/, 83 | include: [path.resolve(__dirname, 'source/scripts')], 84 | loader: 'babel-loader', 85 | 86 | options: { 87 | plugins: ['syntax-dynamic-import'], 88 | 89 | presets: [ 90 | [ 91 | '@babel/preset-env', 92 | { 93 | modules: false, 94 | }, 95 | ], 96 | ], 97 | }, 98 | }, 99 | { 100 | test: /\.scss$/, 101 | use: [ 102 | { 103 | loader: 'file-loader', 104 | options: { 105 | name: '[name].css', 106 | context: './source/styles/', 107 | outputPath: 'css/', 108 | }, 109 | }, 110 | 'extract-loader', 111 | { 112 | loader: 'css-loader', 113 | options: { 114 | sourceMap: nodeEnv === 'development', 115 | }, 116 | }, 117 | { 118 | loader: 'postcss-loader', 119 | options: { 120 | ident: 'postcss', 121 | // eslint-disable-next-line global-require 122 | plugins: [require('autoprefixer')()], 123 | }, 124 | }, 125 | 'resolve-url-loader', 126 | 'sass-loader', 127 | ], 128 | }, 129 | ], 130 | }, 131 | 132 | plugins: [ 133 | new webpack.ProgressPlugin(), 134 | // Generate manifest.json 135 | new WextManifestWebpackPlugin(), 136 | // Generate sourcemaps 137 | new webpack.SourceMapDevToolPlugin({filename: false}), 138 | // Remove style entries js bundle 139 | new FixStyleOnlyEntriesPlugin({silent: true}), 140 | new webpack.EnvironmentPlugin(['NODE_ENV', 'TARGET_BROWSER']), 141 | new CleanWebpackPlugin({ 142 | cleanOnceBeforeBuildPatterns: [ 143 | path.join(process.cwd(), `extension/${targetBrowser}`), 144 | path.join( 145 | process.cwd(), 146 | `extension/${targetBrowser}.${getExtensionFileType(targetBrowser)}` 147 | ), 148 | ], 149 | cleanStaleWebpackAssets: false, 150 | verbose: true, 151 | }), 152 | new HtmlWebpackPlugin({ 153 | template: 'source/options.html', 154 | // inject: false, 155 | chunks: ['options'], 156 | filename: 'options.html', 157 | }), 158 | new HtmlWebpackPlugin({ 159 | template: 'source/popup.html', 160 | // inject: false, 161 | chunks: ['popup'], 162 | filename: 'popup.html', 163 | }), 164 | new CopyWebpackPlugin([{from: 'source/assets', to: 'assets'}]), 165 | extensionReloaderPlugin, 166 | // copy WebAccessibleRecources to js 167 | new CopyWebpackPlugin([{from: 'source/scripts/WebAccessibleRecources', to: 'js'}]), 168 | ], 169 | 170 | optimization: { 171 | minimizer: [ 172 | new TerserPlugin({ 173 | cache: true, 174 | parallel: true, 175 | terserOptions: { 176 | output: { 177 | comments: false, 178 | }, 179 | }, 180 | extractComments: false, 181 | }), 182 | new OptimizeCSSAssetsPlugin({ 183 | cssProcessorPluginOptions: { 184 | preset: ['default', {discardComments: {removeAll: true}}], 185 | }, 186 | }), 187 | new ZipPlugin({ 188 | path: path.resolve(__dirname, 'extension'), 189 | extension: `${getExtensionFileType(targetBrowser)}`, 190 | filename: `${targetBrowser}`, 191 | }), 192 | ], 193 | }, 194 | }; 195 | --------------------------------------------------------------------------------