videomail-client 8.3.1 → 8.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/prototype/js/videomail-client.js +12 -14
- package/prototype/js/videomail-client.min.js +1 -1
- package/prototype/js/videomail-client.min.js.map +1 -1
- package/src/js/client.js +0 -210
- package/src/js/constants.js +0 -11
- package/src/js/events.js +0 -46
- package/src/js/index.js +0 -15
- package/src/js/options.js +0 -180
- package/src/js/resource.js +0 -206
- package/src/js/util/audioRecorder.js +0 -152
- package/src/js/util/browser.js +0 -319
- package/src/js/util/collectLogger.js +0 -72
- package/src/js/util/eventEmitter.js +0 -72
- package/src/js/util/humanize.js +0 -16
- package/src/js/util/mediaEvents.js +0 -148
- package/src/js/util/pretty.js +0 -70
- package/src/js/util/standardize.js +0 -71
- package/src/js/util/videomailError.js +0 -431
- package/src/js/wrappers/buttons.js +0 -670
- package/src/js/wrappers/container.js +0 -797
- package/src/js/wrappers/dimension.js +0 -149
- package/src/js/wrappers/form.js +0 -319
- package/src/js/wrappers/optionsWrapper.js +0 -81
- package/src/js/wrappers/visuals/inside/recorder/countdown.js +0 -83
- package/src/js/wrappers/visuals/inside/recorder/facingMode.js +0 -53
- package/src/js/wrappers/visuals/inside/recorder/pausedNote.js +0 -59
- package/src/js/wrappers/visuals/inside/recorder/recordNote.js +0 -42
- package/src/js/wrappers/visuals/inside/recorder/recordTimer.js +0 -149
- package/src/js/wrappers/visuals/inside/recorderInsides.js +0 -144
- package/src/js/wrappers/visuals/notifier.js +0 -341
- package/src/js/wrappers/visuals/recorder.js +0 -1492
- package/src/js/wrappers/visuals/replay.js +0 -355
- package/src/js/wrappers/visuals/userMedia.js +0 -541
- package/src/js/wrappers/visuals.js +0 -410
- package/src/styles/css/main.min.css.js +0 -1
- package/src/styles/styl/keyframes/blink.styl +0 -16
- package/src/styles/styl/main.styl +0 -126
|
@@ -1,1492 +0,0 @@
|
|
|
1
|
-
import animitter from "animitter";
|
|
2
|
-
import Frame from "canvas-to-buffer";
|
|
3
|
-
import deepmerge from "deepmerge";
|
|
4
|
-
import hidden from "hidden";
|
|
5
|
-
import h from "hyperscript";
|
|
6
|
-
import stringify from "safe-json-stringify";
|
|
7
|
-
import inherits from "inherits";
|
|
8
|
-
|
|
9
|
-
import websocket from "websocket-stream";
|
|
10
|
-
|
|
11
|
-
import Constants from "../../constants";
|
|
12
|
-
import Events from "../../events";
|
|
13
|
-
import Browser from "../../util/browser";
|
|
14
|
-
import EventEmitter from "../../util/eventEmitter";
|
|
15
|
-
import Humanize from "../../util/humanize";
|
|
16
|
-
import pretty from "../../util/pretty";
|
|
17
|
-
import VideomailError from "../../util/videomailError";
|
|
18
|
-
import UserMedia from "./userMedia";
|
|
19
|
-
|
|
20
|
-
// credits http://1lineart.kulaone.com/#/
|
|
21
|
-
const PIPE_SYMBOL = "°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸ ";
|
|
22
|
-
|
|
23
|
-
const Recorder = function (visuals, replay, defaultOptions = {}) {
|
|
24
|
-
EventEmitter.call(this, defaultOptions, "Recorder");
|
|
25
|
-
|
|
26
|
-
const browser = new Browser(defaultOptions);
|
|
27
|
-
|
|
28
|
-
const options = deepmerge(defaultOptions, {
|
|
29
|
-
image: {
|
|
30
|
-
// automatically lower quality when on mobile
|
|
31
|
-
quality: browser.isMobile()
|
|
32
|
-
? defaultOptions.image.quality - 0.05
|
|
33
|
-
: defaultOptions.image.quality,
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// validate some options this class needs
|
|
38
|
-
if (!options.video || !options.video.fps) {
|
|
39
|
-
throw VideomailError.create("FPS must be defined", options);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const self = this;
|
|
43
|
-
const { debug } = options;
|
|
44
|
-
|
|
45
|
-
let loop = null;
|
|
46
|
-
|
|
47
|
-
let originalAnimationFrameObject;
|
|
48
|
-
|
|
49
|
-
let samplesCount = 0;
|
|
50
|
-
let framesCount = 0;
|
|
51
|
-
let { facingMode } = options.video; // default is 'user'
|
|
52
|
-
|
|
53
|
-
let recordingStats = {};
|
|
54
|
-
|
|
55
|
-
let confirmedFrameNumber = 0;
|
|
56
|
-
let confirmedSampleNumber = 0;
|
|
57
|
-
|
|
58
|
-
let recorderElement;
|
|
59
|
-
let userMedia;
|
|
60
|
-
|
|
61
|
-
let userMediaTimeout;
|
|
62
|
-
let retryTimeout;
|
|
63
|
-
|
|
64
|
-
let bytesSum;
|
|
65
|
-
|
|
66
|
-
let frameProgress;
|
|
67
|
-
let sampleProgress;
|
|
68
|
-
|
|
69
|
-
let canvas;
|
|
70
|
-
let ctx;
|
|
71
|
-
|
|
72
|
-
let userMediaLoaded;
|
|
73
|
-
let userMediaLoading;
|
|
74
|
-
let submitting;
|
|
75
|
-
let unloaded;
|
|
76
|
-
let stopTime;
|
|
77
|
-
let stream;
|
|
78
|
-
let connecting;
|
|
79
|
-
let connected;
|
|
80
|
-
let blocking;
|
|
81
|
-
let built;
|
|
82
|
-
let key;
|
|
83
|
-
let waitingTime;
|
|
84
|
-
|
|
85
|
-
let pingInterval;
|
|
86
|
-
|
|
87
|
-
let frame;
|
|
88
|
-
|
|
89
|
-
let recordingBufferLength;
|
|
90
|
-
let recordingBuffer;
|
|
91
|
-
|
|
92
|
-
function writeStream(buffer, opts) {
|
|
93
|
-
if (stream) {
|
|
94
|
-
if (stream.destroyed) {
|
|
95
|
-
// prevents https://github.com/binarykitchen/videomail.io/issues/393
|
|
96
|
-
stopPings();
|
|
97
|
-
|
|
98
|
-
self.emit(
|
|
99
|
-
Events.ERROR,
|
|
100
|
-
VideomailError.create(
|
|
101
|
-
"Already disconnected",
|
|
102
|
-
"Sorry, connection to the server has been destroyed. Please reload.",
|
|
103
|
-
options,
|
|
104
|
-
),
|
|
105
|
-
);
|
|
106
|
-
} else {
|
|
107
|
-
const onFlushedCallback = opts && opts.onFlushedCallback;
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
stream.write(buffer, function () {
|
|
111
|
-
onFlushedCallback && onFlushedCallback(opts);
|
|
112
|
-
});
|
|
113
|
-
} catch (exc) {
|
|
114
|
-
self.emit(
|
|
115
|
-
Events.ERROR,
|
|
116
|
-
VideomailError.create(
|
|
117
|
-
"Failed writing to server",
|
|
118
|
-
`stream.write() failed because of ${pretty(exc)}`,
|
|
119
|
-
options,
|
|
120
|
-
),
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function sendPings() {
|
|
128
|
-
pingInterval = window.setInterval(function () {
|
|
129
|
-
debug("Recorder: pinging...");
|
|
130
|
-
writeStream(Buffer.from(""));
|
|
131
|
-
}, options.timeouts.pingInterval);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function stopPings() {
|
|
135
|
-
clearInterval(pingInterval);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function onAudioSample(audioSample) {
|
|
139
|
-
samplesCount++;
|
|
140
|
-
|
|
141
|
-
const audioBuffer = audioSample.toBuffer();
|
|
142
|
-
|
|
143
|
-
/*
|
|
144
|
-
* if (options.verbose) {
|
|
145
|
-
* debug(
|
|
146
|
-
* 'Sample #' + samplesCount + ' (' + audioBuffer.length + ' bytes):'
|
|
147
|
-
* )
|
|
148
|
-
* }
|
|
149
|
-
*/
|
|
150
|
-
|
|
151
|
-
writeStream(audioBuffer);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function show() {
|
|
155
|
-
recorderElement && hidden(recorderElement, false);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function onUserMediaReady(params = {}) {
|
|
159
|
-
try {
|
|
160
|
-
debug("Recorder: onUserMediaReady()", stringify(params));
|
|
161
|
-
|
|
162
|
-
const { switchingFacingMode } = params;
|
|
163
|
-
|
|
164
|
-
userMediaLoading = blocking = unloaded = submitting = false;
|
|
165
|
-
userMediaLoaded = true;
|
|
166
|
-
|
|
167
|
-
if (!switchingFacingMode) {
|
|
168
|
-
loop = createLoop();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
show();
|
|
172
|
-
|
|
173
|
-
if (params.recordWhenReady) {
|
|
174
|
-
self.record();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
self.emit(Events.USER_MEDIA_READY, {
|
|
178
|
-
switchingFacingMode: params.switchingFacingMode,
|
|
179
|
-
paused: self.isPaused(),
|
|
180
|
-
recordWhenReady: params.recordWhenReady,
|
|
181
|
-
});
|
|
182
|
-
} catch (exc) {
|
|
183
|
-
self.emit(Events.ERROR, exc);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function clearRetryTimeout() {
|
|
188
|
-
debug("Recorder: clearRetryTimeout()");
|
|
189
|
-
|
|
190
|
-
retryTimeout && clearTimeout(retryTimeout);
|
|
191
|
-
retryTimeout = null;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function clearUserMediaTimeout() {
|
|
195
|
-
if (userMediaTimeout) {
|
|
196
|
-
debug("Recorder: clearUserMediaTimeout()");
|
|
197
|
-
|
|
198
|
-
userMediaTimeout && clearTimeout(userMediaTimeout);
|
|
199
|
-
userMediaTimeout = null;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function calculateFrameProgress() {
|
|
204
|
-
return `${((confirmedFrameNumber / (framesCount || 1)) * 100).toFixed(2)}%`;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function calculateSampleProgress() {
|
|
208
|
-
return `${((confirmedSampleNumber / (samplesCount || 1)) * 100).toFixed(2)}%`;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function updateOverallProgress() {
|
|
212
|
-
/*
|
|
213
|
-
* when progresses aren't initialized,
|
|
214
|
-
* then do a first calculation to avoid `infinite` or `null` displays
|
|
215
|
-
*/
|
|
216
|
-
|
|
217
|
-
if (!frameProgress) {
|
|
218
|
-
frameProgress = calculateFrameProgress();
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (!sampleProgress) {
|
|
222
|
-
sampleProgress = calculateSampleProgress();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
self.emit(Events.PROGRESS, frameProgress, sampleProgress);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function updateFrameProgress(args) {
|
|
229
|
-
confirmedFrameNumber = args.frame ? args.frame : confirmedFrameNumber;
|
|
230
|
-
|
|
231
|
-
frameProgress = calculateFrameProgress();
|
|
232
|
-
|
|
233
|
-
updateOverallProgress();
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function updateSampleProgress(args) {
|
|
237
|
-
confirmedSampleNumber = args.sample ? args.sample : confirmedSampleNumber;
|
|
238
|
-
|
|
239
|
-
sampleProgress = calculateSampleProgress();
|
|
240
|
-
|
|
241
|
-
updateOverallProgress();
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function preview(args) {
|
|
245
|
-
confirmedFrameNumber = confirmedSampleNumber = samplesCount = framesCount = 0;
|
|
246
|
-
|
|
247
|
-
sampleProgress = frameProgress = null;
|
|
248
|
-
|
|
249
|
-
key = args.key;
|
|
250
|
-
|
|
251
|
-
/*
|
|
252
|
-
* We are not serving MP4 videos anymore due to licensing but are keeping code
|
|
253
|
-
* for compatibility and documentation
|
|
254
|
-
*/
|
|
255
|
-
if (args.mp4) {
|
|
256
|
-
replay.setMp4Source(
|
|
257
|
-
`${args.mp4 + Constants.SITE_NAME_LABEL}/${options.siteName}/videomail.mp4`,
|
|
258
|
-
true,
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (args.webm) {
|
|
263
|
-
replay.setWebMSource(
|
|
264
|
-
`${args.webm + Constants.SITE_NAME_LABEL}/${options.siteName}/videomail.webm`,
|
|
265
|
-
true,
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
self.hide();
|
|
270
|
-
|
|
271
|
-
const width = self.getRecorderWidth(true);
|
|
272
|
-
const height = self.getRecorderHeight(true);
|
|
273
|
-
|
|
274
|
-
self.emit(Events.PREVIEW, key, width, height);
|
|
275
|
-
|
|
276
|
-
// keep it for recording stats
|
|
277
|
-
waitingTime = Date.now() - stopTime;
|
|
278
|
-
|
|
279
|
-
recordingStats.waitingTime = waitingTime;
|
|
280
|
-
|
|
281
|
-
if (options.debug) {
|
|
282
|
-
debug(
|
|
283
|
-
"While recording, %s have been transferred and waiting time was %s",
|
|
284
|
-
Humanize.filesize(bytesSum, 2),
|
|
285
|
-
Humanize.toTime(waitingTime),
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function initSocket(cb) {
|
|
291
|
-
if (!connected) {
|
|
292
|
-
connecting = true;
|
|
293
|
-
|
|
294
|
-
debug("Recorder: initialising web socket to %s", options.socketUrl);
|
|
295
|
-
|
|
296
|
-
self.emit(Events.CONNECTING);
|
|
297
|
-
|
|
298
|
-
// https://github.com/maxogden/websocket-stream#binary-sockets
|
|
299
|
-
|
|
300
|
-
/*
|
|
301
|
-
* we use query parameters here because we cannot set custom headers in web sockets,
|
|
302
|
-
* see https://github.com/websockets/ws/issues/467
|
|
303
|
-
*/
|
|
304
|
-
|
|
305
|
-
const url2Connect = `${options.socketUrl}?${encodeURIComponent(
|
|
306
|
-
Constants.SITE_NAME_LABEL,
|
|
307
|
-
)}=${encodeURIComponent(options.siteName)}`;
|
|
308
|
-
|
|
309
|
-
try {
|
|
310
|
-
/*
|
|
311
|
-
* websocket options cannot be set on client side, only on server, see
|
|
312
|
-
* https://github.com/maxogden/websocket-stream/issues/116#issuecomment-296421077
|
|
313
|
-
*/
|
|
314
|
-
stream = websocket(url2Connect, {
|
|
315
|
-
perMessageDeflate: false,
|
|
316
|
-
// see https://github.com/maxogden/websocket-stream/issues/117#issuecomment-298826011
|
|
317
|
-
objectMode: true,
|
|
318
|
-
});
|
|
319
|
-
} catch (exc) {
|
|
320
|
-
connecting = connected = false;
|
|
321
|
-
|
|
322
|
-
let err;
|
|
323
|
-
|
|
324
|
-
if (typeof websocket === "undefined") {
|
|
325
|
-
err = VideomailError.create(
|
|
326
|
-
"There is no websocket",
|
|
327
|
-
`Cause: ${pretty(exc)}`,
|
|
328
|
-
options,
|
|
329
|
-
);
|
|
330
|
-
} else {
|
|
331
|
-
err = VideomailError.create(
|
|
332
|
-
"Failed to connect to server",
|
|
333
|
-
"Please upgrade your browser. Your current version does not seem to support websockets.",
|
|
334
|
-
options,
|
|
335
|
-
{
|
|
336
|
-
browserProblem: true,
|
|
337
|
-
},
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
self.emit(Events.ERROR, err);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (stream) {
|
|
345
|
-
// // useful for debugging streams
|
|
346
|
-
|
|
347
|
-
/*
|
|
348
|
-
* if (!stream.originalEmit) {
|
|
349
|
-
* stream.originalEmit = stream.emit
|
|
350
|
-
* }
|
|
351
|
-
*/
|
|
352
|
-
|
|
353
|
-
/*
|
|
354
|
-
* stream.emit = function (type) {
|
|
355
|
-
* if (stream) {
|
|
356
|
-
* debug(PIPE_SYMBOL + 'Debugging stream event:', type)
|
|
357
|
-
* var args = Array.prototype.slice.call(arguments, 0)
|
|
358
|
-
* return stream.originalEmit.apply(stream, args)
|
|
359
|
-
* }
|
|
360
|
-
* }
|
|
361
|
-
*/
|
|
362
|
-
|
|
363
|
-
stream.on("close", function (err) {
|
|
364
|
-
debug(`${PIPE_SYMBOL}Stream has closed`);
|
|
365
|
-
|
|
366
|
-
connecting = connected = false;
|
|
367
|
-
|
|
368
|
-
if (err) {
|
|
369
|
-
self.emit(Events.ERROR, err || "Unhandled websocket error");
|
|
370
|
-
} else {
|
|
371
|
-
self.emit(Events.DISCONNECTED);
|
|
372
|
-
|
|
373
|
-
// prevents from https://github.com/binarykitchen/videomail.io/issues/297 happening
|
|
374
|
-
cancelAnimationFrame();
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
stream.on("connect", function () {
|
|
379
|
-
debug(`${PIPE_SYMBOL}Stream *connect* event emitted`);
|
|
380
|
-
|
|
381
|
-
if (!connected) {
|
|
382
|
-
connected = true;
|
|
383
|
-
connecting = unloaded = false;
|
|
384
|
-
|
|
385
|
-
self.emit(Events.CONNECTED);
|
|
386
|
-
|
|
387
|
-
cb && cb();
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
stream.on("data", function (data) {
|
|
392
|
-
debug(`${PIPE_SYMBOL}Stream *data* event emitted`);
|
|
393
|
-
|
|
394
|
-
let command;
|
|
395
|
-
|
|
396
|
-
try {
|
|
397
|
-
command = JSON.parse(data.toString());
|
|
398
|
-
} catch (exc) {
|
|
399
|
-
debug("Failed to parse command:", exc);
|
|
400
|
-
|
|
401
|
-
self.emit(
|
|
402
|
-
Events.ERROR,
|
|
403
|
-
VideomailError.create(
|
|
404
|
-
"Invalid server command",
|
|
405
|
-
// toString() since https://github.com/binarykitchen/videomail.io/issues/288
|
|
406
|
-
`Contact us asap. Bad command was ${data.toString()}. `,
|
|
407
|
-
options,
|
|
408
|
-
),
|
|
409
|
-
);
|
|
410
|
-
} finally {
|
|
411
|
-
executeCommand.call(self, command);
|
|
412
|
-
}
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
stream.on("error", function (err) {
|
|
416
|
-
debug(`${PIPE_SYMBOL}Stream *error* event emitted`, err);
|
|
417
|
-
|
|
418
|
-
connecting = connected = false;
|
|
419
|
-
|
|
420
|
-
let videomailError;
|
|
421
|
-
|
|
422
|
-
if (browser.isIOS()) {
|
|
423
|
-
/*
|
|
424
|
-
* setting custom text since that err object isn't really an error
|
|
425
|
-
* on iphones when locked, and unlocked, this err is actually
|
|
426
|
-
* an event object with stuff we can't use at all (an external bug)
|
|
427
|
-
*/
|
|
428
|
-
videomailError = VideomailError.create(
|
|
429
|
-
err,
|
|
430
|
-
`iPhones cannot maintain a live connection for too long. Original error message is: ${err.toString()}`,
|
|
431
|
-
options,
|
|
432
|
-
);
|
|
433
|
-
|
|
434
|
-
/*
|
|
435
|
-
* Changed to the above temporarily for better investigations
|
|
436
|
-
* videomailError = VideomailError.create(
|
|
437
|
-
* 'Sorry, connection has timed out',
|
|
438
|
-
* 'iPhones cannot maintain a live connection for too long,
|
|
439
|
-
* options
|
|
440
|
-
* )
|
|
441
|
-
*/
|
|
442
|
-
} else {
|
|
443
|
-
// or else it could be a poor wifi connection...
|
|
444
|
-
videomailError = VideomailError.create(
|
|
445
|
-
"Data exchange interrupted",
|
|
446
|
-
"Please check your network connection and reload.",
|
|
447
|
-
options,
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
self.emit(Events.ERROR, videomailError);
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
// just experimental
|
|
455
|
-
|
|
456
|
-
stream.on("drain", function () {
|
|
457
|
-
debug(`${PIPE_SYMBOL}Stream *drain* event emitted (should not happen!)`);
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
stream.on("preend", function () {
|
|
461
|
-
debug(`${PIPE_SYMBOL}Stream *preend* event emitted`);
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
stream.on("end", function () {
|
|
465
|
-
debug(`${PIPE_SYMBOL}Stream *end* event emitted`);
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
stream.on("drain", function () {
|
|
469
|
-
debug(`${PIPE_SYMBOL}Stream *drain* event emitted`);
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
stream.on("pipe", function () {
|
|
473
|
-
debug(`${PIPE_SYMBOL}Stream *pipe* event emitted`);
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
stream.on("unpipe", function () {
|
|
477
|
-
debug(`${PIPE_SYMBOL}Stream *unpipe* event emitted`);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
stream.on("resume", function () {
|
|
481
|
-
debug(`${PIPE_SYMBOL}Stream *resume* event emitted`);
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
stream.on("uncork", function () {
|
|
485
|
-
debug(`${PIPE_SYMBOL}Stream *uncork* event emitted`);
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
stream.on("readable", function () {
|
|
489
|
-
debug(`${PIPE_SYMBOL}Stream *preend* event emitted`);
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
stream.on("prefinish", function () {
|
|
493
|
-
debug(`${PIPE_SYMBOL}Stream *preend* event emitted`);
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
stream.on("finish", function () {
|
|
497
|
-
debug(`${PIPE_SYMBOL}Stream *preend* event emitted`);
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function showUserMedia() {
|
|
504
|
-
/*
|
|
505
|
-
* use connected flag to prevent this from happening
|
|
506
|
-
* https://github.com/binarykitchen/videomail.io/issues/323
|
|
507
|
-
*/
|
|
508
|
-
return connected && (isNotifying() || !isHidden() || blocking);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
function userMediaErrorCallback(err, extraA, extraB) {
|
|
512
|
-
userMediaLoading = false;
|
|
513
|
-
clearUserMediaTimeout();
|
|
514
|
-
|
|
515
|
-
debug(
|
|
516
|
-
"Recorder: userMediaErrorCallback()",
|
|
517
|
-
", name:",
|
|
518
|
-
err.name,
|
|
519
|
-
", message:",
|
|
520
|
-
err.message,
|
|
521
|
-
", Webcam characteristics:",
|
|
522
|
-
userMedia.getCharacteristics(),
|
|
523
|
-
// added recently in the hope to investigate weird webcam issues
|
|
524
|
-
", extraA arguments:",
|
|
525
|
-
extraA ? extraA.toString() : undefined,
|
|
526
|
-
", extraB arguments:",
|
|
527
|
-
extraB ? extraB.toString() : undefined,
|
|
528
|
-
);
|
|
529
|
-
|
|
530
|
-
const errorListeners = self.listeners(Events.ERROR);
|
|
531
|
-
|
|
532
|
-
if (errorListeners && errorListeners.length) {
|
|
533
|
-
if (err.name !== VideomailError.MEDIA_DEVICE_NOT_SUPPORTED) {
|
|
534
|
-
self.emit(Events.ERROR, VideomailError.create(err, options));
|
|
535
|
-
} else {
|
|
536
|
-
// do not emit but retry since MEDIA_DEVICE_NOT_SUPPORTED can be a race condition
|
|
537
|
-
debug("Recorder: ignore user media error", err);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// retry after a while
|
|
541
|
-
retryTimeout = setTimeout(initSocket, options.timeouts.userMedia);
|
|
542
|
-
} else if (unloaded) {
|
|
543
|
-
/*
|
|
544
|
-
* This can happen when a container is unloaded but some user media related callbacks
|
|
545
|
-
* are still in process. In that case ignore error.
|
|
546
|
-
*/
|
|
547
|
-
debug("Recorder: already unloaded. Not going to throw error", err);
|
|
548
|
-
} else {
|
|
549
|
-
debug("Recorder: no error listeners attached but throwing error", err);
|
|
550
|
-
|
|
551
|
-
// weird situation, throw it instead of emitting since there are no error listeners
|
|
552
|
-
throw VideomailError.create(
|
|
553
|
-
err,
|
|
554
|
-
"Unable to process this error since there are no error listeners anymore.",
|
|
555
|
-
options,
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function getUserMediaCallback(localStream, params) {
|
|
561
|
-
debug("Recorder: getUserMediaCallback()", stringify(params));
|
|
562
|
-
|
|
563
|
-
if (showUserMedia()) {
|
|
564
|
-
try {
|
|
565
|
-
clearUserMediaTimeout();
|
|
566
|
-
|
|
567
|
-
userMedia.init(
|
|
568
|
-
localStream,
|
|
569
|
-
function () {
|
|
570
|
-
onUserMediaReady(params);
|
|
571
|
-
},
|
|
572
|
-
onAudioSample.bind(self),
|
|
573
|
-
function (err) {
|
|
574
|
-
self.emit(Events.ERROR, err);
|
|
575
|
-
},
|
|
576
|
-
params,
|
|
577
|
-
);
|
|
578
|
-
} catch (exc) {
|
|
579
|
-
self.emit(Events.ERROR, exc);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
function loadGenuineUserMedia(params) {
|
|
585
|
-
if (!navigator) {
|
|
586
|
-
throw new Error("Navigator is missing!");
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
debug("Recorder: loadGenuineUserMedia()");
|
|
590
|
-
|
|
591
|
-
self.emit(Events.ASKING_WEBCAM_PERMISSION);
|
|
592
|
-
|
|
593
|
-
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
|
594
|
-
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
595
|
-
// prefer the front camera (if one is available) over the rear one
|
|
596
|
-
const constraints = {
|
|
597
|
-
video: {
|
|
598
|
-
facingMode,
|
|
599
|
-
frameRate: { ideal: options.video.fps },
|
|
600
|
-
},
|
|
601
|
-
audio: options.isAudioEnabled(),
|
|
602
|
-
};
|
|
603
|
-
|
|
604
|
-
if (browser.isOkSafari()) {
|
|
605
|
-
/*
|
|
606
|
-
* do not use those width/height constraints yet,
|
|
607
|
-
* current safari would throw an error
|
|
608
|
-
* todo in https://github.com/binarykitchen/videomail-client/issues/142
|
|
609
|
-
*/
|
|
610
|
-
} else {
|
|
611
|
-
if (options.hasDefinedWidth()) {
|
|
612
|
-
constraints.video.width = { ideal: options.video.width };
|
|
613
|
-
} else {
|
|
614
|
-
/*
|
|
615
|
-
* otherwise try to apply the same width as the element is having
|
|
616
|
-
* but there is no 100% guarantee that this will happen. not
|
|
617
|
-
* all webcam drivers behave the same way
|
|
618
|
-
*/
|
|
619
|
-
constraints.video.width = { ideal: self.limitWidth() };
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
if (options.hasDefinedHeight()) {
|
|
623
|
-
constraints.video.height = { ideal: options.video.height };
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
debug(
|
|
628
|
-
"Recorder: navigator.mediaDevices.getUserMedia()",
|
|
629
|
-
"constraints",
|
|
630
|
-
stringify(constraints),
|
|
631
|
-
);
|
|
632
|
-
|
|
633
|
-
if (navigator.mediaDevices.getSupportedConstraints) {
|
|
634
|
-
debug(
|
|
635
|
-
"Recorder: navigator.mediaDevices.getSupportedConstraints()",
|
|
636
|
-
stringify(navigator.mediaDevices.getSupportedConstraints()),
|
|
637
|
-
);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
const genuineUserMediaRequest = navigator.mediaDevices.getUserMedia(constraints);
|
|
641
|
-
|
|
642
|
-
if (genuineUserMediaRequest) {
|
|
643
|
-
genuineUserMediaRequest
|
|
644
|
-
.then(function (localStream) {
|
|
645
|
-
getUserMediaCallback(localStream, params);
|
|
646
|
-
})
|
|
647
|
-
.catch(userMediaErrorCallback);
|
|
648
|
-
} else {
|
|
649
|
-
/*
|
|
650
|
-
* this to trap errors like these
|
|
651
|
-
* Cannot read property 'then' of undefined
|
|
652
|
-
*/
|
|
653
|
-
|
|
654
|
-
// todo retry with navigator.getUserMedia_() maybe?
|
|
655
|
-
throw VideomailError.create(
|
|
656
|
-
"Sorry, your browser is unable to use cameras.",
|
|
657
|
-
"Try a different browser with better user media functionalities.",
|
|
658
|
-
options,
|
|
659
|
-
);
|
|
660
|
-
}
|
|
661
|
-
} else {
|
|
662
|
-
debug("Recorder: navigator.getUserMedia()");
|
|
663
|
-
|
|
664
|
-
navigator.getUserMedia_(
|
|
665
|
-
{
|
|
666
|
-
video: true,
|
|
667
|
-
audio: options.isAudioEnabled(),
|
|
668
|
-
},
|
|
669
|
-
getUserMediaCallback,
|
|
670
|
-
userMediaErrorCallback,
|
|
671
|
-
);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function loadUserMedia(params = {}) {
|
|
676
|
-
if (userMediaLoaded) {
|
|
677
|
-
debug("Recorder: skipping loadUserMedia() because it is already loaded");
|
|
678
|
-
onUserMediaReady(params);
|
|
679
|
-
return false;
|
|
680
|
-
} else if (userMediaLoading) {
|
|
681
|
-
debug(
|
|
682
|
-
"Recorder: skipping loadUserMedia() because it is already asking for permission",
|
|
683
|
-
);
|
|
684
|
-
return false;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
debug("Recorder: loadUserMedia()", stringify(params));
|
|
688
|
-
|
|
689
|
-
self.emit(Events.LOADING_USER_MEDIA);
|
|
690
|
-
|
|
691
|
-
try {
|
|
692
|
-
userMediaTimeout = setTimeout(function () {
|
|
693
|
-
if (!self.isReady()) {
|
|
694
|
-
self.emit(Events.ERROR, browser.getNoAccessIssue());
|
|
695
|
-
}
|
|
696
|
-
}, options.timeouts.userMedia);
|
|
697
|
-
|
|
698
|
-
userMediaLoading = true;
|
|
699
|
-
|
|
700
|
-
loadGenuineUserMedia(params);
|
|
701
|
-
} catch (exc) {
|
|
702
|
-
debug("Recorder: failed to load genuine user media");
|
|
703
|
-
|
|
704
|
-
userMediaLoading = false;
|
|
705
|
-
|
|
706
|
-
const errorListeners = self.listeners(Events.ERROR);
|
|
707
|
-
|
|
708
|
-
if (errorListeners.length) {
|
|
709
|
-
self.emit(Events.ERROR, exc);
|
|
710
|
-
} else {
|
|
711
|
-
debug("Recorder: no error listeners attached but throwing exception", exc);
|
|
712
|
-
throw exc; // throw it further
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
function executeCommand(command) {
|
|
718
|
-
try {
|
|
719
|
-
debug(
|
|
720
|
-
"Server commanded: %s",
|
|
721
|
-
command.command,
|
|
722
|
-
command.args ? `, ${stringify(command.args)}` : "",
|
|
723
|
-
);
|
|
724
|
-
|
|
725
|
-
switch (command.command) {
|
|
726
|
-
case "ready":
|
|
727
|
-
this.emit(Events.SERVER_READY);
|
|
728
|
-
|
|
729
|
-
if (!userMediaTimeout) {
|
|
730
|
-
if (options.loadUserMediaOnRecord) {
|
|
731
|
-
// Still show it but have it blank
|
|
732
|
-
show();
|
|
733
|
-
} else {
|
|
734
|
-
loadUserMedia();
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
break;
|
|
738
|
-
case "preview":
|
|
739
|
-
preview(command.args);
|
|
740
|
-
break;
|
|
741
|
-
case "error":
|
|
742
|
-
this.emit(
|
|
743
|
-
Events.ERROR,
|
|
744
|
-
VideomailError.create(
|
|
745
|
-
"Oh no, server error!",
|
|
746
|
-
command.args.err.toString() || "(No explanation given)",
|
|
747
|
-
options,
|
|
748
|
-
),
|
|
749
|
-
);
|
|
750
|
-
break;
|
|
751
|
-
case "confirmFrame":
|
|
752
|
-
updateFrameProgress(command.args);
|
|
753
|
-
break;
|
|
754
|
-
case "confirmSample":
|
|
755
|
-
updateSampleProgress(command.args);
|
|
756
|
-
break;
|
|
757
|
-
case "beginAudioEncoding":
|
|
758
|
-
this.emit(Events.BEGIN_AUDIO_ENCODING);
|
|
759
|
-
break;
|
|
760
|
-
case "beginVideoEncoding":
|
|
761
|
-
this.emit(Events.BEGIN_VIDEO_ENCODING);
|
|
762
|
-
break;
|
|
763
|
-
default:
|
|
764
|
-
this.emit(Events.ERROR, `Unknown server command: ${command.command}`);
|
|
765
|
-
break;
|
|
766
|
-
}
|
|
767
|
-
} catch (exc) {
|
|
768
|
-
self.emit(Events.ERROR, exc);
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
function isNotifying() {
|
|
773
|
-
return visuals.isNotifying();
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
function isHidden() {
|
|
777
|
-
return !recorderElement || hidden(recorderElement);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function writeCommand(command, args, cb) {
|
|
781
|
-
if (!cb && args && args.constructor === Function) {
|
|
782
|
-
cb = args;
|
|
783
|
-
args = null;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
if (!connected) {
|
|
787
|
-
debug("Reconnecting for the command", command, "…");
|
|
788
|
-
|
|
789
|
-
initSocket(function () {
|
|
790
|
-
writeCommand(command, args);
|
|
791
|
-
cb && cb();
|
|
792
|
-
});
|
|
793
|
-
} else if (stream) {
|
|
794
|
-
debug("$ %s", command, args ? stringify(args) : "");
|
|
795
|
-
|
|
796
|
-
const commandObj = {
|
|
797
|
-
command,
|
|
798
|
-
args,
|
|
799
|
-
};
|
|
800
|
-
|
|
801
|
-
/*
|
|
802
|
-
* todo commented out because for some reasons server does
|
|
803
|
-
* not accept such a long array of many log lines. to examine later.
|
|
804
|
-
*
|
|
805
|
-
* add some useful debug info to examine weird stuff like this one
|
|
806
|
-
* UnprocessableError: Unable to encode a video with FPS near zero.
|
|
807
|
-
* todo consider removing this later or have it for debug=1 only?
|
|
808
|
-
*
|
|
809
|
-
* if (options.logger && options.logger.getLines) {
|
|
810
|
-
* commandObj.logLines = options.logger.getLines()
|
|
811
|
-
* }
|
|
812
|
-
*/
|
|
813
|
-
|
|
814
|
-
writeStream(Buffer.from(stringify(commandObj)));
|
|
815
|
-
|
|
816
|
-
if (cb) {
|
|
817
|
-
// keep all callbacks async
|
|
818
|
-
setTimeout(function () {
|
|
819
|
-
cb();
|
|
820
|
-
}, 0);
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
function disconnect() {
|
|
826
|
-
if (connected) {
|
|
827
|
-
debug("Recorder: disconnect()");
|
|
828
|
-
|
|
829
|
-
if (userMedia) {
|
|
830
|
-
// prevents https://github.com/binarykitchen/videomail-client/issues/114
|
|
831
|
-
userMedia.unloadRemainingEventListeners();
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
if (submitting) {
|
|
835
|
-
// server will disconnect socket automatically after submitting
|
|
836
|
-
connecting = connected = false;
|
|
837
|
-
} else if (stream) {
|
|
838
|
-
/*
|
|
839
|
-
* force to disconnect socket right now to clean temp files on server
|
|
840
|
-
* event listeners will do the rest
|
|
841
|
-
*/
|
|
842
|
-
stream.end();
|
|
843
|
-
stream = undefined;
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
function cancelAnimationFrame() {
|
|
849
|
-
loop && loop.dispose();
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
function getIntervalSum() {
|
|
853
|
-
return loop.getElapsedTime();
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
function getAvgInterval() {
|
|
857
|
-
return getIntervalSum() / framesCount;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
function getAvgFps() {
|
|
861
|
-
return (framesCount / getIntervalSum()) * 1000;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
this.getRecordingStats = function () {
|
|
865
|
-
return recordingStats;
|
|
866
|
-
};
|
|
867
|
-
|
|
868
|
-
this.getAudioSampleRate = function () {
|
|
869
|
-
return userMedia.getAudioSampleRate();
|
|
870
|
-
};
|
|
871
|
-
|
|
872
|
-
this.stop = function (params) {
|
|
873
|
-
debug("stop()", params);
|
|
874
|
-
|
|
875
|
-
const { limitReached } = params;
|
|
876
|
-
|
|
877
|
-
this.emit(Events.STOPPING, limitReached);
|
|
878
|
-
|
|
879
|
-
loop.complete();
|
|
880
|
-
|
|
881
|
-
const self = this;
|
|
882
|
-
|
|
883
|
-
/*
|
|
884
|
-
* needed to give dom enough time to prepare the replay element
|
|
885
|
-
* to show up upon the STOPPING event so that we can evaluate
|
|
886
|
-
* the right video type
|
|
887
|
-
*/
|
|
888
|
-
setTimeout(function () {
|
|
889
|
-
stopTime = Date.now();
|
|
890
|
-
|
|
891
|
-
recordingStats = {
|
|
892
|
-
/*
|
|
893
|
-
* do not use loop.getFPS() as this will only return the fps from the last delta,
|
|
894
|
-
* not the average. see https://github.com/hapticdata/animitter/issues/3
|
|
895
|
-
*/
|
|
896
|
-
avgFps: getAvgFps(),
|
|
897
|
-
wantedFps: options.video.fps,
|
|
898
|
-
avgInterval: getAvgInterval(),
|
|
899
|
-
wantedInterval: 1e3 / options.video.fps,
|
|
900
|
-
|
|
901
|
-
intervalSum: getIntervalSum(),
|
|
902
|
-
framesCount,
|
|
903
|
-
videoType: replay.getVideoType(),
|
|
904
|
-
};
|
|
905
|
-
|
|
906
|
-
if (options.isAudioEnabled()) {
|
|
907
|
-
recordingStats.samplesCount = samplesCount;
|
|
908
|
-
recordingStats.sampleRate = userMedia.getAudioSampleRate();
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
writeCommand("stop", recordingStats, function () {
|
|
912
|
-
self.emit(Events.STOPPED, { recordingStats });
|
|
913
|
-
});
|
|
914
|
-
|
|
915
|
-
// beware, resetting will set framesCount to zero, so leave this here
|
|
916
|
-
self.reset();
|
|
917
|
-
}, 60);
|
|
918
|
-
};
|
|
919
|
-
|
|
920
|
-
this.back = function (cb) {
|
|
921
|
-
this.emit(Events.GOING_BACK);
|
|
922
|
-
|
|
923
|
-
show();
|
|
924
|
-
this.reset();
|
|
925
|
-
|
|
926
|
-
writeCommand("back", cb);
|
|
927
|
-
};
|
|
928
|
-
|
|
929
|
-
function reInitialiseAudio() {
|
|
930
|
-
debug("Recorder: reInitialiseAudio()");
|
|
931
|
-
|
|
932
|
-
clearUserMediaTimeout();
|
|
933
|
-
|
|
934
|
-
// important to free memory
|
|
935
|
-
userMedia && userMedia.stop();
|
|
936
|
-
|
|
937
|
-
userMediaLoaded = key = canvas = ctx = null;
|
|
938
|
-
|
|
939
|
-
loadUserMedia();
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
this.unload = function (e) {
|
|
943
|
-
if (!unloaded) {
|
|
944
|
-
let cause;
|
|
945
|
-
|
|
946
|
-
if (e) {
|
|
947
|
-
cause = e.name || e.statusText || e.toString();
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
debug(`Recorder: unload()${cause ? `, cause: ${cause}` : ""}`);
|
|
951
|
-
|
|
952
|
-
this.reset();
|
|
953
|
-
|
|
954
|
-
clearUserMediaTimeout();
|
|
955
|
-
|
|
956
|
-
disconnect();
|
|
957
|
-
|
|
958
|
-
unloaded = true;
|
|
959
|
-
|
|
960
|
-
built = false;
|
|
961
|
-
}
|
|
962
|
-
};
|
|
963
|
-
|
|
964
|
-
this.reset = function () {
|
|
965
|
-
// no need to reset when already unloaded
|
|
966
|
-
if (!unloaded) {
|
|
967
|
-
debug("Recorder: reset()");
|
|
968
|
-
|
|
969
|
-
this.emit(Events.RESETTING);
|
|
970
|
-
|
|
971
|
-
cancelAnimationFrame();
|
|
972
|
-
|
|
973
|
-
// important to free memory
|
|
974
|
-
userMedia && userMedia.stop();
|
|
975
|
-
|
|
976
|
-
replay.reset();
|
|
977
|
-
|
|
978
|
-
userMediaLoaded =
|
|
979
|
-
key =
|
|
980
|
-
canvas =
|
|
981
|
-
ctx =
|
|
982
|
-
recordingBuffer =
|
|
983
|
-
recordingBufferLength =
|
|
984
|
-
null;
|
|
985
|
-
}
|
|
986
|
-
};
|
|
987
|
-
|
|
988
|
-
this.validate = function () {
|
|
989
|
-
return connected && framesCount > 0 && canvas === null;
|
|
990
|
-
};
|
|
991
|
-
|
|
992
|
-
this.isReady = function () {
|
|
993
|
-
return userMedia.isReady();
|
|
994
|
-
};
|
|
995
|
-
|
|
996
|
-
this.pause = function (params) {
|
|
997
|
-
const e = params && params.event;
|
|
998
|
-
|
|
999
|
-
if (e instanceof window.Event) {
|
|
1000
|
-
params.eventType = e.type;
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
debug(`pause() at frame ${framesCount}`, params);
|
|
1004
|
-
|
|
1005
|
-
userMedia.pause();
|
|
1006
|
-
loop.stop();
|
|
1007
|
-
|
|
1008
|
-
this.emit(Events.PAUSED);
|
|
1009
|
-
|
|
1010
|
-
sendPings();
|
|
1011
|
-
};
|
|
1012
|
-
|
|
1013
|
-
this.isPaused = function () {
|
|
1014
|
-
return userMedia && userMedia.isPaused();
|
|
1015
|
-
};
|
|
1016
|
-
|
|
1017
|
-
this.resume = function () {
|
|
1018
|
-
debug(`Recorder: resume() with frame ${framesCount}`);
|
|
1019
|
-
|
|
1020
|
-
stopPings();
|
|
1021
|
-
|
|
1022
|
-
this.emit(Events.RESUMING);
|
|
1023
|
-
|
|
1024
|
-
userMedia.resume();
|
|
1025
|
-
loop.start();
|
|
1026
|
-
};
|
|
1027
|
-
|
|
1028
|
-
function onFlushed(opts) {
|
|
1029
|
-
const frameNumber = opts && opts.frameNumber;
|
|
1030
|
-
|
|
1031
|
-
if (frameNumber === 1) {
|
|
1032
|
-
self.emit(Events.FIRST_FRAME_SENT);
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
function draw(deltaTime, elapsedTime) {
|
|
1037
|
-
try {
|
|
1038
|
-
// ctx and stream might become null while unloading
|
|
1039
|
-
if (!self.isPaused() && stream && ctx) {
|
|
1040
|
-
if (framesCount === 0) {
|
|
1041
|
-
self.emit(Events.SENDING_FIRST_FRAME);
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
framesCount++;
|
|
1045
|
-
|
|
1046
|
-
ctx.drawImage(userMedia.getRawVisuals(), 0, 0, canvas.width, canvas.height);
|
|
1047
|
-
|
|
1048
|
-
recordingBuffer = frame.toBuffer();
|
|
1049
|
-
recordingBufferLength = recordingBuffer.length;
|
|
1050
|
-
|
|
1051
|
-
if (recordingBufferLength < 1) {
|
|
1052
|
-
throw VideomailError.create("Failed to extract webcam data.", options);
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
bytesSum += recordingBufferLength;
|
|
1056
|
-
|
|
1057
|
-
const frameControlBuffer = Buffer.from(stringify({ frameNumber: framesCount }));
|
|
1058
|
-
const frameBuffer = Buffer.concat([recordingBuffer, frameControlBuffer]);
|
|
1059
|
-
|
|
1060
|
-
writeStream(frameBuffer, {
|
|
1061
|
-
frameNumber: framesCount,
|
|
1062
|
-
onFlushedCallback: onFlushed,
|
|
1063
|
-
});
|
|
1064
|
-
|
|
1065
|
-
/*
|
|
1066
|
-
* if (options.verbose) {
|
|
1067
|
-
* debug(
|
|
1068
|
-
* 'Frame #' + framesCount + ' (' + recordingBufferLength + ' bytes):',
|
|
1069
|
-
* ' delta=' + deltaTime + 'ms, ' +
|
|
1070
|
-
* ' elapsed=' + elapsedTime + 'ms'
|
|
1071
|
-
* )
|
|
1072
|
-
* }
|
|
1073
|
-
*/
|
|
1074
|
-
|
|
1075
|
-
visuals.checkTimer({ intervalSum: elapsedTime });
|
|
1076
|
-
}
|
|
1077
|
-
} catch (exc) {
|
|
1078
|
-
self.emit(Events.ERROR, exc);
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
function createLoop() {
|
|
1083
|
-
const newLoop = animitter({ fps: options.video.fps }, draw);
|
|
1084
|
-
|
|
1085
|
-
// remember it first
|
|
1086
|
-
originalAnimationFrameObject = newLoop.getRequestAnimationFrameObject();
|
|
1087
|
-
|
|
1088
|
-
return newLoop;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
this.record = function () {
|
|
1092
|
-
if (unloaded) {
|
|
1093
|
-
return false;
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
// reconnect when needed
|
|
1097
|
-
if (!connected) {
|
|
1098
|
-
debug("Recorder: reconnecting before recording ...");
|
|
1099
|
-
|
|
1100
|
-
initSocket(function () {
|
|
1101
|
-
self.once(Events.USER_MEDIA_READY, self.record);
|
|
1102
|
-
});
|
|
1103
|
-
|
|
1104
|
-
return false;
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
if (!userMediaLoaded) {
|
|
1108
|
-
if (options.loadUserMediaOnRecord) {
|
|
1109
|
-
loadUserMedia({ recordWhenReady: true });
|
|
1110
|
-
} else {
|
|
1111
|
-
self.emit(
|
|
1112
|
-
Events.ERROR,
|
|
1113
|
-
VideomailError.create("Load and enable your camera first", options),
|
|
1114
|
-
);
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
return false; // do nothing further
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
try {
|
|
1121
|
-
canvas = userMedia.createCanvas();
|
|
1122
|
-
} catch (exc) {
|
|
1123
|
-
self.emit(Events.ERROR, VideomailError.create(exc, options));
|
|
1124
|
-
|
|
1125
|
-
return false;
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
ctx = canvas.getContext("2d");
|
|
1129
|
-
|
|
1130
|
-
if (!canvas.width) {
|
|
1131
|
-
self.emit(
|
|
1132
|
-
Events.ERROR,
|
|
1133
|
-
VideomailError.create("Canvas has an invalid width.", options),
|
|
1134
|
-
);
|
|
1135
|
-
|
|
1136
|
-
return false;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
if (!canvas.height) {
|
|
1140
|
-
self.emit(
|
|
1141
|
-
Events.ERROR,
|
|
1142
|
-
VideomailError.create("Canvas has an invalid height.", options),
|
|
1143
|
-
);
|
|
1144
|
-
|
|
1145
|
-
return false;
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
bytesSum = 0;
|
|
1149
|
-
|
|
1150
|
-
frame = new Frame(canvas, options.image.types, options.image.quality);
|
|
1151
|
-
|
|
1152
|
-
debug("Recorder: record()");
|
|
1153
|
-
userMedia.record();
|
|
1154
|
-
|
|
1155
|
-
self.emit(Events.RECORDING, framesCount);
|
|
1156
|
-
|
|
1157
|
-
// see https://github.com/hapticdata/animitter/issues/3
|
|
1158
|
-
loop.on("update", function (deltaTime, elapsedTime) {
|
|
1159
|
-
// x1000 because of milliseconds
|
|
1160
|
-
const avgFPS = (framesCount / elapsedTime) * 1000;
|
|
1161
|
-
debug("Recorder: avgFps =", Math.round(avgFPS));
|
|
1162
|
-
});
|
|
1163
|
-
|
|
1164
|
-
loop.start();
|
|
1165
|
-
};
|
|
1166
|
-
|
|
1167
|
-
function setAnimationFrameObject(newObj) {
|
|
1168
|
-
/*
|
|
1169
|
-
* must stop and then start to make it become effective, see
|
|
1170
|
-
* https://github.com/hapticdata/animitter/issues/5#issuecomment-292019168
|
|
1171
|
-
*/
|
|
1172
|
-
if (loop) {
|
|
1173
|
-
const isRecording = self.isRecording();
|
|
1174
|
-
|
|
1175
|
-
loop.stop();
|
|
1176
|
-
loop.setRequestAnimationFrameObject(newObj);
|
|
1177
|
-
|
|
1178
|
-
if (isRecording) {
|
|
1179
|
-
loop.start();
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
function restoreAnimationFrameObject() {
|
|
1185
|
-
debug("Recorder: restoreAnimationFrameObject()");
|
|
1186
|
-
|
|
1187
|
-
setAnimationFrameObject(originalAnimationFrameObject);
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
function loopWithTimeouts() {
|
|
1191
|
-
debug("Recorder: loopWithTimeouts()");
|
|
1192
|
-
|
|
1193
|
-
const wantedInterval = 1e3 / options.video.fps;
|
|
1194
|
-
|
|
1195
|
-
let processingTime = 0;
|
|
1196
|
-
let start;
|
|
1197
|
-
|
|
1198
|
-
function raf(fn) {
|
|
1199
|
-
return setTimeout(
|
|
1200
|
-
function () {
|
|
1201
|
-
start = Date.now();
|
|
1202
|
-
fn();
|
|
1203
|
-
processingTime = Date.now() - start;
|
|
1204
|
-
},
|
|
1205
|
-
/*
|
|
1206
|
-
* reducing wanted interval by respecting the time it takes to
|
|
1207
|
-
* compute internally since this is not multi-threaded like
|
|
1208
|
-
* requestAnimationFrame
|
|
1209
|
-
*/
|
|
1210
|
-
wantedInterval - processingTime,
|
|
1211
|
-
);
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
function cancel(id) {
|
|
1215
|
-
clearTimeout(id);
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
setAnimationFrameObject({
|
|
1219
|
-
requestAnimationFrame: raf,
|
|
1220
|
-
cancelAnimationFrame: cancel,
|
|
1221
|
-
});
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
function buildElement() {
|
|
1225
|
-
recorderElement = h(`video.${options.selectors.userMediaClass}`);
|
|
1226
|
-
|
|
1227
|
-
visuals.appendChild(recorderElement);
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
function correctDimensions() {
|
|
1231
|
-
if (options.hasDefinedWidth()) {
|
|
1232
|
-
recorderElement.width = self.getRecorderWidth(true);
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
if (options.hasDefinedHeight()) {
|
|
1236
|
-
recorderElement.height = self.getRecorderHeight(true);
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
function switchFacingMode() {
|
|
1241
|
-
if (!browser.isMobile()) {
|
|
1242
|
-
return false;
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
if (facingMode === "user") {
|
|
1246
|
-
facingMode = "environment";
|
|
1247
|
-
} else if (facingMode === "environment") {
|
|
1248
|
-
facingMode = "user";
|
|
1249
|
-
} else {
|
|
1250
|
-
debug("Recorder: unsupported facing mode", facingMode);
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
loadGenuineUserMedia({ switchingFacingMode: true });
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
function initEvents() {
|
|
1257
|
-
debug("Recorder: initEvents()");
|
|
1258
|
-
|
|
1259
|
-
self
|
|
1260
|
-
.on(Events.SUBMITTING, function () {
|
|
1261
|
-
submitting = true;
|
|
1262
|
-
})
|
|
1263
|
-
.on(Events.SUBMITTED, function () {
|
|
1264
|
-
submitting = false;
|
|
1265
|
-
self.unload();
|
|
1266
|
-
})
|
|
1267
|
-
.on(Events.BLOCKING, function () {
|
|
1268
|
-
blocking = true;
|
|
1269
|
-
clearUserMediaTimeout();
|
|
1270
|
-
})
|
|
1271
|
-
.on(Events.HIDE, function () {
|
|
1272
|
-
self.hide();
|
|
1273
|
-
})
|
|
1274
|
-
.on(Events.LOADED_META_DATA, function () {
|
|
1275
|
-
correctDimensions();
|
|
1276
|
-
})
|
|
1277
|
-
.on(Events.DISABLING_AUDIO, function () {
|
|
1278
|
-
reInitialiseAudio();
|
|
1279
|
-
})
|
|
1280
|
-
.on(Events.ENABLING_AUDIO, function () {
|
|
1281
|
-
reInitialiseAudio();
|
|
1282
|
-
})
|
|
1283
|
-
.on(Events.INVISIBLE, function () {
|
|
1284
|
-
loopWithTimeouts();
|
|
1285
|
-
})
|
|
1286
|
-
.on(Events.VISIBLE, function () {
|
|
1287
|
-
restoreAnimationFrameObject();
|
|
1288
|
-
})
|
|
1289
|
-
.on(Events.SWITCH_FACING_MODE, function () {
|
|
1290
|
-
switchFacingMode();
|
|
1291
|
-
});
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
this.build = function () {
|
|
1295
|
-
let err = browser.checkRecordingCapabilities();
|
|
1296
|
-
|
|
1297
|
-
if (!err) {
|
|
1298
|
-
err = browser.checkBufferTypes();
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
if (err) {
|
|
1302
|
-
this.emit(Events.ERROR, err);
|
|
1303
|
-
} else {
|
|
1304
|
-
recorderElement = visuals.querySelector(
|
|
1305
|
-
`video.${options.selectors.userMediaClass}`,
|
|
1306
|
-
);
|
|
1307
|
-
|
|
1308
|
-
if (!recorderElement) {
|
|
1309
|
-
buildElement();
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
correctDimensions();
|
|
1313
|
-
|
|
1314
|
-
/*
|
|
1315
|
-
* prevent audio feedback, see
|
|
1316
|
-
* https://github.com/binarykitchen/videomail-client/issues/35
|
|
1317
|
-
*/
|
|
1318
|
-
recorderElement.muted = true;
|
|
1319
|
-
|
|
1320
|
-
// for iphones, see https://github.com/webrtc/samples/issues/929
|
|
1321
|
-
recorderElement.setAttribute("playsinline", true);
|
|
1322
|
-
recorderElement.setAttribute("webkit-playsinline", "webkit-playsinline");
|
|
1323
|
-
|
|
1324
|
-
/*
|
|
1325
|
-
* add these here, not in CSS because users can configure custom
|
|
1326
|
-
* class names
|
|
1327
|
-
*/
|
|
1328
|
-
recorderElement.style.transform = "rotateY(180deg)";
|
|
1329
|
-
recorderElement.style["-webkit-transform"] = "rotateY(180deg)";
|
|
1330
|
-
recorderElement.style["-moz-transform"] = "rotateY(180deg)";
|
|
1331
|
-
|
|
1332
|
-
if (options.video.stretch) {
|
|
1333
|
-
recorderElement.style.width = "100%";
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
if (!userMedia) {
|
|
1337
|
-
userMedia = new UserMedia(this, options);
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
show();
|
|
1341
|
-
|
|
1342
|
-
if (!built) {
|
|
1343
|
-
initEvents();
|
|
1344
|
-
|
|
1345
|
-
if (!connected) {
|
|
1346
|
-
initSocket();
|
|
1347
|
-
} else if (!options.loadUserMediaOnRecord) {
|
|
1348
|
-
loadUserMedia();
|
|
1349
|
-
}
|
|
1350
|
-
} else if (options.loadUserMediaOnRecord) {
|
|
1351
|
-
loadUserMedia();
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
built = true;
|
|
1355
|
-
}
|
|
1356
|
-
};
|
|
1357
|
-
|
|
1358
|
-
this.isPaused = function () {
|
|
1359
|
-
return userMedia && userMedia.isPaused() && !loop.isRunning();
|
|
1360
|
-
};
|
|
1361
|
-
|
|
1362
|
-
this.isRecording = function () {
|
|
1363
|
-
/*
|
|
1364
|
-
* checking for stream.destroyed needed since
|
|
1365
|
-
* https://github.com/binarykitchen/videomail.io/issues/296
|
|
1366
|
-
*/
|
|
1367
|
-
return (
|
|
1368
|
-
loop &&
|
|
1369
|
-
loop.isRunning() &&
|
|
1370
|
-
!this.isPaused() &&
|
|
1371
|
-
!isNotifying() &&
|
|
1372
|
-
stream &&
|
|
1373
|
-
!stream.destroyed
|
|
1374
|
-
);
|
|
1375
|
-
};
|
|
1376
|
-
|
|
1377
|
-
this.hide = function () {
|
|
1378
|
-
if (!isHidden()) {
|
|
1379
|
-
recorderElement && hidden(recorderElement, true);
|
|
1380
|
-
|
|
1381
|
-
clearUserMediaTimeout();
|
|
1382
|
-
clearRetryTimeout();
|
|
1383
|
-
}
|
|
1384
|
-
};
|
|
1385
|
-
|
|
1386
|
-
this.isUnloaded = function () {
|
|
1387
|
-
return unloaded;
|
|
1388
|
-
};
|
|
1389
|
-
|
|
1390
|
-
/*
|
|
1391
|
-
* these two return the true dimensions of the webcam area.
|
|
1392
|
-
* needed because on mobiles they might be different.
|
|
1393
|
-
*/
|
|
1394
|
-
|
|
1395
|
-
this.getRecorderWidth = function (responsive) {
|
|
1396
|
-
if (userMedia && userMedia.hasVideoWidth()) {
|
|
1397
|
-
return userMedia.getRawWidth(responsive);
|
|
1398
|
-
} else if (responsive && options.hasDefinedWidth()) {
|
|
1399
|
-
return this.limitWidth(options.video.width);
|
|
1400
|
-
}
|
|
1401
|
-
};
|
|
1402
|
-
|
|
1403
|
-
this.getRecorderHeight = function (responsive, useBoundingClientRect) {
|
|
1404
|
-
if (userMedia && useBoundingClientRect) {
|
|
1405
|
-
return recorderElement.getBoundingClientRect().height;
|
|
1406
|
-
} else if (userMedia) {
|
|
1407
|
-
return userMedia.getRawHeight(responsive);
|
|
1408
|
-
} else if (responsive && options.hasDefinedHeight()) {
|
|
1409
|
-
return this.calculateHeight(responsive);
|
|
1410
|
-
}
|
|
1411
|
-
};
|
|
1412
|
-
|
|
1413
|
-
function getRatio() {
|
|
1414
|
-
let ratio;
|
|
1415
|
-
|
|
1416
|
-
if (userMedia) {
|
|
1417
|
-
const userMediaVideoWidth = userMedia.getVideoWidth();
|
|
1418
|
-
|
|
1419
|
-
// avoid division by zero
|
|
1420
|
-
if (userMediaVideoWidth < 1) {
|
|
1421
|
-
// use as a last resort fallback computation (needed for safari 11)
|
|
1422
|
-
ratio = visuals.getRatio();
|
|
1423
|
-
} else {
|
|
1424
|
-
ratio = userMedia.getVideoHeight() / userMediaVideoWidth;
|
|
1425
|
-
}
|
|
1426
|
-
} else {
|
|
1427
|
-
ratio = options.getRatio();
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
return ratio;
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
this.calculateWidth = function (responsive) {
|
|
1434
|
-
let videoHeight;
|
|
1435
|
-
|
|
1436
|
-
if (userMedia) {
|
|
1437
|
-
videoHeight = userMedia.getVideoHeight();
|
|
1438
|
-
} else if (recorderElement) {
|
|
1439
|
-
videoHeight = recorderElement.videoHeight || recorderElement.height;
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
return visuals.calculateWidth({
|
|
1443
|
-
responsive,
|
|
1444
|
-
ratio: getRatio(),
|
|
1445
|
-
videoHeight,
|
|
1446
|
-
});
|
|
1447
|
-
};
|
|
1448
|
-
|
|
1449
|
-
this.calculateHeight = function (responsive) {
|
|
1450
|
-
let videoWidth;
|
|
1451
|
-
|
|
1452
|
-
if (userMedia) {
|
|
1453
|
-
videoWidth = userMedia.getVideoWidth();
|
|
1454
|
-
} else if (recorderElement) {
|
|
1455
|
-
videoWidth = recorderElement.videoWidth || recorderElement.width;
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
return visuals.calculateHeight({
|
|
1459
|
-
responsive,
|
|
1460
|
-
ratio: getRatio(),
|
|
1461
|
-
videoWidth,
|
|
1462
|
-
});
|
|
1463
|
-
};
|
|
1464
|
-
|
|
1465
|
-
this.getRawVisualUserMedia = function () {
|
|
1466
|
-
return recorderElement;
|
|
1467
|
-
};
|
|
1468
|
-
|
|
1469
|
-
this.isConnected = function () {
|
|
1470
|
-
return connected;
|
|
1471
|
-
};
|
|
1472
|
-
|
|
1473
|
-
this.isConnecting = function () {
|
|
1474
|
-
return connecting;
|
|
1475
|
-
};
|
|
1476
|
-
|
|
1477
|
-
this.limitWidth = function (width) {
|
|
1478
|
-
return visuals.limitWidth(width);
|
|
1479
|
-
};
|
|
1480
|
-
|
|
1481
|
-
this.limitHeight = function (height) {
|
|
1482
|
-
return visuals.limitHeight(height);
|
|
1483
|
-
};
|
|
1484
|
-
|
|
1485
|
-
this.isUserMediaLoaded = function () {
|
|
1486
|
-
return userMediaLoaded;
|
|
1487
|
-
};
|
|
1488
|
-
};
|
|
1489
|
-
|
|
1490
|
-
inherits(Recorder, EventEmitter);
|
|
1491
|
-
|
|
1492
|
-
export default Recorder;
|