├── .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 |   
5 |
6 | # Capture
7 | 
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 |
31 |
36 |
37 |
44 |
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 |
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 |
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 |
95 | Created with ❤️ by Haikel Fazzani
96 |
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 | { setError(null) }} style={{ marginRight: '10px', cursor: 'pointer' }}>X
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 {convertTime(timer)}
29 | }
30 |
--------------------------------------------------------------------------------
/src/content/components/button/ButtonCameraOff.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 |
3 | export default function ButtonCameraOff({ onClick }) {
4 | return
5 |
6 |
7 |
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/src/content/components/button/ButtonCameraOn.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 |
3 | export default function ButtonCameraOn({ onClick }) {
4 | return
5 |
6 |
7 |
8 |
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
5 |
6 |
7 |
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/src/content/components/button/ButtonDownload.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 |
3 | export default function ButtonDownload({ onClick, style }) {
4 | return
5 |
6 |
7 |
8 | Download
9 |
10 | }
--------------------------------------------------------------------------------
/src/content/components/button/ButtonMicOff.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 |
3 | export default function ButtonMicOff({ onClick, style, className }) {
4 | return
5 |
6 |
7 |
8 |
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/src/content/components/button/ButtonMicOn.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 |
3 | export default function ButtonMicOn({ onClick, style, className }) {
4 | return
5 |
6 |
7 |
8 |
9 |
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
6 |
7 |
8 |
9 |
10 | }
--------------------------------------------------------------------------------
/src/content/components/button/ButtonOpenEditor.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 |
3 | export default function ButtonOpenEditor({ onClick, style }) {
4 | return
5 |
6 |
7 |
8 | Preview
9 |
10 | }
--------------------------------------------------------------------------------
/src/content/components/button/ButtonPause.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 |
3 | export default function ButtonPause({ onClick, style }) {
4 | return
5 |
6 |
7 |
8 |
9 | }
--------------------------------------------------------------------------------
/src/content/components/button/ButtonPlay.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 |
3 | export default function ButtonPlay({ onClick, style }) {
4 | return
5 |
6 |
7 |
8 |
9 | }
--------------------------------------------------------------------------------
/src/content/components/button/ButtonResume.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 |
3 | export default function ButtonResume({ onClick, style }) {
4 | return
5 |
6 |
7 |
8 |
9 | }
--------------------------------------------------------------------------------
/src/content/components/button/ButtonStop.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 |
3 | export default function ButtonStop({ onClick, style, title }) {
4 | return
5 |
6 |
7 |
8 |
9 | }
--------------------------------------------------------------------------------
/src/content/components/button/ButtonTash.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 |
3 | export default function ButtonTash({ onClick, style }) {
4 | return
5 |
6 |
7 |
8 |
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 |
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 |
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 |
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/src/popup/components/Message.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 |
3 | export default function Message({ text, style }) {
4 | return
5 |
6 |
7 |
8 |
9 | {text}
10 |
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
17 | {devices.map((d, i) => {d.label} )}
18 |
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
35 | {supported.map((mimeType, i) => {mimeType} )}
36 |
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 | Resolutions
10 |
11 | {resolutions.map((rs, i) => {rs} )}
12 |
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
15 | {enable
16 | ?
17 |
18 |
19 | :
20 |
21 | }
22 |
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
15 | {enable
16 | ?
17 |
18 |
19 |
20 | :
21 |
22 |
23 | }
24 |
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
17 | Default
18 | {devices.map((d, i) => {d.label} )}
19 |
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
--------------------------------------------------------------------------------