├── .gitignore ├── LICENSE ├── README.md ├── copy.sh ├── docs ├── app.js ├── index.html └── style.css ├── icons ├── 128.png ├── 32.png ├── 512.png └── 64.png ├── manifest-v2.json ├── manifest.json ├── package-lock.json ├── package.json ├── plugin.js ├── rollup.config.js └── src ├── background ├── chooseDesktopMedia.js └── index.js ├── content ├── App.js ├── Camera.js ├── components │ ├── Draggable.js │ ├── Timer.js │ └── button │ │ ├── ButtonCameraOff.js │ │ ├── ButtonCameraOn.js │ │ ├── ButtonClose.js │ │ ├── ButtonDownload.js │ │ ├── ButtonMicOff.js │ │ ├── ButtonMicOn.js │ │ ├── ButtonMove.js │ │ ├── ButtonOpenEditor.js │ │ ├── ButtonPause.js │ │ ├── ButtonPlay.js │ │ ├── ButtonResume.js │ │ ├── ButtonStop.js │ │ └── ButtonTash.js ├── index.js ├── style.css └── utils │ ├── convertTime.js │ ├── createLink.js │ ├── downloadVideo.js │ └── record.js ├── editor ├── index.html ├── index.js └── style.css ├── permission ├── index.html ├── index.js └── style.css └── popup ├── App.js ├── components ├── MediaSource.js ├── Message.js └── icons │ ├── Hd.js │ ├── IconCodec.js │ ├── IconDesk.js │ ├── IconTab.js │ ├── IconWebcam.js │ └── IconWin.js ├── containers ├── AudioInputs.js ├── MimeTypes.js ├── Resolutions.js ├── ToggleCamera.js ├── ToggleMic.js └── VideoInputs.js ├── index.html ├── index.js ├── style.css └── utils ├── checkDevices.js ├── checkPermission.js ├── getCurrentTabId.js ├── isFirefox.js └── resolutions.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | testr/ 3 | dist/ 4 | build/ 5 | 6 | .github 7 | .vscode 8 | .cache 9 | 10 | *.zip 11 | *.rar 12 | 13 | yarn.lock 14 | save-drive.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | https://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2020-2021 Haikel Fazzani, Inc. 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | https://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔥 [Reco](https://chromo-lib.github.io/screen-recorder/) 2 | Browser extension to record audio, webcam, window, screen and tab. 3 | 4 | ![Contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen) ![GitHub release](https://img.shields.io/github/release/Chromo-lib/reco/all?logo=GitHub) ![](https://badgen.net/github/license/Chromo-lib/reco) 5 | 6 | # Capture 7 | ![capture](https://i.giphy.com/media/WVNfkzbtf9gaXVQobR/giphy.webp) 8 | 9 | ## Similar Apps 10 | - [fullpage Screenshot](https://github.com/Chromo-lib/screenshot-fullpage-extension) 11 | 12 | # License 13 | Apache Version 2.0 -------------------------------------------------------------------------------- /copy.sh: -------------------------------------------------------------------------------- 1 | mkdir -p dist \ 2 | && cp src/editor/index.html dist/editor.html \ 3 | && cp src/popup/index.html dist/popup.html \ 4 | && cp src/permission/index.html dist/permission.html \ 5 | && cp manifest.json dist \ 6 | && cp -r LICENSE dist \ 7 | && cp -r icons dist -------------------------------------------------------------------------------- /docs/app.js: -------------------------------------------------------------------------------- 1 | ScrollReveal().reveal(document.querySelectorAll('h1'), { duration: 800 }); 2 | ScrollReveal().reveal(document.querySelectorAll('h2'), { duration: 800 }); 3 | ScrollReveal().reveal(document.querySelectorAll('.lead'), { duration: 800 }); 4 | ScrollReveal().reveal(document.querySelectorAll('img'), { duration: 800 }); 5 | ScrollReveal().reveal(document.querySelectorAll('ul'), { duration: 800 }); 6 | ScrollReveal().reveal(document.querySelectorAll('li'), { interval: 150 }); -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | Reco | Screen Recorder 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 45 | 46 |
47 |
48 |

Free Screen Recorder.

49 |

A user-friendly and full featured browser extension to record screen, tab, window and webcam.

50 | 51 | Add to Edge 54 | Add to Firefox 56 | 57 | recording screen and webcam 59 |
60 |
61 | 62 |
63 |
64 |
65 |
66 |

Why People Choose Reco

67 |
    68 |
  • Free and simple to use.
  • 69 |
  • Record unlimited number of videos.
  • 70 |
  • Record both screen and webcam.
  • 71 |
  • Record in up to 4k resolution.
  • 72 |
  • No watermarks.
  • 73 |
  • No tracking.
  • 74 |
  • Open source.
  • 75 |
76 |
77 | 78 | reco configuration 80 |
81 |
82 |
83 | 84 |
85 |
86 |

Record your screen or camera

87 |

You can record both your screen and camera in high definition, at the same time.

