Completed
Push — master ( 7833ac...c29046 )
by Jesus
13s
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 0

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.pad 0 9 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.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 (cm.blobSize >= cm.maxUploadSize) {
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