Rev 1 | AutorÃa | Comparar con el anterior | Ultima modificación | Ver Log |
{"version":3,"file":"base_recorder.min.js","sources":["../src/base_recorder.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n//\n\n/**\n * Tiny Record RTC type.\n *\n * @module tiny_recordrtc/base_recorder\n * @copyright 2022 Stevani Andolo <stevani@hotmail.com.au>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */
\n\nimport {getString, getStrings} from 'core/str';\nimport {component} from './common';\nimport Pending from 'core/pending';\nimport {getData, isPausingAllowed} from './options';\nimport uploadFile from 'editor_tiny/uploader';\nimport {add as addToast} from 'core/toast';\nimport * as ModalEvents from 'core/modal_events';\nimport * as Templates from 'core/templates';\nimport {saveCancelPromise} from 'core/notification';\nimport {prefetchStrings, prefetchTemplates} from 'core/prefetch';\nimport AlertModal from 'core/local/modal/alert';\n\n/**\n * The RecordRTC base class for audio, video, and any other future types\n */\nexport default class {\n\n stopRequested = false;\n buttonTimer = null;\n pauseTime = null;\n startTime = null;\n\n /**\n * Constructor for the RecordRTC class\n *\n * @param {TinyMCE} editor The Editor to which the content will be inserted\n * @param {Modal} modal The Moodle Modal that contains the interface used for recording\n */\n constructor(editor,
modal) {\n this.ready = false;\n\n if (!this.checkAndWarnAboutBrowserCompatibility()) {\n return;\n }\n\n this.editor = editor;\n this.config = getData(editor).params;\n this.modal = modal;\n this.modalRoot = modal.getRoot()[0];\n this.startStopButton = this.modalRoot.querySelector('button[data-action=\"startstop\"]');\n this.uploadButton = this.modalRoot.querySelector('button[data-action=\"upload\"]');\n this.pauseResumeButton = this.modalRoot.querySelector('button[data-action=\"pauseresume\"]');\n\n // Disable the record button untilt he stream is acquired.\n this.setRecordButtonState(false);\n\n this.player = this.configurePlayer();\n this.registerEventListeners();\n this.ready = true;\n\n this.captureUserMedia();\n this.prefetchContent();\n }\n\n /**\n * Check whether the browser is compatible.\n *\n * @returns {boolean}\n */\n isReady() {\n ret
urn this.ready;\n }\n\n // Disable eslint's valid-jsdoc rule as the following methods are abstract and mnust be overridden by the child class.\n\n /* eslint-disable valid-jsdoc, no-unused-vars */\n\n /**\n * Get the Player element for this type.\n *\n * @returns {HTMLElement} The player element, typically an audio or video tag.\n */\n configurePlayer() {\n throw new Error(`configurePlayer() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Get the list of supported mimetypes for this recorder.\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/isTypeSupported}\n *\n * @returns {string[]} The list of supported mimetypes.\n */\n getSupportedTypes() {\n throw new Error(`getSupportedTypes() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Get any recording options passed into the MediaRecorder.\n * Please note that the mimeType will be fetched from {@link getSuppo
rtedTypes()}.\n *\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/MediaRecorder#options}\n * @returns {Object}\n */\n getRecordingOptions() {\n throw new Error(`getRecordingOptions() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Get a filename for the generated file.\n *\n * Typically this function will take a prefix and add a type-specific suffix such as the extension to it.\n *\n * @param {string} prefix The prefix for the filename generated by the recorder.\n * @returns {string}\n */\n getFileName(prefix) {\n throw new Error(`getFileName() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Get a list of constraints as required by the getUserMedia() function.\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#constraints}\n *\n * @returns {Object}\n */\n getMediaConstraints() {\n throw new Error(
`getMediaConstraints() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Whether to start playing the recording as it is captured.\n * @returns {boolean} Whether to start playing the recording as it is captured.\n */\n playOnCapture() {\n return false;\n }\n\n /**\n * Get the time limit for this recording type.\n *\n * @returns {number} The time limit in seconds.\n */\n getTimeLimit() {\n throw new Error(`getTimeLimit() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Get the name of the template used when embedding the URL in the editor content.\n *\n * @returns {string}\n */\n getEmbedTemplateName() {\n throw new Error(`getEmbedTemplateName() must be implemented in ${this.constructor.name}`);\n }\n\n /**\n * Fetch the Class of the Modal to be displayed.\n *\n * @returns {Modal}\n */\n static getModalClass() {\n throw new Error(`getModalClass() must b
e implemented in ${this.constructor.name}`);\n }\n\n /* eslint-enable valid-jsdoc, no-unused-vars */\n\n /**\n * Get the options for the MediaRecorder.\n *\n * @returns {object} The options for the MediaRecorder instance.\n */\n getParsedRecordingOptions() {\n const requestedTypes = this.getSupportedTypes();\n const possibleTypes = requestedTypes.reduce((result, type) => {\n result.push(type);\n // Safari seems to use codecs: instead of codecs=.\n // It is safe to add both, so we do, but we want them to remain in order.\n result.push(type.replace('=', ':'));\n return result;\n }, []);\n\n const compatTypes = possibleTypes.filter((type) => window.MediaRecorder.isTypeSupported(type));\n\n const options = this.getRecordingOptions();\n if (compatTypes.length !== 0) {\n options.mimeType = compatTypes[0];\n }\n window.console.info(\n `Selected codec ${opti
ons.mimeType} from ${compatTypes.length} options.`,\n compatTypes,\n );\n\n return options;\n }\n\n /**\n * Start capturing the User Media and handle success or failure of the capture.\n */\n async captureUserMedia() {\n try {\n const stream = await navigator.mediaDevices.getUserMedia(this.getMediaConstraints());\n this.handleCaptureSuccess(stream);\n } catch (error) {\n this.handleCaptureFailure(error);\n }\n }\n\n /**\n * Prefetch some of the content that will be used in the UI.\n *\n * Note: not all of the strings used are pre-fetched.\n * Some of the strings will be fetched because their template is used.\n */\n prefetchContent() {\n prefetchStrings(component, [\n 'uploading',\n 'recordagain_title',\n 'recordagain_desc',\n 'discard_title',\n 'discard_desc',\n 'confirm_yes',\n 'recordinguploaded',\n
'maxfilesizehit',\n 'maxfilesizehit_title',\n 'uploadfailed',\n 'pause',\n 'resume',\n ]);\n\n prefetchTemplates([\n this.getEmbedTemplateName(),\n 'tiny_recordrtc/timeremaining',\n ]);\n }\n\n /**\n * Display an error message to the user.\n *\n * @param {Promise<string>} title The error title\n * @param {Promise<string>} content The error message\n * @returns {Promise<Modal>}\n */\n async displayAlert(title, content) {\n const pendingPromise = new Pending('core/confirm:alert');\n const modal = await AlertModal.create({\n title: title,\n body: content,\n removeOnClose: true,\n });\n\n modal.show();\n pendingPromise.resolve();\n\n return modal;\n }\n\n /**\n * Handle successful capture of the User Media.\n *\n * @param {MediaStream} stream The stream as captured by the User Media.\n */
\n handleCaptureSuccess(stream) {\n // Set audio player source to microphone stream.\n this.player.srcObject = stream;\n\n if (this.playOnCapture()) {\n // Mute audio, distracting while recording.\n this.player.muted = true;\n\n this.player.play();\n }\n\n this.stream = stream;\n this.setupPlayerSource();\n this.setRecordButtonState(true);\n }\n\n /**\n * Setup the player to use the stream as a source.\n */\n setupPlayerSource() {\n if (!this.player.srcObject) {\n this.player.srcObject = this.stream;\n\n // Mute audio, distracting while recording.\n this.player.muted = true;\n\n this.player.play();\n }\n }\n\n /**\n * Enable the record button.\n *\n * @param {boolean|null} enabled Set the button state\n */\n setRecordButtonState(enabled) {\n this.startStopButton.disabled = !enabled;\n }\n\n /**\n * Configure b
utton visibility for the record button.\n *\n * @param {boolean} visible Set the visibility of the button.\n */\n setRecordButtonVisibility(visible) {\n const container = this.getButtonContainer('start-stop');\n container.classList.toggle('hide', !visible);\n }\n\n /**\n * Configure button visibility for the pause button.\n *\n * @param {boolean} visible Set the visibility of the button.\n */\n setPauseButtonVisibility(visible) {\n if (this.pauseResumeButton) {\n this.pauseResumeButton.classList.toggle('hidden', !visible);\n }\n }\n\n /**\n * Enable the upload button.\n *\n * @param {boolean|null} enabled Set the button state\n */\n setUploadButtonState(enabled) {\n this.uploadButton.disabled = !enabled;\n }\n\n /**\n * Configure button visibility for the upload button.\n *\n * @param {boolean} visible Set the visibility of the button.\n */\n setUploadButtonVisibility(visibl
e) {\n const container = this.getButtonContainer('upload');\n container.classList.toggle('hide', !visible);\n }\n\n /**\n * Sets the state of the audio player, including visibility, muting, and controls.\n *\n * @param {boolean} state A boolean indicating the audio player state.\n */\n setPlayerState(state) {\n // Mute or unmute the audio player and show or hide controls.\n this.player.muted = !state;\n this.player.controls = state;\n // Toggle the 'hide' class on the player button container based on state.\n this.getButtonContainer('player')?.classList.toggle('hide', !state);\n }\n\n /**\n * Handle failure to capture the User Media.\n *\n * @param {Error} error\n */\n handleCaptureFailure(error) {\n // Changes 'CertainError' -> 'gumcertain' to match language string names.\n var subject = `gum${error.name.replace('Error', '').toLowerCase()}`;\n this.displayAlert(\n getString(`${s
ubject}_title`, component),\n getString(subject, component)\n );\n }\n\n /**\n * Close the modal and stop recording.\n */\n close() {\n // Closing the modal will destroy it and remove it from the DOM.\n // It will also stop the recording via the hidden Modal Event.\n this.modal.hide();\n }\n\n /**\n * Register event listeners for the modal.\n */\n registerEventListeners() {\n this.modalRoot.addEventListener('click', this.handleModalClick.bind(this));\n this.modal.getRoot().on(ModalEvents.outsideClick, this.outsideClickHandler.bind(this));\n this.modal.getRoot().on(ModalEvents.hidden, () => {\n this.cleanupStream();\n this.requestRecordingStop();\n });\n this.player.addEventListener('error', this.handlePlayerError.bind(this));\n this.player.addEventListener('loadedmetadata', this.handlePlayerLoadedMetadata.bind(this));\n }\n\n /**\n * Handle the player `error` even
t.\n *\n * This event is called when the player throws an error.\n */\n handlePlayerError() {\n const error = this.player.error;\n if (error) {\n const message = `An error occurred: ${error.message || 'Unknown error'}. Please try again.`;\n addToast(message, {type: error});\n // Disable the upload button.\n this.setUploadButtonState(false);\n }\n }\n\n /**\n * Handles the event when the player's metadata has been loaded.\n */\n handlePlayerLoadedMetadata() {\n if (isFinite(this.player.duration)) {\n // Note: In Chrome, you need to seek to activate the error listener\n // if an issue arises after inserting the recorded audio into the player source.\n this.player.currentTime = 0.1;\n }\n }\n\n /**\n * Prevent the Modal from closing when recording is on process.\n *\n * @param {MouseEvent} event The click event\n */\n async outsideClickHandler(e
vent) {\n if (this.isRecording() || this.isPaused()) {\n // The user is recording.\n // Do not distract with a confirmation, just prevent closing.\n event.preventDefault();\n } else if (this.hasData()) {\n // If there is a blobsize then there is data that may be lost.\n // Ask the user to confirm they want to close the modal.\n // We prevent default here, and then close the modal if they confirm.\n event.preventDefault();\n\n try {\n await saveCancelPromise(\n await getString(\"discard_title\", component),\n await getString(\"discard_desc\", component),\n await getString(\"confirm_yes\", component),\n );\n this.modal.hide();\n } catch (error) {\n // Do nothing, the modal will not close.\n }\n }\n }\n\n /**\n * Handle a click within the Modal.\n *\n
* @param {MouseEvent} event The click event\n */\n handleModalClick(event) {\n const button = event.target.closest('button');\n if (button && button.dataset.action) {\n const action = button.dataset.action;\n if (action === 'startstop') {\n this.handleRecordingStartStopRequested();\n }\n\n if (action === 'upload') {\n this.uploadRecording();\n }\n\n if (action === 'pauseresume') {\n this.handleRecordingPauseResumeRequested();\n }\n }\n }\n\n /**\n * Handle the click event for the recording start/stop button.\n */\n handleRecordingStartStopRequested() {\n if (this.isRecording() || this.isPaused()) {\n this.requestRecordingStop();\n } else {\n this.startRecording();\n }\n }\n\n /**\n * Handle the click event for the recording pause/resume button.\n */\n handleRecordingPauseResumeReques
ted() {\n if (this.isRecording()) {\n // Pause recording.\n this.mediaRecorder.pause();\n } else if (this.isPaused()) {\n // Resume recording.\n this.mediaRecorder.resume();\n }\n }\n\n /**\n * Handle the media stream after it has finished.\n */\n async onMediaStopped() {\n // Set source of audio player.\n this.blob = new Blob(this.data.chunks, {\n type: this.mediaRecorder.mimeType\n });\n this.player.srcObject = null;\n this.player.src = URL.createObjectURL(this.blob);\n\n // Change the label to \"Record again\".\n this.setRecordButtonTextFromString('recordagain');\n\n // Show upload button.\n this.setUploadButtonVisibility(true);\n this.setPlayerState(true);\n this.setUploadButtonState(true);\n\n // Hide the pause button.\n this.setPauseButtonVisibility(false);\n if (this.mediaRecorder.state === 'inactive') {\n
this.setPauseButtonTextFromString('pause');\n }\n }\n\n /**\n * Upload the recording and insert it into the editor content.\n */\n async uploadRecording() {\n // Trigger error if no recording has been made.\n if (this.data.chunks.length === 0) {\n this.displayAlert('norecordingfound');\n return;\n }\n\n const fileName = this.getFileName((Math.random() * 1000).toString().replace('.', ''));\n\n // Upload recording to server.\n try {\n // Once uploading starts, do not allow any further changes to the recording.\n this.setRecordButtonVisibility(false);\n\n // Disable the upload button.\n this.setUploadButtonState(false);\n\n // Upload the recording.\n const fileURL = await uploadFile(this.editor, 'media', this.blob, fileName, (progress) => {\n this.setUploadButtonTextProgress(progress);\n });\n this.insertMedia(fileURL);
\n this.close();\n addToast(await getString('recordinguploaded', component));\n } catch (error) {\n // Show a toast and unhide the button.\n this.setUploadButtonState(true);\n\n addToast(await getString('uploadfailed', component, {error}), {\n type: 'error',\n });\n\n }\n }\n\n /**\n * Helper to get the container that a button is in.\n *\n * @param {string} purpose The button purpose\n * @returns {HTMLElement}\n */\n getButtonContainer(purpose) {\n return this.modalRoot.querySelector(`[data-purpose=\"${purpose}-container\"]`);\n }\n\n /**\n * Check whether the browser is compatible with capturing media.\n *\n * @returns {boolean}\n */\n static isBrowserCompatible() {\n return this.checkSecure() && this.hasUserMedia();\n }\n\n static async display(editor) {\n const ModalClass = this.getModalClass();\n const modal = await ModalCla
ss.create({\n templateContext: {\n isallowedpausing: isPausingAllowed(editor),\n },\n large: true,\n removeOnClose: true,\n });\n\n // Set up the VideoRecorder.\n const recorder = new this(editor, modal);\n if (recorder.isReady()) {\n modal.show();\n }\n return modal;\n }\n\n /**\n * Check whether the browser is compatible with capturing media, and display a warning if not.\n *\n * @returns {boolean}\n */\n checkAndWarnAboutBrowserCompatibility() {\n if (!this.constructor.checkSecure()) {\n getStrings(['insecurealert_title', 'insecurealert'].map((key) => ({key, component})))\n .then(([title, message]) => addToast(message, {title, type: 'error'}))\n .catch();\n return false;\n }\n\n if (!this.constructor.hasUserMedia) {\n getStrings(['nowebrtc_title', 'nowebrtc'].map((key) => ({key, component
})))\n .then(([title, message]) => addToast(message, {title, type: 'error'}))\n .catch();\n return false;\n }\n\n return true;\n }\n\n /**\n * Check whether the browser supports WebRTC.\n *\n * @returns {boolean}\n */\n static hasUserMedia() {\n return (navigator.mediaDevices && window.MediaRecorder);\n }\n\n /**\n * Check whether the hostname is either hosted over SSL, or from a valid localhost hostname.\n *\n * The UserMedia API can only be used in secure contexts as noted.\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#privacy_and_security}\n *\n * @returns {boolean} Whether the plugin can be loaded.\n */\n static checkSecure() {\n // Note: We can now use window.isSecureContext.\n // https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#feature_detection\n // https://developer.mozilla.org/en-US/docs/Web/
API/isSecureContext\n return window.isSecureContext;\n }\n\n /**\n * Update the content of the stop recording button timer.\n */\n async setStopRecordingButton() {\n const {html, js} = await Templates.renderForPromise('tiny_recordrtc/timeremaining', this.getTimeRemaining());\n Templates.replaceNodeContents(this.startStopButton, html, js);\n this.startButtonTimer();\n }\n\n /**\n * Update the time on the stop recording button.\n */\n updateRecordButtonTime() {\n const {remaining, minutes, seconds} = this.getTimeRemaining();\n if (remaining < 0) {\n this.requestRecordingStop();\n } else {\n this.startStopButton.querySelector('[data-type=\"minutes\"]').textContent = minutes;\n this.startStopButton.querySelector('[data-type=\"seconds\"]').textContent = seconds;\n }\n }\n\n /**\n * Set the text of the record button using a language string.\n *\n * @param {string} string The
string identifier\n */\n async setRecordButtonTextFromString(string) {\n this.startStopButton.textContent = await getString(string, component);\n }\n\n /**\n * Set the text of the pause button using a language string.\n *\n * @param {string} string The string identifier\n */\n async setPauseButtonTextFromString(string) {\n if (this.pauseResumeButton) {\n this.pauseResumeButton.textContent = await getString(string, component);\n }\n }\n\n /**\n * Set the upload button text progress.\n *\n * @param {number} progress The progress\n */\n async setUploadButtonTextProgress(progress) {\n this.uploadButton.textContent = await getString('uploading', component, {\n progress: Math.round(progress * 100) / 100,\n });\n }\n\n async resetUploadButtonText() {\n this.uploadButton.textContent = await getString('upload', component);\n }\n\n /**\n * Clear the timer for the stop recording butto
n.\n */\n clearButtonTimer() {\n if (this.buttonTimer) {\n clearInterval(this.buttonTimer);\n }\n this.buttonTimer = null;\n this.pauseTime = null;\n this.startTime = null;\n }\n\n /**\n * Pause the timer for the stop recording button.\n */\n pauseButtonTimer() {\n // Stop the countdown timer.\n this.pauseTime = new Date().getTime(); // Store pause time.\n if (this.buttonTimer) {\n clearInterval(this.buttonTimer);\n }\n }\n\n /**\n * Start the timer for the start recording button.\n * If the recording was paused, the timer will resume from the pause time.\n */\n startButtonTimer() {\n if (this.pauseTime !== null) {\n // Resume from pause.\n const pauseDuration = new Date().getTime() - this.pauseTime;\n // Adjust start time by pause duration.\n this.startTime += pauseDuration;\n this.pauseTime = null;\n }\n
this.buttonTimer = setInterval(this.updateRecordButtonTime.bind(this), 500);\n }\n\n /**\n * Get the time remaining for the recording.\n *\n * @returns {Object} The minutes and seconds remaining.\n */\n getTimeRemaining() {\n // All times are in milliseconds.\n let now = new Date().getTime();\n if (this.pauseTime !== null) {\n // If paused, use pauseTime instead of current time.\n now = this.pauseTime;\n }\n const remaining = Math.floor(this.getTimeLimit() - ((now - this.startTime) / 1000));\n\n const formatter = new Intl.NumberFormat(navigator.language, {minimumIntegerDigits: 2});\n const seconds = formatter.format(remaining % 60);\n const minutes = formatter.format(Math.floor((remaining - seconds) / 60));\n return {\n remaining,\n minutes,\n seconds,\n };\n }\n\n /**\n * Get the maximum file size that can be uploaded.\n *\n * @returns {numb
er} The max byte size\n */\n getMaxUploadSize() {\n return this.config.maxrecsize;\n }\n\n /**\n * Stop the recording.\n * Please note that this should only stop the recording.\n * Anything related to processing the recording should be handled by the\n * mediaRecorder's stopped event handler which is processed after it has stopped.\n */\n requestRecordingStop() {\n if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {\n this.stopRequested = true;\n if (this.isPaused()) {\n this.stopRecorder();\n }\n } else {\n // There is no recording to stop, but the stream must still be cleaned up.\n this.cleanupStream();\n }\n }\n\n stopRecorder() {\n if (this.isPaused()) {\n this.pauseTime = null;\n }\n this.mediaRecorder.stop();\n\n // Unmute the player so that the audio is heard during playback.\n this.player.muted = fals
e;\n }\n\n /**\n * Clean up the stream.\n *\n * This involves stopping any track which is still active.\n */\n cleanupStream() {\n if (this.stream) {\n this.stream.getTracks()\n .filter((track) => track.readyState !== 'ended')\n .forEach((track) => track.stop());\n }\n }\n\n /**\n * Handle the mediaRecorder `stop` event.\n */\n handleStopped() {\n // Handle the stream data.\n this.onMediaStopped();\n\n // Clear the button timer.\n this.clearButtonTimer();\n }\n\n /**\n * Handle the mediaRecorder `start` event.\n *\n * This event is called when the recording starts.\n */\n handleStarted() {\n this.startTime = new Date().getTime();\n if (isPausingAllowed(this.editor) && !this.isPaused()) {\n this.setPauseButtonVisibility(true);\n }\n this.setStopRecordingButton();\n }\n\n /**\n * Handle the mediaRecorder `pause` ev
ent.\n *\n * This event is called when the recording pauses.\n */\n handlePaused() {\n this.pauseButtonTimer();\n this.setPauseButtonTextFromString('resume');\n }\n\n /**\n * Handle the mediaRecorder `resume` event.\n *\n * This event is called when the recording resumes.\n */\n handleResume() {\n this.startButtonTimer();\n this.setPauseButtonTextFromString('pause');\n }\n\n /**\n * Handle the mediaRecorder `dataavailable` event.\n *\n * @param {Event} event\n */\n handleDataAvailable(event) {\n if (this.isRecording() || this.isPaused()) {\n const newSize = this.data.blobSize + event.data.size;\n // Recording stops when either the maximum upload size is reached, or the time limit expires.\n // The time limit is checked in the `updateButtonTime` function.\n if (newSize >= this.getMaxUploadSize()) {\n this.stopRecorder();\n this.displayFil
eLimitHitMessage();\n } else {\n // Push recording slice to array.\n this.data.chunks.push(event.data);\n\n // Size of all recorded data so far.\n this.data.blobSize = newSize;\n\n if (this.stopRequested) {\n this.stopRecorder();\n }\n }\n }\n }\n\n async displayFileLimitHitMessage() {\n addToast(await getString('maxfilesizehit', component), {\n title: await getString('maxfilesizehit_title', component),\n type: 'error',\n });\n }\n\n /**\n * Check whether the recording is in progress.\n *\n * @returns {boolean}\n */\n isRecording() {\n return this.mediaRecorder?.state === 'recording';\n }\n\n /**\n * Check whether the recording is paused.\n *\n * @returns {boolean}\n */\n isPaused() {\n return this.mediaRecorder?.state === 'paused';\n }\n\n /**\n * Whether an
y data has been recorded.\n *\n * @returns {boolean}\n */\n hasData() {\n return !!this.data?.blobSize;\n }\n\n /**\n * Start the recording\n */\n async startRecording() {\n if (this.mediaRecorder) {\n // Stop the existing recorder if it exists.\n if (this.isRecording() || this.isPaused()) {\n this.mediaRecorder.stop();\n }\n\n if (this.hasData()) {\n const resetRecording = await this.recordAgainConfirmation();\n if (!resetRecording) {\n // User cancelled at the confirmation to reset the data, so exit early.\n return;\n }\n this.setUploadButtonVisibility(false);\n this.setPlayerState(false);\n if (!this.stream.active) {\n await this.captureUserMedia();\n }\n }\n\n this.mediaRecorder = null;\n }\n\n // The optio
ns for the recording codecs and bitrates.\n this.mediaRecorder = new MediaRecorder(this.stream, this.getParsedRecordingOptions());\n\n this.mediaRecorder.addEventListener('dataavailable', this.handleDataAvailable.bind(this));\n this.mediaRecorder.addEventListener('stop', this.handleStopped.bind(this));\n this.mediaRecorder.addEventListener('start', this.handleStarted.bind(this));\n this.mediaRecorder.addEventListener('pause', this.handlePaused.bind(this));\n this.mediaRecorder.addEventListener('resume', this.handleResume.bind(this));\n\n this.data = {\n chunks: [],\n blobSize: 0\n };\n this.setupPlayerSource();\n this.stopRequested = false;\n\n // Capture in 50ms chunks.\n this.mediaRecorder.start(50);\n }\n\n /**\n * Confirm whether the user wants to reset the existing recoring.\n *\n * @returns {Promise<boolean>} Whether the user confirmed the reset.\n */\n async recordAgainCon
firmation() {\n try {\n await saveCancelPromise(\n await getString(\"recordagain_title\", component),\n await getString(\"recordagain_desc\", component),\n await getString(\"confirm_yes\", component)\n );\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Insert the HTML to embed the recording into the editor content.\n *\n * @param {string} source The URL to view the media.\n */\n async insertMedia(source) {\n const {html} = await Templates.renderForPromise(\n this.getEmbedTemplateName(),\n this.getEmbedTemplateContext({\n source,\n })\n );\n this.editor.insertContent(html);\n }\n\n /**\n * Add or modify the template parameters for the specified type.\n *\n * @param {Object} templateContext The Tempalte context to use\n * @returns {Object} The finalised template context\n
*/\n getEmbedTemplateContext(templateContext) {\n return templateContext;\n }\n}\n"],"names":["constructor","editor","modal","ready","this","checkAndWarnAboutBrowserCompatibility","config","params","modalRoot","getRoot","startStopButton","querySelector","uploadButton","pauseResumeButton","setRecordButtonState","player","configurePlayer","registerEventListeners","captureUserMedia","prefetchContent","isReady","Error","name","getSupportedTypes","getRecordingOptions","getFileName","prefix","getMediaConstraints","playOnCapture","getTimeLimit","getEmbedTemplateName","getParsedRecordingOptions","compatTypes","reduce","result","type","push","replace","filter","window","MediaRecorder","isTypeSupported","options","length","mimeType","console","info","stream","navigator","mediaDevices","getUserMedia","handleCaptureSuccess","error","handleCaptureFailure","component","title","content","pendingPromise","Pending","AlertModal","create","body","removeOnClose","show","resolve","srcObject","muted","play","setup
PlayerSource","enabled","disabled","setRecordButtonVisibility","visible","getButtonContainer","classList","toggle","setPauseButtonVisibility","setUploadButtonState","setUploadButtonVisibility","setPlayerState","state","controls","subject","toLowerCase","displayAlert","close","hide","addEventListener","handleModalClick","bind","on","ModalEvents","outsideClick","outsideClickHandler","hidden","cleanupStream","requestRecordingStop","handlePlayerError","handlePlayerLoadedMetadata","message","isFinite","duration","currentTime","event","isRecording","isPaused","preventDefault","hasData","button","target","closest","dataset","action","handleRecordingStartStopRequested","uploadRecording","handleRecordingPauseResumeRequested","startRecording","mediaRecorder","pause","resume","blob","Blob","data","chunks","src","URL","createObjectURL","setRecordButtonTextFromString","setPauseButtonTextFromString","fileName","Math","random","toString","fileURL","progress","setUploadButtonTextProgress","insertMedia","purpose","checkSecur
e","hasUserMedia","ModalClass","getModalClass","templateContext","isallowedpausing","large","map","key","then","_ref2","catch","_ref","isSecureContext","html","js","Templates","renderForPromise","getTimeRemaining","replaceNodeContents","startButtonTimer","updateRecordButtonTime","remaining","minutes","seconds","textContent","string","round","clearButtonTimer","buttonTimer","clearInterval","pauseTime","startTime","pauseButtonTimer","Date","getTime","pauseDuration","setInterval","now","floor","formatter","Intl","NumberFormat","language","minimumIntegerDigits","format","getMaxUploadSize","maxrecsize","stopRequested","stopRecorder","stop","getTracks","track","readyState","forEach","handleStopped","onMediaStopped","handleStarted","setStopRecordingButton","handlePaused","handleResume","handleDataAvailable","newSize","blobSize","size","displayFileLimitHitMessage","_this$data","recordAgainConfirmation","active","start","source","getEmbedTemplateContext","insertContent"],"mappings":"u1DAoDIA,YAAYC,OAAQC,6CAXJ,sCACF,u
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"}