Passed
Pull Request — master (#23)
by Jacob
01:26
created

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

Complexity

Total Complexity 62
Complexity/F 2.07

Size

Lines of Code 368
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 368
rs 4.8235
wmc 62
mnd 5
bc 66
fnc 30
bpm 2.2
cpm 2.0666
noi 3

16 Functions

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