├── .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 | 
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 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/source/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Popup
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
--------------------------------------------------------------------------------