Passed
Push — master ( 4ab539...7833ac )
by Jesus
01:54
created

yui/src/recording/js/commonmodule.js   D

Complexity

Total Complexity 62
Complexity/F 2.07

Size

Lines of Code 370
Function Count 30

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 13
Bugs 0 Features 0
Metric Value
cc 0
c 13
b 0
f 0
nc 4224
dl 0
loc 370
rs 4.8235
wmc 62
mnd 5
bc 66
fnc 30
bpm 2.2
cpm 2.0666
noi 5

16 Functions

Rating   Name   Duplication   Size   Complexity  
A M.atto_recordrtc.commonmodule.check_has_gum 0 7 2
A M.atto_recordrtc.commonmodule.handle_gum_errors 0 18 2
A M.atto_recordrtc.commonmodule.show_alert 0 12 1
A M.atto_recordrtc.commonmodule.capture_user_media 0 3 1
A M.atto_recordrtc.commonmodule.check_browser 0 7 2
B M.atto_recordrtc.commonmodule.check_secure 0 12 5
A M.atto_recordrtc.commonmodule.create_annotation 0 12 2
B M.atto_recordrtc.commonmodule.make_xmlhttprequest 0 27 1
A M.atto_recordrtc.commonmodule.insert_annotation 0 11 2
A M.atto_recordrtc.commonmodule.set_time 0 10 2
B M.atto_recordrtc.commonmodule.handle_data_available 0 18 5
B M.atto_recordrtc.commonmodule.start_recording 0 25 1
B M.atto_recordrtc.commonmodule.handle_stop 0 46 1
A M.atto_recordrtc.commonmodule.pad 0 9 2
B M.atto_recordrtc.commonmodule.select_rec_options 0 33 3
A M.atto_recordrtc.commonmodule.upload_to_server 0 57 1

How to fix   Complexity   

Complexity

Complex classes like yui/src/recording/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
// 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
 * Atto recordrtc library functions
19
 *
20
 * @package    atto_recordrtc
21
 * @author     Jesus Federico (jesus [at] blindsidenetworks [dt] com)
22
 * @author     Jacob Prud'homme (jacob [dt] prudhomme [at] blindsidenetworks [dt] com)
23
 * @copyright  2017 Blindside Networks Inc.
24
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25
 */
26
27
// JSHint directives.
28
/*jshint es5: true */
29
/*jshint onevar: false */
30
/*jshint shadow: true */
31
/*global M */
32
33
// Scrutinizer CI directives.
34
/** global: M */
35
/** global: Y */
36
37
M.atto_recordrtc = M.atto_recordrtc || {};
38
39
// Shorten access to M.atto_recordrtc.commonmodule namespace.
40
var cm = M.atto_recordrtc.commonmodule;
41
42
M.atto_recordrtc.commonmodule = {
43
    // Unitialized variables to be used by the other modules.
44
    editorScope: null,
45
    alertWarning: null,
46
    alertDanger: null,
47
    player: null,
48
    playerDOM: null, // Used to manipulate DOM directly.
49
    startStopBtn: null,
50
    uploadBtn: null,
51
    countdownSeconds: null,
52
    countdownTicker: null,
53
    recType: null,
54
    stream: null,
55
    mediaRecorder: null,
56
    chunks: null,
57
    blobSize: null,
58
    olderMoodle: null,
59
    maxUploadSize: 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
    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', 'atto_recordrtc'),
68
                message: M.util.get_string(subject, 'atto_recordrtc')
69
            });
70
71
            if (onCloseEvent) {
72
                dialogue.after('complete', onCloseEvent);
73
            }
74
        });
75
    },
76
77
    // Handle getUserMedia errors.
78
    handle_gum_errors: function(error, commonConfig) {
79
        var btnLabel = M.util.get_string('recordingfailed', 'atto_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
            cm.show_alert(stringName, treatAsStopped);
90
        } else {
91
            cm.show_alert(stringName, function() {
92
                cm.editorScope.closeDialogue(cm.editorScope);
93
            });
94
        }
95
    },
96
97
    // Show alert and close plugin if browser does not support WebRTC at all.
98
    check_has_gum: function() {
99
        if (!(navigator.mediaDevices && window.MediaRecorder)) {
0 ignored issues
show
Bug introduced by
The variable navigator seems to be never declared. If this is a global, consider adding a /** global: navigator */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
100
            cm.show_alert('nowebrtc', function() {
101
                cm.editorScope.closeDialogue(cm.editorScope);
102
            });
103
        }
104
    },
