Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
// This file is part of Moodle - http://moodle.org/
2
//
3
// Moodle is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// Moodle is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
15
//
16
 
17
/**
18
 * Tiny Record RTC type.
19
 *
20
 * @module      tiny_recordrtc/base_recorder
21
 * @copyright   2022 Stevani Andolo <stevani@hotmail.com.au>
22
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
import {getString, getStrings} from 'core/str';
26
import {component} from './common';
27
import Pending from 'core/pending';
28
import {getData} from './options';
29
import uploadFile from 'editor_tiny/uploader';
30
import {add as addToast} from 'core/toast';
31
import * as ModalEvents from 'core/modal_events';
32
import * as Templates from 'core/templates';
33
import {saveCancelPromise} from 'core/notification';
34
import {prefetchStrings, prefetchTemplates} from 'core/prefetch';
35
import AlertModal from 'core/local/modal/alert';
36
 
37
/**
38
 * The RecordRTC base class for audio, video, and any other future types
39
 */
40
export default class {
41
 
42
    stopRequested = false;
43
 
44
    /**
45
     * Constructor for the RecordRTC class
46
     *
47
     * @param {TinyMCE} editor The Editor to which the content will be inserted
48
     * @param {Modal} modal The Moodle Modal that contains the interface used for recording
49
     */
50
    constructor(editor, modal) {
51
        this.ready = false;
52
 
53
        if (!this.checkAndWarnAboutBrowserCompatibility()) {
54
            return;
55
        }
56
 
57
        this.editor = editor;
58
        this.config = getData(editor).params;
59
        this.modal = modal;
60
        this.modalRoot = modal.getRoot()[0];
61
        this.startStopButton = this.modalRoot.querySelector('button[data-action="startstop"]');
62
        this.uploadButton = this.modalRoot.querySelector('button[data-action="upload"]');
63
 
64
        // Disable the record button untilt he stream is acquired.
65
        this.setRecordButtonState(false);
66
 
67
        this.player = this.configurePlayer();
68
        this.registerEventListeners();
69
        this.ready = true;
70
 
71
        this.captureUserMedia();
72
        this.prefetchContent();
73
    }
74
 
75
    /**
76
     * Check whether the browser is compatible.
77
     *
78
     * @returns {boolean}
79
     */
80
    isReady() {
81
        return this.ready;
82
    }
83
 
84
    // Disable eslint's valid-jsdoc rule as the following methods are abstract and mnust be overridden by the child class.
85
 
86
    /* eslint-disable valid-jsdoc, no-unused-vars */
87
 
88
    /**
89
     * Get the Player element for this type.
90
     *
91
     * @returns {HTMLElement} The player element, typically an audio or video tag.
92
     */
93
    configurePlayer() {
94
        throw new Error(`configurePlayer() must be implemented in ${this.constructor.name}`);
95
    }
96
 
97
    /**
98
     * Get the list of supported mimetypes for this recorder.
99
     * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/isTypeSupported}
100
     *
101
     * @returns {string[]} The list of supported mimetypes.
102
     */
103
    getSupportedTypes() {
104
        throw new Error(`getSupportedTypes() must be implemented in ${this.constructor.name}`);
105
    }
106
 
107
    /**
108
     * Get any recording options passed into the MediaRecorder.
109
     * Please note that the mimeType will be fetched from {@link getSupportedTypes()}.
110
     *
111
     * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/MediaRecorder#options}
112
     * @returns {Object}
113
     */
114
    getRecordingOptions() {
115
        throw new Error(`getRecordingOptions() must be implemented in ${this.constructor.name}`);
116
    }
117
 
118
    /**
119
     * Get a filename for the generated file.
120
     *
121
     * Typically this function will take a prefix and add a type-specific suffix such as the extension to it.
122
     *
123
     * @param {string} prefix The prefix for the filename generated by the recorder.
124
     * @returns {string}
125
     */
126
    getFileName(prefix) {
127
        throw new Error(`getFileName() must be implemented in ${this.constructor.name}`);
128
    }
129
 
130
    /**
131
     * Get a list of constraints as required by the getUserMedia() function.
132
     * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#constraints}
133
     *
134
     * @returns {Object}
135
     */
136
    getMediaConstraints() {
137
        throw new Error(`getMediaConstraints() must be implemented in ${this.constructor.name}`);
138
    }
139
 
140
    /**
141
     * Whether to start playing the recording as it is captured.
142
     * @returns {boolean} Whether to start playing the recording as it is captured.
143
     */
144
    playOnCapture() {
145
        return false;
146
    }
147
 
148
    /**
149
     * Get the time limit for this recording type.
150
     *
151
     * @returns {number} The time limit in seconds.
152
     */
153
    getTimeLimit() {
154
        throw new Error(`getTimeLimit() must be implemented in ${this.constructor.name}`);
155
    }
156
 
157
    /**
158
     * Get the name of the template used when embedding the URL in the editor content.
159
     *
160
     * @returns {string}
161
     */
162
    getEmbedTemplateName() {
163
        throw new Error(`getEmbedTemplateName() must be implemented in ${this.constructor.name}`);
164
    }
165
 
166
    /**
167
     * Fetch the Class of the Modal to be displayed.
168
     *
169
     * @returns {Modal}
170
     */
171
    static getModalClass() {
172
        throw new Error(`getModalClass() must be implemented in ${this.constructor.name}`);
173
    }
174
 
175
    /* eslint-enable valid-jsdoc, no-unused-vars */
176
 
177
    /**
178
     * Get the options for the MediaRecorder.
179
     *
180
     * @returns {object} The options for the MediaRecorder instance.
181
     */
182
    getParsedRecordingOptions() {
183
        const requestedTypes = this.getSupportedTypes();
184
        const possibleTypes = requestedTypes.reduce((result, type) => {
185
            result.push(type);
186
            // Safari seems to use codecs: instead of codecs=.
187
            // It is safe to add both, so we do, but we want them to remain in order.
188
            result.push(type.replace('=', ':'));
189
            return result;
190
        }, []);
191
 
192
        const compatTypes = possibleTypes.filter((type) => window.MediaRecorder.isTypeSupported(type));
193
 
194
        const options = this.getRecordingOptions();
195
        if (compatTypes.length !== 0) {
196
            options.mimeType = compatTypes[0];
197
        }
198
        window.console.info(
199
            `Selected codec ${options.mimeType} from ${compatTypes.length} options.`,
200
            compatTypes,
201
        );
202
 
203
        return options;
204
    }
205
 
206
    /**
207
     * Start capturing the User Media and handle success or failure of the capture.
208
     */
209
    async captureUserMedia() {
210
        try {
211
            const stream = await navigator.mediaDevices.getUserMedia(this.getMediaConstraints());
212
            this.handleCaptureSuccess(stream);
213
        } catch (error) {
214
            this.handleCaptureFailure(error);
215
        }
216
    }
217
 
218
    /**
219
     * Prefetch some of the content that will be used in the UI.
220
     *
221
     * Note: not all of the strings used are pre-fetched.
222
     * Some of the strings will be fetched because their template is used.
223
     */
224
    prefetchContent() {
225
        prefetchStrings(component, [
226
            'uploading',
227
            'recordagain_title',
228
            'recordagain_desc',
229
            'discard_title',
230
            'discard_desc',
231
            'confirm_yes',
232
            'recordinguploaded',
233
            'maxfilesizehit',
234
            'maxfilesizehit_title',
235
            'uploadfailed',
236
        ]);
237
 
238
        prefetchTemplates([
239
            this.getEmbedTemplateName(),
240
            'tiny_recordrtc/timeremaining',
241
        ]);
242
    }
243
 
244
    /**
245
     * Display an error message to the user.
246
     *
247
     * @param {Promise<string>} title The error title
248
     * @param {Promise<string>} content The error message
249
     * @returns {Promise<Modal>}
250
     */
251
    async displayAlert(title, content) {
252
        const pendingPromise = new Pending('core/confirm:alert');
253
        const modal = await AlertModal.create({
254
            title: title,
255
            body: content,
256
            removeOnClose: true,
257
        });
258
 
259
        modal.show();
260
        pendingPromise.resolve();
261
 
262
        return modal;
263
    }
264
 
265
    /**
266
     * Handle successful capture of the User Media.
267
     *
268
     * @param {MediaStream} stream The stream as captured by the User Media.
269
     */
270
    handleCaptureSuccess(stream) {
271
        // Set audio player source to microphone stream.
272
        this.player.srcObject = stream;
273
 
274
        if (this.playOnCapture()) {
275
            // Mute audio, distracting while recording.
276
            this.player.muted = true;
277
 
278
            this.player.play();
279
        }
280
 
281
        this.stream = stream;
282
        this.setupPlayerSource();
283
        this.setRecordButtonState(true);
284
    }
285
 
286
    /**
287
     * Setup the player to use the stream as a source.
288
     */
289
    setupPlayerSource() {
290
        if (!this.player.srcObject) {
291
            this.player.srcObject = this.stream;
292
 
293
            // Mute audio, distracting while recording.
294
            this.player.muted = true;
295
 
296
            this.player.play();
297
        }
298
    }
299
 
300
    /**
301
     * Enable the record button.
302
     *
303
     * @param {boolean|null} enabled Set the button state
304
     */
305
    setRecordButtonState(enabled) {
306
        this.startStopButton.disabled = !enabled;
307
    }
308
 
309
    /**
310
     * Configure button visibility for the record button.
311
     *
312
     * @param {boolean} visible Set the visibility of the button.
313
     */
314
    setRecordButtonVisibility(visible) {
315
        const container = this.getButtonContainer('start-stop');
316
        container.classList.toggle('hide', !visible);
317
    }
318
 
319
    /**
320
     * Enable the upload button.
321
     *
322
     * @param {boolean|null} enabled Set the button state
323
     */
324
    setUploadButtonState(enabled) {
325
        this.uploadButton.disabled = !enabled;
326
    }
327
 
328
    /**
329
     * Configure button visibility for the upload button.
330
     *
331
     * @param {boolean} visible Set the visibility of the button.
332
     */
333
    setUploadButtonVisibility(visible) {
334
        const container = this.getButtonContainer('upload');
335
        container.classList.toggle('hide', !visible);
336
    }
337
    /**
338
     * Handle failure to capture the User Media.
339
     *
340
     * @param {Error} error
341
     */
342
    handleCaptureFailure(error) {
343
        // Changes 'CertainError' -> 'gumcertain' to match language string names.
344
        var subject = `gum${error.name.replace('Error', '').toLowerCase()}`;
345
        this.displayAlert(
346
            getString(`${subject}_title`, component),
347
            getString(subject, component)
348
        );
349
    }
350
 
351
    /**
352
     * Close the modal and stop recording.
353
     */
354
    close() {
355
        // Closing the modal will destroy it and remove it from the DOM.
356
        // It will also stop the recording via the hidden Modal Event.
357
        this.modal.hide();
358
    }
359
 
360
    /**
361
     * Register event listeners for the modal.
362
     */
363
    registerEventListeners() {
364
        this.modalRoot.addEventListener('click', this.handleModalClick.bind(this));
365
        this.modal.getRoot().on(ModalEvents.outsideClick, this.outsideClickHandler.bind(this));
366
        this.modal.getRoot().on(ModalEvents.hidden, () => {
367
            this.cleanupStream();
368
            this.requestRecordingStop();
369
        });
370
    }
371
 
372
    /**
373
     * Prevent the Modal from closing when recording is on process.
374
     *
375
     * @param {MouseEvent} event The click event
376
     */
377
    async outsideClickHandler(event) {
378
        if (this.isRecording()) {
379
            // The user is recording.
380
            // Do not distract with a confirmation, just prevent closing.
381
            event.preventDefault();
382
        } else if (this.hasData()) {
383
            // If there is a blobsize then there is data that may be lost.
384
            // Ask the user to confirm they want to close the modal.
385
            // We prevent default here, and then close the modal if they confirm.
386
            event.preventDefault();
387
 
388
            try {
389
                await saveCancelPromise(
390
                    await getString("discard_title", component),
391
                    await getString("discard_desc", component),
392
                    await getString("confirm_yes", component),
393
                );
394
                this.modal.hide();
395
            } catch (error) {
396
                // Do nothing, the modal will not close.
397
            }
398
        }
399
    }
400
 
401
    /**
402
     * Handle a click within the Modal.
403
     *
404
     * @param {MouseEvent} event The click event
405
     */
406
    handleModalClick(event) {
407
        const button = event.target.closest('button');
408
        if (button && button.dataset.action) {
409
            const action = button.dataset.action;
410
            if (action === 'startstop') {
411
                this.handleRecordingStartStopRequested();
412
            }
413
 
414
            if (action === 'upload') {
415
                this.uploadRecording();
416
            }
417
        }
418
    }
419
 
420
    /**
421
     * Handle the click event for the recording start/stop button.
422
     */
423
    handleRecordingStartStopRequested() {
424
        if (this.mediaRecorder?.state === 'recording') {
425
            this.requestRecordingStop();
426
        } else {
427
            this.startRecording();
428
        }
429
    }
430
 
431
    /**
432
     * Handle the media stream after it has finished.
433
     */
434
    async onMediaStopped() {
435
        // Set source of audio player.
436
        this.blob = new Blob(this.data.chunks, {
437
            type: this.mediaRecorder.mimeType
438
        });
439
        this.player.srcObject = null;
440
        this.player.src = URL.createObjectURL(this.blob);
441
 
442
        // Change the label to "Record again".
443
        this.setRecordButtonTextFromString('recordagain');
444
 
445
        // Show audio player with controls enabled, and unmute.
446
        this.player.muted = false;
447
        this.player.controls = true;
448
        this.getButtonContainer('player')?.classList.toggle('hide', false);
449
 
450
        // Show upload button.
451
        this.setUploadButtonVisibility(true);
452
        this.setUploadButtonState(true);
453
    }
454
 
455
    /**
456
     * Upload the recording and insert it into the editor content.
457
     */
458
    async uploadRecording() {
459
        // Trigger error if no recording has been made.
460
        if (this.data.chunks.length === 0) {
461
            this.displayAlert('norecordingfound');
462
            return;
463
        }
464
 
465
        const fileName = this.getFileName((Math.random() * 1000).toString().replace('.', ''));
466
 
467
        // Upload recording to server.
468
        try {
469
            // Once uploading starts, do not allow any further changes to the recording.
470
            this.setRecordButtonVisibility(false);
471
 
472
            // Disable the upload button.
473
            this.setUploadButtonState(false);
474
 
475
            // Upload the recording.
476
            const fileURL = await uploadFile(this.editor, 'media', this.blob, fileName, (progress) => {
477
                this.setUploadButtonTextProgress(progress);
478
            });
479
            this.insertMedia(fileURL);
480
            this.close();
481
            addToast(await getString('recordinguploaded', component));
482
        } catch (error) {
483
            // Show a toast and unhide the button.
484
            this.setUploadButtonState(true);
485
 
486
            addToast(await getString('uploadfailed', component, {error}), {
487
                type: 'error',
488
            });
489
 
490
        }
491
    }
492
 
493
    /**
494
     * Helper to get the container that a button is in.
495
     *
496
     * @param {string} purpose The button purpose
497
     * @returns {HTMLElement}
498
     */
499
    getButtonContainer(purpose) {
500
        return this.modalRoot.querySelector(`[data-purpose="${purpose}-container"]`);
501
    }
502
 
503
    /**
504
     * Check whether the browser is compatible with capturing media.
505
     *
506
     * @returns {boolean}
507
     */
508
    static isBrowserCompatible() {
509
        return this.checkSecure() && this.hasUserMedia();
510
    }
511
 
512
    static async display(editor) {
513
        const ModalClass = this.getModalClass();
514
        const modal = await ModalClass.create({
515
            templateContext: {},
516
            large: true,
517
            removeOnClose: true,
518
        });
519
 
520
        // Set up the VideoRecorder.
521
        const recorder = new this(editor, modal);
522
        if (recorder.isReady()) {
523
            modal.show();
524
        }
525
        return modal;
526
    }
527
 
528
    /**
529
     * Check whether the browser is compatible with capturing media, and display a warning if not.
530
     *
531
     * @returns {boolean}
532
     */
533
    checkAndWarnAboutBrowserCompatibility() {
534
        if (!this.constructor.checkSecure()) {
535
            getStrings(['insecurealert_title', 'insecurealert'].map((key) => ({key, component})))
536
                .then(([title, message]) => addToast(message, {title, type: 'error'}))
537
                .catch();
538
            return false;
539
        }
540
 
541
        if (!this.constructor.hasUserMedia) {
542
            getStrings(['nowebrtc_title', 'nowebrtc'].map((key) => ({key, component})))
543
                .then(([title, message]) => addToast(message, {title, type: 'error'}))
544
                .catch();
545
            return false;
546
        }
547
 
548
        return true;
549
    }
550
 
551
    /**
552
     * Check whether the browser supports WebRTC.
553
     *
554
     * @returns {boolean}
555
     */
556
    static hasUserMedia() {
557
        return (navigator.mediaDevices && window.MediaRecorder);
558
    }
559
 
560
    /**
561
     * Check whether the hostname is either hosted over SSL, or from a valid localhost hostname.
562
     *
563
     * The UserMedia API can only be used in secure contexts as noted.
564
     * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#privacy_and_security}
565
     *
566
     * @returns {boolean} Whether the plugin can be loaded.
567
     */
568
    static checkSecure() {
569
        // Note: We can now use window.isSecureContext.
570
        // https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#feature_detection
571
        // https://developer.mozilla.org/en-US/docs/Web/API/isSecureContext
572
        return window.isSecureContext;
573
    }
574
 
575
    /**
576
     * Update the content of the stop recording button timer.
577
     */
578
    async setStopRecordingButton() {
579
        const {html, js} = await Templates.renderForPromise('tiny_recordrtc/timeremaining', this.getTimeRemaining());
580
        Templates.replaceNodeContents(this.startStopButton, html, js);
581
        this.buttonTimer = setInterval(this.updateRecordButtonTime.bind(this), 500);
582
    }
583
 
584
    /**
585
     * Update the time on the stop recording button.
586
     */
587
    updateRecordButtonTime() {
588
        const {remaining, minutes, seconds} = this.getTimeRemaining();
589
        if (remaining < 0) {
590
            this.requestRecordingStop();
591
        } else {
592
            this.startStopButton.querySelector('[data-type="minutes"]').textContent = minutes;
593
            this.startStopButton.querySelector('[data-type="seconds"]').textContent = seconds;
594
        }
595
    }
596
 
597
    /**
598
     * Set the text of the record button using a language string.
599
     *
600
     * @param {string} string The string identifier
601
     */
602
    async setRecordButtonTextFromString(string) {
603
        this.startStopButton.textContent = await getString(string, component);
604
    }
605
 
606
    /**
607
     * Set the upload button text progress.
608
     *
609
     * @param {number} progress The progress
610
     */
611
    async setUploadButtonTextProgress(progress) {
612
        this.uploadButton.textContent = await getString('uploading', component, {
613
            progress: Math.round(progress * 100) / 100,
614
        });
615
    }
616
 
617
    async resetUploadButtonText() {
618
        this.uploadButton.textContent = await getString('upload', component);
619
    }
620
 
621
    /**
622
     * Clear the timer for the stop recording button.
623
     */
624
    clearButtonTimer() {
625
        if (this.buttonTimer) {
626
            clearInterval(this.buttonTimer);
627
        }
628
        this.buttonTimer = null;
629
    }
630
 
631
    /**
632
     * Get the time remaining for the recording.
633
     *
634
     * @returns {Object} The minutes and seconds remaining.
635
     */
636
    getTimeRemaining() {
637
        // All times are in milliseconds
638
        const now = new Date().getTime();
639
        const remaining = Math.floor(this.getTimeLimit() - ((now - this.startTime) / 1000));
640
 
641
        const formatter = new Intl.NumberFormat(navigator.language, {minimumIntegerDigits: 2});
642
        const seconds = formatter.format(remaining % 60);
643
        const minutes = formatter.format(Math.floor((remaining - seconds) / 60));
644
        return {
645
            remaining,
646
            minutes,
647
            seconds,
648
        };
649
    }
650
 
651
    /**
652
     * Get the maximum file size that can be uploaded.
653
     *
654
     * @returns {number} The max byte size
655
     */
656
    getMaxUploadSize() {
657
        return this.config.maxrecsize;
658
    }
659
 
660
    /**
661
     * Stop the recording.
662
     * Please note that this should only stop the recording.
663
     * Anything related to processing the recording should be handled by the
664
     * mediaRecorder's stopped event handler which is processed after it has stopped.
665
     */
666
    requestRecordingStop() {
667
        if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
668
            this.stopRequested = true;
669
        } else {
670
            // There is no recording to stop, but the stream must still be cleaned up.
671
            this.cleanupStream();
672
        }
673
    }