88 |
89 | 90 |
91 |
92 |
93 | 94 | 97 | 98 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | :root { 6 | --black: #000; 7 | --dark: #111822; 8 | --gray: #3c3c3c; 9 | --white: #fff; 10 | --red: #f94646; 11 | --orange: #f89b4b; 12 | --blue: #00bcd4; 13 | } 14 | 15 | body { 16 | margin: 0; 17 | background-color: var(--dark); 18 | color: var(--white); 19 | font-family: 'Source Sans Pro', 'Bitter', Helvetica,sans-serif; 20 | } 21 | 22 | footer {margin-top: 150px; padding: 15px; border-top: 2px solid var(--red);} 23 | 24 | nav { 25 | width: 100%; 26 | margin: auto; 27 | padding: 20px 250px; 28 | } 29 | 30 | section { 31 | width: 100%; 32 | max-width: 100%; 33 | display: flex; 34 | align-items: center; 35 | justify-content: center; 36 | margin: 30px auto; 37 | padding: 30px 250px; 38 | overflow: hidden; 39 | } 40 | 41 | nav ul { 42 | list-style: none; 43 | display: flex; 44 | padding: 0; 45 | margin: 0; 46 | } 47 | 48 | li { 49 | font-size: 1.2rem; 50 | padding: 5px; 51 | } 52 | 53 | section li {font-size: 1.5rem;} 54 | 55 | nav li { 56 | list-style: none; 57 | padding: 0 15px; 58 | } 59 | 60 | h1 { 61 | background: transparent; 62 | background: linear-gradient(to right, var(--red), var(--orange)); 63 | -webkit-background-clip: text; 64 | -webkit-text-fill-color: transparent; 65 | 66 | margin: 0.67em 0; 67 | font-family: 'Source Sans Pro',sans-serif; 68 | font-size: 54px; 69 | line-height: .9; 70 | font-weight: 900; 71 | letter-spacing: 2px; 72 | } 73 | 74 | h2 { 75 | margin-top: 0; 76 | font-size: 44px; 77 | } 78 | 79 | h3 { 80 | margin: 0; 81 | font-size: 30px; 82 | } 83 | 84 | a { 85 | color: var(--white); 86 | font-size: 1.2rem; 87 | display: block; 88 | font-weight: 500; 89 | text-decoration: none; 90 | transition: transform 0.1s linear; 91 | } 92 | 93 | p { 94 | font-size: 1.2rem; 95 | word-wrap: break-word; 96 | } 97 | 98 | .btn { 99 | display: inline-block; 100 | padding: 15px 25px; 101 | border-radius: 5px; 102 | margin: 10px; 103 | background-color: var(--red); 104 | color: var(--white); 105 | font-size: 18px; 106 | line-height: 1; 107 | font-weight: 700; 108 | text-align: center; 109 | text-shadow: none; 110 | object-fit: fill; 111 | border: 0; 112 | text-decoration: none; 113 | cursor: pointer; 114 | letter-spacing: 0.6px; 115 | text-transform: uppercase; 116 | box-shadow: 2px 2px 7px 0 rgb(0 0 0 / 13%); 117 | } 118 | 119 | img { 120 | max-width: 100%; 121 | image-rendering: crisp-edges; 122 | image-rendering: -moz-crisp-edges; /* Firefox */ 123 | image-rendering: -o-crisp-edges; /* Opera */ 124 | image-rendering: -webkit-optimize-contrast; /* Webkit (non-standard naming)*/ 125 | -ms-interpolation-mode: nearest-neighbor; /* IE (non-standard property) */ 126 | } 127 | 128 | .capture { 129 | object-fit: contain; 130 | max-width: -webkit-fill-available; 131 | border: 15px solid var(--dark); 132 | overflow: hidden; 133 | } 134 | 135 | .lead { 136 | opacity: .6; 137 | font-size: 1.7rem; 138 | word-wrap: break-word; 139 | } 140 | 141 | .vh-100 {height: 100vh;} 142 | 143 | .w-100 { 144 | width: 100%; 145 | } 146 | 147 | .mx-auto {margin: auto;} 148 | 149 | .m-0 { 150 | margin: 0; 151 | } 152 | 153 | .mt-0 { 154 | margin-top: 0; 155 | } 156 | 157 | .p-4 {padding: 40px;} 158 | 159 | .mt-3 { 160 | margin-top: 30px; 161 | } 162 | 163 | .ml-1 { 164 | margin-left: 10px; 165 | } 166 | 167 | .bg-white { 168 | background-color: var(--white); 169 | color: var(--dark); 170 | } 171 | 172 | .dark { 173 | color: var(--dark); 174 | } 175 | 176 | .blue { 177 | color: var(--blue); 178 | } 179 | 180 | .transparent { 181 | background-color: transparent; 182 | } 183 | 184 | .d-flex { 185 | display: flex; 186 | } 187 | 188 | .mr-2 { 189 | margin-right: 20px; 190 | } 191 | 192 | .pl-0 { 193 | padding-left: 0; 194 | } 195 | 196 | .pr-0 { 197 | padding-right: 0; 198 | } 199 | 200 | .p-0 { 201 | padding: 0; 202 | } 203 | 204 | .p-2 { 205 | padding: 20px; 206 | } 207 | 208 | .text-center { 209 | text-align: center; 210 | } 211 | 212 | .text-uppercase { 213 | text-transform: uppercase; 214 | } 215 | 216 | .shadow-0 { 217 | box-shadow: none; 218 | } 219 | 220 | .shadow {box-shadow: 0 4px 10px 0 rgb(0 0 0 / 10%), 0 1px 18px 0 rgb(0 0 0 / 10%), 0 3px 5px 0 rgb(0 0 0 / 14%);} 221 | 222 | .br7 { 223 | border-radius: 7px; 224 | } 225 | 226 | .vertical-align { 227 | display: flex; 228 | align-items: center; 229 | justify-content: center; 230 | } 231 | 232 | .d-flex {display: flex;} 233 | .justify-between {justify-content: space-between;} 234 | .align-center {align-items: center;} 235 | 236 | .grid-2 { 237 | width: 100%; 238 | display: grid; 239 | grid-template-columns: 1fr 1fr; 240 | gap: 20px; 241 | align-items: center; 242 | justify-content: space-between; 243 | } 244 | 245 | @media only screen and (max-width: 900px) { 246 | .vertical-align { 247 | flex-direction: column; 248 | } 249 | } 250 | 251 | @media only screen and (max-width: 600px) { 252 | 253 | main, 254 | nav { 255 | width: 95%; 256 | } 257 | 258 | section, 259 | nav { 260 | padding: 10px 15px; 261 | } 262 | 263 | section { 264 | padding: 10px 15px; 265 | } 266 | 267 | h1 {font-size: 40px;} 268 | h2 { font-size: 30px; } 269 | h2 { font-size: 25px; } 270 | section li {font-size: 1.2rem;} 271 | 272 | .lead {font-size: 15pt;} 273 | 274 | .grid-2 { 275 | width: 100%; 276 | display: grid; 277 | grid-template-columns: 1fr; 278 | } 279 | 280 | .vertical-align { 281 | flex-direction: column; 282 | } 283 | } -------------------------------------------------------------------------------- /icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chromo-lib/screen-recorder/2734191a37d658e0a4b373d030513c17b0c8001c/icons/128.png -------------------------------------------------------------------------------- /icons/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chromo-lib/screen-recorder/2734191a37d658e0a4b373d030513c17b0c8001c/icons/32.png -------------------------------------------------------------------------------- /icons/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chromo-lib/screen-recorder/2734191a37d658e0a4b373d030513c17b0c8001c/icons/512.png -------------------------------------------------------------------------------- /icons/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chromo-lib/screen-recorder/2734191a37d658e0a4b373d030513c17b0c8001c/icons/64.png -------------------------------------------------------------------------------- /manifest-v2.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Reco - Screen Recorder", 4 | "description": "Reco is screen, audio and camera recorder extension tool", 5 | "version": "2.1.2", 6 | "icons": { 7 | "512": "icons/512.png", 8 | "128": "icons/128.png", 9 | "64": "icons/64.png", 10 | "32": "icons/32.png" 11 | }, 12 | "browser_action": { 13 | "default_title": "Reco - Screen Recorder", 14 | "default_popup": "popup.html" 15 | }, 16 | "permissions": [ 17 | "desktopCapture", 18 | "activeTab", 19 | "tabs", 20 | "" 21 | ], 22 | "background": { 23 | "scripts": [ 24 | "background.js" 25 | ], 26 | "persistent": false 27 | }, 28 | "content_scripts": [ 29 | { 30 | "css": [ 31 | "content.css" 32 | ], 33 | "js": [ 34 | "content.js" 35 | ], 36 | "matches": [ 37 | "*://*/*" 38 | ], 39 | "run_at": "document_end" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Reco - Screen Recorder", 4 | "description": "An effective tool for high quality video record from active tab, desktop or webcam", 5 | "version": "2.1.2", 6 | "icons": { 7 | "512": "icons/512.png", 8 | "128": "icons/128.png", 9 | "64": "icons/64.png", 10 | "32": "icons/32.png" 11 | }, 12 | "action": { 13 | "default_title": "Reco - Screen Recorder", 14 | "default_popup": "popup.html" 15 | }, 16 | "permissions": [ 17 | "desktopCapture", 18 | "activeTab", 19 | "tabs" 20 | ], 21 | "host_permissions": [ 22 | "" 23 | ], 24 | "background": { 25 | "service_worker": "background.js" 26 | }, 27 | "content_scripts": [ 28 | { 29 | "css": [ 30 | "content.css" 31 | ], 32 | "js": [ 33 | "content.js" 34 | ], 35 | "matches": [ 36 | "*://*/*" 37 | ], 38 | "run_at": "document_end" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reco", 3 | "version": "2.1.2", 4 | "description": "Reco is screen, audio and camera recorder extension tool", 5 | "main": "index.js", 6 | "private": true, 7 | "scripts": { 8 | "docs": "servino -r docs -w docs", 9 | "cp": "bash ./copy.sh", 10 | "dev": "NODE_ENV=dev rollup -c -w", 11 | "build": "rm -rf dist && yarn cp && NODE_ENV=production rollup -c", 12 | "zip:chrome": "NODE_ENV=production BROWSER=chrome yarn build && (cd dist; zip -r ../chrome.zip .)", 13 | "zip:firefox": "NODE_ENV=production BROWSER=firefox yarn build && (cd dist; zip -r ../firefox.zip .)", 14 | "zip": "npm run zip:firefox && npm run zip:chrome" 15 | }, 16 | "dependencies": { 17 | "@preact/signals": "^1.1.1", 18 | "preact": "^10.11.0", 19 | "servino": "^2.0.9" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.19.3", 23 | "@babel/plugin-transform-runtime": "^7.19.1", 24 | "@babel/preset-react": "^7.18.6", 25 | "@rollup/plugin-alias": "^3.1.9", 26 | "@rollup/plugin-babel": "^6.0.0", 27 | "@rollup/plugin-commonjs": "^22.0.2", 28 | "@rollup/plugin-node-resolve": "^13.3.0", 29 | "babel-preset-preact": "^2.0.0", 30 | "postcss": "^8.4.17", 31 | "rollup": "^2.79.0", 32 | "rollup-plugin-postcss": "^4.0.2", 33 | "rollup-plugin-terser": "^7.0.2" 34 | }, 35 | "author": "Haikel Fazzani", 36 | "license": "Apache Version 2.0" 37 | } -------------------------------------------------------------------------------- /plugin.js: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from 'node:fs/promises' 2 | import { resolve } from 'node:path' 3 | 4 | export function replaceWord({ from, to }) { 5 | return { 6 | name: 'dist', 7 | writeBundle: { 8 | sequential: true, 9 | order: 'post', 10 | async handler({ file }) { 11 | const fileContent = await readFile(resolve(process.cwd(), file), 'utf8'); 12 | writeFile(file, fileContent.replace(new RegExp(`${from}\\.`, 'g'), to + '.')); 13 | } 14 | } 15 | }; 16 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from 'fs' 2 | import path, { resolve } from 'path'; 3 | import postcss from 'rollup-plugin-postcss' 4 | import babel from '@rollup/plugin-babel'; 5 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 6 | import commonjs from '@rollup/plugin-commonjs'; 7 | import alias from '@rollup/plugin-alias'; 8 | import { terser } from "rollup-plugin-terser"; 9 | import { replaceWord } from './plugin'; 10 | 11 | const pkg = require('./package.json'); 12 | 13 | console.log('Process ===> ', process.env.BROWSER, process.env.NODE_ENV); 14 | 15 | const isChrome = process.env.BROWSER === undefined ? true : process.env.BROWSER === 'chrome'; 16 | const from = isChrome ? 'browser' : 'chrome'; 17 | const to = isChrome ? 'chrome' : 'browser'; 18 | 19 | if (!isChrome) { 20 | const content = readFileSync(resolve(process.cwd(), 'manifest-v2.json'), 'utf8'); 21 | writeFileSync(resolve(process.cwd(), 'dist', 'manifest.json'), content); 22 | } 23 | 24 | const isProduction = process.env.NODE_ENV === 'production'; 25 | const banner = `/*! Reco - v${pkg.version} | Copyright 2022 - Haikel Fazzani */\n`; 26 | 27 | export default [ 28 | { 29 | input: "src/background/index.js", 30 | output: { 31 | file: "dist/background.js", 32 | format: "iife", 33 | sourcemap: !isProduction, 34 | banner 35 | }, 36 | plugins: [ 37 | replaceWord({ from, to }), 38 | isProduction ? terser() : '' 39 | ] 40 | }, 41 | { 42 | input: "src/editor/index.js", 43 | output: { 44 | file: "dist/editor.js", 45 | format: "iife", 46 | sourcemap: !isProduction, 47 | banner 48 | }, 49 | plugins: [ 50 | postcss({ 51 | extract: true, 52 | minimize: isProduction, 53 | extract: path.resolve('dist/editor.css') 54 | }), 55 | replaceWord({ from, to }), 56 | isProduction ? terser() : '' 57 | ] 58 | }, 59 | { 60 | input: "src/permission/index.js", 61 | output: { 62 | file: "dist/permission.js", 63 | format: "iife", 64 | sourcemap: !isProduction, 65 | banner 66 | }, 67 | plugins: [ 68 | postcss({ 69 | extract: true, 70 | minimize: isProduction, 71 | extract: path.resolve('dist/permission.css') 72 | }), 73 | replaceWord({ from, to }), 74 | isProduction ? terser() : '' 75 | ] 76 | }, 77 | { 78 | input: "src/popup/index.js", 79 | output: { 80 | file: "dist/popup.js", 81 | format: "iife", 82 | sourcemap: !isProduction, 83 | banner 84 | }, 85 | plugins: [ 86 | alias({ 87 | entries: [ 88 | { find: 'react', replacement: 'preact/compat' }, 89 | { find: 'react-dom/test-utils', replacement: 'preact/test-utils' }, 90 | { find: 'react-dom', replacement: 'preact/compat' }, 91 | { find: 'react/jsx-runtime', replacement: 'preact/jsx-runtime' } 92 | ] 93 | }), 94 | nodeResolve({ 95 | extensions: [".js"], 96 | }), 97 | babel({ 98 | babelHelpers: 'runtime', 99 | presets: ["@babel/preset-react"], 100 | plugins: [ 101 | "@babel/plugin-transform-runtime", 102 | ["@babel/plugin-transform-react-jsx", { pragma: "h", pragmaFrag: "Fragment", }] 103 | ], 104 | }), 105 | postcss({ 106 | extract: true, 107 | minimize: isProduction, 108 | extract: path.resolve('dist/popup.css') 109 | }), 110 | commonjs(), 111 | replaceWord({ from, to }), 112 | isProduction ? terser() : '' 113 | ] 114 | }, 115 | { 116 | input: "src/content/index.js", 117 | output: { 118 | file: "dist/content.js", 119 | format: "iife", 120 | sourcemap: !isProduction, 121 | banner 122 | }, 123 | plugins: [ 124 | alias({ 125 | entries: [ 126 | { find: 'react', replacement: 'preact/compat' }, 127 | { find: 'react-dom/test-utils', replacement: 'preact/test-utils' }, 128 | { find: 'react-dom', replacement: 'preact/compat' }, 129 | { find: 'react/jsx-runtime', replacement: 'preact/jsx-runtime' } 130 | ] 131 | }), 132 | nodeResolve({ 133 | extensions: [".js"], 134 | }), 135 | babel({ 136 | babelHelpers: 'runtime', 137 | presets: ["@babel/preset-react"], 138 | plugins: [ 139 | "@babel/plugin-transform-runtime", 140 | ["@babel/plugin-transform-react-jsx", { pragma: "h", pragmaFrag: "Fragment", }] 141 | ], 142 | }), 143 | postcss({ 144 | extract: true, 145 | minimize: isProduction, 146 | extract: path.resolve('dist/content.css') 147 | }), 148 | commonjs(), 149 | replaceWord({ from, to }), 150 | isProduction ? terser() : '' 151 | ] 152 | } 153 | ]; -------------------------------------------------------------------------------- /src/background/chooseDesktopMedia.js: -------------------------------------------------------------------------------- 1 | export default async function chooseDesktopMedia(videoMediaSource) { 2 | const tabs = await chrome.tabs.query({ currentWindow: true, active: true }); 3 | 4 | return new Promise((resolve, reject) => { 5 | chrome.desktopCapture.chooseDesktopMedia([videoMediaSource || 'window', 'screen', 'tab'], tabs[0], async (browserMediaSourceId) => { 6 | resolve(browserMediaSourceId) 7 | }); 8 | }); 9 | } -------------------------------------------------------------------------------- /src/background/index.js: -------------------------------------------------------------------------------- 1 | let videoURL = null; 2 | let videoLen = 0; 3 | let tabTitle = null; 4 | 5 | const onMessage = async (request, _, sendResponse) => { 6 | try { 7 | if (request.from === 'popup') { 8 | const response = await chrome.tabs.sendMessage(request.tabId, { ...request, from: 'worker' }); 9 | sendResponse(response || 'Recording start...'); 10 | } 11 | 12 | if (request.from === 'content' && request.videoURL) { 13 | sendResponse(null); 14 | videoURL = request.videoURL; 15 | videoLen = request.videoLen; 16 | tabTitle = request.tabTitle; 17 | chrome.tabs.create({ url: "editor.html", active: true }); 18 | } 19 | 20 | if (request.from === 'editor') { 21 | sendResponse({ tabTitle, videoURL, videoLen, from: 'background' }); 22 | } 23 | } catch (error) { 24 | console.log(error.message); 25 | sendResponse(null) 26 | } 27 | }; 28 | 29 | chrome.runtime.onMessage.addListener(onMessage); -------------------------------------------------------------------------------- /src/content/App.js: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from 'preact'; 2 | import { useCallback, useState } from 'preact/hooks'; 3 | 4 | import Draggable from './components/Draggable'; 5 | import Timer from './components/Timer'; 6 | import downloadVideo from './utils/downloadVideo'; 7 | import record from './utils/record'; 8 | 9 | import ButtonMove from './components/button/ButtonMove'; 10 | import ButtonPause from './components/button/ButtonPause'; 11 | import ButtonPlay from './components/button/ButtonPlay'; 12 | import ButtonResume from './components/button/ButtonResume'; 13 | import ButtonStop from './components/button/ButtonStop'; 14 | import ButtonDownload from './components/button/ButtonDownload'; 15 | 16 | import ButtonMicOn from './components/button/ButtonMicOn'; 17 | import ButtonMicOff from './components/button/ButtonMicOff'; 18 | 19 | import createLink from './utils/createLink'; 20 | import ButtonOpenEditor from './components/button/ButtonOpenEditor'; 21 | import ButtonClose from './components/button/ButtonClose'; 22 | 23 | const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; 24 | 25 | function App({ request }) { 26 | const { tabTitle, autoDownload, enableTimer, enableCamera, isMicrophoneConnected } = request; 27 | 28 | const [mediaRecorder, setMediaRecorder] = useState(null); 29 | const [isRecordingPlay, setIsRecordingPlay] = useState(false); 30 | const [isRecordingPaused, setIsRecordingPaused] = useState(false); 31 | const [isRecordingFinished, setIsRecordingFinished] = useState(false); 32 | 33 | const [audioStream, setAudioStream] = useState(null); 34 | const [isMicOn, setIsMicOn] = useState(enableCamera); 35 | 36 | const [isAppClosed, setIsAppClosed] = useState(false); 37 | 38 | const chunks = []; 39 | 40 | const onMediaControl = async (actionType) => { 41 | try { 42 | if (actionType === 'play' && mediaRecorder === null) { 43 | const { mediaRecorder, stream, audioStream } = await record(request); 44 | 45 | mediaRecorder.onstop = async () => { 46 | stream.getTracks().forEach(track => { track.stop(); }); 47 | 48 | setMediaRecorder(null); 49 | setIsRecordingPlay(false); 50 | setIsRecordingFinished(true); 51 | 52 | console.log('mediaRecorder.onstop: recording is stopped'); 53 | if (autoDownload) onDownload(); 54 | } 55 | 56 | mediaRecorder.ondataavailable = (e) => { 57 | chunks.push(e.data); 58 | } 59 | 60 | setMediaRecorder(mediaRecorder); 61 | setAudioStream(audioStream); 62 | setIsRecordingPlay(true); 63 | } 64 | 65 | if (actionType === 'stop' && mediaRecorder) { mediaRecorder.stop(); } 66 | 67 | if (actionType === 'pause' && mediaRecorder) { 68 | mediaRecorder.pause(); 69 | setIsRecordingPaused(true); 70 | } 71 | 72 | if (actionType === 'resume' && mediaRecorder) { 73 | mediaRecorder.resume(); 74 | setIsRecordingPaused(false); 75 | } 76 | } catch (error) { 77 | console.log('Recording: ', error); 78 | setIsMicOn(false); 79 | } 80 | } 81 | 82 | const onMicControl = () => { 83 | if (audioStream && audioStream.getTracks().length > 0) { 84 | const state = !isMicOn; 85 | audioStream.getTracks().forEach((track) => { track.enabled = state }); 86 | setIsMicOn(state); 87 | } 88 | } 89 | 90 | const onDownload = useCallback(() => { 91 | downloadVideo(chunks, tabTitle || 'reco', 'video/webm'); 92 | }, []); 93 | 94 | const onOpenEditor = useCallback(async () => { 95 | const videoURL = createLink(chunks); 96 | const videoLen = +localStorage.getItem('reco-timer'); 97 | await chrome.runtime.sendMessage({ from: 'content', videoURL, videoLen, tabTitle }); 98 | }, []); 99 | 100 | const onDeleteRecording = () => { 101 | if(window.confirm('Do you really want to delete this?')) { 102 | setIsAppClosed(true) 103 | } 104 | } 105 | 106 | if ((autoDownload && isRecordingFinished) || isAppClosed) { 107 | return 108 | } 109 | if (isRecordingFinished) { 110 | return 111 | 112 | {!autoDownload && } 113 | {!autoDownload && !isFirefox && } 114 | {!autoDownload && } 115 | 116 | } 117 | else { 118 | return 119 | 120 | 121 | 122 | 123 | {isMicrophoneConnected && 124 | {isMicOn 125 | ? 126 | : } 127 | } 128 | 129 | {enableTimer && } 130 | 131 | {isRecordingPlay 132 | ? 133 | { onMediaControl('stop') }} title='Stop Recording' /> 134 | {isRecordingPaused 135 | ? { onMediaControl('resume') }} /> 136 | : { onMediaControl('pause') }} />} 137 | 138 | 139 | : { onMediaControl('play') }} />} 140 | 141 | 142 | } 143 | } 144 | 145 | export default App; -------------------------------------------------------------------------------- /src/content/Camera.js: -------------------------------------------------------------------------------- 1 | import { Fragment, h } from 'preact'; 2 | import { useEffect, useRef, useState } from 'preact/hooks'; 3 | import Draggable from './components/Draggable'; 4 | import ButtonCameraOff from './components/button/ButtonCameraOff'; 5 | import ButtonCameraOn from './components/button/ButtonCameraOn'; 6 | import ButtonClose from './components/button/ButtonClose'; 7 | import ButtonMicOn from './components/button/ButtonMicOn'; 8 | import ButtonMicOff from './components/button/ButtonMicOff'; 9 | 10 | export default function Camera({ request }) { 11 | const { 12 | enableAudioCamera, 13 | enableCamera, 14 | videoInput, 15 | isMicrophoneConnected, 16 | isCameraConnected, 17 | resolution, 18 | placeholder 19 | } = request; 20 | 21 | const videoEl = useRef(); 22 | const [cameraStream, setCameraStream] = useState(null); 23 | const [isCameraOn, setIsCameraOn] = useState(enableCamera); 24 | const [isMicOn, setIsMicOn] = useState(enableAudioCamera); 25 | const [isClosed, setIsClosed] = useState(false); 26 | const [error, setError] = useState(null); 27 | 28 | const isCircle = placeholder.radius === 50; 29 | const borderRadius = isCircle ? '50%' : placeholder.radius + '%'; 30 | 31 | const containerStyle = { 32 | width: isCircle ? placeholder.width + 'px' : 'auto', 33 | height: isCircle ? placeholder.height + 'px' : 'auto', 34 | background: 'transparent', 35 | borderRadius 36 | }; 37 | 38 | const videoStyle = { 39 | width: isCircle ? '80%' : placeholder.width + 'px', 40 | height: isCircle ? '80%' : placeholder.height + 'px', 41 | borderRadius 42 | } 43 | 44 | useEffect(() => { 45 | if (!enableCamera || !isCameraConnected) return; 46 | 47 | const constraints = { 48 | audio: isMicrophoneConnected, 49 | video: { 50 | deviceId: videoInput, 51 | facingMode: 'user', 52 | // height: { exact: videoMediaSource === 'webcam' ? resolution.height : videoEl.current.clientHeight }, 53 | // width: { exact: videoMediaSource === 'webcam' ? resolution.width : videoEl.current.clientWidth } 54 | } 55 | } 56 | 57 | navigator.mediaDevices.getUserMedia(constraints) 58 | .then(stream => { 59 | if (!enableAudioCamera && isMicrophoneConnected) stream.getAudioTracks().forEach((track) => { track.stop(); }); 60 | 61 | videoEl.current.autoplay = true; 62 | videoEl.current.srcObject = stream; 63 | setCameraStream(stream); 64 | }) 65 | .catch(e => { 66 | setError(e.message + ' video/or microphone for this Website, please check yours settings.'); 67 | }); 68 | 69 | return () => { 70 | if (cameraStream) { 71 | cameraStream.getTracks().forEach((track) => { track.stop(); }); 72 | } 73 | } 74 | }, []); 75 | 76 | const onToggleVideo = () => { 77 | if (cameraStream) { 78 | const state = !isCameraOn; 79 | cameraStream.getVideoTracks().forEach((track) => { track.enabled = state; }); 80 | setIsCameraOn(state) 81 | } 82 | } 83 | 84 | const onToggleMic = () => { 85 | if (cameraStream && cameraStream.getAudioTracks().length > 0) { 86 | const state = !isMicOn; 87 | cameraStream.getAudioTracks().forEach((track) => { track.enabled = state; }); 88 | setIsMicOn(state) 89 | } 90 | } 91 | 92 | const onClose = () => { 93 | if (cameraStream) cameraStream.getTracks().forEach((track) => { track.stop(); }); 94 | setIsClosed(!isClosed) 95 | } 96 | 97 | if (isClosed) { 98 | return 99 | } 100 | if (error) { 101 | return 102 | 103 |
{error}
104 |
105 | } 106 | else { 107 | return 109 | 110 |
111 | {isMicrophoneConnected && 112 | {isMicOn 113 | ? 114 | : } 115 | } 116 | 117 | {isCameraOn 118 | ? 119 | : } 120 | 121 | 122 |
123 | 124 | 125 |
126 | } 127 | } -------------------------------------------------------------------------------- /src/content/components/Draggable.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { useCallback, useEffect, useRef } from 'preact/hooks'; 3 | 4 | function Draggable({ children, style, className = "" }) { 5 | const dragRef = useRef(null); 6 | let isMouseDown = false; 7 | let offset = [0, 0]; 8 | 9 | const onMouseDown = (e) => { 10 | isMouseDown = true; 11 | const dragDiv = dragRef.current; 12 | if (!dragDiv) return; 13 | 14 | const isTouch = /touch/g.test(e.type); 15 | const x = isTouch ? e.touches[0].clientX : e.clientX; 16 | const y = isTouch ? e.touches[0].clientY : e.clientY; 17 | 18 | offset = [ 19 | dragDiv.offsetLeft - x, 20 | dragDiv.offsetTop - y 21 | ]; 22 | 23 | dragDiv.addEventListener('mouseup', onMouseUp, true); 24 | dragDiv.addEventListener('touchend', onMouseUp, true); 25 | 26 | document.addEventListener('contextmenu', onContextMenu, false); 27 | document.addEventListener('touchmove', onMouseMove, true); 28 | document.addEventListener('mousemove', onMouseMove, true); 29 | } 30 | 31 | const onMouseUp = () => { 32 | isMouseDown = false; 33 | document.removeEventListener('touchmove', onMouseMove, true); 34 | document.removeEventListener('mousemove', onMouseMove, true); 35 | document.removeEventListener('contextmenu', onContextMenu, false); 36 | } 37 | 38 | const onMouseMove = useCallback((e) => { 39 | const isTouch = /touch/g.test(e.type); 40 | 41 | if (!isTouch) { 42 | e.preventDefault(); 43 | } 44 | 45 | if (isMouseDown && dragRef.current) { 46 | const x = isTouch ? e.touches[0].clientX : e.clientX; 47 | const y = isTouch ? e.touches[0].clientY : e.clientY; 48 | 49 | dragRef.current.style.left = (x + offset[0]) + 'px'; 50 | dragRef.current.style.top = (y + offset[1]) + 'px'; 51 | } 52 | }, []); 53 | 54 | const onContextMenu = () => { 55 | document.removeEventListener('mousemove', onMouseMove, true); 56 | document.removeEventListener('touchmove', onMouseMove, true); 57 | } 58 | 59 | useEffect(() => { 60 | const dragDiv = dragRef.current; 61 | 62 | dragDiv?.addEventListener('touchstart', onMouseDown, true); 63 | dragDiv?.addEventListener('mousedown', onMouseDown, true); 64 | 65 | return () => { 66 | dragDiv?.removeEventListener('mousedown', onMouseDown, true); 67 | dragDiv?.removeEventListener('mouseup', onMouseUp, true); 68 | document.removeEventListener('mousemove', onMouseMove, true); 69 | 70 | dragDiv?.removeEventListener('touchstart', onMouseDown, true); 71 | dragDiv?.removeEventListener('touchend', onMouseUp, true); 72 | document.removeEventListener('touchmove', onMouseMove, true); 73 | 74 | document.removeEventListener('contextmenu', onContextMenu, false); 75 | } 76 | }, []); 77 | 78 | return
{children}
79 | } 80 | 81 | export default Draggable; -------------------------------------------------------------------------------- /src/content/components/Timer.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { useState, useEffect } from 'preact/hooks'; 3 | import convertTime from '../utils/convertTime'; 4 | 5 | export default function Timer({ isRecordingPlay, isRecordingPaused }) { 6 | const [timer, setTimer] = useState(0); 7 | 8 | useEffect(() => { 9 | let timerId; 10 | 11 | if (isRecordingPlay) { 12 | timerId = setInterval(() => { 13 | if (!isRecordingPaused) { 14 | setTimer(prev => { 15 | const counter = prev + 1; 16 | localStorage.setItem('reco-timer', '' + counter) 17 | return counter 18 | }); 19 | } 20 | }, 1000); 21 | } 22 | 23 | return () => { 24 | clearInterval(timerId) 25 | } 26 | }, [isRecordingPlay, isRecordingPaused]); 27 | 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /src/content/components/button/ButtonCameraOff.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function ButtonCameraOff({ onClick }) { 4 | return 9 | } 10 | -------------------------------------------------------------------------------- /src/content/components/button/ButtonCameraOn.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function ButtonCameraOn({ onClick }) { 4 | return 9 | } 10 | -------------------------------------------------------------------------------- /src/content/components/button/ButtonClose.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function ButtonClose({ onClick, className, title = "Close Camera" }) { 4 | return 9 | } 10 | -------------------------------------------------------------------------------- /src/content/components/button/ButtonDownload.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function ButtonDownload({ onClick, style }) { 4 | return 10 | } -------------------------------------------------------------------------------- /src/content/components/button/ButtonMicOff.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function ButtonMicOff({ onClick, style, className }) { 4 | return 10 | } 11 | -------------------------------------------------------------------------------- /src/content/components/button/ButtonMicOn.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function ButtonMicOn({ onClick, style, className }) { 4 | return 10 | } 11 | -------------------------------------------------------------------------------- /src/content/components/button/ButtonMove.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function ButtonMove({ style }) { 4 | const s = {cursor: 'move' }; 5 | return 10 | } -------------------------------------------------------------------------------- /src/content/components/button/ButtonOpenEditor.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function ButtonOpenEditor({ onClick, style }) { 4 | return 10 | } -------------------------------------------------------------------------------- /src/content/components/button/ButtonPause.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function ButtonPause({ onClick, style }) { 4 | return 9 | } -------------------------------------------------------------------------------- /src/content/components/button/ButtonPlay.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function ButtonPlay({ onClick, style }) { 4 | return 9 | } -------------------------------------------------------------------------------- /src/content/components/button/ButtonResume.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function ButtonResume({ onClick, style }) { 4 | return 9 | } -------------------------------------------------------------------------------- /src/content/components/button/ButtonStop.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function ButtonStop({ onClick, style, title }) { 4 | return 9 | } -------------------------------------------------------------------------------- /src/content/components/button/ButtonTash.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function ButtonTash({ onClick, style }) { 4 | return 9 | } -------------------------------------------------------------------------------- /src/content/index.js: -------------------------------------------------------------------------------- 1 | import { h, render } from "preact"; 2 | import App from "./App"; 3 | import Camera from "./Camera"; 4 | import './style.css' 5 | 6 | const createElement = (id = 'reco-controls') => { 7 | let rootElement = document.getElementById(id); 8 | if (rootElement) { 9 | rootElement.parentNode.removeChild(rootElement); 10 | } 11 | 12 | rootElement = document.createElement('div'); 13 | rootElement.id = id; 14 | document.body.appendChild(rootElement); 15 | return rootElement 16 | } 17 | 18 | const onMessage = async (request, _, sendResponse) => { 19 | try { 20 | if (request.message === 'start-record' && request.from === 'worker') { 21 | if (request.enableCamera) render(, createElement('reco-camera')); 22 | render(, createElement()); 23 | sendResponse(request); 24 | } 25 | 26 | if (request.message === 'stop-record' && request.from === 'worker') { 27 | sendResponse(request); 28 | } 29 | } catch (error) { 30 | console.log(error); 31 | sendResponse(error); 32 | } 33 | }; 34 | 35 | chrome.runtime.onMessage.addListener(onMessage); -------------------------------------------------------------------------------- /src/content/style.css: -------------------------------------------------------------------------------- 1 | #reco-controls button, 2 | #reco-camera button { 3 | background: transparent !important; 4 | border: 0; 5 | font-size: 14px; 6 | color: #fff; 7 | box-shadow: none; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | cursor: pointer; 12 | margin: 0 10px !important; 13 | padding: 0 !important; 14 | white-space: pre-wrap; 15 | } 16 | 17 | .drag-reco { 18 | width: max-content; 19 | max-width: max-content; 20 | height: 40px; 21 | 22 | position: fixed; 23 | bottom: 20px; 24 | right: 20px; 25 | 26 | display: flex; 27 | align-items: center; 28 | 29 | border-radius: 20px; 30 | background: #000000c9; 31 | text-transform: uppercase; 32 | text-align: center; 33 | z-index: 2147483647; 34 | cursor: move; 35 | box-shadow: 0 0 3em rgb(0 0 0 / 15%); 36 | } 37 | 38 | .video-container-reco { 39 | position: fixed; 40 | bottom: 20px; 41 | right: 20px; 42 | display: block; 43 | z-index: 2147483647; 44 | cursor: move; 45 | } 46 | 47 | .video-container-reco video { 48 | object-fit: cover !important 49 | } 50 | 51 | .video-container-reco .controls { 52 | width: fit-content; 53 | background-color: #000000c9; 54 | padding: 5px; 55 | margin: 0 auto 10px; 56 | border-radius: 50px; 57 | text-align: center; 58 | opacity: 0; 59 | transition: opacity 0.25s; 60 | } 61 | 62 | .video-container-reco:hover .controls { 63 | opacity: 1; 64 | } 65 | 66 | .alert-reco { 67 | word-break: break-word; 68 | white-space: pre-wrap; 69 | background: #ff3131; 70 | color: #fff; 71 | padding: 5px; 72 | border-radius: 7px 73 | } 74 | 75 | .w-100 { 76 | width: 100%; 77 | } 78 | 79 | .d-flex { 80 | display: flex; 81 | } 82 | 83 | .flex-column { 84 | flex-direction: column; 85 | } 86 | 87 | .justify-between { 88 | justify-content: space-between; 89 | } 90 | 91 | .justify-center { 92 | justify-content: center; 93 | } 94 | 95 | .align-center { 96 | align-items: center; 97 | } 98 | 99 | .red { 100 | color: rgb(255, 36, 36) !important; 101 | } 102 | 103 | .ml-0 { 104 | margin-left: 0; 105 | } 106 | 107 | .shadow-reco { 108 | box-shadow: 0 0 3em rgb(0 0 0 / 15%); 109 | } 110 | 111 | .shadow-reco-0 { 112 | box-shadow: none !important; 113 | } -------------------------------------------------------------------------------- /src/content/utils/convertTime.js: -------------------------------------------------------------------------------- 1 | export default function convertTime(totalSeconds) { 2 | if (totalSeconds === 0) return '00:00' 3 | else { 4 | const minutes = Math.floor(totalSeconds / 60); 5 | const seconds = totalSeconds - minutes * 60; 6 | return (minutes < 10 ? '0' + minutes : minutes) + ':' + (seconds < 10 ? '0' + seconds : seconds); 7 | } 8 | } -------------------------------------------------------------------------------- /src/content/utils/createLink.js: -------------------------------------------------------------------------------- 1 | export default function createLink(data, type = 'video/webm') { 2 | return window.URL.createObjectURL(new Blob(data, { type })); 3 | } 4 | -------------------------------------------------------------------------------- /src/content/utils/downloadVideo.js: -------------------------------------------------------------------------------- 1 | import createLink from "./createLink"; 2 | 3 | export default function downloadVideo(data, filename, type, vidExtension = 'webm') { 4 | const url = createLink(data, type); 5 | 6 | const link = document.createElement('a'); 7 | link.href = url; 8 | link.download = `${new Date().toISOString()}.${vidExtension}`; 9 | 10 | document.body.appendChild(link); 11 | link.click(); 12 | 13 | setTimeout(() => { 14 | document.body.removeChild(link); 15 | window.URL.revokeObjectURL(url); 16 | }, 1000); 17 | 18 | return url; 19 | } -------------------------------------------------------------------------------- /src/content/utils/record.js: -------------------------------------------------------------------------------- 1 | export default async function record(request) { 2 | const { videoMediaSource, mimeType, enableMicrophone, isMicrophoneConnected, resolution, audioInput } = request; 3 | 4 | const isVideoMediaSourceWebcam = videoMediaSource === 'webcam'; 5 | let stream = null; 6 | let audioStream = null; 7 | 8 | const constraints = { 9 | video: { width: { ideal: resolution.width }, height: { ideal: resolution.height } }, 10 | audio: isVideoMediaSourceWebcam 11 | }; 12 | 13 | if (isVideoMediaSourceWebcam) stream = await navigator.mediaDevices.getUserMedia(constraints); 14 | else stream = await navigator.mediaDevices.getDisplayMedia(constraints); 15 | 16 | const mediaRecorder = new MediaRecorder(stream, { mimeType }); 17 | 18 | if (!isVideoMediaSourceWebcam && enableMicrophone && isMicrophoneConnected) { 19 | audioStream = await navigator.mediaDevices.getUserMedia({ audio: { deviceid: audioInput } }); 20 | audioStream.getAudioTracks()[0].enabled = true; 21 | stream.addTrack(audioStream.getAudioTracks()[0]); 22 | } 23 | 24 | setTimeout(() => { mediaRecorder.start(); }, 100); 25 | 26 | mediaRecorder.onerror = async event => { 27 | console.error(`Error recording stream: ${event.error.name}`); 28 | reject(event.error.name) 29 | } 30 | 31 | stream.getVideoTracks()[0].onended = async () => { 32 | mediaRecorder.stop(); 33 | }; 34 | 35 | return { mediaRecorder, stream, audioStream } 36 | } -------------------------------------------------------------------------------- /src/editor/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Editor 12 | 13 | 14 | 15 | 16 |
17 |

Recoding video

18 | 19 |
20 | 21 |
22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/editor/index.js: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | 3 | const formDownload = document.getElementById('form-download'); 4 | let store = { videoURL: null, videoLen: 0, tabTitle: null }; 5 | 6 | function downloadVideo(url, filename, vidExtension = 'webm') { 7 | const link = document.createElement('a'); 8 | link.href = url; 9 | link.download = `${filename}.${vidExtension}`; 10 | 11 | document.body.appendChild(link); 12 | link.click(); 13 | 14 | setTimeout(() => { 15 | document.body.removeChild(link); 16 | window.URL.revokeObjectURL(url); 17 | }, 1000); 18 | } 19 | 20 | chrome.runtime.sendMessage({ from: 'editor' }, (response) => { 21 | if (response && response.videoURL) { 22 | store = { ...response }; 23 | document.querySelector('video').src = response.videoURL + "#t=" + response.videoLen; 24 | document.querySelector('h1').textContent = response.tabTitle; 25 | } 26 | }); 27 | 28 | const onDownload = (e) => { 29 | e.preventDefault(); 30 | if (store.videoURL) { 31 | const filename = e.target.elements[0].value; 32 | downloadVideo(store.videoURL, filename || store.tabTitle); 33 | } 34 | } 35 | 36 | window.onbeforeunload = function (e) { 37 | try { 38 | const confirmationMessage = 'Are you sure you want to leave?'; 39 | (e || window.event).returnValue = confirmationMessage; 40 | if (store.videoURL) { window.URL.revokeObjectURL(store.videoURL) } 41 | return confirmationMessage; 42 | } catch (error) { 43 | if (store.videoURL) window.URL.revokeObjectURL(store.videoURL); 44 | } 45 | }; 46 | 47 | formDownload.addEventListener('submit', onDownload); -------------------------------------------------------------------------------- /src/editor/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light:#999999; 3 | --black:#181717; 4 | --green:#4CAF50; 5 | } 6 | 7 | *, 8 | *::before, 9 | *::after { 10 | box-sizing: border-box; 11 | } 12 | 13 | body { 14 | display: grid; 15 | grid-template-columns: 3fr 1fr; 16 | align-items: flex-start; 17 | gap: 40px; 18 | padding: 20px; 19 | background: rgb(247, 247, 247); 20 | } 21 | 22 | button { 23 | border: none; 24 | text-transform: uppercase; 25 | cursor: pointer; 26 | color: white; 27 | background-color: var(--black); 28 | outline: none; 29 | letter-spacing: 2px; 30 | line-height: 1.5; 31 | transition: opacity .25s; 32 | } 33 | 34 | button:hover {opacity: 0.8;} 35 | 36 | button,input { margin: 10px 0; padding: 12px 25px; border-radius: 4px; } 37 | 38 | label {font-size: 1.2rem;} 39 | 40 | video { 41 | width: 100%; 42 | box-shadow: 0 0 3em rgb(0 0 0 / 15%); 43 | } 44 | 45 | form { 46 | display: grid; 47 | background: white; 48 | padding: 20px; 49 | border-radius: 7px; 50 | box-shadow: 0 0 3em rgb(0 0 0 / 15%); 51 | } 52 | 53 | h1 {margin-top: 0;} 54 | 55 | .br7 {border-radius: 7px;} -------------------------------------------------------------------------------- /src/permission/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Permission Request 11 | 12 | 13 | 14 |

Permission Request

15 | 16 | 17 | -------------------------------------------------------------------------------- /src/permission/index.js: -------------------------------------------------------------------------------- 1 | import './style.css' 2 | 3 | function browserName() { 4 | let browserName = 'No browser detection'; 5 | if (/browser/gi.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor)) browserName = "browser"; 6 | if (/Edg/gi.test(navigator.userAgent)) browserName = "edge"; 7 | return browserName; 8 | } 9 | 10 | window.addEventListener('DOMContentLoaded', async () => { 11 | try { 12 | const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true }); 13 | stream.getTracks().forEach(track => track.stop()); 14 | window.close(); 15 | } catch (error) { 16 | const permission = await navigator.permissions.query({ name: 'microphone' }); 17 | 18 | if (permission.state === 'denied') { 19 | chrome.tabs.create({ 20 | url: browserName() + '://settings/content/siteDetails?site=' + encodeURIComponent(location.href) 21 | }); 22 | 23 | permission.onchange = function () { 24 | console.log('Camera permission state has changed to ', this.state); 25 | if (this.state !== 'granted') chrome.runtime.sendMessage({ from: 'permission', message: 'permission-fail' }); 26 | }; 27 | } 28 | window.close(); 29 | } 30 | }); -------------------------------------------------------------------------------- /src/permission/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 14px; 3 | font-family: Arial,"Helvetica Neue",Helvetica,sans-serif; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | margin: 0; 8 | height: 100vh; 9 | } -------------------------------------------------------------------------------- /src/popup/App.js: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from 'preact'; 2 | import { useState } from 'preact/hooks'; 3 | import IconCodec from './components/icons/IconCodec'; 4 | import ToggleMic from './containers/ToggleMic'; 5 | import ToggleCamera from './containers/ToggleCamera'; 6 | import MediaSource from './components/MediaSource'; 7 | import AudioInputs from './containers/AudioInputs'; 8 | 9 | import MimeTypes from './containers/MimeTypes'; 10 | import Resolutions from './containers/Resolutions'; 11 | import VideoInputs from './containers/VideoInputs'; 12 | 13 | import getCurrentTabId from './utils/getCurrentTabId'; 14 | import checkDevices from './utils/checkDevices'; 15 | import checkPermission from './utils/checkPermission'; 16 | import Message from './components/Message'; 17 | import isFirefox from './utils/isFirefox'; 18 | 19 | function App() { 20 | 21 | const [options, setOptions] = useState({ 22 | videoMediaSource: 'tab', // webcam, tab, desktop, window 23 | 24 | audioInput: 'default', 25 | videoInput: 'default', 26 | 27 | enableMicrophone: true, 28 | enableCamera: true, 29 | enableAudioCamera: false, 30 | 31 | mimeType: isFirefox ? 'video/webm' : 'video/webm;codecs=vp8,opus', 32 | 33 | enableTimer: true, 34 | autoDownload: true, 35 | resolution: '1280x720', 36 | 37 | placeholder: { 38 | radius: 50, 39 | width: 240, 40 | height: 240 41 | } 42 | }); 43 | 44 | const [message, setMessage] = useState(null) 45 | 46 | const onChange = e => { 47 | console.log(e.target.name, e.target.value); 48 | if (e.target.name.startsWith('placeholder')) { 49 | const name = e.target.name.replace(/\w+\./g, ''); 50 | let placeholder = { ...options.placeholder, [name]: e.target.value }; 51 | setOptions({ ...options, placeholder }); 52 | } 53 | else setOptions({ ...options, [e.target.name]: e.target.type === 'checkbox' ? e.target.checked : e.target.value }); 54 | } 55 | 56 | const onStartRecording = async e => { 57 | e.preventDefault(); 58 | const [width, height] = options.resolution.split('x'); 59 | const resolution = { width: +width, height: +height }; 60 | 61 | try { 62 | const permission = await checkPermission(options); 63 | const devicesStatus = await checkDevices(); 64 | const tabInfos = await getCurrentTabId(); 65 | 66 | const response = await chrome.runtime.sendMessage({ 67 | from: 'popup', 68 | message: 'start-record', 69 | ...tabInfos, 70 | ...options, 71 | ...devicesStatus, 72 | resolution 73 | }); 74 | 75 | } catch (error) { 76 | if (!error.message.includes('The message port closed before a response was received')) { 77 | setMessage(error.message) 78 | } 79 | } 80 | } 81 | 82 | return 83 | 84 | 85 | 86 |
87 |
88 | Microphone 89 | { setOptions({ ...options, enableMicrophone: status }) }} value={options.enableMicrophone} /> 90 | 91 |
92 | 93 |
94 | Camera 95 | 96 |
97 | { setOptions({ ...options, enableCamera: status }) }} value={options.enableCamera} /> 98 | 99 |
100 | 101 |
102 | H: 104 | W: 106 | R: 108 |
109 |
110 | 111 |
112 | video Codec 113 | 114 | 115 |
116 | 117 |
118 | options 119 | 120 | 121 | 122 |
123 | 124 | 125 |
126 | 127 |
128 | 129 | 130 |
131 |
132 | 133 | 134 |
135 | 136 | {message && } 137 |
138 | } 139 | 140 | export default App; -------------------------------------------------------------------------------- /src/popup/components/MediaSource.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { useState } from 'preact/hooks'; 3 | import IconDesk from './icons/IconDesk'; 4 | import IconTab from './icons/IconTab'; 5 | import IconWebcam from './icons/IconWebcam'; 6 | import IconWin from './icons/IconWin'; 7 | 8 | const sources = [ 9 | { label: 'tab', icon: }, 10 | { label: 'window', icon: }, 11 | { label: 'desktop', icon: }, 12 | { label: 'webcam', icon: }, 13 | ] 14 | 15 | export default function MediaSource() { 16 | 17 | const [tabIndex, setTabIndex] = useState(0); 18 | 19 | const onTab = index => { 20 | setTabIndex(index) 21 | } 22 | 23 | return
24 | Media Source 25 |
    26 | {sources.map((s, i) =>
  • { onTab(i) }}> 28 | {s.icon} 29 | {s.label} 30 |
  • )} 31 |
