soiz1 commited on
Commit
43b7bbe
·
verified ·
1 Parent(s): fea3159

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
- 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: 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
- abortController = new AbortController();
290
- try {
291
- await Promise.race([
292
- new Promise((resolve) => {
293
- waitingForFlagFunc = () => resolve();
294
- vm.runtime.once("PROJECT_START", waitingForFlagFunc);
295
- }),
296
- new Promise((_, reject) => {
297
- abortController.signal.addEventListener("abort", () => reject("aborted"), { once: true });
298
- }),
299
- ]);
300
- } catch (e) {
301
- if (e.message === "aborted") return;
302
- throw e;
303
- }
304
- }
305
- isWaitingForFlag = false;
306
- waitingForFlagFunc = abortController = null;
307
- const stream = new MediaStream();
308
- if (opts.recordWholeScreen && screenRecordingStream) {
309
- stream.addTrack(screenRecordingStream.getVideoTracks()[0]);
310
- try {
311
- stream.addTrack(screenRecordingStream.getAudioTracks()[0]);
312
- } catch (e) {
313
- console.warn('Cannot add screen recording\'s audio', e);
314
- }
315
- } else {
316
- const videoStream = vm.runtime.renderer.canvas.captureStream();
317
- stream.addTrack(videoStream.getVideoTracks()[0]);
318
- }
319
-
320
- const ctx = new AudioContext();
321
- const dest = ctx.createMediaStreamDestination();
322
- if (opts.audioEnabled) {
323
- const mediaStreamDestination = vm.runtime.audioEngine.audioContext.createMediaStreamDestination();
324
- vm.runtime.audioEngine.inputNode.connect(mediaStreamDestination);
325
- const audioSource = ctx.createMediaStreamSource(mediaStreamDestination.stream);
326
- audioSource.connect(dest);
327
- // literally any other extension
328
- for (const audioData of vm.runtime._extensionAudioObjects.values()) {
329
- if (audioData.audioContext && audioData.gainNode) {
330
- const mediaStreamDestination = audioData.audioContext.createMediaStreamDestination();
331
- audioData.gainNode.connect(mediaStreamDestination);
332
- const audioSource = ctx.createMediaStreamSource(mediaStreamDestination.stream);
333
- audioSource.connect(dest);
334
- }
335
- }
336
- }
337
- if (opts.micEnabled) {
338
- const micSource = ctx.createMediaStreamSource(micStream);
339
- micSource.connect(dest);
340
- }
341
- if (opts.audioEnabled || opts.micEnabled) {
342
- stream.addTrack(dest.stream.getAudioTracks()[0]);
343
- }
344
- recorder = new MediaRecorder(stream, { mimeType:
345
- isMp4CodecSupported ?
346
- "video/webm;codecs=h264"
347
- : "video/webm"
348
- });
349
- recorder.ondataavailable = (e) => {
350
- recordBuffer.push(e.data);
351
- };
352
- recorder.onerror = (e) => {
353
- console.warn("Recorder error:", e.error);
354
- stopRecording(true);
355
- };
356
- timeout = setTimeout(() => stopRecording(false), secs * 1000);
357
- if (opts.useStopSign) {
358
- stopSignFunc = () => stopRecording();
359
- vm.runtime.once("PROJECT_STOP_ALL", stopSignFunc);
360
- }
361
-
362
- // Delay
363
- const delay = opts.delay || 0;
364
- const roundedDelay = Math.floor(delay);
365
- for (let index = 0; index < roundedDelay; index++) {
366
- recordElem.textContent = msg("starting-in", { secs: roundedDelay - index });
367
- await new Promise((resolve) => setTimeout(resolve, 975));
368
- }
369
- setTimeout(() => {
370
- recordElem.textContent = msg("stop");
371
-
372
- recorder.start(1000);
373
- }, (delay - roundedDelay) * 1000);
374
- };
375
- if (!recordElem) {
376
- recordElem = Object.assign(document.createElement("div"), {
377
- className: "sa-record " + elem.className,
378
- textContent: msg("record"),
379
- });
380
- recordElem.addEventListener("click", async () => {
381
- if (isRecording) {
382
- stopRecording();
383
- } else {
384
- const opts = await getOptions();
385
- if (!opts) {
386
- console.log("Canceled");
387
- return;
388
- }
389
- startRecording(opts);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  }
391
- });
392
  }
393
- elem.parentElement.appendChild(recordElem);
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
+ };