Passed
Branch pre-recording-server (553a04)
by Jacob
01:28
created

yui/src/recording/js/commonmodule.js   B

Complexity

Total Complexity 37
Complexity/F 2.06

Size

Lines of Code 264
Function Count 18

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 16
Bugs 0 Features 0
Metric Value
cc 0
c 16
b 0
f 0
nc 8
dl 0
loc 264
rs 8.6
wmc 37
mnd 5
bc 42
fnc 18
bpm 2.3333
cpm 2.0555
noi 2

10 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.handle_data_available 0 21 3
B M.atto_recordrtc.commonmodule.start_recording 0 25 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.upload_to_server 0 54 1
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
// ESLint directives.
28
/* eslint-disable camelcase, no-alert, spaced-comment */
29
30
// JSHint directives.
31
/*global M */
32
/*jshint es5: true */
33
/*jshint onevar: false */
34
/*jshint shadow: true */
35
36
// Scrutinizer CI directives.
37
/** global: M */
38
/** global: Y */
39
40
M.atto_recordrtc = M.atto_recordrtc || {};
41
42
// Shorten access to M.atto_recordrtc.commonmodule namespace.
43
var cm = M.atto_recordrtc.commonmodule,
44
    am = M.atto_recordrtc.abstractmodule;
45
46
M.atto_recordrtc.commonmodule = {
47
    // Unitialized variables to be used by the other modules.
48
    editorScope: null,
49
    alertWarning: null,
50
    alertDanger: null,
51
    player: null,
52
    playerDOM: null, // Used to manipulate DOM directly.
53
    startStopBtn: null,
54
    uploadBtn: null,
55
    countdownSeconds: null,
56
    countdownTicker: null,
57
    recType: null,
58
    stream: null,
59
    mediaRecorder: null,
60
    chunks: null,
61
    blobSize: null,
62
    olderMoodle: null,
63
    maxUploadSize: null,
64
65
    // Capture webcam/microphone stream.
66
    capture_user_media: function(mediaConstraints, successCallback, errorCallback) {
67
        window.navigator.mediaDevices.getUserMedia(mediaConstraints).then(successCallback).catch(errorCallback);
68
    },
69
70
    // Add chunks of audio/video to array when made available.
71
    handle_data_available: function(event) {
72
        // Push recording slice to array.
73
        cm.chunks.push(event.data);
74
        // Size of all recorded data so far.
75
        cm.blobSize += event.data.size;
76
77
        // If total size of recording so far exceeds max upload limit, stop recording.
78
        // An extra condition exists to avoid displaying alert twice.
79
        if (blobSize >= maxUploadSize) {
0 ignored issues
show
Bug introduced by
The variable maxUploadSize seems to be never declared. If this is a global, consider adding a /** global: maxUploadSize */ 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...
Bug introduced by
The variable blobSize seems to be never declared. If this is a global, consider adding a /** global: blobSize */ 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...
80
            if (!window.localStorage.getItem('alerted')) {
81
                window.localStorage.setItem('alerted', 'true');
82
83
                cm.startStopBtn.simulate('click');
84
                am.show_alert('nearingmaxsize');
85
            } else {
86
                window.localStorage.removeItem('alerted');
87
            }
88
89
            cm.chunks.pop();
90
        }
91
    },
92
93
    // Handle recording end.
94
    handle_stop: function() {
95
        // Set source of audio player.
96
        var blob = new window.Blob(cm.chunks, {type: cm.mediaRecorder.mimeType});
97
        cm.player.set('src', window.URL.createObjectURL(blob));
98
99
        // Show audio player with controls enabled, and unmute.
100
        cm.player.set('muted', false);
101
        cm.player.set('controls', true);
102
        cm.player.ancestor().ancestor().removeClass('hide');
103
104
        // Show upload button.
105
        cm.uploadBtn.ancestor().ancestor().removeClass('hide');
106
        cm.uploadBtn.set('textContent', M.util.get_string('attachrecording', 'atto_recordrtc'));
107
        cm.uploadBtn.set('disabled', false);
108
109
        // Handle when upload button is clicked.
110
        cm.uploadBtn.on('click', function() {
111
            // Trigger error if no recording has been made.
112
            if (cm.chunks.length === 0) {
113
                am.show_alert('norecordingfound');
114
            } else {
115
                cm.uploadBtn.set('disabled', true);
116
117
                // Upload recording to server.
118
                cm.upload_to_server(cm.recType, function(progress, fileURLOrError) {
119
                    if (progress === 'ended') { // Insert annotation in text.
120
                        cm.uploadBtn.set('disabled', false);
121
                        cm.insert_annotation(cm.recType, fileURLOrError);
122
                    } else if (progress === 'upload-failed') { // Show error message in upload button.
123
                        cm.uploadBtn.set('disabled', false);
124
                        cm.uploadBtn.set('textContent',
125
                            M.util.get_string('uploadfailed', 'atto_recordrtc') + ' ' + fileURLOrError);
126
                    } else if (progress === 'upload-failed-404') { // 404 error = File too large in Moodle.
127
                        cm.uploadBtn.set('disabled', false);
128
                        cm.uploadBtn.set('textContent', M.util.get_string('uploadfailed404', 'atto_recordrtc'));
129
                    } else if (progress === 'upload-aborted') {
130
                        cm.uploadBtn.set('disabled', false);
131
                        cm.uploadBtn.set('textContent',
132
                            M.util.get_string('uploadaborted', 'atto_recordrtc') + ' ' + fileURLOrError);
133
                    } else {
134
                        cm.uploadBtn.set('textContent', progress);
135
                    }
136
                });
137
            }
138
        });
139
    },
140
141
    // Get everything set up to start recording.
142
    start_recording: function(type, stream) {
143
        // The options for the recording codecs and bitrates.
144
        var options = am.select_rec_options(type);
145
        cm.mediaRecorder = new window.MediaRecorder(stream, options);
146
147
        // Initialize MediaRecorder events and start recording.
148
        cm.mediaRecorder.ondataavailable = cm.handle_data_available;
149
        cm.mediaRecorder.onstop = cm.handle_stop;
150
        cm.mediaRecorder.start(1000); // Capture in 1s chunks. Must be set to work with Firefox.
151
152
        // Mute audio, distracting while recording.
153
        cm.player.set('muted', true);
154
155
        // Set recording timer to the time specified in the settings.
156
        cm.countdownSeconds = cm.editorScope.get('timelimit');
157
        cm.countdownSeconds++;
158
        var timerText = M.util.get_string('stoprecording', 'atto_recordrtc');
159
        timerText += ' (<span id="minutes"></span>:<span id="seconds"></span>)';
160
        cm.startStopBtn.setHTML(timerText);
161
        cm.set_time();
162
        cm.countdownTicker = window.setInterval(cm.set_time, 1000);
163
164
        // Make button clickable again, to allow stopping recording.
165
        cm.startStopBtn.set('disabled', false);
166
    },
167
168
    // Upload recorded audio/video to server.
169
    upload_to_server: function(type, callback) {
170
        var xhr = new window.XMLHttpRequest();
171
172
        // Get src media of audio/video tag.
173
        xhr.open('GET', cm.player.get('src'), true);
174
        xhr.responseType = 'blob';
175
176
        xhr.onload = function() {
177
            if (xhr.status === 200) { // If src media was successfully retrieved.
178
                // blob is now the media that the audio/video tag's src pointed to.
179
                var blob = this.response;
180
181
                // Generate filename with random ID and file extension.
182
                var fileName = (Math.random() * 1000).toString().replace('.', '');
183
                fileName += (type === 'audio') ? '-audio.ogg'
184
                                               : '-video.webm';
185
186
                // Create FormData to send to PHP filepicker-upload script.
187
                var formData = new window.FormData(),
188
                    filepickerOptions = cm.editorScope.get('host').get('filepickeroptions').link,
189
                    repositoryKeys = window.Object.keys(filepickerOptions.repositories);
190
191
                formData.append('repo_upload_file', blob, fileName);
192
                formData.append('itemid', filepickerOptions.itemid);
193
194
                for (var i = 0; i < repositoryKeys.length; i++) {
195
                    if (filepickerOptions.repositories[repositoryKeys[i]].type === 'upload') {
196
                        formData.append('repo_id', filepickerOptions.repositories[repositoryKeys[i]].id);
197
                        break;
198
                    }
199
                }
200
201
                formData.append('env', filepickerOptions.env);
202
                formData.append('sesskey', M.cfg.sesskey);
203
                formData.append('client_id', filepickerOptions.client_id);
204
                formData.append('savepath', '/');
205
                formData.append('ctx_id', filepickerOptions.context.id);
206
207
                // Pass FormData to PHP script using XHR.
208
                var uploadEndpoint = M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload';
209
                cm.make_xmlhttprequest(uploadEndpoint, formData,
210
                    function(progress, responseText) {
211
                        if (progress === 'upload-ended') {
212
                            callback('ended', window.JSON.parse(responseText).url);
213
                        } else {
214
                            callback(progress);
215
                        }
216
                    }
217
                );
218
            }
219
        };
220
221
        xhr.send();
222
    },
223
224
    // Handle XHR sending/receiving/status.
225
    make_xmlhttprequest: function(url, data, callback) {
226
        var xhr = new window.XMLHttpRequest();
227
228
        xhr.onreadystatechange = function() {
229
            if ((xhr.readyState === 4) && (xhr.status === 200)) { // When request is finished and successful.
230
                callback('upload-ended', xhr.responseText);
231
            } else if (xhr.status === 404) { // When request returns 404 Not Found.
232
                callback('upload-failed-404');
233
            }
234
        };
235
236
        xhr.upload.onprogress = function(event) {
237
            callback(Math.round(event.loaded / event.total * 100) + "% " + M.util.get_string('uploadprogress', 'atto_recordrtc'));
238
        };
239
240
        xhr.upload.onerror = function(error) {
241
            callback('upload-failed', error);
242
        };
243
244
        xhr.upload.onabort = function(error) {
245
            callback('upload-aborted', error);
246
        };
247
248
        // POST FormData to PHP script that handles uploading/saving.
249
        xhr.open('POST', url);
250
        xhr.send(data);
251
    },
252
253
    // Makes 1min and 2s display as 1:02 on timer instead of 1:2, for example.
254
    pad: function(val) {
255
        var valString = val + "";
256
257
        if (valString.length < 2) {
258
            return "0" + valString;
259
        } else {
260
            return valString;
261
        }
262
    },
263
264
    // Functionality to make recording timer count down.
265
    // Also makes recording stop when time limit is hit.
266
    set_time: function() {
267
        cm.countdownSeconds--;
268
269
        cm.startStopBtn.one('span#seconds').set('textContent', cm.pad(cm.countdownSeconds % 60));
270
        cm.startStopBtn.one('span#minutes').set('textContent', cm.pad(window.parseInt(cm.countdownSeconds / 60, 10)));
271
272
        if (cm.countdownSeconds === 0) {
273
            cm.startStopBtn.simulate('click');
274
        }
275
    },
276
277
    // Generates link to recorded annotation to be inserted.
278
    create_annotation: function(type, recording_url) {
279
        var linkText = window.prompt(M.util.get_string('annotationprompt', 'atto_recordrtc'),
280
                                     M.util.get_string('annotation:' + type, 'atto_recordrtc'));
281
282
        // Return HTML for annotation link, if user did not press "Cancel".
283
        if (!linkText) {
284
            return undefined;
285
        } else {
286
            var annotation = '<a target="_blank" href="' + recording_url + '">' + linkText + '</a>';
287
            return annotation;
288
        }
289
    },
290
291
    // Inserts link to annotation in editor text area.
292
    insert_annotation: function(type, recording_url) {
293
        var annotation = cm.create_annotation(type, recording_url);
294
295
        // Insert annotation link.
296
        // If user pressed "Cancel", just go back to main recording screen.
297
        if (!annotation) {
298
            cm.uploadBtn.set('textContent', M.util.get_string('attachrecording', 'atto_recordrtc'));
299
        } else {
300
            cm.editorScope.setLink(cm.editorScope, annotation);
301
        }
302
    }
303
};
304