32 |
33 | } 34 | -------------------------------------------------------------------------------- /src/popup/components/Message.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function Message({ text, style }) { 4 | return 11 | } -------------------------------------------------------------------------------- /src/popup/components/icons/Hd.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function Hd() { 4 | return 5 | 6 | 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/popup/components/icons/IconCodec.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function IconCodec() { 4 | return 5 | 6 | 7 | 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/popup/components/icons/IconDesk.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function IconDesk() { 4 | return 5 | 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/popup/components/icons/IconTab.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function IconTab() { 4 | return 5 | 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/popup/components/icons/IconWebcam.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function IconWebcam() { 4 | return 5 | 6 | 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/popup/components/icons/IconWin.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | export default function IconWin() { 4 | return 5 | 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/popup/containers/AudioInputs.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { useEffect, useState } from "preact/hooks"; 3 | 4 | export default function AudioInputs({ onChange, value }) { 5 | 6 | const [devices, setDevices] = useState([]); 7 | 8 | useEffect(() => { 9 | navigator.mediaDevices.enumerateDevices() 10 | .then(enumerator => { 11 | const inputs = enumerator.filter(input => input.kind === "audioinput"); 12 | setDevices(inputs); 13 | }); 14 | }, []); 15 | 16 | return 19 | } -------------------------------------------------------------------------------- /src/popup/containers/MimeTypes.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { useMemo } from "preact/hooks"; 3 | 4 | export default function MimeTypes({ onChange, value }) { 5 | 6 | const mimeTypes = [ 7 | 'video/webm', 8 | 'video/webm;codecs=vp8', 9 | 'video/webm;codecs=vp9', 10 | 'video/webm;codecs=vp8.0', 11 | 'video/webm;codecs=vp9.0', 12 | 'video/webm;codecs=h264', 13 | 'video/webm;codecs=H264', 14 | 'video/webm;codecs=avc1', 15 | 'video/webm;codecs=vp8,opus', 16 | 'video/webm;codecs=VP8,OPUS', 17 | 'video/webm;codecs=vp9,opus', 18 | 'video/webm;codecs=vp8,vp9,opus', 19 | 'video/webm;codecs=h264,opus', 20 | 'video/webm;codecs=h264,vp9,opus', 21 | 'video/x-matroska;codecs=avc1', 22 | // 'audio/webm', 23 | // 'audio/wav' 24 | ]; 25 | 26 | const supported = useMemo(() => { 27 | return mimeTypes.filter(mimeType => { 28 | if (MediaRecorder.isTypeSupported(mimeType)) { 29 | return mimeType 30 | } 31 | }); 32 | }, []); 33 | 34 | return 37 | } 38 | -------------------------------------------------------------------------------- /src/popup/containers/Resolutions.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import Hd from '../components/icons/Hd'; 3 | import resolutions from "../utils/resolutions"; 4 | 5 | export default function Resolutions({ onChange, value }) { 6 | 7 | return
8 | 9 | 10 | 13 |
14 | } -------------------------------------------------------------------------------- /src/popup/containers/ToggleCamera.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { useState } from 'preact/hooks'; 3 | 4 | export default function ToggleCamera({ onClick, value }) { 5 | 6 | const [enable, setEnable] = useState(value) 7 | 8 | const onToggleMic = () => { 9 | const status = !enable; 10 | setEnable(status) 11 | onClick(status); 12 | } 13 | 14 | return 23 | } 24 | -------------------------------------------------------------------------------- /src/popup/containers/ToggleMic.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { useState } from 'preact/hooks'; 3 | 4 | export default function ToggleMic({ onClick, value }) { 5 | 6 | const [enable, setEnable] = useState(value) 7 | 8 | const onToggleMic = () => { 9 | const status = !enable; 10 | setEnable(status) 11 | onClick(status); 12 | } 13 | 14 | return 25 | } 26 | -------------------------------------------------------------------------------- /src/popup/containers/VideoInputs.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { useEffect, useState } from "preact/hooks"; 3 | 4 | export default function VideoInputs({ onChange, value }) { 5 | 6 | const [devices, setDevices] = useState([]); 7 | 8 | useEffect(() => { 9 | navigator.mediaDevices.enumerateDevices() 10 | .then(enumerator => { 11 | const inputs = enumerator.filter(input => input.kind === "videoinput"); 12 | setDevices(inputs); 13 | }); 14 | }, []); 15 | 16 | return 20 | } 21 | -------------------------------------------------------------------------------- /src/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/popup/index.js: -------------------------------------------------------------------------------- 1 | import { h, render } from "preact"; 2 | import App from "./App"; 3 | import './style.css' 4 | 5 | render(, document.getElementById('root')); -------------------------------------------------------------------------------- /src/popup/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light:#999999; 3 | --black:#181717; 4 | --green:#4CAF50; 5 | } 6 | 7 | *, 8 | *::before, 9 | *::after { 10 | box-sizing: border-box; 11 | } 12 | 13 | html { 14 | font-family: sans-serif; 15 | line-height: 1.15; 16 | -webkit-text-size-adjust: 100%; 17 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 18 | } 19 | 20 | 21 | body { 22 | margin: 0; 23 | width: 320px; 24 | display: flex; 25 | flex-direction: column; 26 | align-items: center; 27 | justify-content: center; 28 | padding: 13px; 29 | font-size: 14px; 30 | font-family: Arial, Helvetica, sans-serif; 31 | letter-spacing: 0.6px; 32 | } 33 | 34 | button { 35 | display: flex; 36 | align-items: center; 37 | justify-content: center; 38 | border: none; 39 | border-radius: 4px; 40 | text-transform: uppercase; 41 | cursor: pointer; 42 | color: white; 43 | background-color: var(--black); 44 | padding: 12px 25px; 45 | outline: none; 46 | letter-spacing: 2px; 47 | line-height: 1.5; 48 | transition: opacity .25s; 49 | } 50 | 51 | .btn-svg { 52 | background: transparent; 53 | padding: 0; 54 | margin: 0; 55 | font-size: initial; 56 | } 57 | 58 | input { 59 | padding: 10px 15px; 60 | width: 100%; 61 | border-radius: 4px; 62 | font-size: 14px; 63 | } 64 | 65 | input, select { 66 | width: 100%; 67 | border-radius: 4px; 68 | font-size: 14px; 69 | background-color: #fff; 70 | border: 0; 71 | outline: none; 72 | } 73 | 74 | input[type="checkbox"] {padding: 0; width: 15px; margin-right: 15px;} 75 | input[type="number"] { border: 1px solid #ddd; padding: 5px; width: 60px;} 76 | 77 | label,fieldset { 78 | color: var(--light); 79 | text-transform: uppercase; 80 | line-height: 1.5; 81 | } 82 | 83 | label { 84 | white-space: pre; 85 | } 86 | 87 | fieldset {margin-bottom: 10px; padding: 10px; border: 1px solid #ddd; border-radius: 7px;} 88 | 89 | svg path{ 90 | fill: var(--light); 91 | } 92 | 93 | svg {margin-right: 15px;} 94 | li svg {margin: 0;} 95 | 96 | button:hover {opacity: 0.8;} 97 | 98 | ul { list-style: none; margin: 0; padding: 0; } 99 | 100 | hr { 101 | border: 1px solid #ebebeb; 102 | width: 50%; 103 | margin: 17px auto; 104 | } 105 | 106 | legend {white-space: pre;} 107 | 108 | #video-media-source li { 109 | text-align: center; cursor: pointer; 110 | text-transform: uppercase; 111 | font-size: 11px; 112 | color: var(--light); 113 | } 114 | 115 | #video-media-source li svg { width: 25px; } 116 | 117 | .alert { 118 | width: 100%; 119 | font-size: 12px; 120 | margin-top: 10px; 121 | margin-bottom: 0; 122 | background-color: #e91e63; 123 | color: #fff; 124 | padding: 5px; 125 | overflow: auto; 126 | border-radius: 7px; 127 | white-space: pre-wrap; 128 | } 129 | 130 | .d-flex { display: flex; justify-content: center; align-items: center; } 131 | .flex-column {flex-direction: column;} 132 | .align-center {align-items: center;} 133 | .justify-between {justify-content: space-between;} 134 | .flex-wrap {flex-wrap: wrap;} 135 | 136 | .grid-2 { 137 | display: grid; 138 | grid-template-columns: 1fr 1fr; 139 | gap: 10px; 140 | } 141 | 142 | .vertical-align {display: flex; align-items: center;} 143 | 144 | .w-100 { width: 100%; } 145 | 146 | .green { background-color: #4CAF50 } 147 | .danger {background-color: #e91e63;} 148 | 149 | .active-tab svg path{fill: var(--black);} 150 | 151 | .border-bottom { border-bottom: 1px solid var(--light); } 152 | 153 | .p-2 {padding: 0.9rem;} 154 | .p-5 {padding: 5px;} 155 | 156 | .ml-05{margin-left: 5px;} 157 | .ml-1 { margin-left: 10px } 158 | .mr-1 { margin-right: 10px } 159 | .mb-1 { margin-bottom: 10px } 160 | .mt-1 { margin-top: 10px } 161 | 162 | .mb-2 { margin-bottom: 20px } 163 | 164 | .mt-0 {margin-top: 0;} 165 | 166 | .br7 {border-radius: 7px;} 167 | 168 | .text-uppercase {text-transform: uppercase;} 169 | 170 | small {color: #afafaf;} 171 | 172 | .border-right-0 {border-right: 0;} 173 | 174 | ::-webkit-scrollbar { 175 | width: 7px; 176 | height: 7px; 177 | border-radius: 10px; 178 | } 179 | 180 | ::-webkit-scrollbar-thumb { 181 | border-radius: 10px; 182 | } 183 | 184 | ::-webkit-scrollbar-thumb:hover { 185 | background: var(--light); 186 | } 187 | -------------------------------------------------------------------------------- /src/popup/utils/checkDevices.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @returns Promise<{isCameraConnected: true, isMicrophoneConnected: true}> 3 | */ 4 | export default async function checkDevices() { 5 | const status = { isMicrophoneConnected: false, isCameraConnected: false }; 6 | const enumerator = await navigator.mediaDevices.enumerateDevices(); 7 | 8 | enumerator.forEach(input => { 9 | if (input.kind === "audioinput") { 10 | status.isMicrophoneConnected = input.deviceId !== null && input.label !== null 11 | } 12 | 13 | if (input.kind === "videoinput") { 14 | status.isCameraConnected = input.deviceId !== null && input.label !== null 15 | } 16 | }); 17 | 18 | return status; 19 | } -------------------------------------------------------------------------------- /src/popup/utils/checkPermission.js: -------------------------------------------------------------------------------- 1 | import isFirefox from "./isFirefox"; 2 | 3 | /** 4 | * @param {Object} recordOptions 5 | * @returns boolean 6 | */ 7 | export default async function checkPermission(recordOptions) { 8 | const showPermissionWin = () => chrome.windows.create({ url: 'permission.html?video=true&audio=true', width: 400, height: 400, type: 'popup' }); 9 | 10 | if (isFirefox) { showPermissionWin(); return; } 11 | 12 | if (recordOptions.enableCamera) { 13 | const permissionCam = await navigator.permissions.query({ name: 'camera' }); 14 | 15 | if (permissionCam.state !== 'granted') { 16 | showPermissionWin(); 17 | return; 18 | } 19 | } 20 | 21 | if (recordOptions.enableMicrophone) { 22 | const permissionMic = await navigator.permissions.query({ name: 'microphone' }); 23 | 24 | if (permissionMic.state !== 'granted') { 25 | showPermissionWin(); 26 | return; 27 | } 28 | } 29 | 30 | return true 31 | } -------------------------------------------------------------------------------- /src/popup/utils/getCurrentTabId.js: -------------------------------------------------------------------------------- 1 | const tabsQuery = (options) => new Promise((resolve) => chrome.tabs.query(options, resolve)); 2 | 3 | export default async function getCurrentTabId() { 4 | const tabs = await tabsQuery({ currentWindow: true, active: true }); 5 | const currentTab = tabs[0]; 6 | const tabURL = new URL(currentTab.url).origin; 7 | 8 | if (currentTab.url.includes('browser://')) throw new Error('This page is not supported...'); 9 | else return { tabId: currentTab.id, tabTitle: currentTab.title, tabURL } 10 | } -------------------------------------------------------------------------------- /src/popup/utils/isFirefox.js: -------------------------------------------------------------------------------- 1 | const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; 2 | 3 | export default isFirefox -------------------------------------------------------------------------------- /src/popup/utils/resolutions.js: -------------------------------------------------------------------------------- 1 | const resolutions = [ 2 | '320x240', 3 | '640x480', 4 | '1280x720', 5 | '1920x1080', 6 | '3840x2160', 7 | '4096x2160' 8 | ]; 9 | 10 | export default resolutions --------------------------------------------------------------------------------