Completed
Branch pre-recording-server (c9974e)
by Jacob
04:53
created

tinymce/js/commonmodule.js   C

Complexity

Total Complexity 59
Complexity/F 2.03

Size

Lines of Code 350
Function Count 29

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 16
Bugs 1 Features 0
Metric Value
cc 0
c 16
b 1
f 0
nc 128
dl 0
loc 350
rs 5.2857
wmc 59
mnd 5
bc 63
fnc 29
bpm 2.1724
cpm 2.0344
noi 0

15 Functions

Rating   Name   Duplication   Size   Complexity  
B 0 21 5
A M.tinymce_recordrtc.show_alert 0 12 1
A M.tinymce_recordrtc.handle_gum_errors 0 18 2
A commonmodule.js ➔ d 0 3 1
B M.tinymce_recordrtc.make_xmlhttprequest 0 27 1
A M.tinymce_recordrtc.pad 0 9 2
B M.tinymce_recordrtc.handle_stop 0 44 1
A M.tinymce_recordrtc.insert_annotation 0 12 2
B M.tinymce_recordrtc.handle_data_available 0 20 5
B M.tinymce_recordrtc.select_rec_options 0 33 3
B M.tinymce_recordrtc.start_recording 0 25 1
A M.tinymce_recordrtc.set_time 0 12 2
A M.tinymce_recordrtc.capture_user_media 0 3 1
A M.tinymce_recordrtc.create_annotation 0 12 2
A M.tinymce_recordrtc.upload_to_server 0 56 1

How to fix   Complexity   

Complexity