105
106
    // Notify and redirect user if plugin is used from insecure location.
107
    check_secure: function() {
108
        var isSecureOrigin = (window.location.protocol === 'https:') ||
109
                             (window.location.host.indexOf('localhost') !== -1);
110
111
        if (!isSecureOrigin && (window.bowser.chrome || window.bowser.opera)) {
112
            cm.show_alert('gumsecurity', function() {
113
                cm.editorScope.closeDialogue(cm.editorScope);
114
            });
115
        } else if (!isSecureOrigin) {
116
            cm.alertDanger.ancestor().ancestor().removeClass('hide');
117
        }
118
    },
119
120
    // Display "consider switching browsers" message if not using:
121
    // - Firefox 29+;
122
    // - Chrome 49+;
123
    // - Opera 36+.
124
    check_browser: function() {
125
        if (!((window.bowser.firefox && window.bowser.version >= 29) ||
126
              (window.bowser.chrome && window.bowser.version >= 49) ||
127
              (window.bowser.opera && window.bowser.version >= 36))) {
128
            cm.alertWarning.ancestor().ancestor().removeClass('hide');
129
        }
130
    },
131
132
133
    // Capture webcam/microphone stream.
134
    capture_user_media: function(mediaConstraints, successCallback, errorCallback) {
135
        window.navigator.mediaDevices.getUserMedia(mediaConstraints).then(successCallback).catch(errorCallback);
136
    },
137
138
    // Select best options for the recording codec.
139
    select_rec_options: function(recType) {
140
        var types, options;
141
142
        if (recType === 'audio') {
143
            var types = [
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable types already seems to be declared on line 140. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
144
                    'audio/webm;codecs=opus',
145
                    'audio/ogg;codecs=opus'
146
                ],
147
                options = {
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable options already seems to be declared on line 140. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
148
                    audioBitsPerSecond: window.parseInt(cm.editorScope.get('audiobitrate'))
149
                };
150
        } else {
151
            var types = [
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable types already seems to be declared on line 140. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
152
                    'video/webm;codecs=vp9,opus',
153
                    'video/webm;codecs=h264,opus',
154
                    'video/webm;codecs=vp8,opus'
155
                ],
156
                options = {
0 ignored issues
show
Comprehensibility Naming Best Practice introduced by
The variable options already seems to be declared on line 140. Consider using another variable name or omitting the var keyword.

This check looks for variables that are declared in multiple lines. There may be several reasons for this.

In the simplest case the variable name was reused by mistake. This may lead to very hard to locate bugs.

If you want to reuse a variable for another purpose, consider declaring it at or near the top of your function and just assigning to it subsequently so it is always declared.

Loading history...
157
                    audioBitsPerSecond: window.parseInt(cm.editorScope.get('audiobitrate')),
158
                    videoBitsPerSecond: window.parseInt(cm.editorScope.get('videobitrate'))
159
                };
160
        }
161
162
        var compatTypes = types.filter(function(type) {
163
            return window.MediaRecorder.isTypeSupported(type);
164
        });
165
166
        if (compatTypes.length !== 0) {
167
            options.mimeType = compatTypes[0];
168
        }
169
170
        return options;
171
    },
172
173
    // Add chunks of audio/video to array when made available.
174
    handle_data_available: function(event) {
175
        // Size of all recorded data so far.
176
        cm.blobSize += event.data.size;
177
178
        // Push recording slice to array.
179
        // If total size of recording so far exceeds max upload limit, stop recording.
180
        // An extra condition exists to avoid displaying alert twice.
181
        if ((cm.blobSize >= cm.maxUploadSize) && (!window.localStorage.getItem('alerted'))) {
182
            window.localStorage.setItem('alerted', 'true');
183
184
            cm.startStopBtn.simulate('click');
185
            cm.show_alert('nearingmaxsize');
186
        } else if ((cm.blobSize >= cm.maxUploadSize) && (window.localStorage.getItem('alerted') === 'true')) {
187
            window.localStorage.removeItem('alerted');
188
        } else {
189
            cm.chunks.push(event.data);
190
        }
191
    },
192
193
    // Handle recording end.
