├── .babelrc ├── .editorconfig ├── .gitignore ├── LICENSE.txt ├── README.md ├── config ├── chrome.json ├── development.json ├── firefox.json ├── opera.json └── production.json ├── gulpfile.babel.js ├── manifest.json ├── package-lock.json ├── package.json ├── resources ├── CWS-dl.png ├── FacePause.png ├── FacePause.svg ├── chrome-promo │ ├── large.png │ ├── marquee.png │ └── small.png └── extension-assets.sketch └── src ├── _locales └── en │ └── messages.json ├── icons ├── icon-64.png ├── icon128.png ├── icon16.png └── icon48.png ├── images └── .gitkeep ├── scripts ├── background.js ├── contentscript.js └── livereload.js └── styles ├── inject.scss ├── videocam.svg └── videocam_off.svg /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = false 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | insert_final_newline = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | temp 3 | .tmp 4 | dist 5 | .sass-cache 6 | .DS_Store 7 | build -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mattias Hemmingsson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Face Pause 5 |
6 | FacePause 7 |

8 | 9 | 10 | #### Look Away to Pause Youtube - Experimental Chrome Extension 11 | 12 | 13 | Chrome (v56+) has a new FaceDetector API which basically lets you detect faces in images easily, so what if we could pause Youtube when you look away or go for a sandwich 🥪? 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | ▶️ [Watch a video demo](https://youtu.be/CL_B7iVpg4M) 22 |
23 | 24 | 25 | ## How to install 26 | Download the Zip from this [release](https://github.com/Hemmingsson/Face-Pause/releases/tag/0.1), unzip it and load it as an unpacked extension in Chrome. 27 | 28 | ## Notice 29 | 30 | - 🙀 I don’t trust my webcam and I have it covered, see this more as an experiment of Chromes new technology, than a product you'd use every day. 31 | - 🏴 To get the extension to work you’ll need to enable Chrome Experimental Features here:
32 | `chrome://flags#enable-experimental-web-platform-features` 33 | 34 | - 💡If you’re in a dark setting it will probably be a bit buggy, as FaceDetector API is still not great in bad light. 35 | 36 | ## Development 37 | 38 | ### Installation 39 | 1. Clone the repository `git clone https://github.com/Hemmingsson/Face-Pause` 40 | 2. Run `npm install` 41 | 3. Run `npm run build` 42 | 43 | ##### Load the extension in Chrome 44 | 1. Open Chrome browser and navigate to chrome://extensions 45 | 2. Select "Developer Mode" and then click "Load unpacked extension..." 46 | 3. From the file browser, choose to `Face-Pause/build/chrome` 47 | 48 | 49 | ### Developing 50 | The following task can be used when you want to start developing the extension 51 | 52 | - `npm run chrome-watch` 53 | 54 | 55 | ---- 56 | 57 | 58 | 59 | FacePause - Look away to pause YouTube videos | Product Hunt Embed 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /config/chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": "chrome" 3 | } -------------------------------------------------------------------------------- /config/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": "development" 3 | } -------------------------------------------------------------------------------- /config/firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": "firefox" 3 | } -------------------------------------------------------------------------------- /config/opera.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": "opera" 3 | } -------------------------------------------------------------------------------- /config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": "production" 3 | } -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import gulp from 'gulp' 3 | import {merge} from 'event-stream' 4 | import browserify from 'browserify' 5 | import source from 'vinyl-source-stream' 6 | import buffer from 'vinyl-buffer' 7 | import preprocessify from 'preprocessify' 8 | import gulpif from 'gulp-if' 9 | 10 | const $ = require('gulp-load-plugins')() 11 | 12 | var production = process.env.NODE_ENV === 'production' 13 | var target = process.env.TARGET || 'chrome' 14 | var environment = process.env.NODE_ENV || 'development' 15 | 16 | var generic = JSON.parse(fs.readFileSync(`./config/${environment}.json`)) 17 | var specific = JSON.parse(fs.readFileSync(`./config/${target}.json`)) 18 | var context = Object.assign({}, generic, specific) 19 | 20 | var manifest = { 21 | dev: { 22 | 'background': { 23 | 'scripts': [ 24 | 'scripts/livereload.js', 25 | 'scripts/background.js' 26 | ] 27 | } 28 | }, 29 | 30 | firefox: { 31 | 'applications': { 32 | 'gecko': { 33 | 'id': 'my-app-id@mozilla.org' 34 | } 35 | } 36 | } 37 | } 38 | 39 | // Tasks 40 | gulp.task('clean', () => { 41 | return pipe(`./build/${target}`, $.clean()) 42 | }) 43 | 44 | gulp.task('build', (cb) => { 45 | $.runSequence('clean', 'styles', 'ext', cb) 46 | }) 47 | 48 | gulp.task('watch', ['build'], () => { 49 | $.livereload.listen() 50 | 51 | gulp.watch(['./src/**/*']).on('change', () => { 52 | $.runSequence('build', $.livereload.reload) 53 | }) 54 | }) 55 | 56 | gulp.task('default', ['build']) 57 | 58 | gulp.task('ext', ['manifest', 'js'], () => { 59 | return mergeAll(target) 60 | }) 61 | 62 | // ----------------- 63 | // COMMON 64 | // ----------------- 65 | gulp.task('js', () => { 66 | return buildJS(target) 67 | }) 68 | 69 | gulp.task('styles', () => { 70 | return gulp.src('src/styles/**/*.scss') 71 | .pipe($.plumber()) 72 | .pipe($.sass.sync({ 73 | outputStyle: 'expanded', 74 | precision: 10, 75 | includePaths: ['.'] 76 | }).on('error', $.sass.logError)) 77 | .pipe(gulp.dest(`build/${target}/styles`)) 78 | }) 79 | 80 | gulp.task('manifest', () => { 81 | return gulp.src('./manifest.json') 82 | .pipe(gulpif(!production, $.mergeJson({ 83 | fileName: 'manifest.json', 84 | jsonSpace: ' '.repeat(4), 85 | endObj: manifest.dev 86 | }))) 87 | .pipe(gulpif(target === 'firefox', $.mergeJson({ 88 | fileName: 'manifest.json', 89 | jsonSpace: ' '.repeat(4), 90 | endObj: manifest.firefox 91 | }))) 92 | .pipe(gulp.dest(`./build/${target}`)) 93 | }) 94 | 95 | // ----------------- 96 | // DIST 97 | // ----------------- 98 | gulp.task('dist', (cb) => { 99 | $.runSequence('build', 'zip', cb) 100 | }) 101 | 102 | gulp.task('zip', () => { 103 | return pipe(`./build/${target}/**/*`, $.zip(`${target}.zip`), './dist') 104 | }) 105 | 106 | // Helpers 107 | function pipe (src, ...transforms) { 108 | return transforms.reduce((stream, transform) => { 109 | const isDest = typeof transform === 'string' 110 | return stream.pipe(isDest ? gulp.dest(transform) : transform) 111 | }, gulp.src(src)) 112 | } 113 | 114 | function mergeAll (dest) { 115 | return merge( 116 | pipe('./src/icons/**/*', `./build/${dest}/icons`), 117 | pipe(['./src/_locales/**/*'], `./build/${dest}/_locales`), 118 | pipe([`./src/images/${target}/**/*`], `./build/${dest}/images`), 119 | pipe(['./src/images/shared/**/*'], `./build/${dest}/images`), 120 | pipe(['./src/**/*.html'], `./build/${dest}`) 121 | ) 122 | } 123 | 124 | function buildJS (target) { 125 | const files = [ 126 | 'background.js', 127 | 'contentscript.js', 128 | 'livereload.js' 129 | ] 130 | 131 | let tasks = files.map(file => { 132 | return browserify({ 133 | entries: 'src/scripts/' + file, 134 | debug: true 135 | }) 136 | .transform('babelify', { presets: ['es2015'] }) 137 | .transform(preprocessify, { 138 | includeExtensions: ['.js'], 139 | context: context 140 | }) 141 | .bundle() 142 | .pipe(source(file)) 143 | .pipe(buffer()) 144 | .pipe(gulpif(!production, $.sourcemaps.init({ loadMaps: true }))) 145 | .pipe(gulpif(!production, $.sourcemaps.write('./'))) 146 | .pipe(gulpif(production, $.uglify({ 147 | 'mangle': false, 148 | 'output': { 149 | 'ascii_only': true 150 | } 151 | }))) 152 | .pipe(gulp.dest(`build/${target}/scripts`)) 153 | }) 154 | 155 | return merge.apply(null, tasks) 156 | } 157 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_appName__", 3 | "version": "0.1.0", 4 | "manifest_version": 2, 5 | "description": "__MSG_appDescription__", 6 | "icons": { 7 | "16": "icons/icon16.png", 8 | "128": "icons/icon128.png" 9 | }, 10 | "default_locale": "en", 11 | "background": { 12 | "scripts": [ 13 | "scripts/background.js" 14 | ] 15 | }, 16 | "content_scripts": [ 17 | { 18 | "matches": [ 19 | "https://www.youtube.com/*" 20 | ], 21 | "js": [ 22 | "scripts/contentscript.js" 23 | ], 24 | "css": [ 25 | "styles/inject.css" 26 | ] 27 | } 28 | ], 29 | "browser_action": { 30 | "default_icon": { 31 | "48": "icons/icon48.png" 32 | }, 33 | "default_title": "Face Pause" 34 | } 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Face-Pause", 3 | "version": "0.1.0", 4 | "description": "Experimental Chrome Extension - Look Away to Pause Youtube", 5 | "scripts": { 6 | "chrome-build": "cross-env TARGET=chrome gulp", 7 | "firefox-build": "cross-env TARGET=firefox gulp", 8 | "build": "cross-env NODE_ENV=production npm run chrome-build && cross-env NODE_ENV=production npm run opera-build && cross-env NODE_ENV=production npm run firefox-build", 9 | "chrome-watch": "cross-env TARGET=chrome gulp watch", 10 | "opera-watch": "cross-env TARGET=opera gulp watch", 11 | "firefox-watch": "cross-env TARGET=firefox gulp watch", 12 | "chrome-dist": "cross-env NODE_ENV=production cross-env TARGET=chrome gulp dist", 13 | "opera-dist": "cross-env NODE_ENV=production cross-env TARGET=opera gulp dist", 14 | "firefox-dist": "cross-env NODE_ENV=production cross-env TARGET=firefox gulp dist", 15 | "dist": "npm run chrome-dist && npm run opera-dist && npm run firefox-dist" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/Hemmingsson/Face-Pause" 20 | }, 21 | "author": "Mattias (https://github.com/Hemmingsson)", 22 | "bugs": { 23 | "url": "https://github.com/Hemmingsson/Face-Pause/issues" 24 | }, 25 | "devDependencies": { 26 | "babel-core": "^6.1.2", 27 | "babel-preset-es2015": "^6.1.2", 28 | "babelify": "^7.3.0", 29 | "browserify": "^14.1.0", 30 | "cross-env": "^3.2.4", 31 | "event-stream": "^3.3.4", 32 | "gulp": "^3.9.0", 33 | "gulp-babel": "^6.1.0", 34 | "gulp-clean": "^0.3.1", 35 | "gulp-eslint": "^2.0.0", 36 | "gulp-if": "^2.0.2", 37 | "gulp-livereload": "^3.8.1", 38 | "gulp-load-plugins": "^0.5.3", 39 | "gulp-merge-json": "^1.0.0", 40 | "gulp-plumber": "^1.1.0", 41 | "gulp-rename": "^1.2.2", 42 | "gulp-run-sequence": "*", 43 | "gulp-sass": "^2.2.0", 44 | "gulp-sourcemaps": "^1.6.0", 45 | "gulp-uglify": "^1.5.4", 46 | "gulp-zip": "^2.0.3", 47 | "preprocessify": "^1.0.1", 48 | "vinyl-buffer": "^1.0.0", 49 | "vinyl-source-stream": "^1.1.0" 50 | }, 51 | "dependencies": { 52 | "sentinel-js": "0.0.4" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /resources/CWS-dl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hemmingsson/FacePause/e6f8992e4f582e6a81124d0373e4f9b97c6816fa/resources/CWS-dl.png -------------------------------------------------------------------------------- /resources/FacePause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hemmingsson/FacePause/e6f8992e4f582e6a81124d0373e4f9b97c6816fa/resources/FacePause.png -------------------------------------------------------------------------------- /resources/FacePause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Facepause-64x64 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /resources/chrome-promo/large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hemmingsson/FacePause/e6f8992e4f582e6a81124d0373e4f9b97c6816fa/resources/chrome-promo/large.png -------------------------------------------------------------------------------- /resources/chrome-promo/marquee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hemmingsson/FacePause/e6f8992e4f582e6a81124d0373e4f9b97c6816fa/resources/chrome-promo/marquee.png -------------------------------------------------------------------------------- /resources/chrome-promo/small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hemmingsson/FacePause/e6f8992e4f582e6a81124d0373e4f9b97c6816fa/resources/chrome-promo/small.png -------------------------------------------------------------------------------- /resources/extension-assets.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hemmingsson/FacePause/e6f8992e4f582e6a81124d0373e4f9b97c6816fa/resources/extension-assets.sketch -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "FacePause", 4 | "description": "The name of the extension." 5 | }, 6 | "appDescription": { 7 | "message": "Look Away to Pause Youtube - Experimental Extension", 8 | "description": "The description of the extension." 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/icons/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hemmingsson/FacePause/e6f8992e4f582e6a81124d0373e4f9b97c6816fa/src/icons/icon-64.png -------------------------------------------------------------------------------- /src/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hemmingsson/FacePause/e6f8992e4f582e6a81124d0373e4f9b97c6816fa/src/icons/icon128.png -------------------------------------------------------------------------------- /src/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hemmingsson/FacePause/e6f8992e4f582e6a81124d0373e4f9b97c6816fa/src/icons/icon16.png -------------------------------------------------------------------------------- /src/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hemmingsson/FacePause/e6f8992e4f582e6a81124d0373e4f9b97c6816fa/src/icons/icon48.png -------------------------------------------------------------------------------- /src/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hemmingsson/FacePause/e6f8992e4f582e6a81124d0373e4f9b97c6816fa/src/images/.gitkeep -------------------------------------------------------------------------------- /src/scripts/background.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onMessage.addListener( 2 | function (request, sender, sendResponse) { 3 | if (request === 'openFlags') { 4 | chrome.tabs.create({url: 'chrome://flags#enable-experimental-web-platform-features'}) 5 | } 6 | } 7 | ) 8 | -------------------------------------------------------------------------------- /src/scripts/contentscript.js: -------------------------------------------------------------------------------- 1 | 2 | const sentinel = require('sentinel-js') 3 | 4 | const extensionMarkup = '
FacePause Disabled
' 5 | const detectionBoxColor = '#00d647' 6 | let faceWasDetectedOnce 7 | let faceDetector 8 | let webCamStream 9 | let undetectedTimer 10 | let detectInterval 11 | 12 | // When Video player appears in the wild, Inititate Extension 13 | sentinel.on('#movie_player', elm => initExtension()) 14 | 15 | const initExtension = () => { 16 | if (!isExtensionExisting()) { 17 | appendExtensionMarkup(extensionMarkup) 18 | 'FaceDetector' in window ? enableSwitch() : updateStatus(status.enableExperimental) 19 | interactions() 20 | } 21 | } 22 | 23 | const initCapture = () => { 24 | const elmVideo = document.querySelector('#webcam-player') 25 | const elmCanvas = document.querySelector('#webcam-canvas') 26 | const canvasContext = elmCanvas.getContext('2d') 27 | if (faceDetector === undefined) faceDetector = new FaceDetector() 28 | // show help message to allow webcam connection if no action is taken for 2 seconds 29 | let allowMessageTimeOut = setTimeout(() => { updateStatus(status.allowWebCam) }, 2000) 30 | askForWebCamAccess().then(stream => { 31 | startCapture(stream, elmVideo, elmCanvas, canvasContext) 32 | clearTimeout(allowMessageTimeOut) 33 | }).catch(() => { 34 | failToCapture() 35 | clearTimeout(allowMessageTimeOut) 36 | }) 37 | } 38 | 39 | const startCapture = (stream, video, canvas, context, timeOut) => { 40 | streamWebCam(stream, video) 41 | toggleCameraIcon() 42 | blinkCameraIcon(true) 43 | startFaceDetection(video, canvas, context) 44 | showCaptureWindow(true) 45 | updateStatus(status.startingDetection) 46 | } 47 | 48 | const stopCapture = () => { 49 | faceWasDetectedOnce = false 50 | clearInterval(detectInterval) 51 | stopWebCam() 52 | toggleCameraIcon() 53 | blinkCameraIcon(false) 54 | document.querySelector('.disable input').checked = false 55 | showCaptureWindow(false) 56 | updateStatus(status.extensionDisabled) 57 | } 58 | 59 | const failToCapture = () => { 60 | stopCapture() 61 | updateStatus(status.webCameBlocked) 62 | } 63 | 64 | const toggleCapture = e => e.srcElement.checked ? initCapture() : stopCapture() 65 | 66 | /* ========================================================================== 67 | Interactions 68 | ========================================================================== */ 69 | 70 | const interactions = () => { 71 | const elmStatus = document.querySelector('.status') 72 | const elmDisable = document.querySelector('.disable input') 73 | const elmYtPlayerButton = document.querySelector('#movie_player .ytp-play-button') 74 | 75 | elmStatus.addEventListener('click', togglePlayerVisebility) 76 | elmDisable.addEventListener('click', toggleCapture) 77 | 78 | // When a user hits the play button, space or k, disable extension 79 | elmYtPlayerButton.addEventListener('click', userPause) 80 | window.addEventListener('keydown', e => { if (e.keyCode === 75) { userPause() } }) 81 | } 82 | 83 | const userPause = () => { 84 | if (isExtensionActive() && !isYoutTubePlaying()) { 85 | stopCapture() 86 | videoPause() 87 | } 88 | } 89 | 90 | /* ========================================================================== 91 | Webcam 92 | ========================================================================== */ 93 | 94 | const askForWebCamAccess = () => new Promise((resolve, reject) => { 95 | const mediaDevicesAvalible = navigator.mediaDevices && navigator.mediaDevices.getUserMedia 96 | if (mediaDevicesAvalible) { 97 | navigator.mediaDevices.getUserMedia({ video: true }).then(resolve).catch(reject) 98 | } else { reject() } 99 | }) 100 | 101 | const streamWebCam = (stream, video) => { 102 | video.src = window.URL.createObjectURL(stream) 103 | webCamStream = stream 104 | video.play() 105 | } 106 | 107 | const stopWebCam = () => { 108 | if (webCamStream) webCamStream.getTracks()[0].stop() 109 | } 110 | 111 | /* ========================================================================== 112 | Face Detection 113 | ========================================================================== */ 114 | 115 | const startFaceDetection = (video, canvas, context) => { 116 | detectInterval = setInterval(() => { 117 | drawWebCam(video, canvas, context) 118 | detectFaces(canvas, context) 119 | }, 100) 120 | } 121 | 122 | const detectFaces = async (canvas, context) => { 123 | const faces = await faceDetector.detect(canvas) 124 | const faceWasDetected = faces.length 125 | faceWasDetected ? faceDetected(faces, context) : faceUndetected(faces, canvas) 126 | } 127 | 128 | const faceDetected = (faces, context) => { 129 | clearTimeout(undetectedTimer) 130 | updateStatus(status.detected) 131 | drawFaceBoxes(faces, context) 132 | if (!isYoutTubePlaying()) videoPlays() 133 | if (!faceWasDetectedOnce) { 134 | faceWasDetectedOnce = true 135 | setTimeout(() => { showCaptureWindow(false) }, 2000) 136 | } 137 | } 138 | 139 | const faceUndetected = (faces, canvas) => { 140 | undetectedTimer = setTimeout(async () => { 141 | const faces = await faceDetector.detect(canvas) 142 | if (isExtensionActive() && !faces.length && faceWasDetectedOnce && isYoutTubePlaying()) { 143 | videoPause() 144 | updateStatus(status.undetected) 145 | } 146 | }, 1000) 147 | } 148 | 149 | /* ========================================================================== 150 | Canvas Rendering 151 | ========================================================================== */ 152 | 153 | const drawWebCam = (video, canvas, context) => context.drawImage(video, 0, 0, canvas.width, canvas.height) 154 | 155 | const drawFaceBoxes = (faces, context) => { 156 | faces.forEach(face => { 157 | const { width, height, top, left } = face.boundingBox 158 | context.strokeStyle = detectionBoxColor 159 | context.lineWidth = 4 160 | context.strokeRect(left, top, width, height) 161 | }) 162 | } 163 | 164 | /* ========================================================================== 165 | DOM manupilation 166 | ========================================================================== */ 167 | 168 | const enableSwitch = () => { 169 | let elmDisable = document.querySelector('.disable input') 170 | elmDisable.disabled = false 171 | } 172 | 173 | const showCaptureWindow = visible => { 174 | let elmWebcam = document.querySelector('.webcam') 175 | visible ? elmWebcam.classList.add('--visible') : elmWebcam.classList.remove('--visible') 176 | } 177 | 178 | const blinkCameraIcon = blink => { 179 | let elmIcon = document.querySelector('.status') 180 | let animationSelector = '--recording' 181 | blink ? elmIcon.classList.add(animationSelector) : elmIcon.classList.remove(animationSelector) 182 | } 183 | 184 | const toggleElmClass = (elmSelector, toggleClassName) => { 185 | let elmWebcam = document.querySelector(elmSelector) 186 | elmWebcam.classList.toggle(toggleClassName) 187 | } 188 | 189 | const toggleCameraIcon = () => toggleElmClass('.status', '--on') 190 | 191 | const togglePlayerVisebility = () => isExtensionActive() ? toggleElmClass('.webcam', '--visible') : showCaptureWindow(false) 192 | 193 | const videoPlays = () => document.querySelector('#movie_player video').play() 194 | 195 | const videoPause = () => document.querySelector('#movie_player video').pause() 196 | 197 | const appendExtensionMarkup = html => document.body.insertAdjacentHTML('beforeend', html) 198 | 199 | /* ========================================================================== 200 | Status message 201 | ========================================================================== */ 202 | 203 | const status = { 204 | enableExperimental: 'To Use FaceDetector API, Enable Experimental Features in Chrome here', 205 | allowWebCam: 'Allow Access to the Webcam Through the Dialog in Upper Left Corner', 206 | detected: 'Face Detected', 207 | undetected: 'No Face Detected', 208 | extensionDisabled: 'FacePause Disabled', 209 | startingDetection: 'Detecting face...', 210 | webCameBlocked: 'You have blocked acces to the webcam, click the 🔒 to the left of the address bar to change' 211 | } 212 | 213 | const updateStatus = status => { 214 | let elmMessage = document.querySelector('.message') 215 | if (elmMessage.innerHTML !== status) elmMessage.innerHTML = status 216 | if (status === status.enableExperimental) experimentalFeaturesLink() 217 | } 218 | 219 | /* ========================================================================== 220 | Helpers 221 | ========================================================================== */ 222 | 223 | const isExtensionActive = () => document.querySelector('.disable input').checked 224 | 225 | const isYoutTubePlaying = () => !document.querySelector('#movie_player video').paused 226 | 227 | const isExtensionExisting = () => document.querySelector('.extension') 228 | 229 | const experimentalFeaturesLink = () => document.querySelector('.extension a').addEventListener('click', () => { chrome.runtime.sendMessage('openFlags') }) 230 | -------------------------------------------------------------------------------- /src/scripts/livereload.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var LIVERELOAD_HOST = 'localhost:' 4 | var LIVERELOAD_PORT = 35729 5 | var connection = new WebSocket('ws://' + LIVERELOAD_HOST + LIVERELOAD_PORT + '/livereload') 6 | 7 | connection.onerror = function (error) { 8 | console.log('reload connection got error:', error) 9 | } 10 | 11 | connection.onmessage = function (e) { 12 | if (e.data) { 13 | var data = JSON.parse(e.data) 14 | if (data && data.command === 'reload') { 15 | chrome.runtime.reload() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/styles/inject.scss: -------------------------------------------------------------------------------- 1 | $height: 50px; 2 | $gray: #e0e0e0; 3 | $red: #ff0002; 4 | $green: #00d647; 5 | $border: 1px solid $gray; 6 | 7 | 8 | .extension { 9 | font-family: sans-serif; 10 | font-size: 14px; 11 | 12 | position: fixed; 13 | z-index: 1000; 14 | right: 0; 15 | bottom: 0; 16 | 17 | display: flex; 18 | 19 | height: $height; 20 | 21 | color: darken($gray, 30%); 22 | border-top: $border; 23 | border-left: $border; 24 | background: white; 25 | .webcam { 26 | position: absolute; 27 | right: 0; 28 | bottom: calc(#{$height} + 1px); 29 | 30 | overflow: hidden; 31 | 32 | width: calc(100% + 1px); 33 | height: 260px; 34 | 35 | pointer-events: none;; 36 | &.--visible .webcam__inner { 37 | transform: translateY(0); 38 | } 39 | &__inner { 40 | display: flex; 41 | 42 | width: 100%; 43 | height: 100%; 44 | 45 | transition: .4s; 46 | transform: translateY(100%); 47 | 48 | background: $gray; 49 | 50 | justify-content: center; 51 | align-items: center; 52 | &:before { 53 | position: absolute; 54 | 55 | content: 'waiting for webcam...'; 56 | } 57 | } 58 | canvas { 59 | position: relative; 60 | z-index: 100; 61 | 62 | width: 100%; 63 | height: 100%; 64 | } 65 | } 66 | 67 | .disable { 68 | display: flex; 69 | 70 | width: $height; 71 | 72 | border-left: $border; 73 | 74 | justify-content: center; 75 | align-items: center; 76 | } 77 | 78 | .status { 79 | font-size: 1.5em; 80 | 81 | position: relative; 82 | 83 | display: flex; 84 | 85 | min-width: $height; 86 | 87 | cursor: pointer; 88 | 89 | color: darken($gray, 20%); 90 | border-left: $border; 91 | 92 | justify-content: center; 93 | align-items: center; 94 | &:after { 95 | width: 100%; 96 | height: 100%; 97 | 98 | content: ''; 99 | transform: scaleX(-1); 100 | 101 | opacity: .5; 102 | background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4gICAgPHBhdGggZD0iTTAgMGgyNHYyNEgwem0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTIxIDYuNWwtNCA0VjdjMC0uNTUtLjQ1LTEtMS0xSDkuODJMMjEgMTcuMThWNi41ek0zLjI3IDJMMiAzLjI3IDQuNzMgNkg0Yy0uNTUgMC0xIC40NS0xIDF2MTBjMCAuNTUuNDUgMSAxIDFoMTJjLjIxIDAgLjM5LS4wOC41NC0uMThMMTkuNzMgMjEgMjEgMTkuNzMgMy4yNyAyeiIvPjwvc3ZnPg==); 103 | background-repeat: no-repeat; 104 | background-position: center center; 105 | } 106 | &.--on:after { 107 | content: ''; 108 | 109 | background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4gICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPiAgICA8cGF0aCBkPSJNMTcgMTAuNVY3YzAtLjU1LS40NS0xLTEtMUg0Yy0uNTUgMC0xIC40NS0xIDF2MTBjMCAuNTUuNDUgMSAxIDFoMTJjLjU1IDAgMS0uNDUgMS0xdi0zLjVsNCA0di0xMWwtNCA0eiIvPjwvc3ZnPg==); 110 | } 111 | &.--recording:after { 112 | animation: blink .8s linear infinite; 113 | } 114 | } 115 | 116 | .message { 117 | line-height: $height; 118 | 119 | min-width: 160px; 120 | padding: 0 15px; 121 | 122 | text-align: center; 123 | } 124 | 125 | 126 | 127 | .switch { 128 | position: relative; 129 | 130 | display: inline-block; 131 | 132 | width: 30px; 133 | height: 15px; 134 | input { 135 | display: none; 136 | } 137 | } 138 | 139 | .slider { 140 | position: absolute; 141 | top: 0; 142 | right: 0; 143 | bottom: 0; 144 | left: 0; 145 | 146 | cursor: pointer; 147 | transition: .4s; 148 | 149 | background-color: $red; 150 | &:before { 151 | position: absolute; 152 | bottom: 2px; 153 | left: 2px; 154 | 155 | width: 11px; 156 | height: 11px; 157 | 158 | content: ''; 159 | transition: .2s; 160 | 161 | background-color: white; 162 | } 163 | } 164 | 165 | .switch input { 166 | &:checked + .slider { 167 | background-color: $green; 168 | } 169 | &:checked + .slider:before { 170 | transform: translateX(15px); 171 | } 172 | } 173 | 174 | .slider.round { 175 | border-radius: 34px; 176 | &:before { 177 | border-radius: 50%; 178 | } 179 | } 180 | } 181 | 182 | 183 | @keyframes blink { 184 | 50% { 185 | opacity: .3; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/styles/videocam.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/styles/videocam_off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | --------------------------------------------------------------------------------