Complex classes like tinymce/js/commonmodule.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
// TinyMCE recordrtc library functions.
2
// @package    tinymce_recordrtc.
3
// @author     Jesus Federico  (jesus [at] blindsidenetworks [dt] com).
4
// @author     Jacob Prud'homme (jacob [dt] prudhomme [at] blindsidenetworks [dt] com)
5
// @copyright  2016 onwards, Blindside Networks Inc.
6
// @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
7
8
// ESLint directives.
9
/* global tinyMCE, tinyMCEPopup */
10
/* exported countdownTicker, playerDOM */
11
/* eslint-disable camelcase, no-alert */
12
13
// Scrutinizer CI directives.
14
/** global: navigator */
15
/** global: parent */
16
/** global: M */
17
/** global: Y */
18
/** global: recordrtc */
19
/** global: tinyMCEPopup */
20
21
M.tinymce_recordrtc = M.tinymce_recordrtc || {};
22
23
// Extract plugin settings to params hash.
24
(function() {
25
    var params = {};
26
    var r = /([^&=]+)=?([^&]*)/g;
27
28
    var d = function(s) {
29
        return window.decodeURIComponent(s.replace(/\+/g, ' '));
30
    };
31
32
    var search = window.location.search;
33
    var match = r.exec(search.substring(1));
34
    while (match) {
35
        params[d(match[1])] = d(match[2]);
36
37
        if (d(match[2]) === 'true' || d(match[2]) === 'false') {
38
            params[d(match[1])] = d(match[2]) === 'true' ? true : false;
39
        }
40
        match = r.exec(search.substring(1));
41
    }
42
43
    window.params = params;
44
})();
45
46
// Initialize some variables.
47
var alertWarning = null;
48
var alertDanger = null;
49
var blobSize = null;
50
var chunks = null;
51
var countdownSeconds = null;
52
var countdownTicker = null;
53
var maxUploadSize = null;
54
var mediaRecorder = null;
55
var player = null;
56
var playerDOM = null;
57
var recType = null;
58
var startStopBtn = null;
59
var uploadBtn = null;
60
61
// A helper for making a Moodle alert appear.
62
// Subject is the content of the alert (which error ther alert is for).
63
// Possibility to add on-alert-close event.
64
M.tinymce_recordrtc.show_alert = function(subject, onCloseEvent) {
65
    Y.use('moodle-core-notification-alert', function() {
66
        var dialogue = new M.core.alert({
67
            title: M.util.get_string(subject + '_title', 'tinymce_recordrtc'),
68
            message: M.util.get_string(subject, 'tinymce_recordrtc')
69
        });
70
71
        if (onCloseEvent) {
72
            dialogue.after('complete', onCloseEvent);
73
        }
74
    });
75
};
76
77
// Handle getUserMedia errors.
78
M.tinymce_recordrtc.handle_gum_errors = function(error, commonConfig) {
79
    var btnLabel = M.util.get_string('recordingfailed', 'tinymce_recordrtc'),
80
        treatAsStopped = function() {
81
            commonConfig.onMediaStopped(btnLabel);
82
        };
83
84
    // Changes 'CertainError' -> 'gumcertain' to match language string names.
85
    var stringName = 'gum' + error.name.replace('Error', '').toLowerCase();
86
87
    // After alert, proceed to treat as stopped recording, or close dialogue.
88
    if (stringName !== 'gumsecurity') {
89
        M.tinymce_recordrtc.show_alert(stringName, treatAsStopped);
90
    } else {
91
        M.tinymce_recordrtc.show_alert(stringName, function() {
92
            tinyMCEPopup.close();
93
        });
94
    }
95
};
96
97
// Capture webcam/microphone stream.
98
M.tinymce_recordrtc.capture_user_media = function(mediaConstraints, successCallback, errorCallback) {
99
    window.navigator.mediaDevices.getUserMedia(mediaConstraints).then(successCallback).catch(errorCallback);
100
};
101
102
// Select best options for the recording codec.
103
M.tinymce_recordrtc.select_rec_options = function(recType) {
104
    var types, options;
105
106
    if (recType === 'audio') {
107
        types = [
108
            'audio/webm;codecs=opus',
109
            'audio/ogg;codecs=opus'
110
        ];
111
        options = {
112
            audioBitsPerSecond: window.parseInt(window.params.audiobitrate)
113
        };
114
    } else {
115
        types = [
116
            'video/webm;codecs=vp9,opus',
117
            'video/webm;codecs=h264,opus',
118
            'video/webm;codecs=vp8,opus'
119
        ];
120
        options = {
121
            audioBitsPerSecond: window.parseInt(window.params.audiobitrate),
122
            videoBitsPerSecond: window.parseInt(window.params.videobitrate)
123
        };
124
    }
125
126
    var compatTypes = types.filter(function(type) {
127
        return window.MediaRecorder.isTypeSupported(type);
128
    });
129
130
    if (compatTypes !== []) {
131
        options.mimeType = compatTypes[0];
132
    }
133
134
    return options;
135
};
136
137
// Add chunks of audio/video to array when made available.
138
M.tinymce_recordrtc.handle_data_available = function(event) {
139
    // Size of all recorded data so far.
140
    blobSize += event.data.size;
141
142
    // Push recording slice to array.
143
    // If total size of recording so far exceeds max upload limit, stop recording.
144
    // An extra condition exists to avoid displaying alert twice.
145
    if ((blobSize >= maxUploadSize) && (!window.localStorage.getItem('alerted'))) {
146
        window.localStorage.setItem('alerted', 'true');
147
148
        Y.use('node-event-simulate', function() {
149
            startStopBtn.simulate('click');
150
        });
151
        M.tinymce_recordrtc.show_alert('nearingmaxsize');
152
    } else if ((blobSize >= maxUploadSize) && (window.localStorage.getItem('alerted') === 'true')) {
153
        window.localStorage.removeItem('alerted');
154
    } else {
155
        chunks.push(event.data);
156
    }
157
};
158
159
M.tinymce_recordrtc.handle_stop = function() {
160
    // Set source of audio player.
161
    var blob = new window.Blob(chunks, {type: mediaRecorder.mimeType});
162
    player.set('src', window.URL.createObjectURL(blob));
163
164
    // Show audio player with controls enabled, and unmute.
165
    player.set('muted', false);
166
    player.set('controls', true);
167
    player.ancestor().ancestor().removeClass('hide');
168
169
    // Show upload button.
170
    uploadBtn.ancestor().ancestor().removeClass('hide');
171
    uploadBtn.set('textContent', M.util.get_string('attachrecording', 'tinymce_recordrtc'));
172
    uploadBtn.set('disabled', false);
173
174
    // Handle when upload button is clicked.
175
    uploadBtn.on('click', function() {
176
        // Trigger error if no recording has been made.
177
        if (!player.get('src') || chunks === []) {
178
            M.tinymce_recordrtc.show_alert('norecordingfound');
179
        } else {
180
            uploadBtn.set('disabled', true);
181
182
            // Upload recording to server.
183
            M.tinymce_recordrtc.upload_to_server(recType, function(progress, fileURLOrError) {
184
                if (progress === 'ended') { // Insert annotation in text.
185
                    uploadBtn.set('disabled', false);
186
                    M.tinymce_recordrtc.insert_annotation(recType, fileURLOrError);
187
                } else if (progress === 'upload-failed') { // Show error message in upload button.
188
                    uploadBtn.set('disabled', false);
189
                    uploadBtn.set('textContent', M.util.get_string('uploadfailed', 'tinymce_recordrtc') + ' ' + fileURLOrError);
190
                } else if (progress === 'upload-failed-404') { // 404 error = File too large in Moodle.
191
                    uploadBtn.set('disabled', false);
192
                    uploadBtn.set('textContent', M.util.get_string('uploadfailed404', 'tinymce_recordrtc'));
193
                } else if (progress === 'upload-aborted') {
194
                    uploadBtn.set('disabled', false);
195
                    uploadBtn.set('textContent', M.util.get_string('uploadaborted', 'tinymce_recordrtc') + ' ' + fileURLOrError);
196
                } else {
197
                    uploadBtn.set('textContent', progress);
198
                }
199
            });
200
        }
201
    });
202
};
203
204
// Get everything set up to start recording.
205
M.tinymce_recordrtc.start_recording = function(type, stream) {
206
    // The options for the recording codecs and bitrates.
207
    var options = M.tinymce_recordrtc.select_rec_options(type);
208
    mediaRecorder = new window.MediaRecorder(stream, options);
209
210
    // Initialize MediaRecorder events and start recording.
211
    mediaRecorder.ondataavailable = M.tinymce_recordrtc.handle_data_available;
212
    mediaRecorder.onstop = M.tinymce_recordrtc.handle_stop;
213
    mediaRecorder.start(1000); // Capture in 1s chunks. Must be set to work with Firefox.
214
215
    // Mute audio, distracting while recording.
216
    player.set('muted', true);
217
218
    // Set recording timer to the time specified in the settings.
219
    countdownSeconds = window.params.timelimit;
220
    countdownSeconds++;
221
    var timerText = M.util.get_string('stoprecording', 'tinymce_recordrtc');
222
    timerText += ' (<span id="minutes"></span>:<span id="seconds"></span>)';
223
    startStopBtn.setHTML(timerText);
224
    M.tinymce_recordrtc.set_time();
225
    countdownTicker = window.setInterval(M.tinymce_recordrtc.set_time, 1000);
226
227
    // Make button clickable again, to allow stopping recording.
228
    startStopBtn.set('disabled', false);
229
};
230
231
// Upload recorded audio/video to server.
232
M.tinymce_recordrtc.upload_to_server = function(type, callback) {
233
    var xhr = new window.XMLHttpRequest();
234
235
    // Get src media of audio/video tag.
236
    xhr.open('GET', player.get('src'), true);
237
    xhr.responseType = 'blob';
238
239
    xhr.onload = function() {
240
        if (xhr.status === 200) { // If src media was successfully retrieved.
241
            // blob is now the media that the audio/video tag's src pointed to.
242
            var blob = this.response;
243
244
            // Generate filename with random ID and file extension.
245
            var fileName = (Math.random() * 1000).toString().replace('.', '');
246
            if (type === 'audio') {
247
                fileName += '-audio.ogg';
248
            } else {
249
                fileName += '-video.webm';
250
            }
251
252
            // Create FormData to send to PHP filepicker-upload script.
253
            var formData = new window.FormData(),
254
                editorId = tinyMCE.activeEditor.id,
255
                filepickerOptions = parent.M.editor_tinymce.filepicker_options[editorId].link,
256
                repositoryKeys = window.Object.keys(filepickerOptions.repositories);
257
258
            formData.append('repo_upload_file', blob, fileName);
259
            formData.append('itemid', filepickerOptions.itemid);
260
261
            for (var i = 0; i < repositoryKeys.length; i++) {
262
                if (filepickerOptions.repositories[repositoryKeys[i]].type === 'upload') {
263
                    formData.append('repo_id', filepickerOptions.repositories[repositoryKeys[i]].id);
264
                    break;
265
                }
266
            }
267
268
            formData.append('env', filepickerOptions.env);
269
            formData.append('sesskey', M.cfg.sesskey);
270
            formData.append('client_id', filepickerOptions.client_id);
271
            formData.append('savepath', '/');
272
            formData.append('ctx_id', filepickerOptions.context.id);
273
274
            // Pass FormData to PHP script using XHR.
275
            var uploadEndpoint = M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload';
276
            M.tinymce_recordrtc.make_xmlhttprequest(uploadEndpoint, formData, function(progress, responseText) {
277
                if (progress === 'upload-ended') {
278
                    callback('ended', window.JSON.parse(responseText).url);
279
                } else {
280
                    callback(progress);
281
                }
282
            });
283
        }
284
    };
285
286
    xhr.send();
287
};
288
289
// Handle XHR sending/receiving/status.
290
M.tinymce_recordrtc.make_xmlhttprequest = function(url, data, callback) {
291
    var xhr = new window.XMLHttpRequest();
292
293
    xhr.onreadystatechange = function() {
294
        if ((xhr.readyState === 4) && (xhr.status === 200)) { // When request is finished and successful.
295
            callback('upload-ended', xhr.responseText);
296
        } else if (xhr.status === 404) { // When request returns 404 Not Found.
297
            callback('upload-failed-404');
298
        }
299
    };
300
301
    xhr.upload.onprogress = function(event) {
302
        callback(Math.round(event.loaded / event.total * 100) + "% " + M.util.get_string('uploadprogress', 'tinymce_recordrtc'));
303
    };
304
305
    xhr.upload.onerror = function(error) {
306
        callback('upload-failed', error);
307
    };
308
309
    xhr.upload.onabort = function(error) {
310
        callback('upload-aborted', error);
311
    };
312
313
    // POST FormData to PHP script that handles uploading/saving.
314
    xhr.open('POST', url);
315
    xhr.send(data);
316
};
317
318
// Makes 1min and 2s display as 1:02 on timer instead of 1:2, for example.
319
M.tinymce_recordrtc.pad = function(val) {
320
    var valString = val + "";
321
322
    if (valString.length < 2) {
323
        return "0" + valString;
324
    } else {
325
        return valString;
326
    }
327
};
328
329
// Functionality to make recording timer count down.
330
// Also makes recording stop when time limit is hit.
331
M.tinymce_recordrtc.set_time = function() {
332
    countdownSeconds--;
333
334
    startStopBtn.one('span#seconds').set('textContent', M.tinymce_recordrtc.pad(countdownSeconds % 60));
335
    startStopBtn.one('span#minutes').set('textContent', M.tinymce_recordrtc.pad(window.parseInt(countdownSeconds / 60, 10)));
336
337
    if (countdownSeconds === 0) {
338
        Y.use('node-event-simulate', function() {
339
            startStopBtn.simulate('click');
340
        });
341
    }
342
};
343
344
// Generates link to recorded annotation to be inserted.
345
M.tinymce_recordrtc.create_annotation = function(type, recording_url) {
346
    var linkText = window.prompt(M.util.get_string('annotationprompt', 'tinymce_recordrtc'),
347
                                 M.util.get_string('annotation:' + type, 'tinymce_recordrtc'));
348
349
    // Return HTML for annotation link, if user did not press "Cancel".
350
    if (!linkText) {
351
        return undefined;
352
    } else {
353
        var annotation = '<div><a target="_blank" href="' + recording_url + '">' + linkText + '</a></div>';
354
        return annotation;
355
    }
356
};
357
358
// Inserts link to annotation in editor text area.
359
M.tinymce_recordrtc.insert_annotation = function(type, recording_url) {
360
    var annotation = M.tinymce_recordrtc.create_annotation(type, recording_url);
361
362
    // Insert annotation link.
363
    // If user pressed "Cancel", just go back to main recording screen.
364
    if (!annotation) {
365
        uploadBtn.set('textContent', M.util.get_string('attachrecording', 'tinymce_recordrtc'));
366
    } else {
367
        tinyMCEPopup.editor.execCommand('mceInsertContent', false, annotation);
368
        tinyMCEPopup.close();
369
    }
370
};
371