674
 
675
    stopRecorder() {
676
        this.mediaRecorder.stop();
677
 
678
        // Unmute the player so that the audio is heard during playback.
679
        this.player.muted = false;
680
    }
681
 
682
    /**
683
     * Clean up the stream.
684
     *
685
     * This involves stopping any track which is still active.
686
     */
687
    cleanupStream() {
688
        if (this.stream) {
689
            this.stream.getTracks()
690
                .filter((track) => track.readyState !== 'ended')
691
                .forEach((track) => track.stop());
692
        }
693
    }
694
 
695
    /**
696
     * Handle the mediaRecorder `stop` event.
697
     */
698
    handleStopped() {
699
        // Handle the stream data.
700
        this.onMediaStopped();
701
 
702
        // Clear the button timer.
703
        this.clearButtonTimer();
704
    }
705
 
706
    /**
707
     * Handle the mediaRecorder `start` event.
708
     *
709
     * This event is called when the recording starts.
710
     */
711
    handleStarted() {
712
        this.startTime = new Date().getTime();
713
        this.setStopRecordingButton();
714
    }
715
 
716
    /**
717
     * Handle the mediaRecorder `dataavailable` event.
718
     *
719
     * @param {Event} event
720
     */
721
    handleDataAvailable(event) {
722
        if (this.isRecording()) {
723
            const newSize = this.data.blobSize + event.data.size;
724
            // Recording stops when either the maximum upload size is reached, or the time limit expires.
725
            // The time limit is checked in the `updateButtonTime` function.
726
            if (newSize >= this.getMaxUploadSize()) {
727
                this.stopRecorder();
728
                this.displayFileLimitHitMessage();
729
            } else {
730
                // Push recording slice to array.
731
                this.data.chunks.push(event.data);
732
 
733
                // Size of all recorded data so far.
734
                this.data.blobSize = newSize;
735
 
736
                if (this.stopRequested) {
737
                    this.stopRecorder();
738
                }
739
            }
740
        }
741
    }
