(window["webpackJsonpGUI"] = window["webpackJsonpGUI"] || []).push([["addon-entry-mediarecorder"],{ /***/ "./node_modules/css-loader/index.js!./src/addons/addons/mediarecorder/style.css": /*!*****************************************************************************!*\ !*** ./node_modules/css-loader!./src/addons/addons/mediarecorder/style.css ***! \*****************************************************************************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { exports = module.exports = __webpack_require__(/*! ../../../../node_modules/css-loader/lib/css-base.js */ "./node_modules/css-loader/lib/css-base.js")(false); // imports // module exports.push([module.i, ".mediaRecorderPopup {\n box-sizing: border-box;\n width: 700px;\n max-height: min(800px, 80vh);\n max-width: 85%;\n margin-top: 12vh;\n overflow-y: auto;\n margin-left: auto;\n margin-right: auto;\n}\n\n.mediaRecorderPopupContent {\n padding: 1.5rem 2.25rem;\n}\n\n.mediaRecorderPopup p {\n font-size: 1rem;\n margin: 0.5rem auto;\n}\n\n.mediaRecorderPopup p :last-child {\n margin-left: 1rem;\n}\n\n.mediaRecorderPopup[dir=\"rtl\"] p :last-child {\n margin-left: 0;\n margin-right: 1rem;\n}\n\np.mediaRecorderPopupOption {\n display: flex;\n align-items: center;\n}\n\n.mediaRecorderPopupOption input[type=\"checkbox\"] {\n height: 1.5rem;\n}\n\n#recordOptionSecondsInput,\n#recordOptionDelayInput {\n width: 6rem;\n}\n\n.mediaRecorderPopupButtons {\n margin-top: 1.5rem;\n}\n\n.mediaRecorderPopupButtons button {\n margin-left: 0.5rem;\n}\n\n/* TW: Fixes cancel button in dark mode */\n.mediaRecorderPopupButtons button:nth-of-type(1) {\n color: black;\n}\n", ""]); // exports /***/ }), /***/ "./src/addons/addons/mediarecorder/_runtime_entry.js": /*!***********************************************************!*\ !*** ./src/addons/addons/mediarecorder/_runtime_entry.js ***! \***********************************************************/ /*! exports provided: resources */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "resources", function() { return resources; }); /* harmony import */ var _userscript_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./userscript.js */ "./src/addons/addons/mediarecorder/userscript.js"); /* harmony import */ var _css_loader_style_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! css-loader!./style.css */ "./node_modules/css-loader/index.js!./src/addons/addons/mediarecorder/style.css"); /* harmony import */ var _css_loader_style_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_css_loader_style_css__WEBPACK_IMPORTED_MODULE_1__); /* generated by pull.js */ const resources = { "userscript.js": _userscript_js__WEBPACK_IMPORTED_MODULE_0__["default"], "style.css": _css_loader_style_css__WEBPACK_IMPORTED_MODULE_1___default.a }; /***/ }), /***/ "./src/addons/addons/mediarecorder/userscript.js": /*!*******************************************************!*\ !*** ./src/addons/addons/mediarecorder/userscript.js ***! \*******************************************************/ /*! exports provided: default */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony import */ var _libraries_common_cs_download_blob_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../libraries/common/cs/download-blob.js */ "./src/addons/libraries/common/cs/download-blob.js"); /* harmony default export */ __webpack_exports__["default"] = (async ({ addon, console, msg }) => { let recordElem; let isRecording = false; let isWaitingForFlag = false; let waitingForFlagFunc = null; let abortController = null; let stopSignFunc = null; let recordBuffer = []; let recorder; let timeout; while (true) { const elem = await addon.tab.waitForElement('div[class*="menu-bar_file-group"] > div:last-child:not(.sa-record)', { markAsSeen: true, reduxEvents: ["scratch-gui/mode/SET_PLAYER", "fontsLoaded/SET_FONTS_LOADED", "scratch-gui/locales/SELECT_LOCALE"] }); const getOptions = () => { const { backdrop, container, content, closeButton, remove } = addon.tab.createModal(msg("option-title"), { isOpen: true, useEditorClasses: true }); container.classList.add("mediaRecorderPopup"); content.classList.add("mediaRecorderPopupContent"); content.appendChild(Object.assign(document.createElement("p"), { textContent: msg("record-description"), className: "recordOptionDescription" })); // Seconds const recordOptionSeconds = document.createElement("p"); const recordOptionSecondsInput = Object.assign(document.createElement("input"), { type: "number", min: 1, max: 300, defaultValue: 30, id: "recordOptionSecondsInput", className: addon.tab.scratchClass("prompt_variable-name-text-input") }); const recordOptionSecondsLabel = Object.assign(document.createElement("label"), { htmlFor: "recordOptionSecondsInput", textContent: msg("record-duration") }); recordOptionSeconds.appendChild(recordOptionSecondsLabel); recordOptionSeconds.appendChild(recordOptionSecondsInput); content.appendChild(recordOptionSeconds); // Delay const recordOptionDelay = document.createElement("p"); const recordOptionDelayInput = Object.assign(document.createElement("input"), { type: "number", min: 0, max: 300, defaultValue: 0, id: "recordOptionDelayInput", className: addon.tab.scratchClass("prompt_variable-name-text-input") }); const recordOptionDelayLabel = Object.assign(document.createElement("label"), { htmlFor: "recordOptionDelayInput", textContent: msg("start-delay") }); recordOptionDelay.appendChild(recordOptionDelayLabel); recordOptionDelay.appendChild(recordOptionDelayInput); content.appendChild(recordOptionDelay); // Audio const recordOptionAudio = Object.assign(document.createElement("p"), { className: "mediaRecorderPopupOption" }); const recordOptionAudioInput = Object.assign(document.createElement("input"), { type: "checkbox", defaultChecked: true, id: "recordOptionAudioInput" }); const recordOptionAudioLabel = Object.assign(document.createElement("label"), { htmlFor: "recordOptionAudioInput", textContent: msg("record-audio"), title: msg("record-audio-description") }); recordOptionAudio.appendChild(recordOptionAudioInput); recordOptionAudio.appendChild(recordOptionAudioLabel); content.appendChild(recordOptionAudio); // Mic const recordOptionMic = Object.assign(document.createElement("p"), { className: "mediaRecorderPopupOption" }); const recordOptionMicInput = Object.assign(document.createElement("input"), { type: "checkbox", defaultChecked: false, id: "recordOptionMicInput" }); const recordOptionMicLabel = Object.assign(document.createElement("label"), { htmlFor: "recordOptionMicInput", textContent: msg("record-mic") }); recordOptionMic.appendChild(recordOptionMicInput); recordOptionMic.appendChild(recordOptionMicLabel); content.appendChild(recordOptionMic); // Green flag const recordOptionFlag = Object.assign(document.createElement("p"), { className: "mediaRecorderPopupOption" }); const recordOptionFlagInput = Object.assign(document.createElement("input"), { type: "checkbox", defaultChecked: true, id: "recordOptionFlagInput" }); const recordOptionFlagLabel = Object.assign(document.createElement("label"), { htmlFor: "recordOptionFlagInput", textContent: msg("record-after-flag") }); recordOptionFlag.appendChild(recordOptionFlagInput); recordOptionFlag.appendChild(recordOptionFlagLabel); content.appendChild(recordOptionFlag); // Stop sign const recordOptionStop = Object.assign(document.createElement("p"), { className: "mediaRecorderPopupOption" }); const recordOptionStopInput = Object.assign(document.createElement("input"), { type: "checkbox", defaultChecked: true, id: "recordOptionStopInput" }); const recordOptionStopLabel = Object.assign(document.createElement("label"), { htmlFor: "recordOptionStopInput", textContent: msg("record-until-stop") }); recordOptionFlagInput.addEventListener("change", () => { const disabled = recordOptionStopInput.disabled = !recordOptionFlagInput.checked; if (disabled) { recordOptionStopLabel.title = msg("record-until-stop-disabled", { afterFlagOption: msg("record-after-flag") }); } }); recordOptionStop.appendChild(recordOptionStopInput); recordOptionStop.appendChild(recordOptionStopLabel); content.appendChild(recordOptionStop); let resolvePromise = null; const optionPromise = new Promise(resolve => { resolvePromise = resolve; }); let handleOptionClose = null; backdrop.addEventListener("click", () => handleOptionClose(null)); closeButton.addEventListener("click", () => handleOptionClose(null)); handleOptionClose = value => { resolvePromise(value); remove(); }; const buttonRow = Object.assign(document.createElement("div"), { className: addon.tab.scratchClass("prompt_button-row", { others: "mediaRecorderPopupButtons" }) }); const cancelButton = Object.assign(document.createElement("button"), { textContent: msg("cancel") }); cancelButton.addEventListener("click", () => handleOptionClose(null), { once: true }); buttonRow.appendChild(cancelButton); const startButton = Object.assign(document.createElement("button"), { textContent: msg("start"), className: addon.tab.scratchClass("prompt_ok-button") }); startButton.addEventListener("click", () => handleOptionClose({ secs: Number(recordOptionSecondsInput.value), delay: Number(recordOptionDelayInput.value), audioEnabled: recordOptionAudioInput.checked, micEnabled: recordOptionMicInput.checked, waitUntilFlag: recordOptionFlagInput.checked, useStopSign: !recordOptionStopInput.disabled && recordOptionStopInput.checked }), { once: true }); buttonRow.appendChild(startButton); content.appendChild(buttonRow); return optionPromise; }; const disposeRecorder = () => { isRecording = false; recordElem.textContent = msg("record"); recordElem.title = ""; recorder = null; recordBuffer = []; clearTimeout(timeout); timeout = 0; if (stopSignFunc) { addon.tab.traps.vm.runtime.off("PROJECT_STOP_ALL", stopSignFunc); stopSignFunc = null; } }; const stopRecording = force => { if (isWaitingForFlag) { addon.tab.traps.vm.runtime.off("PROJECT_START", waitingForFlagFunc); isWaitingForFlag = false; waitingForFlagFunc = null; abortController.abort(); abortController = null; disposeRecorder(); return; } if (!isRecording || !recorder || recorder.state === "inactive") return; if (force) { disposeRecorder(); } else { recorder.onstop = () => { const blob = new Blob(recordBuffer, { type: "video/webm" }); Object(_libraries_common_cs_download_blob_js__WEBPACK_IMPORTED_MODULE_0__["default"])("video.webm", blob); disposeRecorder(); }; recorder.stop(); } }; const startRecording = async opts => { // Timer const secs = Math.min(300, Math.max(1, opts.secs)); // Initialize MediaRecorder recordBuffer = []; isRecording = true; const vm = addon.tab.traps.vm; let micStream; if (opts.micEnabled) { // Show permission dialog before green flag is clicked try { micStream = await navigator.mediaDevices.getUserMedia({ audio: true }); } catch (e) { if (e.name !== "NotAllowedError" && e.name !== "NotFoundError") throw e; opts.micEnabled = false; } } if (opts.waitUntilFlag) { isWaitingForFlag = true; Object.assign(recordElem, { textContent: msg("click-flag"), title: msg("click-flag-description") }); abortController = new AbortController(); try { await Promise.race([new Promise(resolve => { waitingForFlagFunc = () => resolve(); vm.runtime.once("PROJECT_START", waitingForFlagFunc); }), new Promise((_, reject) => { abortController.signal.addEventListener("abort", () => reject("aborted"), { once: true }); })]); } catch (e) { if (e.message === "aborted") return; throw e; } } isWaitingForFlag = false; waitingForFlagFunc = abortController = null; const stream = new MediaStream(); const videoStream = vm.runtime.renderer.canvas.captureStream(); stream.addTrack(videoStream.getVideoTracks()[0]); const ctx = new AudioContext(); const dest = ctx.createMediaStreamDestination(); if (opts.audioEnabled) { const mediaStreamDestination = vm.runtime.audioEngine.audioContext.createMediaStreamDestination(); vm.runtime.audioEngine.inputNode.connect(mediaStreamDestination); const audioSource = ctx.createMediaStreamSource(mediaStreamDestination.stream); audioSource.connect(dest); } if (opts.micEnabled) { const micSource = ctx.createMediaStreamSource(micStream); micSource.connect(dest); } if (opts.audioEnabled || opts.micEnabled) { stream.addTrack(dest.stream.getAudioTracks()[0]); } recorder = new MediaRecorder(stream, { mimeType: "video/webm" }); recorder.ondataavailable = e => { recordBuffer.push(e.data); }; recorder.onerror = e => { console.warn("Recorder error:", e.error); stopRecording(true); }; timeout = setTimeout(() => stopRecording(false), secs * 1000); if (opts.useStopSign) { stopSignFunc = () => stopRecording(); vm.runtime.once("PROJECT_STOP_ALL", stopSignFunc); } // Delay const delay = opts.delay || 0; const roundedDelay = Math.floor(delay); for (let index = 0; index < roundedDelay; index++) { recordElem.textContent = msg("starting-in", { secs: roundedDelay - index }); await new Promise(resolve => setTimeout(resolve, 975)); } setTimeout(() => { recordElem.textContent = msg("stop"); recorder.start(1000); }, (delay - roundedDelay) * 1000); }; if (!recordElem) { recordElem = Object.assign(document.createElement("div"), { className: "sa-record " + elem.className, textContent: msg("record") }); recordElem.addEventListener("click", async () => { if (isRecording) { stopRecording(); } else { const opts = await getOptions(); if (!opts) { console.log("Canceled"); return; } startRecording(opts); } }); } elem.parentElement.appendChild(recordElem); } }); /***/ }), /***/ "./src/addons/libraries/common/cs/download-blob.js": /*!*********************************************************!*\ !*** ./src/addons/libraries/common/cs/download-blob.js ***! \*********************************************************/ /*! exports provided: default */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); // From https://github.com/LLK/scratch-gui/blob/develop/src/lib/download-blob.js /* harmony default export */ __webpack_exports__["default"] = ((filename, blob) => { const downloadLink = document.createElement("a"); document.body.appendChild(downloadLink); // Use special ms version if available to get it working on Edge. if (navigator.msSaveOrOpenBlob) { navigator.msSaveOrOpenBlob(blob, filename); return; } if ("download" in HTMLAnchorElement.prototype) { const url = window.URL.createObjectURL(blob); downloadLink.href = url; downloadLink.download = filename; downloadLink.type = blob.type; downloadLink.click(); // remove the link after a timeout to prevent a crash on iOS 13 Safari window.setTimeout(() => { document.body.removeChild(downloadLink); window.URL.revokeObjectURL(url); }, 1000); } else { // iOS 12 Safari, open a new page and set href to data-uri let popup = window.open("", "_blank"); const reader = new FileReader(); reader.onloadend = function () { popup.location.href = reader.result; popup = null; }; reader.readAsDataURL(blob); } }); /***/ }) }]); //# sourceMappingURL=addon-entry-mediarecorder.js.map