194
    handle_stop: function() {
195
        // Set source of audio player.
196
        var blob = new window.Blob(cm.chunks, {type: cm.mediaRecorder.mimeType});
197
        cm.player.set('src', window.URL.createObjectURL(blob));
198
199
        // Show audio player with controls enabled, and unmute.
200
        cm.player.set('muted', false);
201
        cm.player.set('controls', true);
202
        cm.player.ancestor().ancestor().removeClass('hide');
203
204
        // Show upload button.
205
        cm.uploadBtn.ancestor().ancestor().removeClass('hide');
206
        cm.uploadBtn.set('textContent', M.util.get_string('attachrecording', 'atto_recordrtc'));
207
        cm.uploadBtn.set('disabled', false);
208
209
        // Handle when upload button is clicked.
210
        cm.uploadBtn.on('click', function() {
211
            // Trigger error if no recording has been made.
212
            if (!cm.player.get('src') || cm.chunks === []) {
213
                cm.show_alert('norecordingfound');
214
            } else {
215
                cm.uploadBtn.set('disabled', true);
216
217
                // Upload recording to server.
218
                cm.upload_to_server(cm.recType, function(progress, fileURLOrError) {
219
                    if (progress === 'ended') { // Insert annotation in text.
220
                        cm.uploadBtn.set('disabled', false);
221
                        cm.insert_annotation(cm.recType, fileURLOrError);
222
                    } else if (progress === 'upload-failed') { // Show error message in upload button.
223
                        cm.uploadBtn.set('disabled', false);
224
                        cm.uploadBtn.set('textContent',
225
                            M.util.get_string('uploadfailed', 'atto_recordrtc') + ' ' + fileURLOrError);
226
                    } else if (progress === 'upload-failed-404') { // 404 error = File too large in Moodle.
227
                        cm.uploadBtn.set('disabled', false);
228
                        cm.uploadBtn.set('textContent', M.util.get_string('uploadfailed404', 'atto_recordrtc'));
229
                    } else if (progress === 'upload-aborted') {
230
                        cm.uploadBtn.set('disabled', false);
231
                        cm.uploadBtn.set('textContent',
232
                            M.util.get_string('uploadaborted', 'atto_recordrtc') + ' ' + fileURLOrError);
233
                    } else {
234
                        cm.uploadBtn.set('textContent', progress);
235
                    }
236
                });
237
            }
238
        });
239
    },
240
241
    // Get everything set up to start recording.
242
    start_recording: function(type, stream) {
243
        // The options for the recording codecs and bitrates.
244
        var options = cm.select_rec_options(type);
245
        cm.mediaRecorder = new window.MediaRecorder(stream, options);
246
247
        // Initialize MediaRecorder events and start recording.
248
        cm.mediaRecorder.ondataavailable = cm.handle_data_available;
249
        cm.mediaRecorder.onstop = cm.handle_stop;
250
        cm.mediaRecorder.start(1000); // Capture in 1s chunks. Must be set to work with Firefox.
251
252
        // Mute audio, distracting while recording.
253
        cm.player.set('muted', true);
254
255
        // Set recording timer to the time specified in the settings.
256
        cm.countdownSeconds = cm.editorScope.get('timelimit');
257
        cm.countdownSeconds++;
258
        var timerText = M.util.get_string('stoprecording', 'atto_recordrtc');
259
        timerText += ' (<span id="minutes"></span>:<span id="seconds"></span>)';
260
        cm.startStopBtn.setHTML(timerText);
261
        cm.set_time();
262
        cm.countdownTicker = window.setInterval(cm.set_time, 1000);
263
264
        // Make button clickable again, to allow stopping recording.
265
        cm.startStopBtn.set('disabled', false);
266
    },
267
268
    // Upload recorded audio/video to server.
