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.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/prototype/js/videomail-client.js +12 -14
  3. package/prototype/js/videomail-client.min.js +1 -1
  4. package/prototype/js/videomail-client.min.js.map +1 -1
  5. package/src/js/client.js +0 -210
  6. package/src/js/constants.js +0 -11
  7. package/src/js/events.js +0 -46
  8. package/src/js/index.js +0 -15
  9. package/src/js/options.js +0 -180
  10. package/src/js/resource.js +0 -206
  11. package/src/js/util/audioRecorder.js +0 -152
  12. package/src/js/util/browser.js +0 -319
  13. package/src/js/util/collectLogger.js +0 -72
  14. package/src/js/util/eventEmitter.js +0 -72
  15. package/src/js/util/humanize.js +0 -16
  16. package/src/js/util/mediaEvents.js +0 -148
  17. package/src/js/util/pretty.js +0 -70
  18. package/src/js/util/standardize.js +0 -71
  19. package/src/js/util/videomailError.js +0 -431
  20. package/src/js/wrappers/buttons.js +0 -670
  21. package/src/js/wrappers/container.js +0 -797
  22. package/src/js/wrappers/dimension.js +0 -149
  23. package/src/js/wrappers/form.js +0 -319
  24. package/src/js/wrappers/optionsWrapper.js +0 -81
  25. package/src/js/wrappers/visuals/inside/recorder/countdown.js +0 -83
  26. package/src/js/wrappers/visuals/inside/recorder/facingMode.js +0 -53
  27. package/src/js/wrappers/visuals/inside/recorder/pausedNote.js +0 -59
  28. package/src/js/wrappers/visuals/inside/recorder/recordNote.js +0 -42
  29. package/src/js/wrappers/visuals/inside/recorder/recordTimer.js +0 -149
  30. package/src/js/wrappers/visuals/inside/recorderInsides.js +0 -144
  31. package/src/js/wrappers/visuals/notifier.js +0 -341
  32. package/src/js/wrappers/visuals/recorder.js +0 -1492
  33. package/src/js/wrappers/visuals/replay.js +0 -355
  34. package/src/js/wrappers/visuals/userMedia.js +0 -541
  35. package/src/js/wrappers/visuals.js +0 -410
  36. package/src/styles/css/main.min.css.js +0 -1
  37. package/src/styles/styl/keyframes/blink.styl +0 -16
  38. 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;