Passed
Push — master ( 8be289...5c19e6 )
by Jacob
01:49
created

yui/src/recording/js/helpermodule.js   B

Complexity

Total Complexity 40
Complexity/F 2.67

Size

Lines of Code 242
Function Count 15

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 0
c 1
b 0
f 0
nc 64
dl 0
loc 242
rs 8.2608
wmc 40
mnd 5
bc 41
fnc 15
bpm 2.7333
cpm 2.6666
noi 0

7 Functions

Rating   Name   Duplication   Size   Complexity  
B M.atto_recordrtc.helpermodule.upload_to_server 0 42 1
B M.atto_recordrtc.helpermodule.make_xmlhttprequest 0 27 1
A M.atto_recordrtc.helpermodule.pad 0 9 2
B M.atto_recordrtc.helpermodule.handle_data_available 0 18 5
A M.atto_recordrtc.helpermodule.set_time 0 10 2
B M.atto_recordrtc.helpermodule.start_recording 0 61 8
B M.atto_recordrtc.helpermodule.handle_stop 0 46 1

How to fix   Complexity   

Complexity

Complex classes like yui/src/recording/js/helpermodule.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
36
M.atto_recordrtc = M.atto_recordrtc || {};
37
38
// Shorten access to module namespaces.
39
var cm = M.atto_recordrtc.commonmodule;
40
var hm = M.atto_recordrtc.helpermodule;
41
42
M.atto_recordrtc.helpermodule = {
43
    // Unitialized variables to be used by the other modules.
44
    blobSize: null,
45
    chunks: null,
46
    countdownSeconds: null,
47
    countdownTicker: null,
48
    maxUploadSize: null,
49
50
    // Add chunks of audio/video to array when made available.
51
    handle_data_available: function(event) {
52
        // Size of all recorded data so far.
53
        hm.blobSize += event.data.size;
54
55
        // Push recording slice to array.
56
        // If total size of recording so far exceeds max upload limit, stop recording.
57
        // An extra condition exists to avoid displaying alert twice.
58
        if ((hm.blobSize >= hm.maxUploadSize) && (!window.localStorage.getItem('alerted'))) {
59
            window.localStorage.setItem('alerted', 'true');
60
61
            cm.startStopBtn.simulate('click');
62
            cm.show_alert('nearingmaxsize');
63
        } else if ((hm.blobSize >= hm.maxUploadSize) && (window.localStorage.getItem('alerted') === 'true')) {
64
            window.localStorage.removeItem('alerted');
65
        } else {
66
            hm.chunks.push(event.data);
67
        }
68
    },
69
70
    // Handle recording end.
71
    handle_stop: function() {
72
        // Set source of audio player.
73
        var blob = new window.Blob(hm.chunks, {type: cm.mediaRecorder.mimeType});
74
        cm.player.set('src', window.URL.createObjectURL(blob));
75
76
        // Show audio player with controls enabled, and unmute.
77
        cm.player.set('muted', false);
78
        cm.player.set('controls', true);
79
        cm.player.ancestor().ancestor().removeClass('hide'); // Only audio player is hidden at this point.
80
81
        // Show upload button.
82
        cm.uploadBtn.set('disabled', false);
83
        cm.uploadBtn.set('textContent', M.util.get_string('attachrecording', 'atto_recordrtc'));
84
        cm.uploadBtn.ancestor().ancestor().removeClass('hide');
85
86
        // Handle when upload button is clicked.
87
        cm.uploadBtn.on('click', function() {
88
            // Trigger error if no recording has been made.
89
            if (!cm.player.get('src') || hm.chunks === []) {
90
                cm.show_alert('norecordingfound');
91
            } else {
92
                cm.uploadBtn.set('disabled', true);
93
94
                // Upload recording to server.
95
                hm.upload_to_server(cm.recType, function(progress, fileURLOrError) {
96
                    if (progress === 'ended') { // Insert annotation in text.
97
                        cm.uploadBtn.set('disabled', false);
98
                        cm.insert_annotation(cm.recType, fileURLOrError);
99
                    } else if (progress === 'upload-failed') { // Show error message in upload button.
100
                        cm.uploadBtn.set('disabled', false);
101
                        cm.uploadBtn.set('textContent',
102
                            M.util.get_string('uploadfailed', 'atto_recordrtc') + ' ' + fileURLOrError);
103
                    } else if (progress === 'upload-failed-404') { // 404 error = File too large in Moodle.
104
                        cm.uploadBtn.set('disabled', false);
105
                        cm.uploadBtn.set('textContent', M.util.get_string('uploadfailed404', 'atto_recordrtc'));
106
                    } else if (progress === 'upload-aborted') {
107
                        cm.uploadBtn.set('disabled', false);
108
                        cm.uploadBtn.set('textContent',
109
                            M.util.get_string('uploadaborted', 'atto_recordrtc') + ' ' + fileURLOrError);
110
                    } else {
111
                        cm.uploadBtn.set('textContent', progress);
112
                    }
113
                });
114
            }
115
        });
116
    },
117
118
    // Get everything set up to start recording.
119
    start_recording: function(type, stream) {
120
        // The options for the recording codecs and bitrates.
121
        var options = null;
122
        if (type === 'audio') {
123
            if (window.MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) {
124
                options = {
125
                    audioBitsPerSecond: cm.editorScope.get('audiobitrate'),
126
                    mimeType: 'audio/webm;codecs=opus'
127
                };
128
            } else if (window.MediaRecorder.isTypeSupported('audio/ogg;codecs=opus')) {
129
                options = {
130
                    audioBitsPerSecond: cm.editorScope.get('audiobitrate'),
131
                    mimeType: 'audio/ogg;codecs=opus'
132
                };
133
            }
134
        } else {
135
            if (window.MediaRecorder.isTypeSupported('video/webm;codecs=vp9,opus')) {
136
                options = {
137
                    audioBitsPerSecond: cm.editorScope.get('audiobitrate'),
138
                    videoBitsPerSecond: cm.editorScope.get('videobitrate'),
139
                    mimeType: 'video/webm;codecs=vp9,opus'
140
                };
141
            } else if (window.MediaRecorder.isTypeSupported('video/webm;codecs=h264,opus')) {
142
                options = {
143
                    audioBitsPerSecond: cm.editorScope.get('audiobitrate'),
144
                    videoBitsPerSecond: cm.editorScope.get('videobitrate'),
145
                    mimeType: 'video/webm;codecs=h264,opus'
146
                };
147
            } else if (window.MediaRecorder.isTypeSupported('video/webm;codecs=vp8,opus')) {
148
                options = {
149
                    audioBitsPerSecond: cm.editorScope.get('audiobitrate'),
150
                    videoBitsPerSecond: cm.editorScope.get('videobitrate'),
151
                    mimeType: 'video/webm;codecs=vp8,opus'
152
                };
153
            }
154
        }
155
156
        // If none of the options above are supported, fall back on browser defaults.
157
        cm.mediaRecorder = options ? new window.MediaRecorder(stream, options)
158
                                   : new window.MediaRecorder(stream);
159
160
        // Initialize MediaRecorder events and start recording.
161
        cm.mediaRecorder.ondataavailable = hm.handle_data_available;
162
        cm.mediaRecorder.onstop = hm.handle_stop;
163
        cm.mediaRecorder.start(1000); // Capture in 1s chunks. Must be set to work with Firefox.
164
165
        // Mute audio, distracting while recording.
166
        cm.player.set('muted', true);
167
168
        // Set recording timer to the time specified in the settings.
169
        hm.countdownSeconds = cm.editorScope.get('timelimit');
170
        hm.countdownSeconds++;
171
        var timerText = M.util.get_string('stoprecording', 'atto_recordrtc');
172
        timerText += ' (<span id="minutes"></span>:<span id="seconds"></span>)';
173
        cm.startStopBtn.setHTML(timerText);
174
        hm.set_time();
175
        hm.countdownTicker = window.setInterval(hm.set_time, 1000);
176
177
        // Make button clickable again, to allow stopping recording.
178
        cm.startStopBtn.set('disabled', false);
179
    },
180
181
    // Upload recorded audio/video to server.
182
    upload_to_server: function(type, callback) {
183
        var xhr = new window.XMLHttpRequest();
184
185
        // Get src media of audio/video tag.
186
        xhr.open('GET', cm.player.get('src'), true);
187
        xhr.responseType = 'blob';
188
189
        xhr.onload = function() {
190
            if (xhr.status === 200) { // If src media was successfully retrieved.
191
                // blob is now the media that the audio/video tag's src pointed to.
192
                var blob = this.response;
193
194
                // Generate filename with random ID and file extension.
195
                var fileName = (Math.random() * 1000).toString().replace('.', '');
196
                if (type === 'audio') {
197
                    fileName += '-audio.ogg';
198
                } else {
199
                    fileName += '-video.webm';
200
                }
201
202
                // Create FormData to send to PHP upload/save script.
203
                var formData = new window.FormData();
204
                formData.append('contextid', cm.editorScope.get('contextid'));
205
                formData.append('sesskey', cm.editorScope.get('sesskey'));
206
                formData.append(type + '-filename', fileName);
207
                formData.append(type + '-blob', blob);
208
209
                // Pass FormData to PHP script using XHR.
210
                hm.make_xmlhttprequest(cm.editorScope.get('recordrtcroot') + 'save.php', formData,
211
                    function(progress, responseText) {
212
                        if (progress === 'upload-ended') {
213
                            var initialURL = cm.editorScope.get('recordrtcroot') + 'uploads.php/';
214
                            return callback('ended', initialURL + responseText);
215
                        }
216
                        return callback(progress);
217
                    }
218
                );
219
            }
220
        };
221
222
        xhr.send();
223
    },
224
225
    // Handle XHR sending/receiving/status.
226
    make_xmlhttprequest: function(url, data, callback) {
227
        var xhr = new window.XMLHttpRequest();
228
229
        xhr.onreadystatechange = function() {
230
            if ((xhr.readyState === 4) && (xhr.status === 200)) { // When request is finished and successful.
231
                callback('upload-ended', xhr.responseText);
232
            } else if (xhr.status === 404) { // When request returns 404 Not Found.
233
                callback('upload-failed-404');
234
            }
235
        };
236
237
        xhr.upload.onprogress = function(event) {
238
            callback(Math.round(event.loaded / event.total * 100) + "% " + M.util.get_string('uploadprogress', 'atto_recordrtc'));
239
        };
240
241
        xhr.upload.onerror = function(error) {
242
            callback('upload-failed', error);
243
        };
244
245
        xhr.upload.onabort = function(error) {
246
            callback('upload-aborted', error);
247
        };
248
249
        // POST FormData to PHP script that handles uploading/saving.
250
        xhr.open('POST', url);
251
        xhr.send(data);
252
    },
253
254
    // Makes 1min and 2s display as 1:02 on timer instead of 1:2, for example.
255
    pad: function(val) {
256
        var valString = val + "";
257
258
        if (valString.length < 2) {
259
            return "0" + valString;
260
        } else {
261
            return valString;
262
        }
263
    },
264
265
    // Functionality to make recording timer count down.
266
    // Also makes recording stop when time limit is hit.
267
    set_time: function() {
268
        hm.countdownSeconds--;
269
270
        cm.startStopBtn.one('span#seconds').set('textContent', hm.pad(hm.countdownSeconds % 60));
271
        cm.startStopBtn.one('span#minutes').set('textContent', hm.pad(window.parseInt(hm.countdownSeconds / 60, 10)));
272
273
        if (hm.countdownSeconds === 0) {
274
            cm.startStopBtn.simulate('click');
275
        }
276
    }
277
};
278