269
    upload_to_server: function(type, callback) {
270
        var xhr = new window.XMLHttpRequest();
271
272
        // Get src media of audio/video tag.
273
        xhr.open('GET', cm.player.get('src'), true);
274
        xhr.responseType = 'blob';
275
276
        xhr.onload = function() {
277
            if (xhr.status === 200) { // If src media was successfully retrieved.
278
                // blob is now the media that the audio/video tag's src pointed to.
279
                var blob = this.response;
280
281
                // Generate filename with random ID and file extension.
282
                var fileName = (Math.random() * 1000).toString().replace('.', '');
283
                if (type === 'audio') {
284
                    fileName += '-audio.ogg';
285
                } else {
286
                    fileName += '-video.webm';
287
                }
288
289
                // Create FormData to send to PHP filepicker-upload script.
290
                var formData = new window.FormData(),
291
                    filepickerOptions = cm.editorScope.get('host').get('filepickeroptions').link,
292
                    repositoryKeys = window.Object.keys(filepickerOptions.repositories);
293
294
                formData.append('repo_upload_file', blob, fileName);
295
                formData.append('itemid', filepickerOptions.itemid);
296
297
                for (var i = 0; i < repositoryKeys.length; i++) {
298
                    if (filepickerOptions.repositories[repositoryKeys[i]].type === 'upload') {
299
                        formData.append('repo_id', filepickerOptions.repositories[repositoryKeys[i]].id);
300
                        break;
301
                    }
302
                }
303
304
                formData.append('env', filepickerOptions.env);
305
                formData.append('sesskey', M.cfg.sesskey);
306
                formData.append('client_id', filepickerOptions.client_id);
307
                formData.append('savepath', '/');
308
                formData.append('ctx_id', filepickerOptions.context.id);
309
310
                // Pass FormData to PHP script using XHR.
311
                var uploadEndpoint = M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload';
312
                cm.make_xmlhttprequest(uploadEndpoint, formData,
313
                    function(progress, responseText) {
314
                        if (progress === 'upload-ended') {
315
                            callback('ended', window.JSON.parse(responseText).url);
316
                        } else {
317
                            callback(progress);
318
                        }
319
                    }
320
                );
321
            }
322
        };
323
324
        xhr.send();
325
    },
326
327
    // Handle XHR sending/receiving/status.
328
    make_xmlhttprequest: function(url, data, callback) {
329
        var xhr = new window.XMLHttpRequest();
330
331
        xhr.onreadystatechange = function() {
332
            if ((xhr.readyState === 4) && (xhr.status === 200)) { // When request is finished and successful.
333
                callback('upload-ended', xhr.responseText);
334
            } else if (xhr.status === 404) { // When request returns 404 Not Found.
335
                callback('upload-failed-404');
336
            }
337
        };
338
339
        xhr.upload.onprogress = function(event) {
340
            callback(Math.round(event.loaded / event.total * 100) + "% " + M.util.get_string('uploadprogress', 'atto_recordrtc'));
341
        };
342
343
        xhr.upload.onerror = function(error) {
344
            callback('upload-failed', error);
345
        };
346
347
        xhr.upload.onabort = function(error) {
348
            callback('upload-aborted', error);
349
        };
350
351
        // POST FormData to PHP script that handles uploading/saving.
352
        xhr.open('POST', url);
353
        xhr.send(data);
354
    },
355
356
    // Makes 1min and 2s display as 1:02 on timer instead of 1:2, for example.
357
    pad: function(val) {
358
        var valString = val + "";
359
360
        if (valString.length < 2) {
361
            return "0" + valString;
362
        } else {
363
            return valString;
364
        }
365
    },
366
367
    // Functionality to make recording timer count down.
368
    // Also makes recording stop when time limit is hit.
369
    set_time: function() {
370
        cm.countdownSeconds--;
371
372
        cm.startStopBtn.one('span#seconds').set('textContent', cm.pad(cm.countdownSeconds % 60));
373
        cm.startStopBtn.one('span#minutes').set('textContent', cm.pad(window.parseInt(cm.countdownSeconds / 60, 10)));
374
375
        if (cm.countdownSeconds === 0) {
376
            cm.startStopBtn.simulate('click');
377
        }
378
    },
379
380
    // Generates link to recorded annotation to be inserted.
381
    create_annotation: function(type, recording_url) {
382
        var linkText = window.prompt(M.util.get_string('annotationprompt', 'atto_recordrtc'),
383
                                     M.util.get_string('annotation:' + type, 'atto_recordrtc'));
384
385
        // Return HTML for annotation link, if user did not press "Cancel".
386
        if (!linkText) {
387
            return undefined;
388
        } else {
389
            var annotation = '<a target="_blank" href="' + recording_url + '">' + linkText + '</a>';
390
            return annotation;
391
        }
392
    },
393
394
    // Inserts link to annotation in editor text area.
395
    insert_annotation: function(type, recording_url) {
396
        var annotation = cm.create_annotation(type, recording_url);
397
398
        // Insert annotation link.
399
        // If user pressed "Cancel", just go back to main recording screen.
400
        if (!annotation) {
401
            cm.uploadBtn.set('textContent', M.util.get_string('attachrecording', 'atto_recordrtc'));
402
        } else {
403
            cm.editorScope.setLink(cm.editorScope, annotation);
404
        }
405
    }
406
};
407