├── .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 |
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 |
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 = ''
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 |
--------------------------------------------------------------------------------