├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── postcss.config.js ├── preview.png ├── src ├── App.css ├── AppearAfter.js ├── Intro.css ├── Intro.js ├── Options.css ├── Source │ ├── Source.js │ └── index.js ├── analytics.js ├── app.js ├── assets │ ├── globals.css │ ├── icon-128.png │ ├── icon-16.png │ ├── icon-512.png │ ├── svg │ │ ├── Camera.js │ │ ├── Desktop.js │ │ ├── Logo.js │ │ ├── Mic.js │ │ ├── Mute.js │ │ ├── Sound.js │ │ ├── Tab.js │ │ ├── Window.js │ │ └── index.js │ └── variables.css ├── background.js ├── constants.js ├── index.html ├── manifest.json ├── options.html └── options.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "react", 5 | "stage-0" 6 | ], 7 | "plugins": [ 8 | "transform-class-properties" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true, 7 | "jest": true 8 | }, 9 | "globals": { 10 | "chrome": true 11 | }, 12 | "parser": "babel-eslint", 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "jsx": true 16 | }, 17 | "ecmaVersion": 2017, 18 | "sourceType": "module" 19 | }, 20 | "plugins": [ 21 | "react" 22 | ], 23 | "extends": "airbnb-base", 24 | "rules": { 25 | "no-tabs":"off", 26 | "indent": ["error", "tab", { "SwitchCase": 1 }], 27 | "object-curly-newline":"off", 28 | "consistent-return": "off", 29 | "default-case": "off", 30 | "react/jsx-uses-react": [1], 31 | "react/jsx-uses-vars": [1], 32 | "react/react-in-jsx-scope": [1] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | build/ 61 | pages/ 62 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false, 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Erich Behrens 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 | # Screen Recorder 2 | 3 | [![Chrome web store users](https://img.shields.io/chrome-web-store/users/hniebljpgcogalllopnjokppmgbhaden.svg)](https://chrome.google.com/webstore/detail/screen-recorder/hniebljpgcogalllopnjokppmgbhaden) 4 | [![Chrome web store rating](https://img.shields.io/chrome-web-store/rating/hniebljpgcogalllopnjokppmgbhaden.svg)](https://chrome.google.com/webstore/detail/screen-recorder/hniebljpgcogalllopnjokppmgbhaden) 5 | 6 | Chrome extension to record a video from the camera or capture it from the screen (desktop, specific application window or Chrome tab). 7 | 8 | ![Application screenshot](preview.png) 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "screen-recorder", 3 | "version": "1.0.0", 4 | "description": "Chrome extension to record video from camera, screen, specific window or tab", 5 | "main": "src/background.js", 6 | "scripts": { 7 | "start": "webpack --watch", 8 | "build": "webpack -p", 9 | "lint": "eslint src", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/erichbehrens/screen-recorder.git" 15 | }, 16 | "author": "Erich Behrens ", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/erichbehrens/screen-recorder/issues" 20 | }, 21 | "homepage": "https://github.com/erichbehrens/screen-recorder#readme", 22 | "dependencies": { 23 | "classnames": "^2.2.6", 24 | "postcss-import": "^11.1.0", 25 | "react": "^16.2.0", 26 | "react-dom": "^16.2.0" 27 | }, 28 | "devDependencies": { 29 | "babel-eslint": "^8.2.2", 30 | "babel-loader": "^7.1.3", 31 | "babel-plugin-transform-class-properties": "^6.24.1", 32 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 33 | "babel-plugin-transform-react-jsx": "^6.24.1", 34 | "babel-preset-env": "^1.6.1", 35 | "babel-preset-react": "^6.24.1", 36 | "babel-preset-stage-0": "^6.24.1", 37 | "copy-webpack-plugin": "^4.5.0", 38 | "css-loader": "^0.28.10", 39 | "eslint": "^4.18.2", 40 | "eslint-config-airbnb-base": "^12.1.0", 41 | "eslint-plugin-import": "^2.9.0", 42 | "eslint-plugin-react": "^7.7.0", 43 | "extract-text-webpack-plugin": "^3.0.2", 44 | "html-webpack-plugin": "^3.0.4", 45 | "postcss-cssnext": "^3.1.0", 46 | "postcss-loader": "^2.1.1", 47 | "webpack": "^3.8.1", 48 | "webpack-cli": "^2.0.10" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import'), 4 | require('postcss-cssnext'), 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Busterbob/screen-recorder/b10b65e3ba08fdb6c0b137c381017a739f954c98/preview.png -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @import './assets/variables.css'; 2 | 3 | .app { 4 | background: var(--super-gray); 5 | box-sizing: border-box; 6 | padding: 40px; 7 | margin: 0; 8 | width: 100%; 9 | border-radius: 10px; 10 | min-height: calc(100% - 85px); 11 | 12 | @media (max-width: 400px) { 13 | padding: 20px; 14 | } 15 | } 16 | 17 | .controls { 18 | margin: 20px 0; 19 | 20 | &:global(.visible) { 21 | & .title { 22 | &:before { 23 | width: 100%; 24 | } 25 | 26 | & h2 { 27 | transform: translateX(-50%) scale(1); 28 | opacity: 1; 29 | } 30 | } 31 | } 32 | 33 | & .title { 34 | position: relative; 35 | display: block; 36 | 37 | &:before { 38 | position: absolute; 39 | left: 50%; 40 | top: 50%; 41 | transform: translate(-50%, -50%); 42 | content: ''; 43 | display: inline-block; 44 | height: 1px; 45 | background: color(var(--medium-gray) alpha(40%)); 46 | transition: all .5s ease; 47 | transition-delay: .3s; 48 | width: 0; 49 | } 50 | 51 | & h2 { 52 | text-align: center; 53 | display: inline-block; 54 | font-weight: 100; 55 | font-size: 25px; 56 | margin: auto; 57 | position: relative; 58 | left: 50%; 59 | transform: translateX(-50%) scale(0.9); 60 | opacity: 0; 61 | background: blue; 62 | box-sizing: border-box; 63 | padding: 0 20px; 64 | background: var(--super-gray); 65 | transition: all .5s ease; 66 | } 67 | } 68 | 69 | & .buttons { 70 | margin: 10px -40px; 71 | text-align: center; 72 | 73 | @media (max-width: 400px) { 74 | margin: 10px 0; 75 | text-align: left; 76 | } 77 | 78 | &:global(.visible) { 79 | & button { 80 | opacity: 1; 81 | transform: translateY(0); 82 | 83 | &:nth-child(1) { 84 | transition-delay: .1s; 85 | } 86 | 87 | &:nth-child(2) { 88 | transition-delay: .2s; 89 | } 90 | 91 | &:nth-child(3) { 92 | transition-delay: .3s; 93 | } 94 | 95 | &:nth-child(4) { 96 | transition-delay: .4s; 97 | } 98 | 99 | &:nth-child(5) { 100 | transition-delay: .5s; 101 | } 102 | } 103 | } 104 | 105 | 106 | & button { 107 | @apply --resetButton; 108 | margin: 20px; 109 | font-size: var(--small-font-size); 110 | font-family: var(--font-special); 111 | opacity: 0; 112 | transform: translateY(20px); 113 | transition: all .5s ease; 114 | 115 | @media (max-width: 400px) { 116 | width: 100%; 117 | margin: 10px 0; 118 | text-align: left; 119 | } 120 | 121 | &.active { 122 | font-weight: bold; 123 | 124 | & span { 125 | border-color: var(--primary-color); 126 | 127 | & svg { 128 | opacity: 1; 129 | } 130 | } 131 | } 132 | 133 | &:hover { 134 | & span { 135 | & svg { 136 | opacity: 1; 137 | 138 | &:nth-child(2) { 139 | transform: translate(-50%, -50%); 140 | } 141 | 142 | &:nth-child(1) { 143 | transform: translate(-50%, 350%); 144 | } 145 | } 146 | } 147 | } 148 | 149 | & span { 150 | display: inline-block; 151 | width: 42; 152 | height: 42; 153 | border-radius: 50%; 154 | background: var(--white); 155 | position: relative; 156 | vertical-align: middle; 157 | margin-right: 10px; 158 | overflow: hidden; 159 | border: solid 2px transparent; 160 | transition: all .3s ease; 161 | 162 | & svg { 163 | position: absolute; 164 | opacity: 0.5; 165 | transition: all .5s ease; 166 | 167 | &:nth-child(1) { 168 | top: 50%; 169 | left: 50%; 170 | transform: translate(-50%, -50%); 171 | } 172 | 173 | &:nth-child(2) { 174 | top: 50%; 175 | left: 50%; 176 | transform: translate(-50%, -350%); 177 | } 178 | } 179 | } 180 | } 181 | } 182 | } 183 | 184 | @keyframes recording { 185 | 0% { 186 | opacity: 0; 187 | } 188 | 189 | 100% { 190 | opacity: 1; 191 | } 192 | } 193 | 194 | .buttonContainer { 195 | &.flex { 196 | display: flex; 197 | margin: 10px -10px; 198 | 199 | & button { 200 | margin: 10px; 201 | transform: translateY(-20px); 202 | 203 | &.stop { 204 | background: var(--error-color); 205 | position: relative; 206 | 207 | & i { 208 | display: inline-block; 209 | width: 12px; 210 | height: 12px; 211 | border-radius: 50%; 212 | border: solid 1px var(--white); 213 | position: relative; 214 | left: -5px; 215 | top: 2px; 216 | position: relative; 217 | 218 | &:after { 219 | content: ''; 220 | display: inline-block; 221 | width: 8px; 222 | height: 8px; 223 | border-radius: 50%; 224 | background: var(--white); 225 | position: absolute; 226 | top: 50%; 227 | left: 50%; 228 | transform: translate(-50%, -50%); 229 | animation: recording 1s forwards infinite; 230 | } 231 | } 232 | 233 | &:hover { 234 | background: color(var(--error-color) blackness(30%)); 235 | } 236 | } 237 | 238 | &.back { 239 | background: var(--white); 240 | color: var(--primary-color); 241 | 242 | &:hover { 243 | background: var(--secondary-color); 244 | color: var(--white); 245 | } 246 | } 247 | } 248 | } 249 | 250 | &:global(.visible) { 251 | & button { 252 | opacity: 1; 253 | transform: translateY(0); 254 | 255 | &:nth-child(1) { 256 | transition-delay: .1s; 257 | } 258 | 259 | &:nth-child(2) { 260 | transition-delay: .2s; 261 | } 262 | 263 | &:nth-child(3) { 264 | transition-delay: .3s; 265 | } 266 | } 267 | } 268 | 269 | & button { 270 | @apply --resetButton; 271 | width: 100%; 272 | background: var(--primary-color); 273 | color: var(--white); 274 | font-weight: bolder; 275 | font-size: var(--small-font-size); 276 | box-sizing: border-box; 277 | padding: 20px; 278 | border-radius: 30px; 279 | opacity: 0; 280 | transform: translateY(20px); 281 | transition: all .5s ease; 282 | 283 | &:hover { 284 | background: var(--secondary-color); 285 | } 286 | } 287 | } 288 | 289 | .video { 290 | transition: all .5s ease; 291 | opacity: 0; 292 | transform: translateY(20px); 293 | 294 | &:global(.visible) { 295 | opacity: 1; 296 | transform: translateY(0); 297 | } 298 | 299 | 300 | & video { 301 | width: 100%; 302 | height: auto; 303 | } 304 | } 305 | 306 | .footer { 307 | text-align: center; 308 | margin: 20px 0 0 0; 309 | 310 | transition: all .5s ease; 311 | opacity: 0; 312 | transform: translateY(20px); 313 | 314 | &:global(.visible) { 315 | opacity: 1; 316 | transform: translateY(0); 317 | } 318 | 319 | & a { 320 | color: var(--primary-color); 321 | transition: all .3s ease; 322 | cursor: pointer; 323 | 324 | &:hover { 325 | color: var(--secondary-color); 326 | } 327 | } 328 | } -------------------------------------------------------------------------------- /src/AppearAfter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | class AppearAfter extends Component { 5 | componentWillMount() { 6 | setTimeout(() => this.setState({ isVisible: true }), this.props.delay || 0); 7 | } 8 | 9 | constructor(props) { 10 | super(props); 11 | this.state = { isVisible: false }; 12 | } 13 | render() { 14 | const { isVisible } = this.state; 15 | const { children, className } = this.props; 16 | return React.cloneElement(children, { 17 | className: classNames(className, { 18 | visible: isVisible, 19 | hidden: !isVisible, 20 | }), 21 | }); 22 | } 23 | } 24 | 25 | export default AppearAfter; 26 | -------------------------------------------------------------------------------- /src/Intro.css: -------------------------------------------------------------------------------- 1 | @import './assets/variables.css'; 2 | 3 | .logo { 4 | text-align: center; 5 | margin-bottom: 40px; 6 | 7 | &:global(.visible) { 8 | & h1, 9 | & svg { 10 | opacity: 1; 11 | transform: translateY(0); 12 | } 13 | } 14 | 15 | @media (max-width: 400px) { 16 | margin-bottom: 20px; 17 | } 18 | 19 | & h1 { 20 | display: block; 21 | margin: 20px 0 0 0; 22 | font-size: 25px; 23 | color: var(--primary-color); 24 | text-transform: none; 25 | transition: all .5s ease; 26 | opacity: 0; 27 | transform: translateY(20px); 28 | } 29 | 30 | & svg { 31 | opacity: 0; 32 | transform: translateY(-20px); 33 | transition: all .5s ease; 34 | } 35 | } -------------------------------------------------------------------------------- /src/Intro.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import AppearAfter from './AppearAfter'; 3 | import styles from './Intro.css'; 4 | import { Logo } from './assets/svg'; 5 | 6 | function Intro() { 7 | return ( 8 | 9 | 10 |
11 | 12 |

Screen Recoder

13 |
14 |
15 |
16 | ); 17 | } 18 | 19 | export default Intro; 20 | -------------------------------------------------------------------------------- /src/Options.css: -------------------------------------------------------------------------------- 1 | @import './assets/variables.css'; 2 | 3 | .options { 4 | max-width: 800px; 5 | margin: auto; 6 | background: var(--super-gray); 7 | border-radius: 10px; 8 | box-sizing: border-box; 9 | padding: 40px; 10 | 11 | @media (max-width: 400px) { 12 | padding: 20px; 13 | } 14 | 15 | & .message { 16 | text-aling: center; 17 | font-size: 20px; 18 | margin: 10px; 19 | 20 | &:global(.visible) { 21 | & .title { 22 | &:before { 23 | width: 100%; 24 | } 25 | 26 | & h2 { 27 | transform: translateX(-50%) scale(1); 28 | opacity: 1; 29 | } 30 | } 31 | 32 | & p { 33 | opacity: 1; 34 | transform: translateY(0); 35 | 36 | &:nth-child(1) { 37 | transition-delay: .1s; 38 | } 39 | 40 | &:nth-child(2) { 41 | transition-delay: .2s; 42 | } 43 | 44 | &:nth-child(3) { 45 | transition-delay: .3s; 46 | } 47 | } 48 | 49 | & .team { 50 | & li { 51 | opacity: 1; 52 | transform: translateY(0); 53 | 54 | &:nth-child(1) { 55 | transition-delay: .3s; 56 | } 57 | 58 | &:nth-child(2) { 59 | transition-delay: .4s; 60 | } 61 | } 62 | } 63 | } 64 | 65 | & p { 66 | font-size: var(--default-font-size); 67 | line-height: var(--default-line-height); 68 | transition: all .5s ease; 69 | opacity: 0; 70 | transform: translateY(20px); 71 | } 72 | 73 | & .team { 74 | @apply --noList; 75 | margin: 40px 0 0; 76 | display: flex; 77 | justify-content: space-around; 78 | 79 | @media (max-width: 400px) { 80 | margin: 20px 0; 81 | } 82 | 83 | & li { 84 | font-size: var(--default-font-size); 85 | line-height: var(--default-line-height); 86 | text-align: center; 87 | transition: all .5s ease; 88 | opacity: 0; 89 | transform: translateY(20px); 90 | 91 | & img { 92 | width: 120px; 93 | border-radius: 50%; 94 | } 95 | 96 | & a { 97 | display: block; 98 | margin: 10px 0 0 0; 99 | transition: all .3s ease; 100 | color: var(--primary-color); 101 | font-weight: bold; 102 | 103 | &:hover { 104 | color: var(--secondary-color); 105 | } 106 | 107 | & i { 108 | display: block; 109 | font-style: normal; 110 | margin-top: 10px; 111 | } 112 | } 113 | 114 | & span { 115 | margin: 0; 116 | padding: 0; 117 | display: block; 118 | color: var(--medium-gray); 119 | } 120 | } 121 | } 122 | 123 | & .title { 124 | position: relative; 125 | display: block; 126 | 127 | &:before { 128 | position: absolute; 129 | left: 50%; 130 | top: 50%; 131 | transform: translate(-50%, -50%); 132 | content: ''; 133 | display: inline-block; 134 | height: 1px; 135 | background: color(var(--medium-gray) alpha(40%)); 136 | transition: all .5s ease; 137 | transition-delay: .3s; 138 | width: 0; 139 | } 140 | 141 | & h2 { 142 | text-align: center; 143 | display: inline-block; 144 | font-weight: 100; 145 | font-size: 25px; 146 | margin: auto; 147 | position: relative; 148 | left: 50%; 149 | transform: translateX(-50%) scale(0.9); 150 | opacity: 0; 151 | background: blue; 152 | box-sizing: border-box; 153 | padding: 0 20px; 154 | background: var(--super-gray); 155 | transition: all .5s ease; 156 | } 157 | } 158 | 159 | & .info, 160 | & .success, 161 | & .error { 162 | box-sizing: border-box; 163 | padding: 20px; 164 | border-radius: 10px; 165 | margin: 20px 0; 166 | } 167 | 168 | & .info { 169 | color: #31708f; 170 | background-color: #d9edf7; 171 | border-color: #bce8f1; 172 | } 173 | 174 | & .success { 175 | color: #3c763d; 176 | background-color: #dff0d8; 177 | border-color: #d6e9c6; 178 | } 179 | 180 | & .error { 181 | color: #a94442; 182 | background-color: #f2dede; 183 | border-color: #ebccd1; 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Source/Source.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import styles from '../App.css'; 3 | import AppearAfter from '../AppearAfter'; 4 | 5 | function Button({ 6 | onClick, 7 | className, 8 | disabled, 9 | label, 10 | icon, 11 | }) { 12 | const Icon = icon; 13 | return ( 14 | 18 | ); 19 | } 20 | 21 | function Source({ value, isRecording, onChange, sources }) { 22 | return ( 23 | 24 | 25 |
26 | {sources.map(source => ( 27 |
37 |
38 |
39 | ); 40 | } 41 | 42 | export default Source; 43 | -------------------------------------------------------------------------------- /src/Source/index.js: -------------------------------------------------------------------------------- 1 | import Source from './Source'; 2 | 3 | export default Source; 4 | -------------------------------------------------------------------------------- /src/analytics.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | (function () { 3 | let ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; 4 | ga.src = 'https://ssl.google-analytics.com/ga.js'; 5 | let s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); 6 | }()); 7 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import classNames from 'classnames'; 4 | import './assets/globals.css'; 5 | import styles from './App.css'; 6 | import { Desktop, Camera, Mic, Mute, Sound } from './assets/svg'; 7 | import AppearAfter from './AppearAfter'; 8 | import Source from './Source'; 9 | import Intro from './Intro'; 10 | import { RECORDING_STOPPED } from './constants'; 11 | 12 | const videoSources = [ 13 | { 14 | type: 'screen', 15 | icon: Desktop, 16 | label: 'Screen', 17 | }, 18 | /* { 19 | type: 'window', 20 | icon: Window, 21 | label: 'Window', 22 | }, 23 | { 24 | type: 'tab', 25 | icon: Tab, 26 | label: 'Chrome Tab', 27 | }, */ 28 | { 29 | type: 'camera', 30 | icon: Camera, 31 | label: 'Camera', 32 | }, 33 | ]; 34 | 35 | const audioSources = [ 36 | { 37 | type: 'none', 38 | icon: Mute, 39 | label: 'None', 40 | }, 41 | { 42 | type: 'mic', 43 | icon: Mic, 44 | label: 'Microphone', 45 | }, 46 | { 47 | type: 'system', 48 | icon: Sound, 49 | label: 'System', 50 | }, 51 | ]; 52 | 53 | function getUserMediaError() { 54 | chrome.runtime.openOptionsPage(); 55 | console.log('getUserMedia() failed'); 56 | } 57 | 58 | function analytics(data) { 59 | /* eslint-disable no-underscore-dangle */ 60 | if (window._gaq) { 61 | window._gaq.push(data); 62 | } 63 | /* eslint-enable no-underscore-dangle */ 64 | } 65 | 66 | function downloadByteArray(data, name) { 67 | const blob = new Blob(data, { type: 'video/webm' }); 68 | const url = URL.createObjectURL(blob); 69 | const a = document.createElement('a'); 70 | document.body.appendChild(a); 71 | a.style = 'display: none'; 72 | a.target = '_blank'; 73 | a.href = url; 74 | a.download = name; 75 | a.click(); 76 | setTimeout(() => { 77 | document.body.removeChild(a); 78 | window.URL.revokeObjectURL(url); 79 | }, 100); 80 | } 81 | 82 | class App extends React.Component { 83 | state = { 84 | isRecording: false, 85 | includeAudioMic: false, 86 | includeAudioSystem: false, 87 | hasSource: false, 88 | videoSource: undefined, 89 | audioSource: 'none', 90 | }; 91 | 92 | audioStream; 93 | recorder; 94 | localStream; 95 | recordedChunks = []; 96 | 97 | setVideoSource = (type) => { 98 | this.setState({ videoSource: type }); 99 | } 100 | 101 | setAudioSource = (type) => { 102 | analytics(['_trackEvent', 'video', 'setAudio', type]); 103 | switch (type) { 104 | case 'mic': 105 | this.setState({ 106 | includeAudioMic: true, 107 | includeAudioSystem: false, 108 | audioSource: type, 109 | }); 110 | navigator.getUserMedia({ 111 | audio: true, 112 | video: false, 113 | }, this.gotAudio, getUserMediaError); 114 | break; 115 | case 'system': 116 | this.setState({ 117 | includeAudioMic: false, 118 | includeAudioSystem: true, 119 | audioSource: type, 120 | }); 121 | break; 122 | default: 123 | this.setState({ 124 | includeAudioMic: false, 125 | includeAudioSystem: false, 126 | audioSource: type, 127 | }); 128 | } 129 | } 130 | 131 | record = () => { 132 | console.log('Start recording'); 133 | analytics(['_trackEvent', 'video', 'recordingStarted', this.state.videoSource]); 134 | if (this.video) { 135 | this.video.muted = true; // prevent audio loopback 136 | } 137 | this.setState({ hasStarted: true }); 138 | if (window.outerHeight < 600) { 139 | const delta = 600 - window.outerHeight; 140 | window.resizeTo(window.outerWidth, 600); 141 | window.moveTo(window.screenLeft, window.screenTop - (delta / 2)); 142 | } 143 | this.recordedChunks = []; 144 | const sourceType = [this.state.videoSource]; 145 | switch (this.state.videoSource) { 146 | case 'window': 147 | case 'screen': 148 | case 'tab': 149 | if (this.state.includeAudioSystem) { 150 | sourceType.push('audio'); 151 | } 152 | if (this.state.includeAudioMic) { 153 | navigator.getUserMedia({ 154 | audio: true, 155 | video: false, 156 | }, this.gotAudio, getUserMediaError); 157 | } 158 | chrome.desktopCapture.chooseDesktopMedia(['window', 'screen', 'tab'], this.onAccessApproved); 159 | break; 160 | case 'camera': 161 | navigator.getUserMedia({ 162 | audio: false, 163 | video: { 164 | mandatory: { 165 | minWidth: 1280, 166 | minHeight: 720, 167 | }, 168 | }, 169 | }, this.gotMediaStream, getUserMediaError); 170 | break; 171 | } 172 | } 173 | 174 | onAccessApproved = (id) => { 175 | if (!id) { 176 | console.log('Access to media rejected'); 177 | return; 178 | } 179 | 180 | navigator.getUserMedia({ 181 | audio: { 182 | mandatory: { 183 | chromeMediaSource: 'desktop', 184 | chromeMediaSourceId: id, 185 | }, 186 | }, 187 | video: { 188 | mandatory: { 189 | chromeMediaSource: 'desktop', 190 | chromeMediaSourceId: id, 191 | maxWidth: window.screen.width, 192 | maxHeight: window.screen.height, 193 | }, 194 | }, 195 | }, this.gotMediaStream, getUserMediaError); 196 | } 197 | 198 | gotMediaStream = (stream) => { 199 | console.log('Received local stream'); 200 | this.setState({ hasSource: true }, () => { this.video.srcObject = stream; }); 201 | this.localStream = stream; 202 | stream.getTracks().forEach((track) => { 203 | track.addEventListener('ended', () => { 204 | console.log(stream.id, 'track ended', track.kind, track.id); 205 | this.stopRecording(); 206 | }); 207 | }); 208 | 209 | if (this.state.includeAudioMic) { 210 | console.log('Adding mic audio track'); 211 | const audioTracks = this.audioStream.getAudioTracks(); 212 | this.localStream.addTrack(audioTracks[0]); 213 | } 214 | /* if (this.state.includeAudioSystem) { 215 | console.log('Checking for system audio track'); 216 | const audioTracks = stream.getAudioTracks(); 217 | if (audioTracks.length < 1) { 218 | console.log('No audio track in screen stream'); 219 | } 220 | } */ 221 | 222 | try { 223 | this.recorder = new MediaRecorder(stream); 224 | } catch (err) { 225 | console.error('Error creating MediaRecorder', err); 226 | return; 227 | } 228 | this.recorder.ondataavailable = this.recorderOnDataAvailable; 229 | this.recorder.onstop = this.recorderOnStop; 230 | this.recorder.start(); 231 | this.setState({ isRecording: true }); 232 | }; 233 | 234 | gotAudio = (stream) => { 235 | console.log('Received audio stream'); 236 | this.audioStream = stream; 237 | stream.getTracks().forEach((track) => { 238 | track.addEventListener('ended', () => { 239 | console.log(stream.id, 'track ended', track.kind, track.id); 240 | }); 241 | }); 242 | }; 243 | 244 | recorderOnDataAvailable = (event) => { 245 | if (event.data && event.data.size > 0) { 246 | this.recordedChunks.push(event.data); 247 | } 248 | } 249 | 250 | recorderOnStop = () => { 251 | const blob = new Blob(this.recordedChunks, { type: 'video/webm' }); 252 | const src = URL.createObjectURL(blob); 253 | this.video.srcObject = null; 254 | this.setState({ isRecording: false, hasSource: true, src }); 255 | chrome.runtime.sendMessage({ type: RECORDING_STOPPED }); 256 | } 257 | 258 | stopRecording = () => { 259 | console.log('Stop recording'); 260 | this.recorder.stop(); 261 | this.localStream.getVideoTracks()[0].stop(); 262 | this.setState({ isRecording: false }); 263 | analytics(['_trackEvent', 'video', 'recordingStopped']); 264 | } 265 | 266 | save = () => { 267 | downloadByteArray(this.recordedChunks, 'screen-capture.webm'); 268 | analytics(['_trackEvent', 'video', 'saved']); 269 | } 270 | 271 | reset = () => { 272 | this.setState({ 273 | hasSource: false, 274 | videoSource: undefined, 275 | }); 276 | } 277 | 278 | openOptionsPage = () => { 279 | chrome.runtime.openOptionsPage(); 280 | } 281 | 282 | render() { 283 | const { isRecording, videoSource, audioSource, hasSource, src, hasStarted } = this.state; 284 | return ( 285 | 286 |
287 | 288 | {!hasSource &&
289 | 290 |
291 |

What do you want to capture?

292 | 298 |
299 |
300 | {videoSource && 301 |
302 |

Record audio?

303 | 309 |
310 |
} 311 | {!isRecording && videoSource && 312 | 313 |
314 | 319 |
320 |
321 | } 322 |
} 323 | {hasSource &&
324 | 325 |
326 | 329 | 330 | 333 |
334 |
335 | 336 |
337 |
345 |
346 |
} 347 |
348 | 349 | 352 | 353 |
354 | ); 355 | } 356 | } 357 | 358 | ReactDOM.render(, document.getElementById('app')); 359 | 360 | setTimeout(() => { 361 | analytics(['_setAccount', 'UA-114990894-1']); 362 | analytics(['_trackPageview']); 363 | }, 1000); 364 | -------------------------------------------------------------------------------- /src/assets/globals.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500,900'); 2 | @import url('https://fonts.googleapis.com/css?family=Roboto+Mono:300,700'); 3 | @import './variables.css'; 4 | 5 | :global(#app) { 6 | box-sizing: border-box; 7 | padding: 20px; 8 | min-height: 100%; 9 | } 10 | 11 | html, 12 | body { 13 | margin: 0; 14 | padding: 0; 15 | height: 100%; 16 | font-family: var(--font-text); 17 | font-size: var(--default-font-size); 18 | line-height: var(--default-line-height); 19 | } 20 | 21 | body { 22 | font-size-adjust: none; 23 | -moz-osx-font-smoothing: grayscale; 24 | -webkit-text-size-adjust: none; 25 | overflow-x: hidden; 26 | } 27 | 28 | a { 29 | outline: none; 30 | 31 | text-decoration: none; 32 | transition: all 0.3s ease; 33 | } 34 | 35 | figure { 36 | margin: 0; 37 | } 38 | 39 | strong, 40 | b { 41 | font-family: var(--font-head); 42 | font-weight: bold; 43 | } 44 | 45 | h1, 46 | h2, 47 | h3, 48 | h4, 49 | h5, 50 | h6 { 51 | font-family: var(--font-head); 52 | font-weight: bold; 53 | margin: 0; 54 | padding: 0; 55 | } 56 | 57 | h1 { 58 | font-size: var(--h1-font-size); 59 | line-height: var(--h1-line-height); 60 | 61 | @media (--screen-mobile) { 62 | font-size: var(--h1-font-size-mobile); 63 | line-height: var(--h1-line-height-mobile); 64 | } 65 | } 66 | 67 | h2 { 68 | font-size: var(--h2-font-size); 69 | line-height: var(--h2-line-height); 70 | 71 | @media (--screen-mobile) { 72 | font-size: var(--h2-font-size-mobile); 73 | line-height: var(--h2-line-height-mobile); 74 | } 75 | } 76 | 77 | h3 { 78 | font-size: var(--h3-font-size); 79 | line-height: var(--h3-line-height); 80 | 81 | @media (--screen-mobile) { 82 | font-size: var(--h3-font-size-mobile); 83 | line-height: var(--h3-line-height-mobile); 84 | } 85 | } 86 | 87 | p { 88 | font-family: var(--font-text); 89 | color: var(--black); 90 | font-size: var(--small-font-size); 91 | line-height: var(--small-line-height); 92 | font-weight: normal; 93 | } 94 | 95 | * { 96 | outline: none; 97 | } 98 | 99 | hr { 100 | border: none; 101 | border-bottom: solid 1px var(--light-gray); 102 | margin: 10px 0; 103 | } 104 | -------------------------------------------------------------------------------- /src/assets/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Busterbob/screen-recorder/b10b65e3ba08fdb6c0b137c381017a739f954c98/src/assets/icon-128.png -------------------------------------------------------------------------------- /src/assets/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Busterbob/screen-recorder/b10b65e3ba08fdb6c0b137c381017a739f954c98/src/assets/icon-16.png -------------------------------------------------------------------------------- /src/assets/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Busterbob/screen-recorder/b10b65e3ba08fdb6c0b137c381017a739f954c98/src/assets/icon-512.png -------------------------------------------------------------------------------- /src/assets/svg/Camera.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Camera() { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | 11 | export default Camera; 12 | -------------------------------------------------------------------------------- /src/assets/svg/Desktop.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Desktop() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export default Desktop; 16 | -------------------------------------------------------------------------------- /src/assets/svg/Logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Logo() { 4 | return ( 5 | 6 | 7 | 11 | 15 | 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default Logo; 26 | -------------------------------------------------------------------------------- /src/assets/svg/Mic.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Mic() { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | 11 | export default Mic; 12 | -------------------------------------------------------------------------------- /src/assets/svg/Mute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Mute() { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | 11 | export default Mute; 12 | -------------------------------------------------------------------------------- /src/assets/svg/Sound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Sound() { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | } 10 | 11 | export default Sound; 12 | -------------------------------------------------------------------------------- /src/assets/svg/Tab.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Tab() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | 21 | export default Tab; 22 | -------------------------------------------------------------------------------- /src/assets/svg/Window.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Window() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export default Window; 14 | -------------------------------------------------------------------------------- /src/assets/svg/index.js: -------------------------------------------------------------------------------- 1 | import Camera from './Camera'; 2 | import Desktop from './Desktop'; 3 | import Logo from './Logo'; 4 | import Mic from './Mic'; 5 | import Mute from './Mute'; 6 | import Sound from './Sound'; 7 | import Tab from './Tab'; 8 | import Window from './Window'; 9 | 10 | export { 11 | Camera, 12 | Desktop, 13 | Logo, 14 | Mic, 15 | Mute, 16 | Sound, 17 | Tab, 18 | Window, 19 | }; 20 | -------------------------------------------------------------------------------- /src/assets/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #4656DA; 3 | --primary-color-hovered: color(var(--primary-color) lightness(45%)); 4 | --secondary-color: #25307E; 5 | --extra-color: #CFD2ED; 6 | --white: #ffffff; 7 | --black: #000000; 8 | --light-gray: #e5e5e5; 9 | --medium-gray: #7a7a7a; 10 | --dark-gray: #4a4a4a; 11 | --super-gray: #F2F2F2; 12 | --success-color: #2ecc71; 13 | --error-color: #D2464D; 14 | } 15 | 16 | @custom-media --screen-tiny (max-width: 320px); 17 | @custom-media --screen-mobile (max-width: 640px); 18 | @custom-media --screen-phablet (max-width: 800px); 19 | @custom-media --screen-tablet print, (max-width: 1000px); 20 | @custom-media --screen-desktop print, (min-width: 1024px); 21 | @custom-media --screen-middle-max (max-width: 1300px); 22 | @custom-media --screen-middle-min (min-width: 1300px); 23 | @custom-media --screen-big print, (min-width: 2000px); 24 | @custom-media --screen-4k (min-width: 3870px); 25 | 26 | :root { 27 | --font-text: 'Roboto', sans-serif; 28 | --font-head: 'Roboto', sans-serif; 29 | --font-special: 'Roboto Mono', monospace; 30 | 31 | --smaller-font-size: 12px; 32 | --smaller-line-height: 1.3; 33 | 34 | --small-font-size: 14px; 35 | --small-line-height: 1.8; 36 | 37 | --default-font-size: 16px; 38 | --default-line-height: 1.5; 39 | 40 | --big-font-size: 48px; 41 | --big-line-height: 1.3; 42 | 43 | --bigger-font-size: 64px; 44 | --bigger-line-height: 1.4; 45 | 46 | --h1-font-size: 48px; 47 | --h1-line-height: 1.3; 48 | 49 | --h1-font-size-mobile: 32px; 50 | --h1-line-height-mobile: 1.3; 51 | 52 | --h2-font-size: 36px; 53 | --h2-line-height: 1.3; 54 | 55 | --h2-font-size-mobile: 28px; 56 | --h2-line-height-mobile: 1.3; 57 | 58 | --h3-font-size: 28px; 59 | --h3-line-height: 1.3; 60 | 61 | --h3-font-size-mobile: 28px; 62 | --h3-line-height-mobile: 1.3; 63 | 64 | --h4-font-size: 24px; 65 | --h4-line-height: 1.3; 66 | 67 | --h4-font-size-mobile: 24px; 68 | --h4-line-height-mobile: 1.3; 69 | 70 | --h5-font-size: 18px; 71 | --h5-line-height: 1.5; 72 | 73 | --h5-font-size-mobile: 18px; 74 | --h5-line-height-mobile: 1.5; 75 | 76 | --h6-font-size: 16px; 77 | --h6-line-height: 1.5; 78 | 79 | --h6-font-size-mobile: 16px; 80 | --h6-line-height-mobile: 1.5; 81 | 82 | --h3-editor-font-size: var(--h1-font-size); 83 | --h3-editor-line-height: var(--h1-line-height); 84 | 85 | --h4-editor-font-size: var(--h2-font-size); 86 | --h4-editor-line-height: var(--h2-line-height); 87 | } 88 | 89 | :root { 90 | --clearfix: { 91 | clear: both; 92 | 93 | &:before, 94 | &:after { 95 | content: ''; 96 | display: table; 97 | clear: both; 98 | } 99 | } 100 | 101 | --noList: { 102 | box-sizing: border-box; 103 | list-style: none; 104 | margin: 0; 105 | padding: 0; 106 | 107 | & li { 108 | box-sizing: border-box; 109 | padding: 0; 110 | margin: 0; 111 | } 112 | } 113 | 114 | --resetButton: { 115 | appearance: none; 116 | border: none; 117 | background: none; 118 | outline: none; 119 | margin: 0; 120 | padding: 0; 121 | cursor: pointer; 122 | } 123 | 124 | --maxWidth: { 125 | box-sizing: border-box; 126 | max-width: 1100px; 127 | margin: auto; 128 | 129 | @media (--screen-middle-max) { 130 | padding: 0 20px; 131 | } 132 | } 133 | 134 | --fullWidth: { 135 | position: relative; 136 | left: 50%; 137 | transform: translateX(-50%); 138 | width: 100vw; 139 | } 140 | 141 | --container: { 142 | margin: 0 auto; 143 | max-width: 1024px; 144 | box-sizing: border-box; 145 | } 146 | 147 | --centerBg: { 148 | background-size: cover !important; 149 | background-position: center center !important; 150 | background-repeat: no-repeat !important; 151 | } 152 | 153 | --title: { 154 | font-size: var(--big-font-size); 155 | line-height: var(--big-line-height); 156 | font-weight: bold; 157 | margin: 20px 0; 158 | 159 | @media (--screen-mobile) { 160 | font-size: var(--h1-font-size-mobile); 161 | line-height: var(--h1-line-height-mobile); 162 | margin: 0 0 10px; 163 | } 164 | } 165 | 166 | --lineThrough: { 167 | clear: both; 168 | position: relative; 169 | 170 | &:before { 171 | content: ''; 172 | display: block; 173 | width: 0; 174 | height: 4px; 175 | background: var(--primary-color); 176 | position: absolute; 177 | top: 50%; 178 | left: 50%; 179 | transform: translate(-50%, -50%); 180 | transition: all 0.5s ease; 181 | } 182 | 183 | &:global(.visible) { 184 | &:before { 185 | width: 100%; 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | import { RECORDING_STOPPED } from './constants'; 2 | 3 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 4 | const { type } = message; 5 | switch (type) { 6 | case RECORDING_STOPPED: 7 | chrome.windows.update(sender.tab.windowId, { focused: true }); 8 | } 9 | }); 10 | 11 | chrome.browserAction.onClicked.addListener(() => { 12 | const width = 800; 13 | const height = 650; 14 | const top = (window.screen.availHeight - height) / 2; 15 | const left = (window.screen.availWidth - width) / 2; 16 | chrome.windows.create({ 17 | url: chrome.extension.getURL('index.html'), 18 | width, 19 | height, 20 | top, 21 | left, 22 | type: 'popup', 23 | }); 24 | }); 25 | 26 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const RECORDING_STOPPED = 'recordingStopped'; 2 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Screen Recorder 5 | 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Screen Recorder", 4 | "description": "Chrome extension to record video from camera, screen, specific window or tab", 5 | "version": "1.0.0", 6 | "minimum_chrome_version": "49", 7 | "content_security_policy": "script-src 'self' https://ssl.google-analytics.com; object-src 'self'", 8 | "icons": { 9 | "16": "assets/icon-16.png", 10 | "128": "assets/icon-128.png" 11 | }, 12 | "background": { 13 | "scripts": [ 14 | "./background.js" 15 | ] 16 | }, 17 | "browser_action": { 18 | "default_icon": "assets/icon-128.png" 19 | }, 20 | "options_page": "options.html", 21 | "permissions": [ 22 | "desktopCapture" 23 | ], 24 | "offline_enabled": true 25 | } 26 | -------------------------------------------------------------------------------- /src/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Screen Recorder 5 | 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import AppearAfter from './AppearAfter'; 4 | import './assets/globals.css'; 5 | import styles from './Options.css'; 6 | import Intro from './Intro'; 7 | 8 | class Options extends React.Component { 9 | state = { permissionRequested: false } 10 | 11 | componentDidMount() { 12 | this.setState({ permissionRequested: true }); 13 | navigator.getUserMedia({ audio: true, video: true }, () => { 14 | this.setState({ permissionRequested: false, permissionGranted: true }); 15 | }, () => { 16 | this.setState({ permissionRequested: false, permissionGranted: false }); 17 | }); 18 | } 19 | 20 | render() { 21 | const { permissionRequested, permissionGranted } = this.state; 22 | return
23 | 24 | 25 |
26 |

Recording permission

27 | {permissionRequested &&
Please grant permission
} 28 | {permissionGranted === true &&
Permission granted
} 29 | {permissionGranted === false &&
Permission denied
} 30 |
31 |
32 | 33 |
34 |

About

35 |

36 | Screen Recorder is a Chrome extension to record a video from the 37 | camera or capture it from the screen 38 | (desktop, specific application window or Chrome tab). 39 |

40 |

41 | Designed and Developed by: 42 |

43 | 59 |
60 |
61 |
; 62 | } 63 | } 64 | 65 | ReactDOM.render(, document.getElementById('app')); 66 | 67 | setTimeout(() => { 68 | /* eslint-disable no-underscore-dangle */ 69 | if (window._gaq) { 70 | window._gaq.push(['_setAccount', 'UA-114990894-1']); 71 | window._gaq.push(['_trackPageview']); 72 | } 73 | }, 1000); 74 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: { 8 | app: './src/app.js', 9 | options: './src/options.js', 10 | background: './src/background.js', 11 | analytics: './src/analytics.js', 12 | }, 13 | output: { 14 | path: path.join(__dirname, 'build'), 15 | filename: '[name].js', 16 | }, 17 | 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.js$/, 22 | exclude: /node_modules/, 23 | loader: 'babel-loader', 24 | }, 25 | { 26 | test: /\.css$/, 27 | exclude: /node_modules/, 28 | loader: ExtractTextPlugin.extract('css-loader?modules&importLoaders=1&localIdentName=[name]__[local]__[hash:base64:5]!postcss-loader'), 29 | }, 30 | { 31 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 32 | exclude: /(node_modules|bower_components)/, 33 | loader: 'url-loader?limit=10000&mimetype=image/svg+xml', 34 | }, 35 | ], 36 | }, 37 | 38 | plugins: [ 39 | new ExtractTextPlugin('[name].css'), 40 | new HtmlWebpackPlugin({ 41 | inject: true, 42 | chunks: ['analytics', 'app'], 43 | chunksSortMode: (chunk1, chunk2) => { 44 | const orders = ['analytics', 'app']; 45 | const order1 = orders.indexOf(chunk1.names[0]); 46 | const order2 = orders.indexOf(chunk2.names[0]); 47 | if (order1 > order2) { 48 | return 1; 49 | } else if (order1 < order2) { 50 | return -1; 51 | } 52 | return 0; 53 | }, 54 | filename: 'index.html', 55 | template: './src/index.html', 56 | }), 57 | new HtmlWebpackPlugin({ 58 | inject: true, 59 | chunks: ['analytics', 'options'], 60 | chunksSortMode: (chunk1, chunk2) => { 61 | const orders = ['analytics', 'options']; 62 | const order1 = orders.indexOf(chunk1.names[0]); 63 | const order2 = orders.indexOf(chunk2.names[0]); 64 | if (order1 > order2) { 65 | return 1; 66 | } else if (order1 < order2) { 67 | return -1; 68 | } 69 | return 0; 70 | }, 71 | filename: 'options.html', 72 | template: './src/index.html', 73 | }), 74 | new CopyWebpackPlugin([ 75 | { context: path.resolve(__dirname), from: 'src/manifest.json' }, 76 | { context: './src/assets', from: 'icon-**', to: 'assets' }, 77 | ]), 78 | 79 | ], 80 | 81 | devtool: 'source-map', 82 | }; 83 | --------------------------------------------------------------------------------