Spaces:
Running
Running
Update src/addons/addons/mediarecorder/userscript.js
Browse files
src/addons/addons/mediarecorder/userscript.js
CHANGED
@@ -1,395 +1,399 @@
|
|
1 |
import downloadBlob from "../../libraries/common/cs/download-blob.js";
|
2 |
|
3 |
export default async ({ addon, console, msg }) => {
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
});
|
20 |
-
const getOptions = () => {
|
21 |
-
const { backdrop, container, content, closeButton, remove } = addon.tab.createModal(msg("option-title"), {
|
22 |
-
isOpen: true,
|
23 |
-
useEditorClasses: true,
|
24 |
-
});
|
25 |
-
container.classList.add("mediaRecorderPopup");
|
26 |
-
content.classList.add("mediaRecorderPopupContent");
|
27 |
-
|
28 |
-
content.appendChild(
|
29 |
-
Object.assign(document.createElement("p"), {
|
30 |
-
textContent: msg("record-description"),
|
31 |
-
className: "recordOptionDescription",
|
32 |
-
})
|
33 |
-
);
|
34 |
-
|
35 |
-
// Seconds
|
36 |
-
const recordOptionSeconds = document.createElement("p");
|
37 |
-
const recordOptionSecondsInput = Object.assign(document.createElement("input"), {
|
38 |
-
type: "number",
|
39 |
-
min: 1,
|
40 |
-
defaultValue: 300,
|
41 |
-
id: "recordOptionSecondsInput",
|
42 |
-
className: addon.tab.scratchClass("prompt_variable-name-text-input"),
|
43 |
-
});
|
44 |
-
const recordOptionSecondsLabel = Object.assign(document.createElement("label"), {
|
45 |
-
htmlFor: "recordOptionSecondsInput",
|
46 |
-
textContent: msg("record-duration"),
|
47 |
-
});
|
48 |
-
recordOptionSeconds.appendChild(recordOptionSecondsLabel);
|
49 |
-
recordOptionSeconds.appendChild(recordOptionSecondsInput);
|
50 |
-
content.appendChild(recordOptionSeconds);
|
51 |
-
|
52 |
-
// Delay
|
53 |
-
const recordOptionDelay = document.createElement("p");
|
54 |
-
const recordOptionDelayInput = Object.assign(document.createElement("input"), {
|
55 |
-
type: "number",
|
56 |
-
min: 0,
|
57 |
-
defaultValue: 0,
|
58 |
-
id: "recordOptionDelayInput",
|
59 |
-
className: addon.tab.scratchClass("prompt_variable-name-text-input"),
|
60 |
-
});
|
61 |
-
const recordOptionDelayLabel = Object.assign(document.createElement("label"), {
|
62 |
-
htmlFor: "recordOptionDelayInput",
|
63 |
-
textContent: msg("start-delay"),
|
64 |
-
});
|
65 |
-
recordOptionDelay.appendChild(recordOptionDelayLabel);
|
66 |
-
recordOptionDelay.appendChild(recordOptionDelayInput);
|
67 |
-
content.appendChild(recordOptionDelay);
|
68 |
-
|
69 |
-
// Audio
|
70 |
-
const recordOptionAudio = Object.assign(document.createElement("p"), {
|
71 |
-
className: "mediaRecorderPopupOption",
|
72 |
-
});
|
73 |
-
const recordOptionAudioInput = Object.assign(document.createElement("input"), {
|
74 |
-
type: "checkbox",
|
75 |
-
defaultChecked: true,
|
76 |
-
id: "recordOptionAudioInput",
|
77 |
-
});
|
78 |
-
const recordOptionAudioLabel = Object.assign(document.createElement("label"), {
|
79 |
-
htmlFor: "recordOptionAudioInput",
|
80 |
-
textContent: msg("record-audio"),
|
81 |
-
title: msg("record-audio-description"),
|
82 |
-
});
|
83 |
-
recordOptionAudio.appendChild(recordOptionAudioInput);
|
84 |
-
recordOptionAudio.appendChild(recordOptionAudioLabel);
|
85 |
-
content.appendChild(recordOptionAudio);
|
86 |
-
|
87 |
-
// Mic
|
88 |
-
const recordOptionMic = Object.assign(document.createElement("p"), {
|
89 |
-
className: "mediaRecorderPopupOption",
|
90 |
-
});
|
91 |
-
const recordOptionMicInput = Object.assign(document.createElement("input"), {
|
92 |
-
type: "checkbox",
|
93 |
-
defaultChecked: false,
|
94 |
-
id: "recordOptionMicInput",
|
95 |
-
});
|
96 |
-
const recordOptionMicLabel = Object.assign(document.createElement("label"), {
|
97 |
-
htmlFor: "recordOptionMicInput",
|
98 |
-
textContent: msg("record-mic"),
|
99 |
-
});
|
100 |
-
recordOptionMic.appendChild(recordOptionMicInput);
|
101 |
-
recordOptionMic.appendChild(recordOptionMicLabel);
|
102 |
-
content.appendChild(recordOptionMic);
|
103 |
-
|
104 |
-
// Green flag
|
105 |
-
const recordOptionFlag = Object.assign(document.createElement("p"), {
|
106 |
-
className: "mediaRecorderPopupOption",
|
107 |
-
});
|
108 |
-
const recordOptionFlagInput = Object.assign(document.createElement("input"), {
|
109 |
-
type: "checkbox",
|
110 |
-
defaultChecked: true,
|
111 |
-
id: "recordOptionFlagInput",
|
112 |
-
});
|
113 |
-
const recordOptionFlagLabel = Object.assign(document.createElement("label"), {
|
114 |
-
htmlFor: "recordOptionFlagInput",
|
115 |
-
textContent: msg("record-after-flag"),
|
116 |
-
});
|
117 |
-
recordOptionFlag.appendChild(recordOptionFlagInput);
|
118 |
-
recordOptionFlag.appendChild(recordOptionFlagLabel);
|
119 |
-
content.appendChild(recordOptionFlag);
|
120 |
-
|
121 |
-
// Stop sign
|
122 |
-
const recordOptionStop = Object.assign(document.createElement("p"), {
|
123 |
-
className: "mediaRecorderPopupOption",
|
124 |
-
});
|
125 |
-
const recordOptionStopInput = Object.assign(document.createElement("input"), {
|
126 |
-
type: "checkbox",
|
127 |
-
defaultChecked: true,
|
128 |
-
id: "recordOptionStopInput",
|
129 |
-
});
|
130 |
-
const recordOptionStopLabel = Object.assign(document.createElement("label"), {
|
131 |
-
htmlFor: "recordOptionStopInput",
|
132 |
-
textContent: msg("record-until-stop"),
|
133 |
-
});
|
134 |
-
recordOptionFlagInput.addEventListener("change", () => {
|
135 |
-
const disabled = (recordOptionStopInput.disabled = !recordOptionFlagInput.checked);
|
136 |
-
if (disabled) {
|
137 |
-
recordOptionStopLabel.title = msg("record-until-stop-disabled", {
|
138 |
-
afterFlagOption: msg("record-after-flag"),
|
139 |
-
});
|
140 |
-
}
|
141 |
-
});
|
142 |
-
recordOptionStop.appendChild(recordOptionStopInput);
|
143 |
-
recordOptionStop.appendChild(recordOptionStopLabel);
|
144 |
-
content.appendChild(recordOptionStop);
|
145 |
-
|
146 |
-
// Record screen
|
147 |
-
const recordOptionScreen = Object.assign(document.createElement("p"), {
|
148 |
-
className: "mediaRecorderPopupOption",
|
149 |
-
});
|
150 |
-
const recordOptionScreenInput = Object.assign(document.createElement("input"), {
|
151 |
-
type: "checkbox",
|
152 |
-
defaultChecked: false,
|
153 |
-
id: "recordOptionScreen",
|
154 |
-
});
|
155 |
-
const recordOptionScreenLabel = Object.assign(document.createElement("label"), {
|
156 |
-
htmlFor: "recordOptionScreen",
|
157 |
-
textContent: msg("record-entire-screen") || "Record the entire screen",
|
158 |
-
});
|
159 |
-
recordOptionScreen.appendChild(recordOptionScreenInput);
|
160 |
-
recordOptionScreen.appendChild(recordOptionScreenLabel);
|
161 |
-
content.appendChild(recordOptionScreen);
|
162 |
-
recordOptionScreenInput.disabled = true;
|
163 |
-
if ('mediaDevices' in navigator && typeof navigator.mediaDevices.getDisplayMedia === 'function') {
|
164 |
-
recordOptionScreenInput.disabled = false;
|
165 |
-
}
|
166 |
-
|
167 |
-
let resolvePromise = null;
|
168 |
-
const optionPromise = new Promise((resolve) => {
|
169 |
-
resolvePromise = resolve;
|
170 |
-
});
|
171 |
-
let handleOptionClose = null;
|
172 |
-
|
173 |
-
backdrop.addEventListener("click", () => handleOptionClose(null));
|
174 |
-
closeButton.addEventListener("click", () => handleOptionClose(null));
|
175 |
-
|
176 |
-
handleOptionClose = (value) => {
|
177 |
-
resolvePromise(value);
|
178 |
-
remove();
|
179 |
-
};
|
180 |
-
|
181 |
-
const buttonRow = Object.assign(document.createElement("div"), {
|
182 |
-
className: addon.tab.scratchClass("prompt_button-row", { others: "mediaRecorderPopupButtons" }),
|
183 |
-
});
|
184 |
-
const cancelButton = Object.assign(document.createElement("button"), {
|
185 |
-
textContent: msg("cancel"),
|
186 |
-
});
|
187 |
-
cancelButton.addEventListener("click", () => handleOptionClose(null), { once: true });
|
188 |
-
buttonRow.appendChild(cancelButton);
|
189 |
-
const startButton = Object.assign(document.createElement("button"), {
|
190 |
-
textContent: msg("start"),
|
191 |
-
className: addon.tab.scratchClass("prompt_ok-button"),
|
192 |
-
});
|
193 |
-
startButton.addEventListener(
|
194 |
-
"click",
|
195 |
-
() =>
|
196 |
-
handleOptionClose({
|
197 |
-
secs: Number(recordOptionSecondsInput.value),
|
198 |
-
delay: Number(recordOptionDelayInput.value),
|
199 |
-
audioEnabled: recordOptionAudioInput.checked,
|
200 |
-
micEnabled: recordOptionMicInput.checked,
|
201 |
-
waitUntilFlag: recordOptionFlagInput.checked,
|
202 |
-
useStopSign: !recordOptionStopInput.disabled && recordOptionStopInput.checked,
|
203 |
-
recordWholeScreen: recordOptionScreenInput.checked,
|
204 |
-
}),
|
205 |
-
{ once: true }
|
206 |
-
);
|
207 |
-
buttonRow.appendChild(startButton);
|
208 |
-
content.appendChild(buttonRow);
|
209 |
-
|
210 |
-
return optionPromise;
|
211 |
-
};
|
212 |
-
const disposeRecorder = () => {
|
213 |
-
isRecording = false;
|
214 |
-
recordElem.textContent = msg("record");
|
215 |
-
recordElem.title = "";
|
216 |
-
recorder = null;
|
217 |
-
recordBuffer = [];
|
218 |
-
clearTimeout(timeout);
|
219 |
-
timeout = 0;
|
220 |
-
if (stopSignFunc) {
|
221 |
-
addon.tab.traps.vm.runtime.off("PROJECT_STOP_ALL", stopSignFunc);
|
222 |
-
stopSignFunc = null;
|
223 |
-
}
|
224 |
-
};
|
225 |
-
const stopRecording = (force) => {
|
226 |
-
if (isWaitingForFlag) {
|
227 |
-
addon.tab.traps.vm.runtime.off("PROJECT_START", waitingForFlagFunc);
|
228 |
-
isWaitingForFlag = false;
|
229 |
-
waitingForFlagFunc = null;
|
230 |
-
abortController.abort();
|
231 |
-
abortController = null;
|
232 |
-
disposeRecorder();
|
233 |
-
return;
|
234 |
-
}
|
235 |
-
if (!isRecording || !recorder || recorder.state === "inactive") return;
|
236 |
-
if (force) {
|
237 |
-
disposeRecorder();
|
238 |
-
} else {
|
239 |
-
recorder.onstop = () => {
|
240 |
-
const blob = new Blob(recordBuffer, {
|
241 |
-
type: isMp4CodecSupported ?
|
242 |
-
"video/mp4"
|
243 |
-
: "video/webm"
|
244 |
-
});
|
245 |
-
downloadBlob(isMp4CodecSupported ? "video.mp4" : "video.webm", blob);
|
246 |
-
disposeRecorder();
|
247 |
-
};
|
248 |
-
recorder.stop();
|
249 |
-
}
|
250 |
-
};
|
251 |
-
const startRecording = async (opts) => {
|
252 |
-
// Timer
|
253 |
-
const secs = Math.max(1, opts.secs);
|
254 |
-
|
255 |
-
// Initialize MediaRecorder
|
256 |
-
recordBuffer = [];
|
257 |
-
isRecording = true;
|
258 |
-
const vm = addon.tab.traps.vm;
|
259 |
-
let micStream;
|
260 |
-
if (opts.micEnabled) {
|
261 |
-
// Show permission dialog before green flag is clicked
|
262 |
-
try {
|
263 |
-
micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
264 |
-
} catch (e) {
|
265 |
-
if (e.name !== "NotAllowedError" && e.name !== "NotFoundError") throw e;
|
266 |
-
opts.micEnabled = false;
|
267 |
-
}
|
268 |
-
}
|
269 |
-
let screenRecordingStream;
|
270 |
-
if (opts.recordWholeScreen) {
|
271 |
-
// Show permission dialog before green flag is clicked
|
272 |
-
try {
|
273 |
-
screenRecordingStream = await navigator.mediaDevices.getDisplayMedia({
|
274 |
-
audio: opts.audioEnabled,
|
275 |
-
video: { mediaSource: "screen" }
|
276 |
-
});
|
277 |
-
} catch (e) {
|
278 |
-
const errorentirescreenmsg = msg("error-entire-screen");
|
279 |
-
console.warn(errorentirescreenmsg && errorentirescreenmsg.trim() !== "" ? errorentirescreenmsg : "An error occurred trying to record the whole screen", e);
|
280 |
-
opts.recordWholeScreen = false;
|
281 |
-
}
|
282 |
-
}
|
283 |
-
if (opts.waitUntilFlag) {
|
284 |
-
isWaitingForFlag = true;
|
285 |
-
Object.assign(recordElem, {
|
286 |
-
textContent: msg("click-flag"),
|
287 |
-
title: msg("click-flag-description"),
|
288 |
});
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
const
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
390 |
}
|
391 |
-
|
392 |
}
|
393 |
-
|
394 |
-
}
|
395 |
-
};
|
|
|
1 |
import downloadBlob from "../../libraries/common/cs/download-blob.js";
|
2 |
|
3 |
export default async ({ addon, console, msg }) => {
|
4 |
+
let recordElem;
|
5 |
+
let isRecording = false;
|
6 |
+
let isWaitingForFlag = false;
|
7 |
+
let waitingForFlagFunc = null;
|
8 |
+
let abortController = null;
|
9 |
+
let stopSignFunc = null;
|
10 |
+
let recordBuffer = [];
|
11 |
+
let recorder;
|
12 |
+
let timeout;
|
13 |
+
const isMp4CodecSupported = false;
|
14 |
+
// const isMp4CodecSupported = MediaRecorder.isTypeSupported('video/webm;codecs=h264');
|
15 |
+
while (true) {
|
16 |
+
const elem = await addon.tab.waitForElement('div[class*="menu-bar_file-group"] > div:last-child:not(.sa-record)', {
|
17 |
+
markAsSeen: true,
|
18 |
+
reduxEvents: ["scratch-gui/mode/SET_PLAYER", "fontsLoaded/SET_FONTS_LOADED", "scratch-gui/locales/SELECT_LOCALE"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
});
|
20 |
+
const getOptions = () => {
|
21 |
+
const { backdrop, container, content, closeButton, remove } = addon.tab.createModal(msg("option-title"), {
|
22 |
+
isOpen: true,
|
23 |
+
useEditorClasses: true,
|
24 |
+
});
|
25 |
+
container.classList.add("mediaRecorderPopup");
|
26 |
+
content.classList.add("mediaRecorderPopupContent");
|
27 |
+
|
28 |
+
content.appendChild(
|
29 |
+
Object.assign(document.createElement("p"), {
|
30 |
+
textContent: msg("record-description"),
|
31 |
+
className: "recordOptionDescription",
|
32 |
+
})
|
33 |
+
);
|
34 |
+
|
35 |
+
// Seconds
|
36 |
+
const recordOptionSeconds = document.createElement("p");
|
37 |
+
const recordOptionSecondsInput = Object.assign(document.createElement("input"), {
|
38 |
+
type: "number",
|
39 |
+
min: 1,
|
40 |
+
defaultValue: 300,
|
41 |
+
id: "recordOptionSecondsInput",
|
42 |
+
className: addon.tab.scratchClass("prompt_variable-name-text-input"),
|
43 |
+
});
|
44 |
+
const recordOptionSecondsLabel = Object.assign(document.createElement("label"), {
|
45 |
+
htmlFor: "recordOptionSecondsInput",
|
46 |
+
textContent: msg("record-duration"),
|
47 |
+
});
|
48 |
+
recordOptionSeconds.appendChild(recordOptionSecondsLabel);
|
49 |
+
recordOptionSeconds.appendChild(recordOptionSecondsInput);
|
50 |
+
content.appendChild(recordOptionSeconds);
|
51 |
+
|
52 |
+
// Delay
|
53 |
+
const recordOptionDelay = document.createElement("p");
|
54 |
+
const recordOptionDelayInput = Object.assign(document.createElement("input"), {
|
55 |
+
type: "number",
|
56 |
+
min: 0,
|
57 |
+
defaultValue: 0,
|
58 |
+
id: "recordOptionDelayInput",
|
59 |
+
className: addon.tab.scratchClass("prompt_variable-name-text-input"),
|
60 |
+
});
|
61 |
+
const recordOptionDelayLabel = Object.assign(document.createElement("label"), {
|
62 |
+
htmlFor: "recordOptionDelayInput",
|
63 |
+
textContent: msg("start-delay"),
|
64 |
+
});
|
65 |
+
recordOptionDelay.appendChild(recordOptionDelayLabel);
|
66 |
+
recordOptionDelay.appendChild(recordOptionDelayInput);
|
67 |
+
content.appendChild(recordOptionDelay);
|
68 |
+
|
69 |
+
// Audio
|
70 |
+
const recordOptionAudio = Object.assign(document.createElement("p"), {
|
71 |
+
className: "mediaRecorderPopupOption",
|
72 |
+
});
|
73 |
+
const recordOptionAudioInput = Object.assign(document.createElement("input"), {
|
74 |
+
type: "checkbox",
|
75 |
+
defaultChecked: true,
|
76 |
+
id: "recordOptionAudioInput",
|
77 |
+
});
|
78 |
+
const recordOptionAudioLabel = Object.assign(document.createElement("label"), {
|
79 |
+
htmlFor: "recordOptionAudioInput",
|
80 |
+
textContent: msg("record-audio"),
|
81 |
+
title: msg("record-audio-description"),
|
82 |
+
});
|
83 |
+
recordOptionAudio.appendChild(recordOptionAudioInput);
|
84 |
+
recordOptionAudio.appendChild(recordOptionAudioLabel);
|
85 |
+
content.appendChild(recordOptionAudio);
|
86 |
+
|
87 |
+
// Mic
|
88 |
+
const recordOptionMic = Object.assign(document.createElement("p"), {
|
89 |
+
className: "mediaRecorderPopupOption",
|
90 |
+
});
|
91 |
+
const recordOptionMicInput = Object.assign(document.createElement("input"), {
|
92 |
+
type: "checkbox",
|
93 |
+
defaultChecked: false,
|
94 |
+
id: "recordOptionMicInput",
|
95 |
+
});
|
96 |
+
const recordOptionMicLabel = Object.assign(document.createElement("label"), {
|
97 |
+
htmlFor: "recordOptionMicInput",
|
98 |
+
textContent: msg("record-mic"),
|
99 |
+
});
|
100 |
+
recordOptionMic.appendChild(recordOptionMicInput);
|
101 |
+
recordOptionMic.appendChild(recordOptionMicLabel);
|
102 |
+
content.appendChild(recordOptionMic);
|
103 |
+
|
104 |
+
// Green flag
|
105 |
+
const recordOptionFlag = Object.assign(document.createElement("p"), {
|
106 |
+
className: "mediaRecorderPopupOption",
|
107 |
+
});
|
108 |
+
const recordOptionFlagInput = Object.assign(document.createElement("input"), {
|
109 |
+
type: "checkbox",
|
110 |
+
defaultChecked: true,
|
111 |
+
id: "recordOptionFlagInput",
|
112 |
+
});
|
113 |
+
const recordOptionFlagLabel = Object.assign(document.createElement("label"), {
|
114 |
+
htmlFor: "recordOptionFlagInput",
|
115 |
+
textContent: msg("record-after-flag"),
|
116 |
+
});
|
117 |
+
recordOptionFlag.appendChild(recordOptionFlagInput);
|
118 |
+
recordOptionFlag.appendChild(recordOptionFlagLabel);
|
119 |
+
content.appendChild(recordOptionFlag);
|
120 |
+
|
121 |
+
// Stop sign
|
122 |
+
const recordOptionStop = Object.assign(document.createElement("p"), {
|
123 |
+
className: "mediaRecorderPopupOption",
|
124 |
+
});
|
125 |
+
const recordOptionStopInput = Object.assign(document.createElement("input"), {
|
126 |
+
type: "checkbox",
|
127 |
+
defaultChecked: true,
|
128 |
+
id: "recordOptionStopInput",
|
129 |
+
});
|
130 |
+
const recordOptionStopLabel = Object.assign(document.createElement("label"), {
|
131 |
+
htmlFor: "recordOptionStopInput",
|
132 |
+
textContent: msg("record-until-stop"),
|
133 |
+
});
|
134 |
+
recordOptionFlagInput.addEventListener("change", () => {
|
135 |
+
const disabled = (recordOptionStopInput.disabled = !recordOptionFlagInput.checked);
|
136 |
+
if (disabled) {
|
137 |
+
recordOptionStopLabel.title = msg("record-until-stop-disabled", {
|
138 |
+
afterFlagOption: msg("record-after-flag"),
|
139 |
+
});
|
140 |
+
}
|
141 |
+
});
|
142 |
+
recordOptionStop.appendChild(recordOptionStopInput);
|
143 |
+
recordOptionStop.appendChild(recordOptionStopLabel);
|
144 |
+
content.appendChild(recordOptionStop);
|
145 |
+
|
146 |
+
// Record screen
|
147 |
+
const recordOptionScreen = Object.assign(document.createElement("p"), {
|
148 |
+
className: "mediaRecorderPopupOption",
|
149 |
+
});
|
150 |
+
const recordOptionScreenInput = Object.assign(document.createElement("input"), {
|
151 |
+
type: "checkbox",
|
152 |
+
defaultChecked: false,
|
153 |
+
id: "recordOptionScreen",
|
154 |
+
});
|
155 |
+
const recordOptionScreenLabel = Object.assign(document.createElement("label"), {
|
156 |
+
htmlFor: "recordOptionScreen",
|
157 |
+
textContent: 'Record the entire screen',
|
158 |
+
});
|
159 |
+
recordOptionScreen.appendChild(recordOptionScreenInput);
|
160 |
+
recordOptionScreen.appendChild(recordOptionScreenLabel);
|
161 |
+
content.appendChild(recordOptionScreen);
|
162 |
+
recordOptionScreenInput.disabled = true;
|
163 |
+
if ('mediaDevices' in navigator && typeof navigator.mediaDevices.getDisplayMedia === 'function') {
|
164 |
+
recordOptionScreenInput.disabled = false;
|
165 |
+
}
|
166 |
+
|
167 |
+
let resolvePromise = null;
|
168 |
+
const optionPromise = new Promise((resolve) => {
|
169 |
+
resolvePromise = resolve;
|
170 |
+
});
|
171 |
+
let handleOptionClose = null;
|
172 |
+
|
173 |
+
backdrop.addEventListener("click", () => handleOptionClose(null));
|
174 |
+
closeButton.addEventListener("click", () => handleOptionClose(null));
|
175 |
+
|
176 |
+
handleOptionClose = (value) => {
|
177 |
+
resolvePromise(value);
|
178 |
+
remove();
|
179 |
+
};
|
180 |
+
|
181 |
+
const buttonRow = Object.assign(document.createElement("div"), {
|
182 |
+
className: addon.tab.scratchClass("prompt_button-row", { others: "mediaRecorderPopupButtons" }),
|
183 |
+
});
|
184 |
+
const cancelButton = Object.assign(document.createElement("button"), {
|
185 |
+
textContent: msg("cancel"),
|
186 |
+
});
|
187 |
+
cancelButton.addEventListener("click", () => handleOptionClose(null), { once: true });
|
188 |
+
buttonRow.appendChild(cancelButton);
|
189 |
+
const startButton = Object.assign(document.createElement("button"), {
|
190 |
+
textContent: msg("start"),
|
191 |
+
className: addon.tab.scratchClass("prompt_ok-button"),
|
192 |
+
});
|
193 |
+
startButton.addEventListener(
|
194 |
+
"click",
|
195 |
+
() =>
|
196 |
+
handleOptionClose({
|
197 |
+
secs: Number(recordOptionSecondsInput.value),
|
198 |
+
delay: Number(recordOptionDelayInput.value),
|
199 |
+
audioEnabled: recordOptionAudioInput.checked,
|
200 |
+
micEnabled: recordOptionMicInput.checked,
|
201 |
+
waitUntilFlag: recordOptionFlagInput.checked,
|
202 |
+
useStopSign: !recordOptionStopInput.disabled && recordOptionStopInput.checked,
|
203 |
+
recordWholeScreen: recordOptionScreenInput.checked,
|
204 |
+
}),
|
205 |
+
{ once: true }
|
206 |
+
);
|
207 |
+
buttonRow.appendChild(startButton);
|
208 |
+
content.appendChild(buttonRow);
|
209 |
+
|
210 |
+
return optionPromise;
|
211 |
+
};
|
212 |
+
const disposeRecorder = () => {
|
213 |
+
isRecording = false;
|
214 |
+
recordElem.textContent = msg("record");
|
215 |
+
recordElem.title = "";
|
216 |
+
recorder = null;
|
217 |
+
recordBuffer = [];
|
218 |
+
clearTimeout(timeout);
|
219 |
+
timeout = 0;
|
220 |
+
if (stopSignFunc) {
|
221 |
+
addon.tab.traps.vm.runtime.off("PROJECT_STOP_ALL", stopSignFunc);
|
222 |
+
stopSignFunc = null;
|
223 |
+
}
|
224 |
+
};
|
225 |
+
const stopRecording = (force) => {
|
226 |
+
if (isWaitingForFlag) {
|
227 |
+
addon.tab.traps.vm.runtime.off("PROJECT_START", waitingForFlagFunc);
|
228 |
+
isWaitingForFlag = false;
|
229 |
+
waitingForFlagFunc = null;
|
230 |
+
abortController.abort();
|
231 |
+
abortController = null;
|
232 |
+
disposeRecorder();
|
233 |
+
return;
|
234 |
+
}
|
235 |
+
if (!isRecording || !recorder || recorder.state === "inactive") return;
|
236 |
+
if (force) {
|
237 |
+
disposeRecorder();
|
238 |
+
} else {
|
239 |
+
recorder.onstop = () => {
|
240 |
+
const blob = new Blob(recordBuffer, {
|
241 |
+
type: isMp4CodecSupported ?
|
242 |
+
"video/mp4"
|
243 |
+
: "video/webm"
|
244 |
+
});
|
245 |
+
downloadBlob(isMp4CodecSupported ? "video.mp4" : "video.webm", blob);
|
246 |
+
disposeRecorder();
|
247 |
+
};
|
248 |
+
recorder.stop();
|
249 |
+
}
|
250 |
+
};
|
251 |
+
const startRecording = async (opts) => {
|
252 |
+
// Timer
|
253 |
+
const secs = Math.max(1, opts.secs);
|
254 |
+
|
255 |
+
// Initialize MediaRecorder
|
256 |
+
recordBuffer = [];
|
257 |
+
isRecording = true;
|
258 |
+
const vm = addon.tab.traps.vm;
|
259 |
+
let micStream;
|
260 |
+
if (opts.micEnabled) {
|
261 |
+
// Show permission dialog before green flag is clicked
|
262 |
+
try {
|
263 |
+
micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
264 |
+
} catch (e) {
|
265 |
+
if (e.name !== "NotAllowedError" && e.name !== "NotFoundError") throw e;
|
266 |
+
opts.micEnabled = false;
|
267 |
+
}
|
268 |
+
}
|
269 |
+
let screenRecordingStream;
|
270 |
+
if (opts.recordWholeScreen) {
|
271 |
+
// Show permission dialog before green flag is clicked
|
272 |
+
try {
|
273 |
+
screenRecordingStream = await navigator.mediaDevices.getDisplayMedia({
|
274 |
+
audio: opts.audioEnabled,
|
275 |
+
video: { mediaSource: "screen" }
|
276 |
+
});
|
277 |
+
} catch (e) {
|
278 |
+
console.warn('An error occurred trying to record the whole screen', e);
|
279 |
+
opts.recordWholeScreen = false;
|
280 |
+
}
|
281 |
+
}
|
282 |
+
if (opts.waitUntilFlag) {
|
283 |
+
isWaitingForFlag = true;
|
284 |
+
Object.assign(recordElem, {
|
285 |
+
textContent: msg("click-flag"),
|
286 |
+
title: msg("click-flag-description"),
|
287 |
+
});
|
288 |
+
abortController = new AbortController();
|
289 |
+
try {
|
290 |
+
await Promise.race([
|
291 |
+
new Promise((resolve) => {
|
292 |
+
waitingForFlagFunc = () => resolve();
|
293 |
+
vm.runtime.once("PROJECT_START", waitingForFlagFunc);
|
294 |
+
}),
|
295 |
+
new Promise((_, reject) => {
|
296 |
+
abortController.signal.addEventListener("abort", () => reject("aborted"), { once: true });
|
297 |
+
}),
|
298 |
+
]);
|
299 |
+
} catch (e) {
|
300 |
+
if (e.message === "aborted") return;
|
301 |
+
throw e;
|
302 |
+
}
|
303 |
+
}
|
304 |
+
isWaitingForFlag = false;
|
305 |
+
waitingForFlagFunc = abortController = null;
|
306 |
+
const stream = new MediaStream();
|
307 |
+
if (opts.recordWholeScreen && screenRecordingStream) {
|
308 |
+
stream.addTrack(screenRecordingStream.getVideoTracks()[0]);
|
309 |
+
try {
|
310 |
+
stream.addTrack(screenRecordingStream.getAudioTracks()[0]);
|
311 |
+
} catch (e) {
|
312 |
+
console.warn('Cannot add screen recording\'s audio', e);
|
313 |
+
}
|
314 |
+
} else {
|
315 |
+
const videoStream = vm.runtime.renderer.canvas.captureStream();
|
316 |
+
stream.addTrack(videoStream.getVideoTracks()[0]);
|
317 |
+
}
|
318 |
+
|
319 |
+
const ctx = new AudioContext();
|
320 |
+
const dest = ctx.createMediaStreamDestination();
|
321 |
+
if (opts.audioEnabled) {
|
322 |
+
const mediaStreamDestination = vm.runtime.audioEngine.audioContext.createMediaStreamDestination();
|
323 |
+
vm.runtime.audioEngine.inputNode.connect(mediaStreamDestination);
|
324 |
+
const audioSource = ctx.createMediaStreamSource(mediaStreamDestination.stream);
|
325 |
+
audioSource.connect(dest);
|
326 |
+
// literally any other extension
|
327 |
+
for (const audioData of vm.runtime._extensionAudioObjects.values()) {
|
328 |
+
if (audioData.audioContext && audioData.gainNode) {
|
329 |
+
const mediaStreamDestination = audioData.audioContext.createMediaStreamDestination();
|
330 |
+
audioData.gainNode.connect(mediaStreamDestination);
|
331 |
+
const audioSource = ctx.createMediaStreamSource(mediaStreamDestination.stream);
|
332 |
+
audioSource.connect(dest);
|
333 |
+
}
|
334 |
+
}
|
335 |
+
}
|
336 |
+
if (opts.micEnabled) {
|
337 |
+
const micSource = ctx.createMediaStreamSource(micStream);
|
338 |
+
micSource.connect(dest);
|
339 |
+
}
|
340 |
+
if (opts.audioEnabled || opts.micEnabled) {
|
341 |
+
stream.addTrack(dest.stream.getAudioTracks()[0]);
|
342 |
+
}
|
343 |
+
try {
|
344 |
+
recorder = new MediaRecorder(stream, { mimeType: "video/webm;codecs=vp9" });
|
345 |
+
} catch (err) {
|
346 |
+
console.error('Could not make a transparency compatable video', err);
|
347 |
+
recorder = new MediaRecorder(stream, { mimeType:
|
348 |
+
isMp4CodecSupported ?
|
349 |
+
"video/webm;codecs=h264"
|
350 |
+
: "video/webm"
|
351 |
+
});
|
352 |
+
}
|
353 |
+
recorder.ondataavailable = (e) => {
|
354 |
+
recordBuffer.push(e.data);
|
355 |
+
};
|
356 |
+
recorder.onerror = (e) => {
|
357 |
+
console.warn("Recorder error:", e.error);
|
358 |
+
stopRecording(true);
|
359 |
+
};
|
360 |
+
timeout = setTimeout(() => stopRecording(false), secs * 1000);
|
361 |
+
if (opts.useStopSign) {
|
362 |
+
stopSignFunc = () => stopRecording();
|
363 |
+
vm.runtime.once("PROJECT_STOP_ALL", stopSignFunc);
|
364 |
+
}
|
365 |
+
|
366 |
+
// Delay
|
367 |
+
const delay = opts.delay || 0;
|
368 |
+
const roundedDelay = Math.floor(delay);
|
369 |
+
for (let index = 0; index < roundedDelay; index++) {
|
370 |
+
recordElem.textContent = msg("starting-in", { secs: roundedDelay - index });
|
371 |
+
await new Promise((resolve) => setTimeout(resolve, 975));
|
372 |
+
}
|
373 |
+
setTimeout(() => {
|
374 |
+
recordElem.textContent = msg("stop");
|
375 |
+
|
376 |
+
recorder.start(1000);
|
377 |
+
}, (delay - roundedDelay) * 1000);
|
378 |
+
};
|
379 |
+
if (!recordElem) {
|
380 |
+
recordElem = Object.assign(document.createElement("div"), {
|
381 |
+
className: "sa-record " + elem.className,
|
382 |
+
textContent: msg("record"),
|
383 |
+
});
|
384 |
+
recordElem.addEventListener("click", async () => {
|
385 |
+
if (isRecording) {
|
386 |
+
stopRecording();
|
387 |
+
} else {
|
388 |
+
const opts = await getOptions();
|
389 |
+
if (!opts) {
|
390 |
+
console.log("Canceled");
|
391 |
+
return;
|
392 |
+
}
|
393 |
+
startRecording(opts);
|
394 |
+
}
|
395 |
+
});
|
396 |
}
|
397 |
+
elem.parentElement.appendChild(recordElem);
|
398 |
}
|
399 |
+
};
|
|
|
|