1 |
efrain |
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 |
maxUploadSize: null,
|
|
|
63 |
|
|
|
64 |
// Capture webcam/microphone stream.
|
|
|
65 |
capture_user_media: function(mediaConstraints, successCallback, errorCallback) {
|
|
|
66 |
window.navigator.mediaDevices.getUserMedia(mediaConstraints).then(successCallback).catch(errorCallback);
|
|
|
67 |
},
|
|
|
68 |
|
|
|
69 |
// Add chunks of audio/video to array when made available.
|
|
|
70 |
handle_data_available: function(event) {
|
|
|
71 |
// Push recording slice to array.
|
|
|
72 |
cm.chunks.push(event.data);
|
|
|
73 |
// Size of all recorded data so far.
|
|
|
74 |
cm.blobSize += event.data.size;
|
|
|
75 |
|
|
|
76 |
// If total size of recording so far exceeds max upload limit, stop recording.
|
|
|
77 |
// An extra condition exists to avoid displaying alert twice.
|
|
|
78 |
if (cm.blobSize >= cm.maxUploadSize) {
|
|
|
79 |
if (!window.localStorage.getItem('alerted')) {
|
|
|
80 |
window.localStorage.setItem('alerted', 'true');
|
|
|
81 |
|
|
|
82 |
cm.startStopBtn.simulate('click');
|
|
|
83 |
am.show_alert('nearingmaxsize');
|
|
|
84 |
} else {
|
|
|
85 |
window.localStorage.removeItem('alerted');
|
|
|
86 |
}
|
|
|
87 |
|
|
|
88 |
cm.chunks.pop();
|
|
|
89 |
}
|
|
|
90 |
},
|
|
|
91 |
|
|
|
92 |
// Handle recording end.
|
|
|
93 |
handle_stop: function() {
|
|
|
94 |
// Set source of audio player.
|
|
|
95 |
var blob = new window.Blob(cm.chunks, {type: cm.mediaRecorder.mimeType});
|
|
|
96 |
cm.player.set('srcObject', null);
|
|
|
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 |
// Get dialogue centered.
|
|
|
110 |
cm.editorScope.getDialogue().centered();
|
|
|
111 |
|
|
|
112 |
// Handle when upload button is clicked.
|
|
|
113 |
cm.uploadBtn.on('click', function() {
|
|
|
114 |
// Trigger error if no recording has been made.
|
|
|
115 |
if (cm.chunks.length === 0) {
|
|
|
116 |
am.show_alert('norecordingfound');
|
|
|
117 |
} else {
|
|
|
118 |
cm.uploadBtn.set('disabled', true);
|
|
|
119 |
|
|
|
120 |
// Upload recording to server.
|
|
|
121 |
cm.upload_to_server(cm.recType, function(progress, fileURLOrError) {
|
|
|
122 |
if (progress === 'ended') { // Insert annotation in text.
|
|
|
123 |
cm.uploadBtn.set('disabled', false);
|
|
|
124 |
cm.insert_annotation(cm.recType, fileURLOrError);
|
|
|
125 |
} else if (progress === 'upload-failed') { // Show error message in upload button.
|
|
|
126 |
cm.uploadBtn.set('disabled', false);
|
|
|
127 |
cm.uploadBtn.set('textContent',
|
|
|
128 |
M.util.get_string('uploadfailed', 'atto_recordrtc') + ' ' + fileURLOrError);
|
|
|
129 |
} else if (progress === 'upload-failed-404') { // 404 error = File too large in Moodle.
|
|
|
130 |
cm.uploadBtn.set('disabled', false);
|
|
|
131 |
cm.uploadBtn.set('textContent', M.util.get_string('uploadfailed404', 'atto_recordrtc'));
|
|
|
132 |
} else if (progress === 'upload-aborted') {
|
|
|
133 |
cm.uploadBtn.set('disabled', false);
|
|
|
134 |
cm.uploadBtn.set('textContent',
|
|
|
135 |
M.util.get_string('uploadaborted', 'atto_recordrtc') + ' ' + fileURLOrError);
|
|
|
136 |
} else {
|
|
|
137 |
cm.uploadBtn.set('textContent', progress);
|
|
|
138 |
}
|
|
|
139 |
});
|
|
|
140 |
}
|
|
|
141 |
});
|
|
|
142 |
},
|
|
|
143 |
|
|
|
144 |
// Get everything set up to start recording.
|
|
|
145 |
start_recording: function(type, stream) {
|
|
|
146 |
// The options for the recording codecs and bitrates.
|
|
|
147 |
var options = am.select_rec_options(type);
|
|
|
148 |
cm.mediaRecorder = new window.MediaRecorder(stream, options);
|
|
|
149 |
|
|
|
150 |
// Initialize MediaRecorder events and start recording.
|
|
|
151 |
cm.mediaRecorder.ondataavailable = cm.handle_data_available;
|
|
|
152 |
cm.mediaRecorder.onstop = cm.handle_stop;
|
|
|
153 |
cm.mediaRecorder.start(1000); // Capture in 1s chunks. Must be set to work with Firefox.
|
|
|
154 |
|
|
|
155 |
// Mute audio, distracting while recording.
|
|
|
156 |
cm.player.set('muted', true);
|
|
|
157 |
|
|
|
158 |
// Set recording timer to the time specified in the settings.
|
|
|
159 |
if (type === 'audio') {
|
|
|
160 |
cm.countdownSeconds = cm.editorScope.get('audiotimelimit');
|
|
|
161 |
} else if (type === 'video') {
|
|
|
162 |
cm.countdownSeconds = cm.editorScope.get('videotimelimit');
|
|
|
163 |
} else {
|
|
|
164 |
// Default timer.
|
|
|
165 |
cm.countdownSeconds = cm.editorScope.get('defaulttimelimit');
|
|
|
166 |
}
|
|
|
167 |
cm.countdownSeconds++;
|
|
|
168 |
var timerText = M.util.get_string('stoprecording', 'atto_recordrtc');
|
|
|
169 |
timerText += ' (<span id="minutes"></span>:<span id="seconds"></span>)';
|
|
|
170 |
cm.startStopBtn.setHTML(timerText);
|
|
|
171 |
cm.set_time();
|
|
|
172 |
cm.countdownTicker = window.setInterval(cm.set_time, 1000);
|
|
|
173 |
|
|
|
174 |
// Make button clickable again, to allow stopping recording.
|
|
|
175 |
cm.startStopBtn.set('disabled', false);
|
|
|
176 |
},
|
|
|
177 |
|
|
|
178 |
// Get everything set up to stop recording.
|
|
|
179 |
stop_recording: function(stream) {
|
|
|
180 |
// Stop recording stream.
|
|
|
181 |
cm.mediaRecorder.stop();
|
|
|
182 |
|
|
|
183 |
// Stop each individual MediaTrack.
|
|
|
184 |
var tracks = stream.getTracks();
|
|
|
185 |
for (var i = 0; i < tracks.length; i++) {
|
|
|
186 |
tracks[i].stop();
|
|
|
187 |
}
|
|
|
188 |
},
|
|
|
189 |
|
|
|
190 |
getFileExtension: function(type) {
|
|
|
191 |
if (type === 'audio') {
|
|
|
192 |
if (window.MediaRecorder.isTypeSupported('audio/ogg')) {
|
|
|
193 |
return 'ogg';
|
|
|
194 |
} else if (window.MediaRecorder.isTypeSupported('audio/mp4')) {
|
|
|
195 |
return 'mp4';
|
|
|
196 |
}
|
|
|
197 |
} else {
|
|
|
198 |
if (window.MediaRecorder.isTypeSupported('audio/webm')) {
|
|
|
199 |
return 'webm';
|
|
|
200 |
} else if (window.MediaRecorder.isTypeSupported('audio/mp4')) {
|
|
|
201 |
return 'mp4';
|
|
|
202 |
}
|
|
|
203 |
}
|
|
|
204 |
|
|
|
205 |
window.console.warn('Unknown file type for MediaRecorder API');
|
|
|
206 |
return '';
|
|
|
207 |
},
|
|
|
208 |
|
|
|
209 |
// Upload recorded audio/video to server.
|
|
|
210 |
upload_to_server: function(type, callback) {
|
|
|
211 |
var xhr = new window.XMLHttpRequest();
|
|
|
212 |
var fileExtension = this.getFileExtension(type);
|
|
|
213 |
|
|
|
214 |
// Get src media of audio/video tag.
|
|
|
215 |
xhr.open('GET', cm.player.get('src'), true);
|
|
|
216 |
xhr.responseType = 'blob';
|
|
|
217 |
|
|
|
218 |
xhr.onload = function() {
|
|
|
219 |
if (xhr.status === 200) { // If src media was successfully retrieved.
|
|
|
220 |
// blob is now the media that the audio/video tag's src pointed to.
|
|
|
221 |
var blob = this.response;
|
|
|
222 |
|
|
|
223 |
// Generate filename with random ID and file extension.
|
|
|
224 |
var fileName = (Math.random() * 1000).toString().replace('.', '');
|
|
|
225 |
fileName += (type === 'audio') ? '-audio.' + fileExtension
|
|
|
226 |
: '-video.' + fileExtension;
|
|
|
227 |
|
|
|
228 |
// Create FormData to send to PHP filepicker-upload script.
|
|
|
229 |
var formData = new window.FormData(),
|
|
|
230 |
filepickerOptions = cm.editorScope.get('host').get('filepickeroptions').link,
|
|
|
231 |
repositoryKeys = window.Object.keys(filepickerOptions.repositories);
|
|
|
232 |
|
|
|
233 |
formData.append('repo_upload_file', blob, fileName);
|
|
|
234 |
formData.append('itemid', filepickerOptions.itemid);
|
|
|
235 |
|
|
|
236 |
for (var i = 0; i < repositoryKeys.length; i++) {
|
|
|
237 |
if (filepickerOptions.repositories[repositoryKeys[i]].type === 'upload') {
|
|
|
238 |
formData.append('repo_id', filepickerOptions.repositories[repositoryKeys[i]].id);
|
|
|
239 |
break;
|
|
|
240 |
}
|
|
|
241 |
}
|
|
|
242 |
|
|
|
243 |
formData.append('env', filepickerOptions.env);
|
|
|
244 |
formData.append('sesskey', M.cfg.sesskey);
|
|
|
245 |
formData.append('client_id', filepickerOptions.client_id);
|
|
|
246 |
formData.append('savepath', '/');
|
|
|
247 |
formData.append('ctx_id', filepickerOptions.context.id);
|
|
|
248 |
|
|
|
249 |
// Pass FormData to PHP script using XHR.
|
|
|
250 |
var uploadEndpoint = M.cfg.wwwroot + '/repository/repository_ajax.php?action=upload';
|
|
|
251 |
cm.make_xmlhttprequest(uploadEndpoint, formData,
|
|
|
252 |
function(progress, responseText) {
|
|
|
253 |
if (progress === 'upload-ended') {
|
|
|
254 |
callback('ended', window.JSON.parse(responseText).url);
|
|
|
255 |
} else {
|
|
|
256 |
callback(progress);
|
|
|
257 |
}
|
|
|
258 |
}
|
|
|
259 |
);
|
|
|
260 |
}
|
|
|
261 |
};
|
|
|
262 |
|
|
|
263 |
xhr.send();
|
|
|
264 |
},
|
|
|
265 |
|
|
|
266 |
// Handle XHR sending/receiving/status.
|
|
|
267 |
make_xmlhttprequest: function(url, data, callback) {
|
|
|
268 |
var xhr = new window.XMLHttpRequest();
|
|
|
269 |
|
|
|
270 |
xhr.onreadystatechange = function() {
|
|
|
271 |
if ((xhr.readyState === 4) && (xhr.status === 200)) { // When request is finished and successful.
|
|
|
272 |
callback('upload-ended', xhr.responseText);
|
|
|
273 |
} else if (xhr.status === 404) { // When request returns 404 Not Found.
|
|
|
274 |
callback('upload-failed-404');
|
|
|
275 |
}
|
|
|
276 |
};
|
|
|
277 |
|
|
|
278 |
xhr.upload.onprogress = function(event) {
|
|
|
279 |
callback(Math.round(event.loaded / event.total * 100) + "% " + M.util.get_string('uploadprogress', 'atto_recordrtc'));
|
|
|
280 |
};
|
|
|
281 |
|
|
|
282 |
xhr.upload.onerror = function(error) {
|
|
|
283 |
callback('upload-failed', error);
|
|
|
284 |
};
|
|
|
285 |
|
|
|
286 |
xhr.upload.onabort = function(error) {
|
|
|
287 |
callback('upload-aborted', error);
|
|
|
288 |
};
|
|
|
289 |
|
|
|
290 |
// POST FormData to PHP script that handles uploading/saving.
|
|
|
291 |
xhr.open('POST', url);
|
|
|
292 |
xhr.send(data);
|
|
|
293 |
},
|
|
|
294 |
|
|
|
295 |
// Makes 1min and 2s display as 1:02 on timer instead of 1:2, for example.
|
|
|
296 |
pad: function(val) {
|
|
|
297 |
var valString = val + "";
|
|
|
298 |
|
|
|
299 |
if (valString.length < 2) {
|
|
|
300 |
return "0" + valString;
|
|
|
301 |
} else {
|
|
|
302 |
return valString;
|
|
|
303 |
}
|
|
|
304 |
},
|
|
|
305 |
|
|
|
306 |
// Functionality to make recording timer count down.
|
|
|
307 |
// Also makes recording stop when time limit is hit.
|
|
|
308 |
set_time: function() {
|
|
|
309 |
cm.countdownSeconds--;
|
|
|
310 |
|
|
|
311 |
cm.startStopBtn.one('span#seconds').set('textContent', cm.pad(cm.countdownSeconds % 60));
|
|
|
312 |
cm.startStopBtn.one('span#minutes').set('textContent', cm.pad(window.parseInt(cm.countdownSeconds / 60, 10)));
|
|
|
313 |
|
|
|
314 |
if (cm.countdownSeconds === 0) {
|
|
|
315 |
cm.startStopBtn.simulate('click');
|
|
|
316 |
}
|
|
|
317 |
},
|
|
|
318 |
|
|
|
319 |
// Generates link to recorded annotation to be inserted.
|
|
|
320 |
create_annotation: function(type, recording_url) {
|
|
|
321 |
var html = '';
|
|
|
322 |
if (type == 'audio') {
|
|
|
323 |
html = "<audio controls='true'>";
|
|
|
324 |
} else { // Must be video.
|
|
|
325 |
html = "<video controls='true'>";
|
|
|
326 |
}
|
|
|
327 |
|
|
|
328 |
html += "<source src='" + recording_url + "'>" + recording_url;
|
|
|
329 |
|
|
|
330 |
if (type == 'audio') {
|
|
|
331 |
html += "</audio>";
|
|
|
332 |
} else { // Must be video.
|
|
|
333 |
html += "</video>";
|
|
|
334 |
}
|
|
|
335 |
|
|
|
336 |
return html;
|
|
|
337 |
},
|
|
|
338 |
|
|
|
339 |
// Inserts link to annotation in editor text area.
|
|
|
340 |
insert_annotation: function(type, recording_url) {
|
|
|
341 |
var annotation = cm.create_annotation(type, recording_url);
|
|
|
342 |
|
|
|
343 |
// Insert annotation link.
|
|
|
344 |
// If user pressed "Cancel", just go back to main recording screen.
|
|
|
345 |
if (!annotation) {
|
|
|
346 |
cm.uploadBtn.set('textContent', M.util.get_string('attachrecording', 'atto_recordrtc'));
|
|
|
347 |
} else {
|
|
|
348 |
cm.editorScope.setLink(cm.editorScope, annotation);
|
|
|
349 |
}
|
|
|
350 |
}
|
|
|
351 |
};
|