Proyectos de Subversion Moodle

Rev

Autoría | 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} 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\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\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        return 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 getSupportedTypes()}.\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 be 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 ${options.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        ]);\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 button 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     * 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(visible) {\n        const container = this.getButtonContainer('upload');\n        container.classList.toggle('hide', !visible);\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(`${subject}_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    }\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(event) {\n        if (this.isRecording()) {\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    }\n\n    /**\n     * Handle the click event for the recording start/stop button.\n     */\n    handleRecordingStartStopRequested() {\n        if (this.mediaRecorder?.state === 'recording') {\n            this.requestRecordingStop();\n        } else {\n            this.startRecording();\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 audio player with controls enabled, and unmute.\n        this.player.muted = false;\n        this.player.controls = true;\n        this.getButtonContainer('player')?.classList.toggle('hide', false);\n\n        // Show upload button.\n        this.setUploadButtonVisibility(true);\n        this.setUploadButtonState(true);\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 ModalClass.create({\n            templateContext: {},\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.buttonTimer = setInterval(this.updateRecordButtonTime.bind(this), 500);\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 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 button.\n     */\n    clearButtonTimer() {\n        if (this.buttonTimer) {\n            clearInterval(this.buttonTimer);\n        }\n        this.buttonTimer = null;\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        const now = new Date().getTime();\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 {number} 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        } 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        this.mediaRecorder.stop();\n\n        // Unmute the player so that the audio is heard during playback.\n        this.player.muted = false;\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        this.setStopRecordingButton();\n    }\n\n    /**\n     * Handle the mediaRecorder `dataavailable` event.\n     *\n     * @param {Event} event\n     */\n    handleDataAvailable(event) {\n        if (this.isRecording()) {\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.displayFileLimitHitMessage();\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     * Whether any 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()) {\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            }\n\n            this.mediaRecorder = null;\n        }\n\n        // The options 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\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 recordAgainConfirmation() {\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","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","setupPlayerSource","enabled","disabled","setRecordButtonVisibility","visible","getButtonContainer","classList","toggle","setUploadButtonState","setUploadButtonVisibility","subject","toLowerCase","displayAlert","close","hide","addEventListener","handleModalClick","bind","on","ModalEvents","outsideClick","outsideClickHandler","hidden","cleanupStream","requestRecordingStop","event","isRecording","preventDefault","hasData","button","target","closest","dataset","action","handleRecordingStartStopRequested","uploadRecording","mediaRecorder","state","startRecording","blob","Blob","data","chunks","src","URL","createObjectURL","setRecordButtonTextFromString","controls","fileName","Math","random","toString","fileURL","progress","setUploadButtonTextProgress","insertMedia","purpose","checkSecure","hasUserMedia","ModalClass","getModalClass","templateContext","large","map","key","then","_ref2","message","catch","_ref","isSecureContext","html","js","Templates","renderForPromise","getTimeRemaining","replaceNodeContents","buttonTimer","setInterval","updateRecordButtonTime","remaining","minutes","seconds","textContent","string","round","clearButtonTimer","clearInterval","now","Date","getTime","floor","startTime","formatter","Intl","NumberFormat","language","minimumIntegerDigits","format","getMaxUploadSize","maxrecsize","stopRequested","stopRecorder","stop","getTracks","track","readyState","forEach","handleStopped","onMediaStopped","handleStarted","setStopRecordingButton","handleDataAvailable","newSize","blobSize","size","displayFileLimitHitMessage","_this$data","recordAgainConfirmation","start","source","getEmbedTemplateContext","insertContent"],"mappings":"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"}