742
 
743
    async displayFileLimitHitMessage() {
744
        addToast(await getString('maxfilesizehit', component), {
745
            title: await getString('maxfilesizehit_title', component),
746
            type: 'error',
747
        });
748
    }
749
 
750
    /**
751
     * Check whether the recording is in progress.
752
     *
753
     * @returns {boolean}
754
     */
755
    isRecording() {
756
        return this.mediaRecorder?.state === 'recording';
757
    }
758
 
759
    /**
760
     * Whether any data has been recorded.
761
     *
762
     * @returns {boolean}
763
     */
764
    hasData() {
765
        return !!this.data?.blobSize;
766
    }
767
 
768
    /**
769
     * Start the recording
770
     */
771
    async startRecording() {
772
        if (this.mediaRecorder) {
773
            // Stop the existing recorder if it exists.
774
            if (this.isRecording()) {
775
                this.mediaRecorder.stop();
776
            }
777
 
778
            if (this.hasData()) {
779
                const resetRecording = await this.recordAgainConfirmation();
780
                if (!resetRecording) {
781
                    // User cancelled at the confirmation to reset the data, so exit early.
782
                    return;
783
                }
784
                this.setUploadButtonVisibility(false);
785
            }
786
 
787
            this.mediaRecorder = null;
788
        }
789
 
790
        // The options for the recording codecs and bitrates.
791
        this.mediaRecorder = new MediaRecorder(this.stream, this.getParsedRecordingOptions());
792
 
793
        this.mediaRecorder.addEventListener('dataavailable', this.handleDataAvailable.bind(this));
794
        this.mediaRecorder.addEventListener('stop', this.handleStopped.bind(this));
795
        this.mediaRecorder.addEventListener('start', this.handleStarted.bind(this));
796
 
797
        this.data = {
798
            chunks: [],
799
            blobSize: 0
800
        };
801
        this.setupPlayerSource();
802
        this.stopRequested = false;
803
 
804
        // Capture in 50ms chunks.
805
        this.mediaRecorder.start(50);
806
    }
807
 
808
    /**
809
     * Confirm whether the user wants to reset the existing recoring.
810
     *
811
     * @returns {Promise<boolean>} Whether the user confirmed the reset.
812
     */
813
    async recordAgainConfirmation() {
814
        try {
815
            await saveCancelPromise(
816
                await getString("recordagain_title", component),
817
                await getString("recordagain_desc", component),
818
                await getString("confirm_yes", component)
819
            );
820
            return true;
821
        } catch {
822
            return false;
823
        }
824
    }
825
 
826
    /**
827
     * Insert the HTML to embed the recording into the editor content.
828
     *
829
     * @param {string} source The URL to view the media.
830
     */
831
    async insertMedia(source) {
832
        const {html} = await Templates.renderForPromise(
833
            this.getEmbedTemplateName(),
834
            this.getEmbedTemplateContext({
835
                source,
836
            })
837
        );
838
        this.editor.insertContent(html);
839
    }
840
 
841
    /**
842
     * Add or modify the template parameters for the specified type.
843
     *
844
     * @param {Object} templateContext The Tempalte context to use
845
     * @returns {Object} The finalised template context
846
     */
847
    getEmbedTemplateContext(templateContext) {
848
        return templateContext;
849
    }
850
}