videomail-client 16.1.0 → 16.2.0

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/dist/umd/index.js CHANGED
@@ -5,13 +5,13 @@
5
5
  else root["VideomailClient"] = factory();
6
6
  })(globalThis, ()=>(()=>{
7
7
  var __webpack_modules__ = {
8
- eq (module1, __webpack_exports__, __webpack_require__) {
8
+ pJ (module1, __webpack_exports__, __webpack_require__) {
9
9
  "use strict";
10
- var _node_modules_rsbuild_core_compiled_css_loader_noSourceMaps_js__rspack_import_1 = __webpack_require__("erX");
11
- var _node_modules_rsbuild_core_compiled_css_loader_noSourceMaps_js__rspack_import_1_default = /*#__PURE__*/ __webpack_require__.n(_node_modules_rsbuild_core_compiled_css_loader_noSourceMaps_js__rspack_import_1);
12
- var _node_modules_rsbuild_core_compiled_css_loader_api_js__rspack_import_0 = __webpack_require__("zm");
13
- var _node_modules_rsbuild_core_compiled_css_loader_api_js__rspack_import_0_default = /*#__PURE__*/ __webpack_require__.n(_node_modules_rsbuild_core_compiled_css_loader_api_js__rspack_import_0);
14
- var ___CSS_LOADER_EXPORT___ = _node_modules_rsbuild_core_compiled_css_loader_api_js__rspack_import_0_default()(_node_modules_rsbuild_core_compiled_css_loader_noSourceMaps_js__rspack_import_1_default());
10
+ var _node_modules_rslib_core_node_modules_rsbuild_core_compiled_css_loader_noSourceMaps_js__rspack_import_1 = __webpack_require__("K7");
11
+ var _node_modules_rslib_core_node_modules_rsbuild_core_compiled_css_loader_noSourceMaps_js__rspack_import_1_default = /*#__PURE__*/ __webpack_require__.n(_node_modules_rslib_core_node_modules_rsbuild_core_compiled_css_loader_noSourceMaps_js__rspack_import_1);
12
+ var _node_modules_rslib_core_node_modules_rsbuild_core_compiled_css_loader_api_js__rspack_import_0 = __webpack_require__("Ly");
13
+ var _node_modules_rslib_core_node_modules_rsbuild_core_compiled_css_loader_api_js__rspack_import_0_default = /*#__PURE__*/ __webpack_require__.n(_node_modules_rslib_core_node_modules_rsbuild_core_compiled_css_loader_api_js__rspack_import_0);
14
+ var ___CSS_LOADER_EXPORT___ = _node_modules_rslib_core_node_modules_rsbuild_core_compiled_css_loader_api_js__rspack_import_0_default()(_node_modules_rslib_core_node_modules_rsbuild_core_compiled_css_loader_noSourceMaps_js__rspack_import_1_default());
15
15
  ___CSS_LOADER_EXPORT___.push([
16
16
  module1.id,
17
17
  `@keyframes blink {
@@ -159,7 +159,7 @@
159
159
  });
160
160
  },
161
161
  b6 (module1, exports1, __webpack_require__) {
162
- var EventEmitter = __webpack_require__("q").EventEmitter, inherits = __webpack_require__("c5"), raf = __webpack_require__("jY"), methods;
162
+ var EventEmitter = __webpack_require__("qU").EventEmitter, inherits = __webpack_require__("c5"), raf = __webpack_require__("jY"), methods;
163
163
  var defaultRAFObject = {
164
164
  requestAnimationFrame: raf,
165
165
  cancelAnimationFrame: raf.cancel
@@ -1654,7 +1654,7 @@
1654
1654
  return 0 === comparison || comparison & DOCUMENT_POSITION_CONTAINED_BY;
1655
1655
  }
1656
1656
  },
1657
- T (__unused_rspack_module, exports1, __webpack_require__) {
1657
+ TU (__unused_rspack_module, exports1, __webpack_require__) {
1658
1658
  function isArray(arg) {
1659
1659
  if (Array.isArray) return Array.isArray(arg);
1660
1660
  return '[object Array]' === objectToString(arg);
@@ -1837,7 +1837,7 @@
1837
1837
  },
1838
1838
  xF (module1, __unused_rspack_exports, __webpack_require__) {
1839
1839
  "use strict";
1840
- var document1 = __webpack_require__("g");
1840
+ var document1 = __webpack_require__("gM");
1841
1841
  var Event1 = __webpack_require__("cE");
1842
1842
  var Keys = __webpack_require__("Dy");
1843
1843
  module1.exports = Visibility;
@@ -2119,7 +2119,7 @@
2119
2119
  return keys;
2120
2120
  };
2121
2121
  module1.exports = Duplex;
2122
- var util = Object.create(__webpack_require__("T"));
2122
+ var util = Object.create(__webpack_require__("TU"));
2123
2123
  util.inherits = __webpack_require__("c5");
2124
2124
  var Readable = __webpack_require__("rp");
2125
2125
  var Writable = __webpack_require__("pv");
@@ -2173,7 +2173,7 @@
2173
2173
  "use strict";
2174
2174
  module1.exports = PassThrough;
2175
2175
  var Transform = __webpack_require__("It");
2176
- var util = Object.create(__webpack_require__("T"));
2176
+ var util = Object.create(__webpack_require__("TU"));
2177
2177
  util.inherits = __webpack_require__("c5");
2178
2178
  util.inherits(PassThrough, Transform);
2179
2179
  function PassThrough(options) {
@@ -2192,7 +2192,7 @@
2192
2192
  var isArray = __webpack_require__("ab");
2193
2193
  var Duplex;
2194
2194
  Readable.ReadableState = ReadableState;
2195
- __webpack_require__("q").EventEmitter;
2195
+ __webpack_require__("qU").EventEmitter;
2196
2196
  var EElistenerCount = function(emitter, type) {
2197
2197
  return emitter.listeners(type).length;
2198
2198
  };
@@ -2205,7 +2205,7 @@
2205
2205
  function _isUint8Array(obj) {
2206
2206
  return Buffer.isBuffer(obj) || obj instanceof OurUint8Array;
2207
2207
  }
2208
- var util = Object.create(__webpack_require__("T"));
2208
+ var util = Object.create(__webpack_require__("TU"));
2209
2209
  util.inherits = __webpack_require__("c5");
2210
2210
  var debugUtil = __webpack_require__("AN");
2211
2211
  var debug = void 0;
@@ -2832,7 +2832,7 @@
2832
2832
  "use strict";
2833
2833
  module1.exports = Transform;
2834
2834
  var Duplex = __webpack_require__("OO");
2835
- var util = Object.create(__webpack_require__("T"));
2835
+ var util = Object.create(__webpack_require__("TU"));
2836
2836
  util.inherits = __webpack_require__("c5");
2837
2837
  util.inherits(Transform, Duplex);
2838
2838
  function afterTransform(er, data) {
@@ -2932,7 +2932,7 @@
2932
2932
  ].indexOf(process.version.slice(0, 5)) > -1 ? setImmediate : pna.nextTick;
2933
2933
  var Duplex;
2934
2934
  Writable.WritableState = WritableState;
2935
- var util = Object.create(__webpack_require__("T"));
2935
+ var util = Object.create(__webpack_require__("TU"));
2936
2936
  util.inherits = __webpack_require__("c5");
2937
2937
  var internalUtil = {
2938
2938
  deprecate: __webpack_require__("NQ")
@@ -3456,7 +3456,7 @@
3456
3456
  };
3457
3457
  },
3458
3458
  tb (module1, __unused_rspack_exports, __webpack_require__) {
3459
- module1.exports = __webpack_require__("q").EventEmitter;
3459
+ module1.exports = __webpack_require__("qU").EventEmitter;
3460
3460
  },
3461
3461
  GB (module1, exports1, __webpack_require__) {
3462
3462
  exports1 = module1.exports = __webpack_require__("rp");
@@ -3853,7 +3853,7 @@
3853
3853
  "use strict";
3854
3854
  module1.exports = Object;
3855
3855
  },
3856
- q (module1) {
3856
+ qU (module1) {
3857
3857
  "use strict";
3858
3858
  var R = 'object' == typeof Reflect ? Reflect : null;
3859
3859
  var ReflectApply = R && 'function' == typeof R.apply ? R.apply : function(target, receiver, args) {
@@ -4903,7 +4903,7 @@
4903
4903
  return tuple.listen;
4904
4904
  }
4905
4905
  },
4906
- g (module1, __unused_rspack_exports, __webpack_require__) {
4906
+ gM (module1, __unused_rspack_exports, __webpack_require__) {
4907
4907
  var topLevel = void 0 !== __webpack_require__.g ? __webpack_require__.g : "u" > typeof window ? window : {};
4908
4908
  var minDoc = __webpack_require__("MM");
4909
4909
  var doccy;
@@ -8254,7 +8254,7 @@
8254
8254
  });
8255
8255
  });
8256
8256
  },
8257
- Sy (__unused_rspack_module, exports1, __webpack_require__) {
8257
+ S (__unused_rspack_module, exports1, __webpack_require__) {
8258
8258
  var process = __webpack_require__("zT");
8259
8259
  var getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors || function(obj) {
8260
8260
  var keys = Object.keys(obj);
@@ -8733,7 +8733,7 @@
8733
8733
  return keys;
8734
8734
  };
8735
8735
  module1.exports = Duplex;
8736
- var util = Object.create(__webpack_require__("T"));
8736
+ var util = Object.create(__webpack_require__("TU"));
8737
8737
  util.inherits = __webpack_require__("c5");
8738
8738
  var Readable = __webpack_require__("w");
8739
8739
  var Writable = __webpack_require__("S5");
@@ -8787,7 +8787,7 @@
8787
8787
  "use strict";
8788
8788
  module1.exports = PassThrough;
8789
8789
  var Transform = __webpack_require__("Ge");
8790
- var util = Object.create(__webpack_require__("T"));
8790
+ var util = Object.create(__webpack_require__("TU"));
8791
8791
  util.inherits = __webpack_require__("c5");
8792
8792
  util.inherits(PassThrough, Transform);
8793
8793
  function PassThrough(options) {
@@ -8806,7 +8806,7 @@
8806
8806
  var isArray = __webpack_require__("N3");
8807
8807
  var Duplex;
8808
8808
  Readable.ReadableState = ReadableState;
8809
- __webpack_require__("q").EventEmitter;
8809
+ __webpack_require__("qU").EventEmitter;
8810
8810
  var EElistenerCount = function(emitter, type) {
8811
8811
  return emitter.listeners(type).length;
8812
8812
  };
@@ -8819,13 +8819,13 @@
8819
8819
  function _isUint8Array(obj) {
8820
8820
  return Buffer.isBuffer(obj) || obj instanceof OurUint8Array;
8821
8821
  }
8822
- var util = Object.create(__webpack_require__("T"));
8822
+ var util = Object.create(__webpack_require__("TU"));
8823
8823
  util.inherits = __webpack_require__("c5");
8824
8824
  var debugUtil = __webpack_require__("eD");
8825
8825
  var debug = void 0;
8826
8826
  debug = debugUtil && debugUtil.debuglog ? debugUtil.debuglog('stream') : function() {};
8827
8827
  var BufferList = __webpack_require__("aI");
8828
- var destroyImpl = __webpack_require__("R5");
8828
+ var destroyImpl = __webpack_require__("R");
8829
8829
  var StringDecoder;
8830
8830
  util.inherits(Readable, Stream);
8831
8831
  var kProxyEvents = [
@@ -9446,7 +9446,7 @@
9446
9446
  "use strict";
9447
9447
  module1.exports = Transform;
9448
9448
  var Duplex = __webpack_require__("S7");
9449
- var util = Object.create(__webpack_require__("T"));
9449
+ var util = Object.create(__webpack_require__("TU"));
9450
9450
  util.inherits = __webpack_require__("c5");
9451
9451
  util.inherits(Transform, Duplex);
9452
9452
  function afterTransform(er, data) {
@@ -9546,7 +9546,7 @@
9546
9546
  ].indexOf(process.version.slice(0, 5)) > -1 ? setImmediate : pna.nextTick;
9547
9547
  var Duplex;
9548
9548
  Writable.WritableState = WritableState;
9549
- var util = Object.create(__webpack_require__("T"));
9549
+ var util = Object.create(__webpack_require__("TU"));
9550
9550
  util.inherits = __webpack_require__("c5");
9551
9551
  var internalUtil = {
9552
9552
  deprecate: __webpack_require__("NQ")
@@ -9560,7 +9560,7 @@
9560
9560
  function _isUint8Array(obj) {
9561
9561
  return Buffer.isBuffer(obj) || obj instanceof OurUint8Array;
9562
9562
  }
9563
- var destroyImpl = __webpack_require__("R5");
9563
+ var destroyImpl = __webpack_require__("R");
9564
9564
  util.inherits(Writable, Stream);
9565
9565
  function nop() {}
9566
9566
  function WritableState(options, stream) {
@@ -10014,7 +10014,7 @@
10014
10014
  return this.constructor.name + ' ' + obj;
10015
10015
  };
10016
10016
  },
10017
- R5 (module1, __unused_rspack_exports, __webpack_require__) {
10017
+ R (module1, __unused_rspack_exports, __webpack_require__) {
10018
10018
  "use strict";
10019
10019
  var pna = __webpack_require__("sj");
10020
10020
  function destroy(err, cb) {
@@ -10070,7 +10070,7 @@
10070
10070
  };
10071
10071
  },
10072
10072
  O (module1, __unused_rspack_exports, __webpack_require__) {
10073
- module1.exports = __webpack_require__("q").EventEmitter;
10073
+ module1.exports = __webpack_require__("qU").EventEmitter;
10074
10074
  },
10075
10075
  Fo (module1, exports1, __webpack_require__) {
10076
10076
  exports1 = module1.exports = __webpack_require__("w");
@@ -10560,7 +10560,7 @@
10560
10560
  a () {},
10561
10561
  Av () {},
10562
10562
  eD () {},
10563
- zm (module1) {
10563
+ Ly (module1) {
10564
10564
  "use strict";
10565
10565
  module1.exports = function(cssWithMappingToString) {
10566
10566
  var list = [];
@@ -10614,13 +10614,13 @@
10614
10614
  return list;
10615
10615
  };
10616
10616
  },
10617
- erX (module1) {
10617
+ K7 (module1) {
10618
10618
  "use strict";
10619
10619
  module1.exports = function(i) {
10620
10620
  return i[1];
10621
10621
  };
10622
10622
  },
10623
- R (module1) {
10623
+ q (module1) {
10624
10624
  "use strict";
10625
10625
  var stylesInDOM = [];
10626
10626
  function getIndexByIdentifier(identifier) {
@@ -10699,7 +10699,7 @@
10699
10699
  };
10700
10700
  };
10701
10701
  },
10702
- uZ (module1) {
10702
+ f (module1) {
10703
10703
  "use strict";
10704
10704
  var memo = {};
10705
10705
  function getTarget(target) {
@@ -10721,7 +10721,7 @@
10721
10721
  }
10722
10722
  module1.exports = insertBySelector;
10723
10723
  },
10724
- mK (module1) {
10724
+ z7 (module1) {
10725
10725
  "use strict";
10726
10726
  function insertStyleElement(options) {
10727
10727
  var element = document.createElement("style");
@@ -10731,7 +10731,7 @@
10731
10731
  }
10732
10732
  module1.exports = insertStyleElement;
10733
10733
  },
10734
- kU (module1, __unused_rspack_exports, __webpack_require__) {
10734
+ g (module1, __unused_rspack_exports, __webpack_require__) {
10735
10735
  "use strict";
10736
10736
  function setAttributesWithoutAttributes(styleElement) {
10737
10737
  var nonce = __webpack_require__.nc;
@@ -10739,7 +10739,7 @@
10739
10739
  }
10740
10740
  module1.exports = setAttributesWithoutAttributes;
10741
10741
  },
10742
- Bd (module1) {
10742
+ V (module1) {
10743
10743
  "use strict";
10744
10744
  function apply(styleElement, options, obj) {
10745
10745
  var css = "";
@@ -10776,7 +10776,7 @@
10776
10776
  }
10777
10777
  module1.exports = domAPI;
10778
10778
  },
10779
- snK (module1) {
10779
+ AD (module1) {
10780
10780
  "use strict";
10781
10781
  function styleTagTransform(css, styleElement) {
10782
10782
  if (styleElement.styleSheet) styleElement.styleSheet.cssText = css;
@@ -11121,7 +11121,7 @@
11121
11121
  var client = __webpack_require__("Q");
11122
11122
  var client_default = /*#__PURE__*/ __webpack_require__.n(client);
11123
11123
  var package_namespaceObject = {
11124
- rE: "16.1.0"
11124
+ rE: "16.2.0"
11125
11125
  };
11126
11126
  function isAudioEnabled(options) {
11127
11127
  return Boolean(options.audio.enabled);
@@ -11133,7 +11133,7 @@
11133
11133
  function isAutoPauseEnabled(options) {
11134
11134
  return options.enableAutoPause && options.enablePause;
11135
11135
  }
11136
- var util = __webpack_require__("Sy");
11136
+ var util = __webpack_require__("S");
11137
11137
  var util_default = /*#__PURE__*/ __webpack_require__.n(util);
11138
11138
  function getEventDetails(event) {
11139
11139
  const eventWithErrorDetails = event;
@@ -14996,6 +14996,14 @@
14996
14996
  }
14997
14997
  }
14998
14998
  const util_Despot = Despot;
14999
+ const EMAIL_SEPARATOR_REGEX = /[ ,]+/gu;
15000
+ function trimEmail(email) {
15001
+ return email.replace(EMAIL_SEPARATOR_REGEX, "");
15002
+ }
15003
+ function trimEmails(emails) {
15004
+ const trimmedEmails = emails.split(EMAIL_SEPARATOR_REGEX).map((item)=>item.trim()).filter(Boolean);
15005
+ return trimmedEmails;
15006
+ }
14999
15007
  function getValidity(element) {
15000
15008
  const validity = "validity" in element ? element.validity : void 0;
15001
15009
  return validity;
@@ -15015,14 +15023,6 @@
15015
15023
  element.style.removeProperty("display");
15016
15024
  }
15017
15025
  const html_showElement = showElement;
15018
- const EMAIL_SEPARATOR_REGEX = /[ ,]+/gu;
15019
- function trimEmail(email) {
15020
- return email.replace(EMAIL_SEPARATOR_REGEX, "");
15021
- }
15022
- function trimEmails(emails) {
15023
- const trimmedEmails = emails.split(EMAIL_SEPARATOR_REGEX).map((item)=>item.trim()).filter(Boolean);
15024
- return trimmedEmails;
15025
- }
15026
15026
  const FormMethod = {
15027
15027
  POST: "post",
15028
15028
  PUT: "put",
@@ -15689,19 +15689,19 @@
15689
15689
  return newOptions;
15690
15690
  }
15691
15691
  const options_mergeWithDefaultOptions = mergeWithDefaultOptions;
15692
- var injectStylesIntoStyleTag = __webpack_require__("R");
15692
+ var injectStylesIntoStyleTag = __webpack_require__("q");
15693
15693
  var injectStylesIntoStyleTag_default = /*#__PURE__*/ __webpack_require__.n(injectStylesIntoStyleTag);
15694
- var styleDomAPI = __webpack_require__("Bd");
15694
+ var styleDomAPI = __webpack_require__("V");
15695
15695
  var styleDomAPI_default = /*#__PURE__*/ __webpack_require__.n(styleDomAPI);
15696
- var insertBySelector = __webpack_require__("uZ");
15696
+ var insertBySelector = __webpack_require__("f");
15697
15697
  var insertBySelector_default = /*#__PURE__*/ __webpack_require__.n(insertBySelector);
15698
- var setAttributesWithoutAttributes = __webpack_require__("kU");
15698
+ var setAttributesWithoutAttributes = __webpack_require__("g");
15699
15699
  var setAttributesWithoutAttributes_default = /*#__PURE__*/ __webpack_require__.n(setAttributesWithoutAttributes);
15700
- var insertStyleElement = __webpack_require__("mK");
15700
+ var insertStyleElement = __webpack_require__("z7");
15701
15701
  var insertStyleElement_default = /*#__PURE__*/ __webpack_require__.n(insertStyleElement);
15702
- var styleTagTransform = __webpack_require__("snK");
15702
+ var styleTagTransform = __webpack_require__("AD");
15703
15703
  var styleTagTransform_default = /*#__PURE__*/ __webpack_require__.n(styleTagTransform);
15704
- var main = __webpack_require__("eq");
15704
+ var main = __webpack_require__("pJ");
15705
15705
  var main_options = {};
15706
15706
  main_options.styleTagTransform = styleTagTransform_default();
15707
15707
  main_options.setAttributes = setAttributesWithoutAttributes_default();
@@ -16705,11 +16705,11 @@
16705
16705
  const originalDocument = document1;
16706
16706
  const currentScript = originalDocument.currentScript;
16707
16707
  window1.DocumentFragment;
16708
- const HTMLTemplateElement = window1.HTMLTemplateElement, Node = window1.Node, Element = window1.Element, NodeFilter = window1.NodeFilter;
16708
+ const HTMLTemplateElement = window1.HTMLTemplateElement, Node = window1.Node, Element1 = window1.Element, NodeFilter = window1.NodeFilter;
16709
16709
  void 0 === window1.NamedNodeMap && (window1.NamedNodeMap || window1.MozNamedAttrMap);
16710
16710
  window1.HTMLFormElement;
16711
16711
  const DOMParser = window1.DOMParser, trustedTypes = window1.trustedTypes;
16712
- const ElementPrototype = Element.prototype;
16712
+ const ElementPrototype = Element1.prototype;
16713
16713
  const cloneNode = lookupGetter(ElementPrototype, "cloneNode");
16714
16714
  const remove = lookupGetter(ElementPrototype, "remove");
16715
16715
  const removeAttributeNode = lookupGetter(ElementPrototype, "removeAttributeNode");
@@ -17792,6 +17792,19 @@
17792
17792
  return DOMPurify;
17793
17793
  }
17794
17794
  var purify_default = createDOMPurify();
17795
+ function isRecipientSelectionValid(recipients) {
17796
+ const hasTo = Boolean(recipients.to?.length);
17797
+ const hasCc = Boolean(recipients.cc?.length);
17798
+ const hasBcc = Boolean(recipients.bcc?.length);
17799
+ const hasConfiguredTo = "to" in recipients;
17800
+ const hasConfiguredCc = "cc" in recipients;
17801
+ const hasConfiguredBcc = "bcc" in recipients;
17802
+ let isValid = true;
17803
+ if (hasConfiguredTo) isValid = hasTo || hasConfiguredCc && hasCc || hasConfiguredBcc && hasBcc;
17804
+ else if (hasConfiguredCc) isValid = hasCc || !hasConfiguredBcc || hasBcc;
17805
+ return isValid;
17806
+ }
17807
+ const email_isRecipientSelectionValid = isRecipientSelectionValid;
17795
17808
  function limitHeight(height, options, calledFrom) {
17796
17809
  const dimension = {
17797
17810
  unit: "px"
@@ -18388,7 +18401,7 @@
18388
18401
  html_showElement(this.facingModeElement);
18389
18402
  }
18390
18403
  }
18391
- const facingMode = FacingMode;
18404
+ const recorder_facingMode = FacingMode;
18392
18405
  class PausedNote {
18393
18406
  visuals;
18394
18407
  options;
@@ -18609,7 +18622,7 @@
18609
18622
  this.recordTimer = new recordTimer(visuals, this.recordNote, options);
18610
18623
  const browser = util_getBrowser(options);
18611
18624
  if (options.video.countdown) this.countdown = new countdown(visuals, options);
18612
- if (options.video.facingModeButton && browser.isMobile()) this.facingMode = new facingMode(visuals, options);
18625
+ if (options.video.facingModeButton && browser.isMobile()) this.facingMode = new recorder_facingMode(visuals, options);
18613
18626
  if (options.enablePause) this.pausedNote = new pausedNote(visuals, options);
18614
18627
  }
18615
18628
  startRecording() {
@@ -18942,6 +18955,18 @@
18942
18955
  }
18943
18956
  }
18944
18957
  const notifier = Notifier;
18958
+ function summarize(title, contents) {
18959
+ const lines = Object.entries(contents).sort(([keyA], [keyB])=>keyA.localeCompare(keyB)).map(([key, value])=>{
18960
+ const formattedValue = void 0 === value ? "undefined" : "object" == typeof value ? string_pretty(value) : value;
18961
+ return ` • ${key}: ${formattedValue}`;
18962
+ });
18963
+ const line = [
18964
+ `🔎 ${title}`,
18965
+ ...lines
18966
+ ].join("\n");
18967
+ return line;
18968
+ }
18969
+ const string_summarize = summarize;
18945
18970
  var animitter = __webpack_require__("b6");
18946
18971
  var animitter_default = /*#__PURE__*/ __webpack_require__.n(animitter);
18947
18972
  var typedarray_to_buffer = __webpack_require__("E");
@@ -19005,34 +19030,6 @@
19005
19030
  canvas_to_buffer_modern_r.atob = void 0;
19006
19031
  var websocket_stream_stream = __webpack_require__("U");
19007
19032
  var stream_default = /*#__PURE__*/ __webpack_require__.n(websocket_stream_stream);
19008
- const DEFAULT_MAX_QUEUED_BYTES = 1048576;
19009
- class BoundedWriteQueue {
19010
- maxBytes;
19011
- writes = [];
19012
- queuedBytes = 0;
19013
- constructor(maxBytes = DEFAULT_MAX_QUEUED_BYTES){
19014
- this.maxBytes = maxBytes;
19015
- }
19016
- enqueue(buffer, params) {
19017
- if (this.queuedBytes + buffer.length > this.maxBytes) return false;
19018
- this.writes.push({
19019
- buffer,
19020
- params
19021
- });
19022
- this.queuedBytes += buffer.length;
19023
- return true;
19024
- }
19025
- dequeue() {
19026
- const write = this.writes.shift();
19027
- if (write) this.queuedBytes -= write.buffer.length;
19028
- return write;
19029
- }
19030
- clear() {
19031
- this.writes.length = 0;
19032
- this.queuedBytes = 0;
19033
- }
19034
- }
19035
- const boundedWriteQueue = BoundedWriteQueue;
19036
19033
  function getEdgeCodes(value, amount, fromEnd) {
19037
19034
  const chars = fromEnd ? value.slice(-amount) : value.slice(0, amount);
19038
19035
  const codes = Array.from(chars, (char)=>String(char.codePointAt(0))).join(".");
@@ -19112,1804 +19109,2115 @@
19112
19109
  };
19113
19110
  }
19114
19111
  const error_getWebSocketDiagnostic = getWebSocketDiagnostic;
19115
- function figureMinHeight(height, options) {
19116
- let minHeight;
19117
- if (options.video.height) {
19118
- minHeight = height ? Math.min(options.video.height, height) : options.video.height;
19119
- if (minHeight < 1) throw error_createError({
19120
- message: `Got a min height less than 1 (${minHeight})!`,
19121
- options
19122
- });
19123
- } else minHeight = height;
19124
- return minHeight;
19125
- }
19126
- const dimensions_figureMinHeight = figureMinHeight;
19127
- function getRatio(options, videoHeight, videoWidth) {
19128
- let ratio = 1;
19129
- const hasVideoDimensions = videoHeight && videoWidth;
19130
- const desiredHeight = options.video.height;
19131
- const desiredWidth = options.video.width;
19132
- const hasDesiredDimensions = desiredHeight && desiredWidth;
19133
- if (hasDesiredDimensions) ratio = hasVideoDimensions ? videoHeight < desiredHeight || videoWidth < desiredWidth ? videoHeight / videoWidth : desiredHeight / desiredWidth : desiredHeight / desiredWidth;
19134
- else if (hasVideoDimensions) ratio = videoHeight / videoWidth;
19135
- return ratio;
19136
- }
19137
- const dimensions_getRatio = getRatio;
19138
- function calculateHeight(responsive, videoWidth, options, ratio, element) {
19139
- const dimension = {
19140
- unit: "px"
19141
- };
19142
- let width = videoWidth;
19143
- if (responsive && element) {
19144
- const limitedDimension = dimensions_limitWidth(element, options, width);
19145
- width = limitedDimension.value;
19146
- }
19147
- const chosenRatio = ratio ?? dimensions_getRatio(options, void 0, videoWidth);
19148
- const height = width ? Math.round(width * chosenRatio) : void 0;
19149
- if (height && Number.isInteger(height) && height < 1) throw error_createError({
19150
- message: "Just calculated a height less than 1 which is wrong.",
19151
- options
19152
- });
19153
- const minHeight = dimensions_figureMinHeight(height, options);
19154
- dimension.value = minHeight;
19155
- return dimension;
19156
- }
19157
- const dimensions_calculateHeight = calculateHeight;
19158
- function calculateWidth(responsive, options, videoHeight, ratio) {
19159
- const dimension = {
19160
- unit: "px"
19161
- };
19162
- let height = dimensions_figureMinHeight(videoHeight, options);
19163
- if (responsive) {
19164
- const limitedDimension = dimensions_limitHeight(height, options, "calculateWidth");
19165
- height = limitedDimension.value;
19166
- }
19167
- if (!height || height < 1) throw error_createError({
19168
- message: `Height ${height} cannot be smaller than 1 when calculating width.`,
19169
- options
19170
- });
19171
- const chosenRatio = ratio ?? dimensions_getRatio(options, videoHeight);
19172
- const calculatedWidth = Math.round(height / chosenRatio);
19173
- if (calculatedWidth < 1) throw error_createError({
19174
- message: "Calculated width cannot be smaller than 1!",
19175
- options
19176
- });
19177
- dimension.value = calculatedWidth;
19178
- return dimension;
19179
- }
19180
- const dimensions_calculateWidth = calculateWidth;
19181
19112
  function isAutomatedUserAgent(userAgent = navigator.userAgent) {
19182
19113
  return /bot|crawler|spider|headlesschrome|phantomjs|puppeteer|playwright/iu.test(userAgent);
19183
19114
  }
19184
19115
  const util_isAutomatedUserAgent = isAutomatedUserAgent;
19185
- function summarize(title, contents) {
19186
- const lines = Object.entries(contents).sort(([keyA], [keyB])=>keyA.localeCompare(keyB)).map(([key, value])=>{
19187
- const formattedValue = void 0 === value ? "undefined" : "object" == typeof value ? string_pretty(value) : value;
19188
- return ` • ${key}: ${formattedValue}`;
19189
- });
19190
- const line = [
19191
- `🔎 ${title}`,
19192
- ...lines
19193
- ].join("\n");
19194
- return line;
19195
- }
19196
- const string_summarize = summarize;
19197
- function getFirstVideoTrack(localMediaStream) {
19198
- const videoTracks = localMediaStream.getVideoTracks();
19199
- let videoTrack;
19200
- if (videoTracks[0]) videoTrack = videoTracks[0];
19201
- return videoTrack;
19202
- }
19203
- const media_getFirstVideoTrack = getFirstVideoTrack;
19204
- const MEDIA_EVENTS = [
19205
- "loadstart",
19206
- "suspend",
19207
- "progress",
19208
- "abort",
19209
- "emptied",
19210
- "stalled",
19211
- "pause",
19212
- "loadeddata",
19213
- "waiting",
19214
- "playing",
19215
- "canplay",
19216
- "canplaythrough",
19217
- "seeking",
19218
- "seeked",
19219
- "ended",
19220
- "ratechange",
19221
- "durationchange",
19222
- "volumechange"
19223
- ];
19224
- const mediaEvents = MEDIA_EVENTS;
19225
- function _define_property(obj, key, value) {
19226
- if (key in obj) Object.defineProperty(obj, key, {
19227
- value: value,
19228
- enumerable: true,
19229
- configurable: true,
19230
- writable: true
19231
- });
19232
- else obj[key] = value;
19233
- return obj;
19234
- }
19235
- class AudioSample {
19236
- toBuffer() {
19237
- const l = this.float32Array.length;
19238
- const arr = new Int16Array(l);
19239
- this.float32Array.forEach((sample, i)=>{
19240
- const clampedSample = Number.isFinite(sample) ? Math.max(-1, sample) : sample;
19241
- arr[i] = 0x7fff * Math.min(1, clampedSample);
19116
+ const DEFAULT_MAX_QUEUED_BYTES = 1048576;
19117
+ class BoundedWriteQueue {
19118
+ maxBytes;
19119
+ writes = [];
19120
+ queuedBytes = 0;
19121
+ constructor(maxBytes = DEFAULT_MAX_QUEUED_BYTES){
19122
+ this.maxBytes = maxBytes;
19123
+ }
19124
+ enqueue(buffer, params) {
19125
+ if (this.queuedBytes + buffer.length > this.maxBytes) return false;
19126
+ this.writes.push({
19127
+ buffer,
19128
+ params
19242
19129
  });
19243
- return typedarray_to_buffer(arr);
19130
+ this.queuedBytes += buffer.length;
19131
+ return true;
19244
19132
  }
19245
- constructor(float32Array){
19246
- _define_property(this, "float32Array", void 0);
19247
- this.float32Array = float32Array;
19133
+ dequeue() {
19134
+ const write = this.writes.shift();
19135
+ if (write) this.queuedBytes -= write.buffer.length;
19136
+ return write;
19137
+ }
19138
+ clear() {
19139
+ this.writes.length = 0;
19140
+ this.queuedBytes = 0;
19248
19141
  }
19249
19142
  }
19250
- const esm_src = AudioSample;
19251
- const esm = esm_src;
19252
- var is_power_of_two = __webpack_require__("dZ");
19253
- var is_power_of_two_default = /*#__PURE__*/ __webpack_require__.n(is_power_of_two);
19254
- const CHANNELS = 1;
19255
- function getAudioContextClass() {
19256
- return window.AudioContext;
19257
- }
19258
- class AudioRecorder {
19259
- scriptProcessor;
19260
- audioInput;
19261
- volume;
19262
- vcAudioContext;
19263
- userMedia;
19264
- options;
19265
- constructor(userMedia, options){
19266
- this.options = options;
19267
- this.userMedia = userMedia;
19143
+ const boundedWriteQueue = BoundedWriteQueue;
19144
+ const PIPE_SYMBOL = "°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸ ";
19145
+ class RecorderStreamWriter extends util_Despot {
19146
+ pendingWrites = new boundedWriteQueue();
19147
+ samplesCount = 0;
19148
+ framesCount = 0;
19149
+ recordingStats;
19150
+ confirmedFrameNumber = 0;
19151
+ confirmedSampleNumber = 0;
19152
+ backpressureTimeout;
19153
+ recordingDimensions;
19154
+ stream;
19155
+ streamBackpressured = false;
19156
+ resumeAfterDrain = false;
19157
+ mediaTransmissionStarted = false;
19158
+ recordingActive = false;
19159
+ recordingTransportFailed = false;
19160
+ connecting = false;
19161
+ connected = false;
19162
+ reconnecting = false;
19163
+ constructor(options){
19164
+ super("Recorder", options);
19268
19165
  }
19269
- hasAudioContext() {
19270
- return Boolean(getAudioContextClass()) && Boolean(this.getAudioContext());
19166
+ hasExceededWebSocketSendBuffer(buffer, opts) {
19167
+ return buffer.length > 0 && !this.pendingWrites.enqueue(buffer, opts);
19271
19168
  }
19272
- getAudioContext() {
19273
- if (!this.vcAudioContext) {
19274
- const AudioContext = getAudioContextClass();
19275
- this.vcAudioContext = new AudioContext();
19276
- }
19277
- return this.vcAudioContext;
19278
- }
19279
- onAudioProcess(e, cb) {
19280
- if (!this.userMedia.isRecording() || this.userMedia.isPaused()) return;
19281
- const float32Array = e.inputBuffer.getChannelData(0);
19282
- cb(new esm(float32Array));
19283
- }
19284
- disconnect() {
19285
- if (this.scriptProcessor) {
19286
- this.scriptProcessor.onaudioprocess = null;
19287
- this.scriptProcessor.disconnect();
19288
- }
19289
- this.audioInput?.disconnect();
19290
- this.volume?.disconnect();
19169
+ writeStream(buffer, opts) {
19170
+ if (this.stream) if (this.stream.destroyed) {
19171
+ this.stopPings();
19172
+ const err = error_createError({
19173
+ message: "Already disconnected",
19174
+ explanation: "Sorry, connection to the server has been destroyed. Please reload.",
19175
+ options: this.options
19176
+ });
19177
+ this.handleWriteFailure(err);
19178
+ } else if (this.streamBackpressured) {
19179
+ if (this.hasExceededWebSocketSendBuffer(buffer, opts)) this.failMediaTransmission(error_createError({
19180
+ message: "WebSocket send buffer limit exceeded",
19181
+ explanation: "The connection to the server is too slow to keep up with this recording. The recording has been stopped; please start a new one.",
19182
+ options: this.options
19183
+ }));
19184
+ } else this.writeToStream(this.stream, buffer, opts);
19291
19185
  }
19292
- init(localMediaStream) {
19293
- this.options.logger.debug("AudioRecorder: init()");
19294
- this.disconnect();
19295
- const volume = this.getAudioContext().createGain();
19296
- this.volume = volume;
19186
+ writeToStream(stream, buffer, opts) {
19187
+ const onFlushedCallback = opts?.onFlushedCallback;
19297
19188
  try {
19298
- this.audioInput = this.getAudioContext().createMediaStreamSource(localMediaStream);
19189
+ const canContinue = stream.write(buffer, ()=>{
19190
+ if (!onFlushedCallback) return;
19191
+ try {
19192
+ onFlushedCallback(opts);
19193
+ } catch (exc) {
19194
+ const err = error_createError({
19195
+ message: "Failed to write stream buffer",
19196
+ explanation: `stream.write() failed because of ${string_pretty(exc)}`,
19197
+ options: this.options,
19198
+ exc
19199
+ });
19200
+ this.emit("ERROR", {
19201
+ err
19202
+ });
19203
+ }
19204
+ });
19205
+ if (!canContinue) {
19206
+ this.streamBackpressured = true;
19207
+ this.startBackpressureTimeout();
19208
+ this.pauseForBackpressure();
19209
+ return false;
19210
+ }
19211
+ return true;
19299
19212
  } catch (exc) {
19300
- throw error_createError({
19301
- message: "Webcam has no audio",
19302
- exc,
19303
- options: this.options
19213
+ const err = error_createError({
19214
+ message: "Failed writing to server",
19215
+ explanation: `stream.write() failed because of ${string_pretty(exc)}`,
19216
+ options: this.options,
19217
+ exc
19304
19218
  });
19219
+ this.handleWriteFailure(err);
19220
+ return false;
19305
19221
  }
19306
- let { bufferSize } = this.options.audio;
19307
- if ("auto" === bufferSize) bufferSize = util_getBrowser(this.options).isFirefox() ? 512 : 2048;
19308
- if (void 0 === bufferSize || !is_power_of_two_default()(bufferSize)) throw error_createError({
19309
- message: `Audio buffer size must be a power of two. The current buffer size is ${bufferSize}.`,
19310
- options: this.options
19311
- });
19312
- if (!Number.isFinite(this.options.audio.volume) || this.options.audio.volume < 0 || this.options.audio.volume > 1) throw error_createError({
19313
- message: `Audio volume must be between zero and one. The current volume is ${this.options.audio.volume}.`,
19314
- options: this.options
19315
- });
19316
- volume.gain.value = this.options.audio.volume;
19317
- this.scriptProcessor = this.getAudioContext().createScriptProcessor(bufferSize, CHANNELS, CHANNELS);
19318
- this.audioInput.connect(volume);
19319
- volume.connect(this.scriptProcessor);
19320
- this.scriptProcessor.connect(this.getAudioContext().destination);
19321
19222
  }
19322
- record(cb) {
19323
- this.options.logger.debug("AudioRecorder: record()");
19324
- if (this.scriptProcessor) this.scriptProcessor.onaudioprocess = (e)=>{
19325
- this.onAudioProcess(e, cb);
19326
- };
19223
+ flushPendingWrites(stream) {
19224
+ if (this.isStaleStream(stream)) return;
19225
+ this.streamBackpressured = false;
19226
+ this.clearBackpressureTimeout();
19227
+ let pendingWrite = this.pendingWrites.dequeue();
19228
+ while(pendingWrite){
19229
+ if (!this.writeToStream(stream, pendingWrite.buffer, pendingWrite.params)) return;
19230
+ pendingWrite = this.pendingWrites.dequeue();
19231
+ }
19232
+ this.resumeRecordingAfterDrain();
19327
19233
  }
19328
- async resume() {
19329
- const context = this.getAudioContext();
19330
- if ("running" !== context.state) await context.resume();
19234
+ isStaleStream(stream) {
19235
+ return this.stream !== stream || stream.destroyed;
19331
19236
  }
19332
- stop() {
19333
- this.options.logger.debug("AudioRecorder: stop()");
19334
- this.disconnect();
19335
- if (this.hasAudioContext()) this.getAudioContext().close().then(()=>{
19336
- this.options.logger.debug("AudioRecorder: audio context is closed");
19337
- this.vcAudioContext = void 0;
19338
- }).catch((err)=>{
19339
- if (err instanceof Error) return void this.options.logger.error(error_createError({
19340
- err,
19341
- options: this.options
19342
- }));
19343
- this.options.logger.error(err);
19344
- });
19237
+ canPauseForBackpressure() {
19238
+ return this.recordingActive && !this.isUserMediaPaused() && this.isCaptureLoopRunning();
19345
19239
  }
19346
- getSampleRate() {
19347
- if (this.hasAudioContext()) return this.getAudioContext().sampleRate;
19348
- return -1;
19240
+ pauseForBackpressure() {
19241
+ if (!this.canPauseForBackpressure()) return;
19242
+ this.resumeAfterDrain = true;
19243
+ this.stopCaptureLoop();
19349
19244
  }
19350
- }
19351
- const media_AudioRecorder = AudioRecorder;
19352
- const VIRTUAL_KEYWORDS = [
19353
- "obs",
19354
- "virtual",
19355
- "manycam",
19356
- "vcam",
19357
- "splitcam",
19358
- "droidcam"
19359
- ];
19360
- function isVirtualCamera(videoTrack) {
19361
- const capabilities = videoTrack.getCapabilities();
19362
- if (!capabilities.frameRate) return true;
19363
- const isVirtual = VIRTUAL_KEYWORDS.some((keyword)=>videoTrack.label.toLowerCase().includes(keyword));
19364
- return isVirtual;
19365
- }
19366
- const util_isVirtualCamera = isVirtualCamera;
19367
- const EVENT_ASCII = "|—O—|";
19368
- class UserMedia extends util_Despot {
19369
- recorder;
19370
- rawVisualUserMedia;
19371
- paused = false;
19372
- recording = false;
19373
- audioRecorder;
19374
- currentVisualStream;
19375
- onPlayReached = false;
19376
- onLoadedMetaDataReached = false;
19377
- playingPromiseReached = false;
19378
- videoTrackLabel;
19379
- clearInitializationListeners;
19380
- initializationId = 0;
19381
- outputEvent = (e)=>{
19382
- this.logEvent(e.type, {
19383
- readyState: this.rawVisualUserMedia?.readyState
19384
- });
19385
- this.rawVisualUserMedia?.removeEventListener(e.type, this.outputEvent);
19386
- };
19387
- handleMediaError = (event)=>{
19388
- this.options.logger.warn(`Caught video element error event: ${string_pretty(event)}`);
19389
- };
19390
- constructor(recorder, options){
19391
- super("UserMedia", options);
19392
- this.recorder = recorder;
19393
- this.rawVisualUserMedia = recorder.getRawVisualUserMedia();
19394
- mediaEvents.forEach((eventName)=>{
19395
- this.rawVisualUserMedia?.addEventListener(eventName, this.outputEvent, false);
19396
- });
19245
+ resumeRecordingAfterDrain() {
19246
+ if (!this.resumeAfterDrain) return;
19247
+ this.resumeAfterDrain = false;
19248
+ if (this.canResumeAfterDrain()) this.startCaptureLoop();
19397
19249
  }
19398
- attachMediaStream(stream) {
19399
- if (this.rawVisualUserMedia) {
19400
- this.rawVisualUserMedia.srcObject = stream;
19401
- const previousStream = this.currentVisualStream;
19402
- this.currentVisualStream = stream;
19403
- if (previousStream && previousStream !== stream) previousStream.getTracks().forEach((track)=>{
19404
- track.stop();
19405
- });
19406
- } else throw error_createError({
19407
- message: "Error attaching stream to element.",
19408
- explanation: "Contact the developer about this",
19409
- options: this.options
19250
+ canResumeAfterDrain() {
19251
+ return this.recordingActive && !this.isUserMediaPaused();
19252
+ }
19253
+ handleWriteFailure(err) {
19254
+ if (this.mediaTransmissionStarted) return void this.failMediaTransmission(err);
19255
+ this.emit("ERROR", {
19256
+ err
19410
19257
  });
19411
19258
  }
19412
- setVisualStream(localMediaStream) {
19413
- if (localMediaStream) this.attachMediaStream(localMediaStream);
19414
- else {
19415
- if (this.rawVisualUserMedia) {
19416
- this.rawVisualUserMedia.srcObject = null;
19417
- this.rawVisualUserMedia.removeAttribute("src");
19418
- }
19419
- this.currentVisualStream = void 0;
19420
- }
19259
+ failMediaTransmission(err) {
19260
+ if (this.recordingTransportFailed) return;
19261
+ this.recordingTransportFailed = true;
19262
+ this.recordingActive = false;
19263
+ this.mediaTransmissionStarted = false;
19264
+ this.streamBackpressured = false;
19265
+ this.resumeAfterDrain = false;
19266
+ this.pendingWrites.clear();
19267
+ this.connected = false;
19268
+ this.connecting = false;
19269
+ this.reconnecting = false;
19270
+ this.clearReconnectTimeout();
19271
+ this.clearBackpressureTimeout();
19272
+ this.discardSocket();
19273
+ this.reset();
19274
+ this.framesCount = this.samplesCount = 0;
19275
+ this.confirmedFrameNumber = this.confirmedSampleNumber = 0;
19276
+ delete this.recordingStats;
19277
+ this.recordingDimensions = void 0;
19278
+ this.emit("ERROR", {
19279
+ err
19280
+ });
19421
19281
  }
19422
- async restorePlayback() {
19423
- if (!this.rawVisualUserMedia) throw new Error("No camera preview is available.");
19424
- await this.rawVisualUserMedia.play();
19425
- await this.audioRecorder?.resume();
19282
+ startBackpressureTimeout() {
19283
+ if (!this.shouldStartBackpressureTimeout()) return;
19284
+ this.backpressureTimeout = window.setTimeout(()=>{
19285
+ this.backpressureTimeout = void 0;
19286
+ if (this.shouldIgnoreBackpressureTimeout()) return;
19287
+ const err = error_createError({
19288
+ message: "WebSocket write timed out",
19289
+ explanation: "The server stopped accepting recording data. The recording has been stopped; please check your connection and start a new one.",
19290
+ options: this.options
19291
+ });
19292
+ this.failMediaTransmission(err);
19293
+ }, this.options.timeouts.connection);
19426
19294
  }
19427
- hasLiveStream() {
19428
- const videoReady = this.currentVisualStream?.getVideoTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
19429
- const audioReady = !isAudioEnabled(this.options) || this.currentVisualStream?.getAudioTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
19430
- return Boolean(this.currentVisualStream?.active && videoReady && audioReady);
19295
+ shouldStartBackpressureTimeout() {
19296
+ return void 0 === this.backpressureTimeout && this.mediaTransmissionStarted;
19431
19297
  }
19432
- hasEnded() {
19433
- if (this.rawVisualUserMedia?.ended) return this.rawVisualUserMedia.ended;
19434
- return !this.currentVisualStream?.active;
19298
+ shouldIgnoreBackpressureTimeout() {
19299
+ return !this.streamBackpressured || this.recordingTransportFailed;
19435
19300
  }
19436
- hasInvalidDimensions() {
19437
- if (!this.rawVisualUserMedia) return false;
19438
- const { videoWidth, videoHeight } = this.rawVisualUserMedia;
19439
- return videoWidth > 0 && videoWidth < 3 || videoHeight > 0 && videoHeight < 3;
19301
+ failInterruptedRecording() {
19302
+ const unconfirmedFrames = Math.max(0, this.framesCount - this.confirmedFrameNumber);
19303
+ const unconfirmedSamples = Math.max(0, this.samplesCount - this.confirmedSampleNumber);
19304
+ const err = error_createError({
19305
+ message: "Recording interrupted",
19306
+ explanation: "The server connection closed before this recording completed. It cannot be safely resumed, so it has been stopped. Please start a new recording.",
19307
+ options: this.options,
19308
+ cause: {
19309
+ unconfirmedFrames,
19310
+ unconfirmedSamples
19311
+ }
19312
+ });
19313
+ this.failMediaTransmission(err);
19440
19314
  }
19441
- logEvent(eventType, params) {
19442
- this.options.logger.debug(`UserMedia: ... ${EVENT_ASCII} event ${eventType}: ${string_pretty(params)}`);
19315
+ handleStreamDrain(stream) {
19316
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *drain* event emitted`);
19317
+ this.flushPendingWrites(stream);
19443
19318
  }
19444
- unloadRemainingEventListeners() {
19445
- this.options.logger.debug("UserMedia: unloadRemainingEventListeners()");
19446
- this.clearInitializationListeners?.();
19447
- mediaEvents.forEach((eventName)=>{
19448
- this.rawVisualUserMedia?.removeEventListener(eventName, this.outputEvent);
19319
+ clearBackpressureTimeout() {
19320
+ if (void 0 === this.backpressureTimeout) return;
19321
+ window.clearTimeout(this.backpressureTimeout);
19322
+ this.backpressureTimeout = void 0;
19323
+ }
19324
+ discardSocket() {
19325
+ this.options.logger.debug("Recorder: discarding socket stream ...");
19326
+ this.clearBackpressureTimeout();
19327
+ this.pendingWrites.clear();
19328
+ this.streamBackpressured = false;
19329
+ this.resumeAfterDrain = false;
19330
+ const stream = this.stream;
19331
+ this.stream = void 0;
19332
+ if (stream && !stream.destroyed) stream.destroy();
19333
+ }
19334
+ }
19335
+ const streamWriter = RecorderStreamWriter;
19336
+ var Buffer = __webpack_require__("dY").Buffer;
19337
+ class RecorderConnection extends streamWriter {
19338
+ reconnectTimeout;
19339
+ connectionTimeout;
19340
+ userMediaLoaded;
19341
+ userMediaLoading = false;
19342
+ unloaded;
19343
+ connectionFailed = false;
19344
+ reconnectAttempts = 0;
19345
+ blocking = false;
19346
+ connectingStartedAt;
19347
+ lastCloseEvent;
19348
+ lastSocketError;
19349
+ pingInterval;
19350
+ constructor(options){
19351
+ super(options);
19352
+ }
19353
+ canReconnectAfterStreamClose() {
19354
+ return Boolean(this.connected && this.userMediaLoaded);
19355
+ }
19356
+ shouldReportStreamClose() {
19357
+ return !this.connecting && !this.blocking;
19358
+ }
19359
+ handleStreamClose(url2Connect) {
19360
+ const debugLine = string_summarize(`${PIPE_SYMBOL}Stream has closed:`, {
19361
+ connecting: this.connecting,
19362
+ connected: this.connected,
19363
+ userMediaLoaded: this.userMediaLoaded,
19364
+ blocking: this.blocking
19449
19365
  });
19450
- this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
19366
+ this.options.logger.debug(debugLine);
19367
+ const canReconnect = this.canReconnectAfterStreamClose();
19368
+ this.connected = false;
19369
+ this.stopPings();
19370
+ if (this.shouldIgnoreStreamClose()) return;
19371
+ if (this.mediaTransmissionStarted) this.failInterruptedRecording();
19372
+ else if (canReconnect) {
19373
+ this.pendingWrites.clear();
19374
+ this.streamBackpressured = false;
19375
+ this.resumeAfterDrain = false;
19376
+ this.reconnecting = true;
19377
+ this.initSocket();
19378
+ } else if (this.shouldReportStreamClose()) window.setTimeout(()=>{
19379
+ this.handleConnectionFailure({
19380
+ url2Connect,
19381
+ cause: "closed"
19382
+ });
19383
+ }, 0);
19451
19384
  }
19452
- audioRecord(audioCallback) {
19453
- this.removeListener("SENDING_FIRST_FRAME");
19454
- this.audioRecorder?.record(audioCallback);
19385
+ shouldIgnoreStreamClose() {
19386
+ return this.recordingTransportFailed || Boolean(this.unloaded) || this.validate();
19455
19387
  }
19456
- init(localMediaStream, videoCallback, audioCallback, endedEarlyCallback, params) {
19457
- this.clearInitializationListeners?.();
19458
- this.stop(localMediaStream, {
19459
- aboutToInitialize: true,
19460
- preserveRecordingState: Boolean(params?.switchingFacingMode || params?.recovering)
19388
+ handleStreamError(url2Connect, err) {
19389
+ if ("u" > typeof Event && err instanceof Event) this.lastSocketError = error_getEventDetails(err);
19390
+ else this.lastSocketError = serializeError(err);
19391
+ const debugLine = string_summarize(`${PIPE_SYMBOL}Stream *error* event emitted:`, {
19392
+ error: serializeError(err)
19461
19393
  });
19462
- const initializationId = ++this.initializationId;
19463
- this.onPlayReached = false;
19464
- this.onLoadedMetaDataReached = false;
19465
- this.playingPromiseReached = false;
19466
- this.videoTrackLabel = void 0;
19467
- if (isAudioEnabled(this.options)) this.audioRecorder ??= new media_AudioRecorder(this, this.options);
19468
- const unloadAllEventListeners = ()=>{
19469
- this.options.logger.debug("UserMedia: unloadAllEventListeners()");
19470
- this.unloadRemainingEventListeners();
19471
- this.removeListener("SENDING_FIRST_FRAME");
19472
- this.rawVisualUserMedia?.removeEventListener("play", onPlay);
19473
- this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
19474
- };
19475
- this.clearInitializationListeners = ()=>{
19476
- this.rawVisualUserMedia?.removeEventListener("play", onPlay);
19477
- this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
19478
- this.clearInitializationListeners = void 0;
19394
+ this.options.logger.debug(debugLine);
19395
+ if (this.validate()) {
19396
+ this.connected = false;
19397
+ this.stopPings();
19398
+ this.discardSocket();
19399
+ return;
19400
+ }
19401
+ if (!this.connected) return void this.handleConnectionFailure({
19402
+ url2Connect,
19403
+ cause: "error"
19404
+ });
19405
+ const streamError = err instanceof Error ? err : new Error("WebSocket stream emitted an error event", {
19406
+ cause: this.lastSocketError ?? err
19407
+ });
19408
+ if (this.mediaTransmissionStarted) return void this.failInterruptedRecording();
19409
+ this.emit("ERROR", {
19410
+ err: error_createError({
19411
+ message: "WebSocket stream error",
19412
+ explanation: `The WebSocket stream emitted an error event. Details: ${string_pretty(err)}`,
19413
+ options: this.options,
19414
+ exc: streamError
19415
+ })
19416
+ });
19417
+ }
19418
+ sendPings() {
19419
+ this.stopPings();
19420
+ this.pingInterval = window.setInterval(()=>{
19421
+ this.options.logger.debug("Recorder: pinging...");
19422
+ this.writeStream(Buffer.from(""));
19423
+ }, this.options.timeouts.pingInterval);
19424
+ }
19425
+ stopPings() {
19426
+ if (void 0 === this.pingInterval) return;
19427
+ this.options.logger.debug("Recorder: stopping pings...");
19428
+ window.clearInterval(this.pingInterval);
19429
+ this.pingInterval = void 0;
19430
+ }
19431
+ clearConnectionTimeout() {
19432
+ if (void 0 === this.connectionTimeout) return;
19433
+ this.options.logger.debug("Recorder: clearConnectionTimeout()");
19434
+ window.clearTimeout(this.connectionTimeout);
19435
+ this.connectionTimeout = void 0;
19436
+ }
19437
+ clearReconnectTimeout() {
19438
+ if (void 0 === this.reconnectTimeout) return;
19439
+ this.options.logger.debug("Recorder: clearReconnectTimeout()");
19440
+ window.clearTimeout(this.reconnectTimeout);
19441
+ this.reconnectTimeout = void 0;
19442
+ }
19443
+ handleConnectionFailure(params) {
19444
+ if (this.retryConnection(params)) return;
19445
+ this.failConnection(params);
19446
+ }
19447
+ retryConnection(params) {
19448
+ if (!this.canRetryConnection()) return false;
19449
+ if (void 0 !== this.reconnectTimeout) return true;
19450
+ const delayMs = Math.min(1000 * 2 ** this.reconnectAttempts, 10000);
19451
+ this.reconnectAttempts++;
19452
+ this.connecting = false;
19453
+ this.clearConnectionTimeout();
19454
+ this.discardSocket();
19455
+ this.options.logger.debug(`Recorder: retrying WebSocket connection in ${delayMs}ms (cause ${params.cause}).`);
19456
+ this.reconnectTimeout = window.setTimeout(()=>{
19457
+ this.reconnectTimeout = void 0;
19458
+ if (this.shouldContinueReconnect()) this.initSocket();
19459
+ }, delayMs);
19460
+ return true;
19461
+ }
19462
+ canRetryConnection() {
19463
+ return Boolean(this.reconnecting && this.userMediaLoaded && this.isOnline() && !this.unloaded && !this.blocking);
19464
+ }
19465
+ shouldContinueReconnect() {
19466
+ return this.reconnecting && !this.unloaded && !this.blocking;
19467
+ }
19468
+ isOnline() {
19469
+ return navigator.onLine;
19470
+ }
19471
+ failConnection(params) {
19472
+ if (this.shouldIgnoreConnectionFailure()) return;
19473
+ this.connectionFailed = true;
19474
+ this.connecting = false;
19475
+ this.reconnecting = false;
19476
+ this.reconnectAttempts = 0;
19477
+ this.clearReconnectTimeout();
19478
+ this.clearConnectionTimeout();
19479
+ const { url2Connect, cause } = params;
19480
+ const online = this.isOnline();
19481
+ const elapsedMs = this.connectingStartedAt ? Date.now() - this.connectingStartedAt : void 0;
19482
+ const closeEvent = this.lastCloseEvent;
19483
+ const socketError = this.lastSocketError;
19484
+ const contents = {
19485
+ cause,
19486
+ closeCode: closeEvent?.code,
19487
+ closeReason: closeEvent?.reason,
19488
+ elapsedMs,
19489
+ online,
19490
+ secureContext: globalThis.isSecureContext,
19491
+ socketReadyState: this.stream?.socket.readyState,
19492
+ timeoutMs: this.options.timeouts.connection,
19493
+ url: url2Connect,
19494
+ unloaded: this.unloaded,
19495
+ userMediaLoaded: this.userMediaLoaded,
19496
+ userMediaLoading: this.userMediaLoading,
19497
+ wasClean: closeEvent?.wasClean,
19498
+ socketError,
19499
+ blocking: this.blocking,
19500
+ visibilityState: document.visibilityState
19479
19501
  };
19480
- const play = ()=>{
19481
- try {
19482
- this.rawVisualUserMedia?.load();
19483
- if (this.rawVisualUserMedia?.paused) {
19484
- this.options.logger.debug(`UserMedia: play(): media.readyState=${this.rawVisualUserMedia.readyState}, media.paused=${this.rawVisualUserMedia.paused}, media.ended=${this.rawVisualUserMedia.ended}, media.played=${string_pretty(this.rawVisualUserMedia.played)}`);
19485
- this.rawVisualUserMedia.play().then(()=>{
19486
- if (initializationId !== this.initializationId) return;
19487
- if (!this.playingPromiseReached) {
19488
- this.options.logger.debug("UserMedia: play promise successful. Playing now.");
19489
- this.playingPromiseReached = true;
19490
- }
19491
- }).catch((exc)=>{
19492
- if (initializationId !== this.initializationId) return;
19493
- if (exc instanceof Error && !params?.recovering) this.options.logger.warn(`Caught pending user media promise exception: ${exc.toString()}`);
19494
- else {
19495
- unloadAllEventListeners();
19496
- endedEarlyCallback(error_createError({
19497
- message: "Failed to play user media upon play event.",
19498
- exc,
19499
- options: this.options
19500
- }));
19501
- }
19502
- });
19503
- }
19504
- } catch (exc) {
19505
- unloadAllEventListeners();
19506
- endedEarlyCallback(exc);
19507
- }
19508
- };
19509
- const fireCallbacks = ()=>{
19510
- const readyState = this.rawVisualUserMedia?.readyState;
19511
- this.options.logger.debug(`UserMedia: fireCallbacks(readyState=${readyState}, onPlayReached=${this.onPlayReached}, onLoadedMetaDataReached=${this.onLoadedMetaDataReached})`);
19512
- if (this.onPlayReached && this.onLoadedMetaDataReached) if (this.audioRecorder) try {
19513
- this.audioRecorder.init(localMediaStream);
19514
- const onAudioReady = ()=>{
19515
- if (initializationId !== this.initializationId || this.currentVisualStream !== localMediaStream) return;
19516
- if (this.recording) this.audioRecord(audioCallback);
19517
- else this.on("SENDING_FIRST_FRAME", ()=>{
19518
- this.audioRecord(audioCallback);
19519
- });
19520
- this.clearInitializationListeners?.();
19521
- videoCallback();
19502
+ const debugLine = string_summarize("Recorder: failConnection() diagnostic", contents);
19503
+ this.options.logger.debug(debugLine);
19504
+ let explanation;
19505
+ if (online) if ("timeout" === cause) explanation = `The server at ${url2Connect} did not respond within ${this.options.timeouts.connection}ms, even though your device is online. This usually points to a firewall or proxy silently dropping the connection. Please try a different network. If the problem persists, contact us.`;
19506
+ else if ("error" === cause) explanation = `An error occurred while connecting to ${url2Connect}. Please check your internet connection and try again. If the problem persists, contact us.`;
19507
+ else {
19508
+ const closeSuffix = closeEvent ? ` (code ${closeEvent.code})` : "";
19509
+ explanation = `Connection to ${url2Connect} is closed${closeSuffix}. Please check your internet connection and try again. If the problem persists, contact us.`;
19510
+ }
19511
+ else explanation = "Your device appears to be offline. Please check your internet connection and try again.";
19512
+ this.discardSocket();
19513
+ const err = error_createError({
19514
+ message: "Unable to connect to the server",
19515
+ explanation,
19516
+ options: this.options,
19517
+ cause: contents,
19518
+ exc: socketError ? new Error("WebSocket connection error", {
19519
+ cause: socketError
19520
+ }) : void 0
19521
+ });
19522
+ this.emit("ERROR", {
19523
+ err
19524
+ });
19525
+ }
19526
+ shouldIgnoreConnectionFailure() {
19527
+ return this.connectionFailed || this.connected || Boolean(this.unloaded) || this.validate();
19528
+ }
19529
+ initSocket(cb) {
19530
+ if (this.connected) {
19531
+ this.options.logger.debug("Recorder: already **connected**. Not going to initialize a new WebSocket connection.");
19532
+ cb?.();
19533
+ return;
19534
+ }
19535
+ if (this.connecting) return void this.options.logger.debug("Recorder: already **connecting**. Not going to initialize a new WebSocket connection.");
19536
+ this.clearReconnectTimeout();
19537
+ if (util_isAutomatedUserAgent()) {
19538
+ this.connecting = false;
19539
+ this.options.logger.debug("Recorder: skipping web socket connection for an automated crawler");
19540
+ return;
19541
+ }
19542
+ this.connecting = true;
19543
+ this.connectionFailed = false;
19544
+ this.connectingStartedAt = Date.now();
19545
+ this.lastCloseEvent = void 0;
19546
+ this.lastSocketError = void 0;
19547
+ this.emit("CONNECTING");
19548
+ let url2Connect;
19549
+ try {
19550
+ const socketUrlObj = new URL(this.options.socketUrl);
19551
+ socketUrlObj.searchParams.set(constants.WHITELIST_KEY_LABEL, this.options.whitelistKey);
19552
+ url2Connect = socketUrlObj.toString();
19553
+ } catch (exc) {
19554
+ this.connecting = this.connected = false;
19555
+ const err = error_createError({
19556
+ message: "Invalid WebSocket URL",
19557
+ explanation: `The configured socketUrl "${this.options.socketUrl}" is not a valid URL. Please check your videomail-client configuration.`,
19558
+ options: this.options,
19559
+ exc
19560
+ });
19561
+ this.emit("ERROR", {
19562
+ err
19563
+ });
19564
+ return;
19565
+ }
19566
+ this.options.logger.debug(`Recorder: initializing web socket stream to ${url2Connect}`);
19567
+ let nativeSocket;
19568
+ try {
19569
+ nativeSocket = new WebSocket(url2Connect);
19570
+ nativeSocket.addEventListener("close", (event)=>{
19571
+ this.lastCloseEvent = {
19572
+ code: event.code,
19573
+ reason: event.reason,
19574
+ wasClean: event.wasClean
19522
19575
  };
19523
- if (params?.recovering) this.audioRecorder.resume().then(onAudioReady).catch(endedEarlyCallback);
19524
- else onAudioReady();
19525
- } catch (exc) {
19526
- unloadAllEventListeners();
19527
- this.stop(void 0, {
19528
- preserveRecordingState: Boolean(params?.recovering)
19529
- });
19530
- endedEarlyCallback(exc);
19531
- }
19532
- else {
19533
- this.clearInitializationListeners?.();
19534
- videoCallback();
19535
- }
19536
- };
19537
- const onPlay = ()=>{
19538
- try {
19539
- this.logEvent("play", {
19540
- readyState: this.rawVisualUserMedia?.readyState,
19541
- audio: isAudioEnabled(this.options),
19542
- width: this.rawVisualUserMedia?.width,
19543
- height: this.rawVisualUserMedia?.height,
19544
- videoWidth: this.rawVisualUserMedia?.videoWidth,
19545
- videoHeight: this.rawVisualUserMedia?.videoHeight
19576
+ });
19577
+ nativeSocket.addEventListener("error", (event)=>{
19578
+ this.lastSocketError = error_getEventDetails(event);
19579
+ });
19580
+ } catch (exc) {
19581
+ this.connecting = this.connected = false;
19582
+ const diagnostic = error_getWebSocketDiagnostic(url2Connect);
19583
+ const message = diagnostic.looksAutomated ? "Automated crawler: WebSocket not supported in this environment" : `Failed to construct WebSocket to ${url2Connect}`;
19584
+ const explanation = diagnostic.looksAutomated ? "Headless crawlers cannot use Videomail's WebSocket. No action needed." : `Please check your connection and try again. If the problem persists, contact us. Diagnostic: ${diagnostic.text}`;
19585
+ const err = error_createError({
19586
+ message,
19587
+ explanation,
19588
+ options: this.options,
19589
+ exc
19590
+ });
19591
+ this.emit("ERROR", {
19592
+ err
19593
+ });
19594
+ return;
19595
+ }
19596
+ try {
19597
+ this.stream = stream_default()(nativeSocket);
19598
+ } catch (exc) {
19599
+ this.connecting = this.connected = false;
19600
+ const err = error_createError({
19601
+ message: `Failed to create a stream to ${url2Connect}`,
19602
+ explanation: "Please check your connection and try again. If the problem persists, contact us.",
19603
+ options: this.options,
19604
+ exc
19605
+ });
19606
+ this.emit("ERROR", {
19607
+ err
19608
+ });
19609
+ }
19610
+ const stream = this.stream;
19611
+ if (stream) {
19612
+ const connectionTimeoutMs = this.options.timeouts.connection;
19613
+ this.connectionTimeout = window.setTimeout(()=>{
19614
+ if (this.isOnline() && this.isUserMediaLoaded()) {
19615
+ this.options.logger.debug(`${PIPE_SYMBOL}Reconnecting due to connection timeout.`);
19616
+ this.reconnecting = true;
19617
+ this.handleConnectionFailure({
19618
+ url2Connect,
19619
+ cause: "timeout"
19620
+ });
19621
+ } else this.handleConnectionFailure({
19622
+ url2Connect,
19623
+ cause: "timeout"
19546
19624
  });
19547
- this.rawVisualUserMedia?.removeEventListener("play", onPlay);
19548
- if (this.hasEnded() || this.hasInvalidDimensions()) endedEarlyCallback(error_createError({
19549
- message: "Already busy",
19550
- explanation: "Probably another browser window is using your webcam?",
19551
- options: this.options
19552
- }));
19553
- else {
19554
- this.onPlayReached = true;
19555
- fireCallbacks();
19625
+ }, connectionTimeoutMs);
19626
+ stream.on("close", ()=>{
19627
+ this.handleStreamClose(url2Connect);
19628
+ });
19629
+ stream.on("connect", (args)=>{
19630
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *connect* event emitted with args: ${string_pretty(args)}`);
19631
+ this.clearConnectionTimeout();
19632
+ const isClosing = this.stream?.socket.readyState === WebSocket.CLOSING;
19633
+ if (!this.connected && !isClosing && !this.unloaded) {
19634
+ this.connected = true;
19635
+ this.connecting = this.unloaded = false;
19636
+ this.reconnecting = false;
19637
+ this.reconnectAttempts = 0;
19638
+ this.clearReconnectTimeout();
19639
+ this.emit("CONNECTED");
19640
+ cb?.();
19556
19641
  }
19557
- } catch (exc) {
19558
- unloadAllEventListeners();
19559
- endedEarlyCallback(exc);
19642
+ });
19643
+ stream.on("data", (data)=>{
19644
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *data* event emitted`);
19645
+ try {
19646
+ const command = JSON.parse(data.toString());
19647
+ this.executeCommand(command);
19648
+ } catch (exc) {
19649
+ this.options.logger.error(`Failed to parse command: ${exc}`);
19650
+ const err = error_createError({
19651
+ message: "Invalid server command",
19652
+ explanation: `Contact us. The invalid command was: ${data.toString()}.`,
19653
+ options: this.options,
19654
+ exc
19655
+ });
19656
+ this.emit("ERROR", {
19657
+ err
19658
+ });
19659
+ }
19660
+ });
19661
+ stream.on("error", (err)=>{
19662
+ this.handleStreamError(url2Connect, err);
19663
+ });
19664
+ stream.on("drain", ()=>{
19665
+ this.handleStreamDrain(stream);
19666
+ });
19667
+ stream.on("preend", (args)=>{
19668
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *preend* event emitted with args: ${string_pretty(args)}`);
19669
+ });
19670
+ stream.on("end", ()=>{
19671
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *end* event emitted`);
19672
+ });
19673
+ stream.on("pipe", (src)=>{
19674
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *pipe* event emitted with src: ${string_pretty(src)}`);
19675
+ });
19676
+ stream.on("unpipe", (src)=>{
19677
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *unpipe* event emitted with src: ${string_pretty(src)}`);
19678
+ });
19679
+ stream.on("resume", (args)=>{
19680
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *resume* event emitted with args: ${string_pretty(args)}`);
19681
+ });
19682
+ stream.on("uncork", (args)=>{
19683
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *uncork* event emitted with args: ${string_pretty(args)}`);
19684
+ });
19685
+ stream.on("readable", (args)=>{
19686
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *readable* event emitted with args: ${string_pretty(args)}`);
19687
+ });
19688
+ stream.on("prefinish", (args)=>{
19689
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *prefinish* event emitted with args: ${string_pretty(args)}`);
19690
+ });
19691
+ stream.on("finish", (args)=>{
19692
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *finish* event emitted with args: ${string_pretty(args)}`);
19693
+ });
19694
+ }
19695
+ }
19696
+ writeCommand(command, args, cb) {
19697
+ if (this.connected) {
19698
+ if (this.stream) {
19699
+ if (args) this.options.logger.debug(`$ ${command} with ${string_pretty(args)}`);
19700
+ else this.options.logger.debug(`$ ${command}`);
19701
+ const commandObj = {
19702
+ command,
19703
+ args
19704
+ };
19705
+ this.writeStream(Buffer.from(JSON.stringify(commandObj)), cb ? {
19706
+ onFlushedCallback: ()=>{
19707
+ queueMicrotask(cb);
19708
+ }
19709
+ } : void 0);
19560
19710
  }
19561
- };
19562
- const onLoadedMetaData = ()=>{
19563
- this.logEvent("loadedmetadata", {
19564
- readyState: this.rawVisualUserMedia?.readyState,
19565
- paused: this.rawVisualUserMedia?.paused,
19566
- width: this.rawVisualUserMedia?.width,
19567
- height: this.rawVisualUserMedia?.height,
19568
- videoWidth: this.rawVisualUserMedia?.videoWidth,
19569
- videoHeight: this.rawVisualUserMedia?.videoHeight
19711
+ } else {
19712
+ this.options.logger.debug(`Reconnecting for the command ${command} …`);
19713
+ this.initSocket(()=>{
19714
+ this.writeCommand(command, args, cb);
19715
+ });
19716
+ }
19717
+ }
19718
+ }
19719
+ const connection = RecorderConnection;
19720
+ function createUserMediaConstraints({ options, facingMode, limitedWidth, userMediaParams }) {
19721
+ const video = {
19722
+ frameRate: {
19723
+ ideal: options.video.fps
19724
+ }
19725
+ };
19726
+ if (userMediaParams?.switchingFacingMode || userMediaParams?.recovering) video.facingMode = userMediaParams.switchingFacingMode || facingMode;
19727
+ if (options.video.width) video.width = {
19728
+ ideal: options.video.width
19729
+ };
19730
+ else if (limitedWidth?.value) video.width = {
19731
+ ideal: limitedWidth.value
19732
+ };
19733
+ if (options.video.height) video.height = {
19734
+ ideal: options.video.height
19735
+ };
19736
+ return {
19737
+ audio: isAudioEnabled(options),
19738
+ video
19739
+ };
19740
+ }
19741
+ const mediaConstraints = createUserMediaConstraints;
19742
+ class RecorderMedia extends connection {
19743
+ visuals;
19744
+ recorderElement;
19745
+ userMedia;
19746
+ userMediaTimeout;
19747
+ retryTimeout;
19748
+ stopTimeout;
19749
+ mediaRequestId = 0;
19750
+ cameraRecovery;
19751
+ resolveCameraRecovery;
19752
+ resumeAfterCameraRecovery = false;
19753
+ submitting = false;
19754
+ built = false;
19755
+ key;
19756
+ facingMode;
19757
+ constructor(visuals, options){
19758
+ super(options);
19759
+ this.visuals = visuals;
19760
+ this.facingMode = options.video.facingMode;
19761
+ }
19762
+ show() {
19763
+ html_showElement(this.recorderElement);
19764
+ }
19765
+ clearRetryTimeout() {
19766
+ if (void 0 === this.retryTimeout) return;
19767
+ this.options.logger.debug("Recorder: clearRetryTimeout()");
19768
+ window.clearTimeout(this.retryTimeout);
19769
+ this.retryTimeout = void 0;
19770
+ }
19771
+ showUserMedia() {
19772
+ if (!this.connected) return false;
19773
+ const hidden = this.isHidden();
19774
+ if (!hidden) return true;
19775
+ const notifying = this.isNotifying();
19776
+ if (notifying) return true;
19777
+ return this.blocking;
19778
+ }
19779
+ userMediaErrorCallback(err, usedConstraints) {
19780
+ if (this.cameraRecovery) return void this.failCameraRecovery(err);
19781
+ if (this.unloaded) return void this.options.logger.debug(`Recorder: already unloaded. Not going to throw error ${string_pretty(err)}`);
19782
+ this.userMediaLoading = false;
19783
+ this.clearUserMediaTimeout();
19784
+ const characteristics = this.userMedia?.getCharacteristics();
19785
+ this.options.logger.debug(`Recorder: userMediaErrorCallback(), name: ${err.name}, message: ${err.message} and Webcam characteristics: ${characteristics ? string_pretty(characteristics) : "none"}`);
19786
+ const errorListeners = this.getListeners("ERROR");
19787
+ if (errorListeners?.length) if (err.name !== error_VideomailError.MEDIA_DEVICE_NOT_SUPPORTED) {
19788
+ const videomailError = error_createError({
19789
+ err,
19790
+ options: this.options,
19791
+ usedConstraints
19570
19792
  });
19571
- this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
19572
- if (!this.hasEnded() && !this.hasInvalidDimensions()) {
19573
- this.emit("LOADED_META_DATA");
19574
- this.onLoadedMetaDataReached = true;
19575
- fireCallbacks();
19576
- }
19577
- };
19578
- try {
19579
- const videoTrack = media_getFirstVideoTrack(localMediaStream);
19580
- this.videoTrackLabel = videoTrack?.label;
19581
- if (videoTrack) if (videoTrack.enabled) if (util_isVirtualCamera(videoTrack)) throw error_createError({
19582
- message: "Virtual cameras are not allowed.",
19583
- explanation: `Please use a real camera, not "${videoTrack.label}".`,
19793
+ this.emit("ERROR", {
19794
+ err: videomailError
19795
+ });
19796
+ } else {
19797
+ this.options.logger.debug(`Recorder: ignore user media error ${string_pretty(err)}`);
19798
+ this.retryTimeout = window.setTimeout(()=>{
19799
+ this.retryTimeout = void 0;
19800
+ if (!this.unloaded) this.loadUserMedia();
19801
+ }, this.options.timeouts.userMedia);
19802
+ }
19803
+ else {
19804
+ this.options.logger.debug(`Recorder: no error listeners attached but throwing error ${string_pretty(err)}`);
19805
+ throw error_createError({
19806
+ err,
19807
+ message: "Unable to process this error since there are no error listeners anymore.",
19584
19808
  options: this.options
19585
19809
  });
19586
- else {
19587
- let description = "";
19588
- if (videoTrack.label.length > 0) description = description.concat(videoTrack.label);
19589
- description = description.concat(` with enabled=${videoTrack.enabled}, muted=${videoTrack.muted}, readyState=${videoTrack.readyState}`);
19590
- this.options.logger.debug(`UserMedia: ${videoTrack.kind} detected. ${description}`);
19591
- }
19592
- else throw error_createError({
19593
- message: "Webcam is disabled",
19594
- explanation: "The video track seems to be disabled. Enable it in your system.",
19810
+ }
19811
+ }
19812
+ getUserMediaCallback(localStream, params, requestId = this.mediaRequestId) {
19813
+ if (!this.userMedia) {
19814
+ localStream.getTracks().forEach((track)=>{
19815
+ track.stop();
19816
+ });
19817
+ throw error_createError({
19818
+ message: "No user media is defined",
19595
19819
  options: this.options
19596
19820
  });
19597
- else this.options.logger.debug("UserMedia: detected (but no video tracks exist");
19598
- this.rawVisualUserMedia?.addEventListener("loadedmetadata", onLoadedMetaData);
19599
- this.rawVisualUserMedia?.addEventListener("play", onPlay);
19600
- this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
19601
- this.rawVisualUserMedia?.addEventListener("error", this.handleMediaError);
19602
- this.setVisualStream(localMediaStream);
19603
- play();
19821
+ }
19822
+ this.options.logger.debug(`Recorder: getUserMediaCallback(${params ? string_pretty(params) : ""})`);
19823
+ if (this.showUserMedia()) try {
19824
+ this.userMedia.init(localStream, ()=>{
19825
+ if (!this.unloaded && requestId === this.mediaRequestId) this.onUserMediaReady(params);
19826
+ }, this.onAudioSample.bind(this), (err)=>{
19827
+ if (!this.unloaded && requestId === this.mediaRequestId) this.userMediaErrorCallback(err, {
19828
+ audio: isAudioEnabled(this.options)
19829
+ });
19830
+ }, params);
19604
19831
  } catch (exc) {
19605
- unloadAllEventListeners();
19606
- endedEarlyCallback(exc);
19832
+ localStream.getTracks().forEach((track)=>{
19833
+ track.stop();
19834
+ });
19835
+ this.userMediaLoading = false;
19836
+ this.clearUserMediaTimeout();
19837
+ this.userMediaErrorCallback(exc, {
19838
+ audio: isAudioEnabled(this.options)
19839
+ });
19840
+ }
19841
+ else {
19842
+ localStream.getTracks().forEach((track)=>{
19843
+ track.stop();
19844
+ });
19845
+ this.userMediaLoading = false;
19846
+ this.clearUserMediaTimeout();
19847
+ this.cancelCameraRecovery();
19607
19848
  }
19608
19849
  }
19609
- getVideoTrackLabel() {
19610
- return this.videoTrackLabel;
19611
- }
19612
- isReady() {
19613
- return Boolean(this.rawVisualUserMedia?.srcObject || this.rawVisualUserMedia?.currentSrc);
19850
+ loadGenuineUserMedia(params) {
19851
+ this.options.logger.debug(`Recorder: loadGenuineUserMedia(${params ? string_pretty(params) : ""})`);
19852
+ this.emit("ASKING_WEBCAM_PERMISSION");
19853
+ this.userMediaLoading = true;
19854
+ const requestId = ++this.mediaRequestId;
19855
+ const limitedWidth = this.options.video.width ? void 0 : this.visuals.limitWidth();
19856
+ const constraints = mediaConstraints({
19857
+ facingMode: this.facingMode,
19858
+ limitedWidth,
19859
+ options: this.options,
19860
+ userMediaParams: params
19861
+ });
19862
+ this.options.logger.debug(string_summarize("Recorder: our webcam constraints are:", constraints));
19863
+ this.options.logger.debug(string_summarize("Recorder: available webcam constraints are:", navigator.mediaDevices.getSupportedConstraints()));
19864
+ const streamPromise = navigator.mediaDevices.getUserMedia(constraints);
19865
+ streamPromise.then((localStream)=>{
19866
+ if (this.unloaded || requestId !== this.mediaRequestId) return void localStream.getTracks().forEach((track)=>{
19867
+ track.stop();
19868
+ });
19869
+ this.getUserMediaCallback(localStream, params, requestId);
19870
+ }).catch((reason)=>{
19871
+ if (!this.unloaded && requestId === this.mediaRequestId) this.userMediaErrorCallback(reason, constraints);
19872
+ });
19614
19873
  }
19615
- stop(visualStream, params) {
19874
+ loadUserMedia(params) {
19875
+ if (this.userMediaLoaded) {
19876
+ this.options.logger.debug("Recorder: skipping loadUserMedia() because it is already loaded");
19877
+ this.onUserMediaReady(params);
19878
+ return;
19879
+ } else if (this.userMediaLoading) return void this.options.logger.debug("Recorder: skipping loadUserMedia() because it is already asking for permission");
19880
+ this.options.logger.debug(`Recorder: loadUserMedia(${params ? string_pretty(params) : ""})`);
19881
+ this.emit("LOADING_USER_MEDIA");
19616
19882
  try {
19617
- let chosenStream = visualStream;
19618
- if (!params?.aboutToInitialize) {
19619
- this.initializationId++;
19620
- this.clearInitializationListeners?.();
19621
- chosenStream ??= this.currentVisualStream;
19622
- const tracks = chosenStream?.getTracks();
19623
- if (tracks) tracks.forEach((track)=>{
19624
- track.stop();
19625
- });
19626
- this.setVisualStream();
19627
- this.audioRecorder?.stop();
19628
- this.audioRecorder = void 0;
19629
- }
19630
- if (!params?.preserveRecordingState) this.paused = this.recording = false;
19883
+ if (!navigator.mediaDevices) throw new Error("No media devices are available.");
19884
+ this.userMediaTimeout = window.setTimeout(()=>{
19885
+ if (!this.userMediaLoaded) {
19886
+ this.userMediaLoading = false;
19887
+ const err = util_getBrowser(this.options).getNoAccessIssue();
19888
+ this.userMediaErrorCallback(err, {
19889
+ audio: isAudioEnabled(this.options)
19890
+ });
19891
+ }
19892
+ }, this.options.timeouts.userMedia);
19893
+ this.userMediaLoading = true;
19894
+ this.loadGenuineUserMedia(params);
19631
19895
  } catch (exc) {
19632
- this.emit("ERROR", {
19896
+ this.clearUserMediaTimeout();
19897
+ this.options.logger.debug("Recorder: failed to load genuine user media");
19898
+ this.userMediaLoading = false;
19899
+ if (this.cameraRecovery) return void this.failCameraRecovery(exc);
19900
+ const errorListeners = this.getListeners("ERROR");
19901
+ if (errorListeners?.length) this.emit("ERROR", {
19633
19902
  exc
19634
19903
  });
19904
+ else {
19905
+ this.options.logger.debug("Recorder: no error listeners attached but throwing exception further");
19906
+ throw exc;
19907
+ }
19635
19908
  }
19636
19909
  }
19637
- createCanvas() {
19638
- const canvas = document.createElement("canvas");
19639
- const rawWidthDimension = this.getRawWidth(true);
19640
- if (rawWidthDimension.value) canvas.width = rawWidthDimension.value;
19641
- const rawHeight = this.getRawHeight(true);
19642
- if (rawHeight) canvas.height = rawHeight;
19643
- return canvas;
19644
- }
19645
- getVideoHeight() {
19646
- if (!this.rawVisualUserMedia) return;
19647
- return this.rawVisualUserMedia.videoHeight || this.rawVisualUserMedia.height;
19648
- }
19649
- getVideoWidth() {
19650
- if (!this.rawVisualUserMedia) return;
19651
- return this.rawVisualUserMedia.videoWidth || this.rawVisualUserMedia.width;
19910
+ isNotifying() {
19911
+ return this.visuals.isNotifying();
19652
19912
  }
19653
- hasVideoWidth() {
19654
- const videoWidth = this.getVideoWidth();
19655
- return videoWidth && videoWidth > 0;
19913
+ isHidden() {
19914
+ return !this.recorderElement || html_isHidden(this.recorderElement);
19656
19915
  }
19657
- getRawWidth(responsive) {
19658
- let rawWidth;
19659
- const widthDimension = {
19660
- unit: "px"
19661
- };
19662
- rawWidth = responsive ? this.getVideoWidth() : this.options.video.width ? this.options.video.width : this.getVideoWidth();
19663
- if (responsive) {
19664
- const widthDimension = this.recorder.limitWidth(rawWidth);
19665
- rawWidth = widthDimension?.value;
19916
+ clearUserMediaTimeout() {
19917
+ if (void 0 !== this.userMediaTimeout) {
19918
+ this.options.logger.debug("Recorder: clearUserMediaTimeout()");
19919
+ window.clearTimeout(this.userMediaTimeout);
19920
+ this.userMediaTimeout = void 0;
19666
19921
  }
19667
- widthDimension.value = rawWidth;
19668
- return widthDimension;
19669
19922
  }
19670
- getRawHeight(responsive) {
19671
- let rawHeight;
19672
- if (responsive) {
19673
- rawHeight = this.options.video.height || this.getVideoHeight();
19674
- const ratio = this.recorder.getRatio();
19675
- const rawWidth = this.getRawWidth(responsive);
19676
- if (void 0 !== ratio && rawWidth.value) rawHeight = rawWidth.value * ratio;
19677
- } else rawHeight = this.options.video.height ? this.options.video.height : this.getVideoHeight();
19678
- if (responsive) {
19679
- const heightDimension = this.recorder.limitHeight(rawHeight);
19680
- rawHeight = heightDimension.value;
19681
- }
19682
- return rawHeight;
19923
+ clearStopTimeout() {
19924
+ if (void 0 === this.stopTimeout) return;
19925
+ window.clearTimeout(this.stopTimeout);
19926
+ this.stopTimeout = void 0;
19683
19927
  }
19684
- getRawVisuals() {
19685
- return this.rawVisualUserMedia;
19928
+ isReady() {
19929
+ return this.userMedia?.isReady();
19686
19930
  }
19687
- pause() {
19688
- this.paused = true;
19931
+ finishCameraRecovery(restored) {
19932
+ this.clearUserMediaTimeout();
19933
+ const resolve = this.resolveCameraRecovery;
19934
+ const resumeRequested = restored && this.resumeAfterCameraRecovery;
19935
+ this.cameraRecovery = void 0;
19936
+ this.resolveCameraRecovery = void 0;
19937
+ this.resumeAfterCameraRecovery = false;
19938
+ resolve?.(restored);
19939
+ if (resumeRequested && this.isPaused()) this.resume();
19689
19940
  }
19690
- isPaused() {
19691
- return this.paused;
19941
+ cancelCameraRecovery() {
19942
+ if (!this.cameraRecovery) return;
19943
+ this.mediaRequestId++;
19944
+ this.userMediaLoading = false;
19945
+ this.userMediaLoaded = false;
19946
+ this.clearUserMediaTimeout();
19947
+ this.userMedia?.stop(void 0, {
19948
+ preserveRecordingState: true
19949
+ });
19950
+ this.finishCameraRecovery(false);
19951
+ }
19952
+ failCameraRecovery(exc) {
19953
+ this.cancelCameraRecovery();
19954
+ const err = error_createError({
19955
+ message: "Camera recovery failed",
19956
+ explanation: "Your recording is still paused. Check camera permissions, then try Resume again or stop to keep the recorded part.",
19957
+ exc,
19958
+ options: this.options
19959
+ });
19960
+ this.emit("CAMERA_RECOVERY_FAILED", {
19961
+ err
19962
+ });
19692
19963
  }
19693
- resume() {
19694
- this.paused = false;
19964
+ recoverCamera() {
19965
+ if (this.cameraRecovery) return this.cameraRecovery;
19966
+ if (!this.built || this.unloaded || !this.userMedia || this.userMediaLoading || this.submitting || this.key || void 0 !== this.stopTimeout || this.recordingTransportFailed) return Promise.resolve(false);
19967
+ const liveStream = this.userMedia.hasLiveStream();
19968
+ if (liveStream && this.isRecording()) return Promise.resolve(true);
19969
+ if (!liveStream && this.recordingActive && !this.isPaused()) this.pause();
19970
+ const recovery = new Promise((resolve)=>{
19971
+ this.resolveCameraRecovery = resolve;
19972
+ });
19973
+ this.cameraRecovery = recovery;
19974
+ this.emit("CAMERA_RECOVERING");
19975
+ if (liveStream) {
19976
+ this.userMediaTimeout = window.setTimeout(()=>{
19977
+ this.failCameraRecovery(new Error("Camera playback recovery timed out."));
19978
+ }, this.options.timeouts.userMedia);
19979
+ this.userMedia.restorePlayback().then(()=>{
19980
+ if (this.cameraRecovery === recovery) this.finishCameraRecovery(true);
19981
+ }).catch((exc)=>{
19982
+ if (this.cameraRecovery === recovery) this.failCameraRecovery(exc);
19983
+ });
19984
+ } else {
19985
+ this.userMediaLoaded = false;
19986
+ this.loadUserMedia({
19987
+ recovering: true
19988
+ });
19989
+ }
19990
+ return recovery;
19695
19991
  }
19696
- record() {
19697
- this.recording = true;
19992
+ switchFacingMode() {
19993
+ if (this.cameraRecovery || !util_getBrowser(this.options).isMobile()) return;
19994
+ if ("user" === this.facingMode) this.facingMode = "environment";
19995
+ else if ("environment" === this.facingMode) this.facingMode = "user";
19996
+ else this.options.logger.warn(`Recorder: unsupported facing mode ${string_pretty(this.facingMode)}`);
19997
+ this.loadGenuineUserMedia({
19998
+ switchingFacingMode: this.facingMode
19999
+ });
19698
20000
  }
19699
- isRecording() {
19700
- return this.recording;
20001
+ hide() {
20002
+ if (!this.isHidden()) {
20003
+ if (this.recorderElement) html_hideElement(this.recorderElement);
20004
+ this.clearUserMediaTimeout();
20005
+ this.clearRetryTimeout();
20006
+ }
19701
20007
  }
19702
- getAudioSampleRate() {
19703
- if (this.audioRecorder) return this.audioRecorder.getSampleRate();
19704
- return -1;
20008
+ isUnloaded() {
20009
+ return this.unloaded;
19705
20010
  }
19706
- getCharacteristics() {
19707
- return {
19708
- audioSampleRate: this.getAudioSampleRate(),
19709
- muted: this.rawVisualUserMedia?.muted,
19710
- width: this.rawVisualUserMedia?.width,
19711
- height: this.rawVisualUserMedia?.height,
19712
- videoWidth: this.rawVisualUserMedia?.videoWidth,
19713
- videoHeight: this.rawVisualUserMedia?.videoHeight
19714
- };
20011
+ isUserMediaLoaded() {
20012
+ return this.userMediaLoaded;
19715
20013
  }
19716
20014
  }
19717
- const visuals_userMedia = UserMedia;
19718
- var Buffer = __webpack_require__("dY").Buffer;
19719
- const PIPE_SYMBOL = "°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸ ";
19720
- class Recorder extends util_Despot {
19721
- visuals;
20015
+ const media = RecorderMedia;
20016
+ function calculateProgress(confirmedCount, totalCount) {
20017
+ return `${(confirmedCount / (totalCount || 1) * 100).toFixed(2)}%`;
20018
+ }
20019
+ function getConfirmedProgressNumber({ args, kind, options }) {
20020
+ const label = "frame" === kind ? "frame" : "audio sample";
20021
+ if (!args) throw error_createError({
20022
+ message: `Arguments are missing for updating the ${label} progress`,
20023
+ options
20024
+ });
20025
+ const confirmedNumber = args[kind];
20026
+ if (!confirmedNumber) throw error_createError({
20027
+ message: `The ${label} number is missing`,
20028
+ options
20029
+ });
20030
+ return confirmedNumber;
20031
+ }
20032
+ var capture_Buffer = __webpack_require__("dY").Buffer;
20033
+ class RecorderCapture extends media {
19722
20034
  replay;
19723
- pendingWrites = new boundedWriteQueue();
19724
20035
  loop;
19725
20036
  originalAnimationFrameObject;
19726
- samplesCount = 0;
19727
- framesCount = 0;
19728
20037
  recordingAudioSampleRate;
19729
- recordingStats;
19730
- confirmedFrameNumber = 0;
19731
- confirmedSampleNumber = 0;
19732
- recorderElement;
19733
- userMedia;
19734
- userMediaTimeout;
19735
- retryTimeout;
19736
- reconnectTimeout;
19737
- connectionTimeout;
19738
- backpressureTimeout;
19739
- stopTimeout;
19740
20038
  frameProgress;
19741
20039
  sampleProgress;
19742
20040
  canvas;
19743
- recordingDimensions;
19744
20041
  ctx;
19745
- userMediaLoaded;
19746
- userMediaLoading = false;
19747
- mediaRequestId = 0;
19748
- cameraRecovery;
19749
- resolveCameraRecovery;
19750
- resumeAfterCameraRecovery = false;
19751
- submitting = false;
19752
- unloaded;
19753
20042
  stopTime;
19754
- stream;
19755
- streamBackpressured = false;
19756
- resumeAfterDrain = false;
19757
- mediaTransmissionStarted = false;
19758
- recordingActive = false;
19759
- recordingTransportFailed = false;
19760
- connecting = false;
19761
- connected = false;
19762
- connectionFailed = false;
19763
- reconnecting = false;
19764
- reconnectAttempts = 0;
19765
- blocking = false;
19766
- built = false;
19767
- key;
19768
20043
  waitingTime;
19769
- connectingStartedAt;
19770
- lastCloseEvent;
19771
- lastSocketError;
19772
- pingInterval;
19773
20044
  frame;
19774
20045
  recordingBuffer;
19775
- facingMode;
19776
20046
  constructor(visuals, replay, options){
19777
- super("Recorder", options);
19778
- this.visuals = visuals;
20047
+ super(visuals, options);
19779
20048
  this.replay = replay;
19780
- this.facingMode = options.video.facingMode;
19781
- }
19782
- hasExceededWebSocketSendBuffer(buffer, opts) {
19783
- return buffer.length > 0 && !this.pendingWrites.enqueue(buffer, opts);
19784
20049
  }
19785
- writeStream(buffer, opts) {
19786
- if (this.stream) if (this.stream.destroyed) {
19787
- this.stopPings();
19788
- const err = error_createError({
19789
- message: "Already disconnected",
19790
- explanation: "Sorry, connection to the server has been destroyed. Please reload.",
19791
- options: this.options
19792
- });
19793
- this.handleWriteFailure(err);
19794
- } else if (this.streamBackpressured) {
19795
- if (this.hasExceededWebSocketSendBuffer(buffer, opts)) this.failMediaTransmission(error_createError({
19796
- message: "WebSocket send buffer limit exceeded",
19797
- explanation: "The connection to the server is too slow to keep up with this recording. The recording has been stopped; please start a new one.",
19798
- options: this.options
19799
- }));
19800
- } else this.writeToStream(this.stream, buffer, opts);
20050
+ onAudioSample(audioSample) {
20051
+ if (this.recordingTransportFailed || this.cameraRecovery) return;
20052
+ const sampleRate = this.userMedia?.getAudioSampleRate();
20053
+ if (sampleRate && sampleRate > 0) this.recordingAudioSampleRate ??= sampleRate;
20054
+ this.samplesCount++;
20055
+ this.mediaTransmissionStarted = true;
20056
+ const audioBuffer = audioSample.toBuffer();
20057
+ this.writeStream(audioBuffer);
19801
20058
  }
19802
- writeToStream(stream, buffer, opts) {
19803
- const onFlushedCallback = opts?.onFlushedCallback;
20059
+ onUserMediaReady(params) {
19804
20060
  try {
19805
- const canContinue = stream.write(buffer, ()=>{
19806
- if (!onFlushedCallback) return;
19807
- try {
19808
- onFlushedCallback(opts);
19809
- } catch (exc) {
19810
- const err = error_createError({
19811
- message: "Failed to write stream buffer",
19812
- explanation: `stream.write() failed because of ${string_pretty(exc)}`,
19813
- options: this.options,
19814
- exc
19815
- });
19816
- this.emit("ERROR", {
19817
- err
19818
- });
19819
- }
19820
- });
19821
- if (!canContinue) {
19822
- this.streamBackpressured = true;
19823
- this.startBackpressureTimeout();
19824
- this.pauseForBackpressure();
19825
- return false;
20061
+ this.options.logger.debug(`Recorder: onUserMediaReady(${params ? string_pretty(params) : ""})`);
20062
+ const switchingFacingMode = params?.switchingFacingMode;
20063
+ if (params?.recovering) {
20064
+ if (!this.userMedia?.hasLiveStream()) return void this.failCameraRecovery(new Error("The restored camera has no live tracks."));
20065
+ if (void 0 !== this.recordingAudioSampleRate && this.userMedia.getAudioSampleRate() !== this.recordingAudioSampleRate) return void this.failCameraRecovery(new Error("The audio sample rate changed during recovery."));
19826
20066
  }
19827
- return true;
20067
+ this.userMediaLoading = this.blocking = this.unloaded = this.submitting = false;
20068
+ this.userMediaLoaded = true;
20069
+ this.clearUserMediaTimeout();
20070
+ if (!switchingFacingMode && !params?.recovering) this.loop = this.createLoop();
20071
+ this.show();
20072
+ if (params?.recordWhenReady) this.record();
20073
+ this.emit("USER_MEDIA_READY", {
20074
+ switchingFacingMode: params?.switchingFacingMode,
20075
+ recovering: params?.recovering,
20076
+ paused: this.isPaused(),
20077
+ recordWhenReady: params?.recordWhenReady
20078
+ });
20079
+ if (params?.recovering) this.finishCameraRecovery(true);
19828
20080
  } catch (exc) {
19829
- const err = error_createError({
19830
- message: "Failed writing to server",
19831
- explanation: `stream.write() failed because of ${string_pretty(exc)}`,
19832
- options: this.options,
20081
+ if (params?.recovering) this.failCameraRecovery(exc);
20082
+ else this.emit("ERROR", {
19833
20083
  exc
19834
20084
  });
19835
- this.handleWriteFailure(err);
19836
- return false;
19837
20085
  }
19838
20086
  }
19839
- flushPendingWrites(stream) {
19840
- if (this.isStaleStream(stream)) return;
19841
- this.streamBackpressured = false;
19842
- this.clearBackpressureTimeout();
19843
- let pendingWrite = this.pendingWrites.dequeue();
19844
- while(pendingWrite){
19845
- if (!this.writeToStream(stream, pendingWrite.buffer, pendingWrite.params)) return;
19846
- pendingWrite = this.pendingWrites.dequeue();
19847
- }
19848
- this.resumeRecordingAfterDrain();
20087
+ calculateFrameProgress() {
20088
+ return calculateProgress(this.confirmedFrameNumber, this.framesCount);
20089
+ }
20090
+ calculateSampleProgress() {
20091
+ return calculateProgress(this.confirmedSampleNumber, this.samplesCount);
20092
+ }
20093
+ updateOverallProgress() {
20094
+ this.frameProgress = this.calculateFrameProgress();
20095
+ if (isAudioEnabled(this.options)) this.sampleProgress = this.calculateSampleProgress();
20096
+ this.emit("PROGRESS", {
20097
+ frameProgress: this.frameProgress,
20098
+ sampleProgress: this.sampleProgress
20099
+ });
20100
+ }
20101
+ updateFrameProgress(args) {
20102
+ this.confirmedFrameNumber = getConfirmedProgressNumber({
20103
+ args,
20104
+ kind: "frame",
20105
+ options: this.options
20106
+ });
20107
+ this.frameProgress = this.calculateFrameProgress();
20108
+ this.updateOverallProgress();
20109
+ }
20110
+ updateSampleProgress(args) {
20111
+ this.confirmedSampleNumber = getConfirmedProgressNumber({
20112
+ args,
20113
+ kind: "sample",
20114
+ options: this.options
20115
+ });
20116
+ this.sampleProgress = this.calculateSampleProgress();
20117
+ this.updateOverallProgress();
20118
+ }
20119
+ preview(args) {
20120
+ if (!args) throw error_createError({
20121
+ message: "Preview arguments are missing.",
20122
+ options: this.options
20123
+ });
20124
+ const hasAudio = this.samplesCount > 0;
20125
+ this.mediaTransmissionStarted = false;
20126
+ this.recordingActive = false;
20127
+ this.recordingTransportFailed = false;
20128
+ this.confirmedFrameNumber = this.confirmedSampleNumber = this.samplesCount = this.framesCount = 0;
20129
+ this.sampleProgress = this.frameProgress = void 0;
20130
+ this.key = args.key;
20131
+ if (args.mp4) this.replay.setMp4Source(`${args.mp4 + constants.WHITELIST_KEY_LABEL}/${this.options.whitelistKey}/videomail.mp4`, true);
20132
+ if (args.webm) this.replay.setWebMSource(`${args.webm + constants.WHITELIST_KEY_LABEL}/${this.options.whitelistKey}/videomail.webm`, true);
20133
+ this.hide();
20134
+ const widthDimension = this.getRecorderWidth(true);
20135
+ const heightDimension = this.getRecorderHeight(true);
20136
+ const duration = args.duration ?? -1;
20137
+ this.emit("PREVIEW", {
20138
+ key: this.key,
20139
+ width: widthDimension?.value,
20140
+ height: heightDimension?.value,
20141
+ hasAudio,
20142
+ duration
20143
+ });
20144
+ if (this.stopTime) this.waitingTime = Date.now() - this.stopTime;
20145
+ this.recordingStats ??= {};
20146
+ this.recordingStats.waitingTime = this.waitingTime;
20147
+ }
20148
+ cancelAnimationFrame() {
20149
+ this.loop?.dispose();
19849
20150
  }
19850
- isStaleStream(stream) {
19851
- return this.stream !== stream || stream.destroyed;
20151
+ getIntervalSum() {
20152
+ if (!this.loop) return;
20153
+ return this.loop.getElapsedTime();
19852
20154
  }
19853
- canPauseForBackpressure() {
19854
- return Boolean(this.recordingActive && !this.userMedia?.isPaused() && this.loop?.isRunning());
20155
+ getAvgInterval() {
20156
+ const intervalSum = this.getIntervalSum() ?? 0;
20157
+ if (!this.hasValidInterval(intervalSum)) return;
20158
+ return intervalSum / this.framesCount;
19855
20159
  }
19856
- pauseForBackpressure() {
19857
- if (!this.canPauseForBackpressure()) return;
19858
- this.resumeAfterDrain = true;
19859
- this.loop.stop();
20160
+ getAvgFps() {
20161
+ const intervalSum = this.getIntervalSum() ?? 0;
20162
+ if (!this.hasValidInterval(intervalSum)) return;
20163
+ return this.framesCount / intervalSum * 1000;
19860
20164
  }
19861
- resumeRecordingAfterDrain() {
19862
- if (!this.resumeAfterDrain) return;
19863
- this.resumeAfterDrain = false;
19864
- if (this.canResumeAfterDrain()) this.loop?.start();
20165
+ hasValidInterval(intervalSum) {
20166
+ return intervalSum > 0;
19865
20167
  }
19866
- canResumeAfterDrain() {
19867
- return this.recordingActive && !this.userMedia?.isPaused();
20168
+ getRecordingStats() {
20169
+ return this.recordingStats;
19868
20170
  }
19869
- handleWriteFailure(err) {
19870
- if (this.mediaTransmissionStarted) return void this.failMediaTransmission(err);
19871
- this.emit("ERROR", {
19872
- err
19873
- });
20171
+ getAudioSampleRate() {
20172
+ return this.recordingAudioSampleRate ?? this.userMedia?.getAudioSampleRate();
19874
20173
  }
19875
- failMediaTransmission(err) {
19876
- if (this.recordingTransportFailed) return;
19877
- this.recordingTransportFailed = true;
20174
+ stop(params) {
20175
+ this.cancelCameraRecovery();
20176
+ if (!this.userMedia) throw new Error("No user media defined, unable to stop");
20177
+ this.options.logger.debug(`Recorder: stop(${params ? string_pretty(params) : ""})`);
19878
20178
  this.recordingActive = false;
19879
- this.mediaTransmissionStarted = false;
19880
- this.streamBackpressured = false;
19881
- this.resumeAfterDrain = false;
19882
- this.pendingWrites.clear();
19883
- this.connected = false;
19884
- this.connecting = false;
19885
- this.reconnecting = false;
19886
- this.clearReconnectTimeout();
19887
- this.clearBackpressureTimeout();
19888
- this.discardSocket();
19889
- this.reset();
19890
- this.framesCount = this.samplesCount = 0;
19891
- this.confirmedFrameNumber = this.confirmedSampleNumber = 0;
19892
- delete this.recordingStats;
19893
- this.recordingDimensions = void 0;
19894
- this.emit("ERROR", {
19895
- err
20179
+ const limitReached = params?.limitReached;
20180
+ this.emit("STOPPING", {
20181
+ limitReached
19896
20182
  });
19897
- }
19898
- startBackpressureTimeout() {
19899
- if (!this.shouldStartBackpressureTimeout()) return;
19900
- this.backpressureTimeout = window.setTimeout(()=>{
19901
- this.backpressureTimeout = void 0;
19902
- if (this.shouldIgnoreBackpressureTimeout()) return;
19903
- const err = error_createError({
19904
- message: "WebSocket write timed out",
19905
- explanation: "The server stopped accepting recording data. The recording has been stopped; please check your connection and start a new one.",
19906
- options: this.options
20183
+ this.loop?.complete?.();
20184
+ this.stopTimeout = window.setTimeout(()=>{
20185
+ this.stopTimeout = void 0;
20186
+ this.stopTime = Date.now();
20187
+ const videoType = this.replay.getVideoType();
20188
+ if (!videoType) throw new Error("Unable to video record when no video type is defined.");
20189
+ this.recordingStats = {
20190
+ avgFps: this.getAvgFps(),
20191
+ wantedFps: this.options.video.fps,
20192
+ avgInterval: this.getAvgInterval(),
20193
+ wantedInterval: 1e3 / this.options.video.fps,
20194
+ intervalSum: this.getIntervalSum(),
20195
+ framesCount: this.framesCount,
20196
+ videoType
20197
+ };
20198
+ if (this.userMedia) this.recordingStats.videoTrackLabel = this.userMedia.getVideoTrackLabel();
20199
+ if (isAudioEnabled(this.options) && this.userMedia) {
20200
+ this.recordingStats.samplesCount = this.samplesCount;
20201
+ this.recordingStats.sampleRate = this.recordingAudioSampleRate ?? this.userMedia.getAudioSampleRate();
20202
+ }
20203
+ this.writeCommand("stop", this.recordingStats, ()=>{
20204
+ this.emit("STOPPED", {
20205
+ recordingStats: this.recordingStats
20206
+ });
19907
20207
  });
19908
- this.failMediaTransmission(err);
19909
- }, this.options.timeouts.connection);
19910
- }
19911
- shouldStartBackpressureTimeout() {
19912
- return void 0 === this.backpressureTimeout && this.mediaTransmissionStarted;
20208
+ this.reset();
20209
+ }, 60);
19913
20210
  }
19914
- shouldIgnoreBackpressureTimeout() {
19915
- return !this.streamBackpressured || this.recordingTransportFailed;
20211
+ back(cb) {
20212
+ this.emit("GOING_BACK");
20213
+ this.key = void 0;
20214
+ this.recordingDimensions = void 0;
20215
+ this.unloaded = false;
20216
+ this.show();
20217
+ this.writeCommand("back", void 0, cb);
19916
20218
  }
19917
- failInterruptedRecording() {
19918
- const unconfirmedFrames = Math.max(0, this.framesCount - this.confirmedFrameNumber);
19919
- const unconfirmedSamples = Math.max(0, this.samplesCount - this.confirmedSampleNumber);
19920
- const err = error_createError({
19921
- message: "Recording interrupted",
19922
- explanation: "The server connection closed before this recording completed. It cannot be safely resumed, so it has been stopped. Please start a new recording.",
19923
- options: this.options,
19924
- cause: {
19925
- unconfirmedFrames,
19926
- unconfirmedSamples
19927
- }
19928
- });
19929
- this.failMediaTransmission(err);
20219
+ reInitializeAudio() {
20220
+ this.options.logger.debug("Recorder: reInitializeAudio()");
20221
+ this.clearUserMediaTimeout();
20222
+ this.userMediaLoading = false;
20223
+ this.userMedia?.stop();
20224
+ this.recordingDimensions = void 0;
20225
+ this.userMediaLoaded = this.key = this.canvas = this.ctx = void 0;
20226
+ this.loadUserMedia();
19930
20227
  }
19931
- handleStreamDrain(stream) {
19932
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *drain* event emitted`);
19933
- this.flushPendingWrites(stream);
20228
+ reset() {
20229
+ this.cancelCameraRecovery();
20230
+ if (!this.unloaded) {
20231
+ this.options.logger.debug("Recorder: reset()");
20232
+ this.emit("RESETTING");
20233
+ this.cancelAnimationFrame();
20234
+ this.userMedia?.stop();
20235
+ this.replay.reset();
20236
+ this.userMediaLoaded = this.key = this.canvas = this.ctx = this.recordingBuffer = void 0;
20237
+ }
19934
20238
  }
19935
- canReconnectAfterStreamClose() {
19936
- return Boolean(this.connected && this.userMediaLoaded);
20239
+ validate() {
20240
+ return Boolean(this.key) && void 0 === this.canvas;
19937
20241
  }
19938
- shouldReportStreamClose() {
19939
- return !this.connecting && !this.blocking;
20242
+ pause(params) {
20243
+ this.cancelCameraRecovery();
20244
+ this.resumeAfterDrain = false;
20245
+ const loop = this.loop;
20246
+ if (!loop) throw new Error("No recording loop defined");
20247
+ if (params) this.options.logger.debug(`pause() at frame ${this.framesCount} with ${string_pretty(params)}`);
20248
+ else this.options.logger.debug(`pause() at frame ${this.framesCount}`);
20249
+ this.userMedia?.pause();
20250
+ loop.stop();
20251
+ this.emit("PAUSED");
20252
+ this.sendPings();
19940
20253
  }
19941
- handleStreamClose(url2Connect) {
19942
- const debugLine = string_summarize(`${PIPE_SYMBOL}Stream has closed:`, {
19943
- connecting: this.connecting,
19944
- connected: this.connected,
19945
- userMediaLoaded: this.userMediaLoaded,
19946
- blocking: this.blocking
19947
- });
19948
- this.options.logger.debug(debugLine);
19949
- const canReconnect = this.canReconnectAfterStreamClose();
19950
- this.connected = false;
20254
+ resume() {
20255
+ if (this.cameraRecovery || !this.userMedia?.hasLiveStream()) {
20256
+ this.recoverCamera();
20257
+ this.resumeAfterCameraRecovery = Boolean(this.cameraRecovery);
20258
+ return;
20259
+ }
20260
+ const loop = this.loop;
20261
+ if (!loop) throw new Error("No recording loop defined");
20262
+ this.options.logger.debug(`Recorder: resume() with frame ${this.framesCount}`);
19951
20263
  this.stopPings();
19952
- if (this.shouldIgnoreStreamClose()) return;
19953
- if (this.mediaTransmissionStarted) this.failInterruptedRecording();
19954
- else if (canReconnect) {
19955
- this.pendingWrites.clear();
19956
- this.streamBackpressured = false;
19957
- this.resumeAfterDrain = false;
19958
- this.reconnecting = true;
19959
- this.initSocket();
19960
- } else if (this.shouldReportStreamClose()) window.setTimeout(()=>{
19961
- this.handleConnectionFailure({
19962
- url2Connect,
19963
- cause: "closed"
20264
+ this.emit("RESUMING");
20265
+ this.userMedia.resume();
20266
+ if (this.streamBackpressured) this.resumeAfterDrain = true;
20267
+ else loop.start();
20268
+ }
20269
+ onFlushed(opts) {
20270
+ const frameNumber = opts.frameNumber;
20271
+ if (1 === frameNumber) this.emit("FIRST_FRAME_SENT");
20272
+ }
20273
+ draw(_deltaTime, elapsedTime) {
20274
+ if (this.recordingTransportFailed) return;
20275
+ if (!this.userMedia) throw new Error("No user media defined, unable to draw on canvas");
20276
+ try {
20277
+ if (!this.isPaused() && this.stream && this.ctx) {
20278
+ if (0 === this.framesCount) this.emit("SENDING_FIRST_FRAME");
20279
+ this.framesCount++;
20280
+ const imageSource = this.userMedia.getRawVisuals();
20281
+ if (this.canvas && imageSource) this.ctx.drawImage(imageSource, 0, 0, this.canvas.width, this.canvas.height);
20282
+ else throw new Error("Unable to draw an image without a defined canvas");
20283
+ this.recordingBuffer = this.frame?.toBuffer();
20284
+ const recordingBufferLength = this.recordingBuffer?.length;
20285
+ if (recordingBufferLength) {
20286
+ if (this.recordingBuffer) {
20287
+ const frameControlBuffer = capture_Buffer.from(JSON.stringify({
20288
+ frameNumber: this.framesCount
20289
+ }));
20290
+ const frameBuffer = capture_Buffer.concat([
20291
+ this.recordingBuffer,
20292
+ frameControlBuffer
20293
+ ]);
20294
+ this.mediaTransmissionStarted = true;
20295
+ this.writeStream(frameBuffer, {
20296
+ frameNumber: this.framesCount,
20297
+ onFlushedCallback: this.onFlushed.bind(this)
20298
+ });
20299
+ this.visuals.checkTimer(elapsedTime);
20300
+ }
20301
+ } else throw error_createError({
20302
+ message: "Failed to extract webcam data.",
20303
+ options: this.options
20304
+ });
20305
+ }
20306
+ } catch (exc) {
20307
+ this.emit("ERROR", {
20308
+ exc
20309
+ });
20310
+ }
20311
+ }
20312
+ createLoop() {
20313
+ const newLoop = animitter_default()({
20314
+ fps: this.options.video.fps
20315
+ }, this.draw.bind(this));
20316
+ this.originalAnimationFrameObject = newLoop.getRequestAnimationFrameObject();
20317
+ return newLoop;
20318
+ }
20319
+ record() {
20320
+ if (this.unloaded || this.cameraRecovery) return;
20321
+ if (!this.connected) {
20322
+ this.options.logger.debug("Recorder: reconnecting before recording …");
20323
+ this.initSocket(()=>{
20324
+ this.once("SERVER_READY", ()=>{
20325
+ if (this.unloaded) return;
20326
+ if (this.userMediaLoaded || this.options.loadUserMediaOnRecord) this.record();
20327
+ else this.once("USER_MEDIA_READY", ()=>{
20328
+ if (!this.unloaded) this.record();
20329
+ });
20330
+ });
19964
20331
  });
19965
- }, 0);
19966
- }
19967
- shouldIgnoreStreamClose() {
19968
- return this.recordingTransportFailed || Boolean(this.unloaded) || this.validate();
19969
- }
19970
- handleStreamError(url2Connect, err) {
19971
- if ("u" > typeof Event && err instanceof Event) this.lastSocketError = error_getEventDetails(err);
19972
- else this.lastSocketError = serializeError(err);
19973
- const debugLine = string_summarize(`${PIPE_SYMBOL}Stream *error* event emitted:`, {
19974
- error: serializeError(err)
19975
- });
19976
- this.options.logger.debug(debugLine);
19977
- if (this.validate()) {
19978
- this.connected = false;
19979
- this.stopPings();
19980
- this.discardSocket();
19981
20332
  return;
19982
20333
  }
19983
- if (!this.connected) return void this.handleConnectionFailure({
19984
- url2Connect,
19985
- cause: "error"
19986
- });
19987
- const streamError = err instanceof Error ? err : new Error("WebSocket stream emitted an error event", {
19988
- cause: this.lastSocketError ?? err
20334
+ if (!this.userMediaLoaded) {
20335
+ if (this.options.loadUserMediaOnRecord) this.loadUserMedia({
20336
+ recordWhenReady: true
20337
+ });
20338
+ else {
20339
+ const err = error_createError({
20340
+ message: "Load and enable your camera first",
20341
+ options: this.options
20342
+ });
20343
+ this.emit("ERROR", {
20344
+ err
20345
+ });
20346
+ }
20347
+ return;
20348
+ }
20349
+ const loop = this.loop;
20350
+ if (!loop) throw new Error("No recording loop defined");
20351
+ this.recordingDimensions = void 0;
20352
+ try {
20353
+ if (!this.userMedia) throw new Error("No user media defined, unable to create canvas");
20354
+ this.canvas = this.userMedia.createCanvas();
20355
+ } catch (exc) {
20356
+ const err = error_createError({
20357
+ exc,
20358
+ options: this.options
20359
+ });
20360
+ this.emit("ERROR", {
20361
+ err
20362
+ });
20363
+ return;
20364
+ }
20365
+ this.ctx = this.canvas.getContext("2d");
20366
+ if (!this.canvas.width) {
20367
+ const err = error_createError({
20368
+ message: "Canvas has an invalid width.",
20369
+ options: this.options
20370
+ });
20371
+ this.emit("ERROR", {
20372
+ err
20373
+ });
20374
+ return;
20375
+ }
20376
+ if (!this.canvas.height) {
20377
+ const err = error_createError({
20378
+ message: "Canvas has an invalid height.",
20379
+ options: this.options
20380
+ });
20381
+ this.emit("ERROR", {
20382
+ err
20383
+ });
20384
+ return;
20385
+ }
20386
+ this.recordingDimensions = {
20387
+ width: this.canvas.width,
20388
+ height: this.canvas.height
20389
+ };
20390
+ this.frame = new canvas_to_buffer_modern_r(this.canvas, this.options.image.types, this.options.image.quality);
20391
+ this.options.logger.debug("Recorder: record()");
20392
+ this.recordingActive = true;
20393
+ this.recordingTransportFailed = false;
20394
+ this.mediaTransmissionStarted = false;
20395
+ this.recordingAudioSampleRate = void 0;
20396
+ this.userMedia.record();
20397
+ this.emit("RECORDING", {
20398
+ framesCount: this.framesCount
19989
20399
  });
19990
- if (this.mediaTransmissionStarted) return void this.failInterruptedRecording();
19991
- this.emit("ERROR", {
19992
- err: error_createError({
19993
- message: "WebSocket stream error",
19994
- explanation: `The WebSocket stream emitted an error event. Details: ${string_pretty(err)}`,
19995
- options: this.options,
19996
- exc: streamError
19997
- })
20400
+ loop.on("update", (_deltaTime, elapsedTime)=>{
20401
+ let avgFPS;
20402
+ avgFPS = 0 !== elapsedTime ? Math.round(this.framesCount / elapsedTime * 1000) : void 0;
20403
+ this.options.logger.debug(`Recorder updates avgFps = ${avgFPS} at frame ${this.framesCount}`);
19998
20404
  });
20405
+ loop.start();
19999
20406
  }
20000
- sendPings() {
20001
- this.stopPings();
20002
- this.pingInterval = window.setInterval(()=>{
20003
- this.options.logger.debug("Recorder: pinging...");
20004
- this.writeStream(Buffer.from(""));
20005
- }, this.options.timeouts.pingInterval);
20006
- }
20007
- stopPings() {
20008
- if (void 0 === this.pingInterval) return;
20009
- this.options.logger.debug("Recorder: stopping pings...");
20010
- window.clearInterval(this.pingInterval);
20011
- this.pingInterval = void 0;
20407
+ setAnimationFrameObject(newObj) {
20408
+ if (this.loop) {
20409
+ const isRecording = this.isRecording();
20410
+ this.loop.stop();
20411
+ this.loop.setRequestAnimationFrameObject(newObj);
20412
+ if (isRecording) this.loop.start();
20413
+ }
20012
20414
  }
20013
- onAudioSample(audioSample) {
20014
- if (this.recordingTransportFailed || this.cameraRecovery) return;
20015
- const sampleRate = this.userMedia?.getAudioSampleRate();
20016
- if (sampleRate && sampleRate > 0) this.recordingAudioSampleRate ??= sampleRate;
20017
- this.samplesCount++;
20018
- this.mediaTransmissionStarted = true;
20019
- const audioBuffer = audioSample.toBuffer();
20020
- this.writeStream(audioBuffer);
20415
+ restoreAnimationFrameObject() {
20416
+ this.options.logger.debug("Recorder: restoreAnimationFrameObject()");
20417
+ if (this.originalAnimationFrameObject) this.setAnimationFrameObject(this.originalAnimationFrameObject);
20021
20418
  }
20022
- show() {
20023
- html_showElement(this.recorderElement);
20419
+ loopWithTimeouts() {
20420
+ this.options.logger.debug("Recorder: loopWithTimeouts()");
20421
+ const wantedInterval = 1e3 / this.options.video.fps;
20422
+ let processingTime = 0;
20423
+ const raf = (fn)=>window.setTimeout(()=>{
20424
+ const start = Date.now();
20425
+ fn();
20426
+ processingTime = Date.now() - start;
20427
+ }, wantedInterval - processingTime);
20428
+ const cancel = (id)=>{
20429
+ window.clearTimeout(id);
20430
+ };
20431
+ this.setAnimationFrameObject({
20432
+ requestAnimationFrame: raf,
20433
+ cancelAnimationFrame: cancel
20434
+ });
20024
20435
  }
20025
- onUserMediaReady(params) {
20026
- try {
20027
- this.options.logger.debug(`Recorder: onUserMediaReady(${params ? string_pretty(params) : ""})`);
20028
- const switchingFacingMode = params?.switchingFacingMode;
20029
- if (params?.recovering) {
20030
- if (!this.userMedia?.hasLiveStream()) return void this.failCameraRecovery(new Error("The restored camera has no live tracks."));
20031
- if (void 0 !== this.recordingAudioSampleRate && this.userMedia.getAudioSampleRate() !== this.recordingAudioSampleRate) return void this.failCameraRecovery(new Error("The audio sample rate changed during recovery."));
20032
- }
20033
- this.userMediaLoading = this.blocking = this.unloaded = this.submitting = false;
20034
- this.userMediaLoaded = true;
20035
- this.clearUserMediaTimeout();
20036
- if (!switchingFacingMode && !params?.recovering) this.loop = this.createLoop();
20037
- this.show();
20038
- if (params?.recordWhenReady) this.record();
20039
- this.emit("USER_MEDIA_READY", {
20040
- switchingFacingMode: params?.switchingFacingMode,
20041
- recovering: params?.recovering,
20042
- paused: this.isPaused(),
20043
- recordWhenReady: params?.recordWhenReady
20044
- });
20045
- if (params?.recovering) this.finishCameraRecovery(true);
20046
- } catch (exc) {
20047
- if (params?.recovering) this.failCameraRecovery(exc);
20048
- else this.emit("ERROR", {
20049
- exc
20050
- });
20051
- }
20436
+ isPaused() {
20437
+ return this.userMedia?.isPaused() && !this.loop?.isRunning();
20052
20438
  }
20053
- clearRetryTimeout() {
20054
- if (void 0 === this.retryTimeout) return;
20055
- this.options.logger.debug("Recorder: clearRetryTimeout()");
20056
- window.clearTimeout(this.retryTimeout);
20057
- this.retryTimeout = void 0;
20439
+ isUserMediaPaused() {
20440
+ return this.userMedia?.isPaused() ?? false;
20058
20441
  }
20059
- clearConnectionTimeout() {
20060
- if (void 0 === this.connectionTimeout) return;
20061
- this.options.logger.debug("Recorder: clearConnectionTimeout()");
20062
- window.clearTimeout(this.connectionTimeout);
20063
- this.connectionTimeout = void 0;
20442
+ isCaptureLoopRunning() {
20443
+ return this.loop?.isRunning() ?? false;
20064
20444
  }
20065
- clearBackpressureTimeout() {
20066
- if (void 0 === this.backpressureTimeout) return;
20067
- window.clearTimeout(this.backpressureTimeout);
20068
- this.backpressureTimeout = void 0;
20445
+ stopCaptureLoop() {
20446
+ this.loop?.stop();
20069
20447
  }
20070
- clearReconnectTimeout() {
20071
- if (void 0 === this.reconnectTimeout) return;
20072
- this.options.logger.debug("Recorder: clearReconnectTimeout()");
20073
- window.clearTimeout(this.reconnectTimeout);
20074
- this.reconnectTimeout = void 0;
20448
+ startCaptureLoop() {
20449
+ this.loop?.start();
20075
20450
  }
20076
- handleConnectionFailure(params) {
20077
- if (this.retryConnection(params)) return;
20078
- this.failConnection(params);
20451
+ isRecording() {
20452
+ return Boolean(this.loop?.isRunning() && !this.isPaused() && !this.isNotifying() && this.stream && !this.stream.destroyed);
20079
20453
  }
20080
- retryConnection(params) {
20081
- if (!this.canRetryConnection()) return false;
20082
- if (void 0 !== this.reconnectTimeout) return true;
20083
- const delayMs = Math.min(1000 * 2 ** this.reconnectAttempts, 10000);
20084
- this.reconnectAttempts++;
20085
- this.connecting = false;
20086
- this.clearConnectionTimeout();
20087
- this.discardSocket();
20088
- this.options.logger.debug(`Recorder: retrying WebSocket connection in ${delayMs}ms (cause ${params.cause}).`);
20089
- this.reconnectTimeout = window.setTimeout(()=>{
20090
- this.reconnectTimeout = void 0;
20091
- if (this.shouldContinueReconnect()) this.initSocket();
20092
- }, delayMs);
20093
- return true;
20454
+ getRecordingDimensions() {
20455
+ return this.recordingDimensions && {
20456
+ ...this.recordingDimensions
20457
+ };
20094
20458
  }
20095
- canRetryConnection() {
20096
- return Boolean(this.reconnecting && this.userMediaLoaded && this.isOnline() && !this.unloaded && !this.blocking);
20459
+ }
20460
+ const capture = RecorderCapture;
20461
+ function figureMinHeight(height, options) {
20462
+ let minHeight;
20463
+ if (options.video.height) {
20464
+ minHeight = height ? Math.min(options.video.height, height) : options.video.height;
20465
+ if (minHeight < 1) throw error_createError({
20466
+ message: `Got a min height less than 1 (${minHeight})!`,
20467
+ options
20468
+ });
20469
+ } else minHeight = height;
20470
+ return minHeight;
20471
+ }
20472
+ const dimensions_figureMinHeight = figureMinHeight;
20473
+ function getRatio(options, videoHeight, videoWidth) {
20474
+ let ratio = 1;
20475
+ const hasVideoDimensions = videoHeight && videoWidth;
20476
+ const desiredHeight = options.video.height;
20477
+ const desiredWidth = options.video.width;
20478
+ const hasDesiredDimensions = desiredHeight && desiredWidth;
20479
+ if (hasDesiredDimensions) ratio = hasVideoDimensions ? videoHeight < desiredHeight || videoWidth < desiredWidth ? videoHeight / videoWidth : desiredHeight / desiredWidth : desiredHeight / desiredWidth;
20480
+ else if (hasVideoDimensions) ratio = videoHeight / videoWidth;
20481
+ return ratio;
20482
+ }
20483
+ const dimensions_getRatio = getRatio;
20484
+ function calculateHeight(responsive, videoWidth, options, ratio, element) {
20485
+ const dimension = {
20486
+ unit: "px"
20487
+ };
20488
+ let width = videoWidth;
20489
+ if (responsive && element) {
20490
+ const limitedDimension = dimensions_limitWidth(element, options, width);
20491
+ width = limitedDimension.value;
20097
20492
  }
20098
- shouldContinueReconnect() {
20099
- return this.reconnecting && !this.unloaded && !this.blocking;
20493
+ const chosenRatio = ratio ?? dimensions_getRatio(options, void 0, videoWidth);
20494
+ const height = width ? Math.round(width * chosenRatio) : void 0;
20495
+ if (height && Number.isInteger(height) && height < 1) throw error_createError({
20496
+ message: "Just calculated a height less than 1 which is wrong.",
20497
+ options
20498
+ });
20499
+ const minHeight = dimensions_figureMinHeight(height, options);
20500
+ dimension.value = minHeight;
20501
+ return dimension;
20502
+ }
20503
+ const dimensions_calculateHeight = calculateHeight;
20504
+ function calculateWidth(responsive, options, videoHeight, ratio) {
20505
+ const dimension = {
20506
+ unit: "px"
20507
+ };
20508
+ let height = dimensions_figureMinHeight(videoHeight, options);
20509
+ if (responsive) {
20510
+ const limitedDimension = dimensions_limitHeight(height, options, "calculateWidth");
20511
+ height = limitedDimension.value;
20100
20512
  }
20101
- discardSocket() {
20102
- this.options.logger.debug("Recorder: discarding socket stream ...");
20103
- this.clearBackpressureTimeout();
20104
- this.pendingWrites.clear();
20105
- this.streamBackpressured = false;
20106
- this.resumeAfterDrain = false;
20107
- const stream = this.stream;
20108
- this.stream = void 0;
20109
- if (stream && !stream.destroyed) stream.destroy();
20513
+ if (!height || height < 1) throw error_createError({
20514
+ message: `Height ${height} cannot be smaller than 1 when calculating width.`,
20515
+ options
20516
+ });
20517
+ const chosenRatio = ratio ?? dimensions_getRatio(options, videoHeight);
20518
+ const calculatedWidth = Math.round(height / chosenRatio);
20519
+ if (calculatedWidth < 1) throw error_createError({
20520
+ message: "Calculated width cannot be smaller than 1!",
20521
+ options
20522
+ });
20523
+ dimension.value = calculatedWidth;
20524
+ return dimension;
20525
+ }
20526
+ const dimensions_calculateWidth = calculateWidth;
20527
+ class RecorderDimensions {
20528
+ context;
20529
+ constructor(context){
20530
+ this.context = context;
20110
20531
  }
20111
- isOnline() {
20112
- return navigator.onLine;
20532
+ correctDimensions() {
20533
+ const recorderElement = this.context.getRecorderElement();
20534
+ if (!recorderElement) return;
20535
+ const widthDimension = dimensions_useFullWidth(this.context.options.video.mobileBreakPoint);
20536
+ if (this.context.options.video.stretch || widthDimension) {
20537
+ recorderElement.style.width = "100%";
20538
+ recorderElement.style.removeProperty("height");
20539
+ } else {
20540
+ if (this.context.options.video.width) {
20541
+ const recorderWidth = this.getRecorderWidth(true);
20542
+ if (recorderWidth?.value) recorderElement.width = recorderWidth.value;
20543
+ else recorderElement.style.removeProperty("width");
20544
+ }
20545
+ if (this.context.options.video.height) {
20546
+ const recorderHeight = this.getRecorderHeight(true);
20547
+ if (recorderHeight?.value) recorderElement.height = recorderHeight.value;
20548
+ else recorderElement.style.removeProperty("height");
20549
+ }
20550
+ }
20113
20551
  }
20114
- failConnection(params) {
20115
- if (this.shouldIgnoreConnectionFailure()) return;
20116
- this.connectionFailed = true;
20117
- this.connecting = false;
20118
- this.reconnecting = false;
20119
- this.reconnectAttempts = 0;
20120
- this.clearReconnectTimeout();
20121
- this.clearConnectionTimeout();
20122
- const { url2Connect, cause } = params;
20123
- const online = this.isOnline();
20124
- const elapsedMs = this.connectingStartedAt ? Date.now() - this.connectingStartedAt : void 0;
20125
- const closeEvent = this.lastCloseEvent;
20126
- const socketError = this.lastSocketError;
20127
- const contents = {
20128
- cause,
20129
- closeCode: closeEvent?.code,
20130
- closeReason: closeEvent?.reason,
20131
- elapsedMs,
20132
- online,
20133
- secureContext: globalThis.isSecureContext,
20134
- socketReadyState: this.stream?.socket.readyState,
20135
- timeoutMs: this.options.timeouts.connection,
20136
- url: url2Connect,
20137
- unloaded: this.unloaded,
20138
- userMediaLoaded: this.userMediaLoaded,
20139
- userMediaLoading: this.userMediaLoading,
20140
- wasClean: closeEvent?.wasClean,
20141
- socketError,
20142
- blocking: this.blocking,
20143
- visibilityState: document.visibilityState
20552
+ getRecorderWidth(responsive) {
20553
+ const userMedia = this.context.getUserMedia();
20554
+ if (userMedia?.hasVideoWidth()) return userMedia.getRawWidth(responsive);
20555
+ else if (responsive && this.context.options.video.width) return this.context.visuals.limitWidth(this.context.options.video.width);
20556
+ const dimension = {
20557
+ unit: "px",
20558
+ value: this.context.options.video.width
20144
20559
  };
20145
- const debugLine = string_summarize("Recorder: failConnection() diagnostic", contents);
20146
- this.options.logger.debug(debugLine);
20147
- let explanation;
20148
- if (online) if ("timeout" === cause) explanation = `The server at ${url2Connect} did not respond within ${this.options.timeouts.connection}ms, even though your device is online. This usually points to a firewall or proxy silently dropping the connection. Please try a different network. If the problem persists, contact us.`;
20149
- else if ("error" === cause) explanation = `An error occurred while connecting to ${url2Connect}. Please check your internet connection and try again. If the problem persists, contact us.`;
20150
- else {
20151
- const closeSuffix = closeEvent ? ` (code ${closeEvent.code})` : "";
20152
- explanation = `Connection to ${url2Connect} is closed${closeSuffix}. Please check your internet connection and try again. If the problem persists, contact us.`;
20153
- }
20154
- else explanation = "Your device appears to be offline. Please check your internet connection and try again.";
20155
- this.discardSocket();
20156
- const err = error_createError({
20157
- message: "Unable to connect to the server",
20158
- explanation,
20159
- options: this.options,
20160
- cause: contents,
20161
- exc: socketError ? new Error("WebSocket connection error", {
20162
- cause: socketError
20163
- }) : void 0
20164
- });
20165
- this.emit("ERROR", {
20166
- err
20167
- });
20560
+ return dimension;
20168
20561
  }
20169
- shouldIgnoreConnectionFailure() {
20170
- return this.connectionFailed || this.connected || Boolean(this.unloaded) || this.validate();
20562
+ getRecorderHeight(responsive, useBoundingClientRect) {
20563
+ const recorderElement = this.context.getRecorderElement();
20564
+ const userMedia = this.context.getUserMedia();
20565
+ let recorderHeight;
20566
+ if (recorderElement && useBoundingClientRect) {
20567
+ const height = recorderElement.getBoundingClientRect().height;
20568
+ recorderHeight = {
20569
+ unit: "px",
20570
+ value: height
20571
+ };
20572
+ } else if (userMedia) {
20573
+ let height = userMedia.getRawHeight(responsive);
20574
+ if (void 0 !== height && this.context.options.video.width) {
20575
+ const ratio = this.getRatio();
20576
+ if (void 0 !== ratio) {
20577
+ const idealHeight = this.context.options.video.width * ratio;
20578
+ height = Math.min(idealHeight, height);
20579
+ }
20580
+ }
20581
+ recorderHeight = {
20582
+ unit: "px",
20583
+ value: height
20584
+ };
20585
+ } else if (responsive && this.context.options.video.height) recorderHeight = this.calculateHeight(responsive);
20586
+ else if (this.context.options.video.height) {
20587
+ const height = this.context.options.video.height;
20588
+ recorderHeight = {
20589
+ unit: "px",
20590
+ value: height
20591
+ };
20592
+ }
20593
+ return recorderHeight;
20171
20594
  }
20172
- calculateFrameProgress() {
20173
- return `${(this.confirmedFrameNumber / (this.framesCount || 1) * 100).toFixed(2)}%`;
20595
+ getRatio() {
20596
+ const userMedia = this.context.getUserMedia();
20597
+ let ratio;
20598
+ if (userMedia) {
20599
+ const userMediaVideoWidth = userMedia.getVideoWidth();
20600
+ const userMediaVideoHeight = userMedia.getVideoHeight();
20601
+ if (!userMediaVideoWidth || userMediaVideoWidth < 1) ratio = this.context.visuals.getRatio();
20602
+ else if (userMediaVideoHeight) ratio = userMediaVideoHeight / userMediaVideoWidth;
20603
+ } else ratio = dimensions_getRatio(this.context.options);
20604
+ return ratio;
20174
20605
  }
20175
- calculateSampleProgress() {
20176
- return `${(this.confirmedSampleNumber / (this.samplesCount || 1) * 100).toFixed(2)}%`;
20606
+ calculateWidth(responsive) {
20607
+ const userMedia = this.context.getUserMedia();
20608
+ const recorderElement = this.context.getRecorderElement();
20609
+ let videoHeight;
20610
+ if (userMedia) videoHeight = userMedia.getVideoHeight();
20611
+ else if (recorderElement) videoHeight = recorderElement.videoHeight || recorderElement.height;
20612
+ return dimensions_calculateWidth(responsive, this.context.options, videoHeight, this.getRatio());
20177
20613
  }
20178
- updateOverallProgress() {
20179
- this.frameProgress = this.calculateFrameProgress();
20180
- if (isAudioEnabled(this.options)) this.sampleProgress = this.calculateSampleProgress();
20181
- this.emit("PROGRESS", {
20182
- frameProgress: this.frameProgress,
20183
- sampleProgress: this.sampleProgress
20184
- });
20614
+ calculateHeight(responsive) {
20615
+ const userMedia = this.context.getUserMedia();
20616
+ const recorderElement = this.context.getRecorderElement();
20617
+ let videoDimension;
20618
+ if (userMedia) {
20619
+ const videoHeight = userMedia.getVideoHeight();
20620
+ videoDimension = {
20621
+ value: videoHeight,
20622
+ unit: "px"
20623
+ };
20624
+ } else if (recorderElement) {
20625
+ const videoHeight = recorderElement.videoHeight || recorderElement.height;
20626
+ videoDimension = {
20627
+ value: videoHeight,
20628
+ unit: "px"
20629
+ };
20630
+ } else videoDimension = dimensions_calculateHeight(responsive, void 0, this.context.options, this.getRatio(), recorderElement);
20631
+ return videoDimension;
20185
20632
  }
20186
- updateFrameProgress(args) {
20187
- if (args) {
20188
- if (!args.frame) throw error_createError({
20189
- message: "The frame number is missing",
20190
- options: this.options
20191
- });
20192
- } else throw error_createError({
20193
- message: "Arguments are missing for updating the frame progress",
20194
- options: this.options
20633
+ }
20634
+ const recorder_dimensions = RecorderDimensions;
20635
+ function getFirstVideoTrack(localMediaStream) {
20636
+ const videoTracks = localMediaStream.getVideoTracks();
20637
+ let videoTrack;
20638
+ if (videoTracks[0]) videoTrack = videoTracks[0];
20639
+ return videoTrack;
20640
+ }
20641
+ const media_getFirstVideoTrack = getFirstVideoTrack;
20642
+ const MEDIA_EVENTS = [
20643
+ "loadstart",
20644
+ "suspend",
20645
+ "progress",
20646
+ "abort",
20647
+ "emptied",
20648
+ "stalled",
20649
+ "pause",
20650
+ "loadeddata",
20651
+ "waiting",
20652
+ "playing",
20653
+ "canplay",
20654
+ "canplaythrough",
20655
+ "seeking",
20656
+ "seeked",
20657
+ "ended",
20658
+ "ratechange",
20659
+ "durationchange",
20660
+ "volumechange"
20661
+ ];
20662
+ const mediaEvents = MEDIA_EVENTS;
20663
+ function _define_property(obj, key, value) {
20664
+ if (key in obj) Object.defineProperty(obj, key, {
20665
+ value: value,
20666
+ enumerable: true,
20667
+ configurable: true,
20668
+ writable: true
20669
+ });
20670
+ else obj[key] = value;
20671
+ return obj;
20672
+ }
20673
+ class AudioSample {
20674
+ toBuffer() {
20675
+ const l = this.float32Array.length;
20676
+ const arr = new Int16Array(l);
20677
+ this.float32Array.forEach((sample, i)=>{
20678
+ const clampedSample = Number.isFinite(sample) ? Math.max(-1, sample) : sample;
20679
+ arr[i] = 0x7fff * Math.min(1, clampedSample);
20195
20680
  });
20196
- this.confirmedFrameNumber = args.frame;
20197
- this.frameProgress = this.calculateFrameProgress();
20198
- this.updateOverallProgress();
20681
+ return typedarray_to_buffer(arr);
20199
20682
  }
20200
- updateSampleProgress(args) {
20201
- if (args) {
20202
- if (!args.sample) throw error_createError({
20203
- message: "The audio sample number is missing",
20204
- options: this.options
20205
- });
20206
- } else throw error_createError({
20207
- message: "Arguments are missing for updating the audio sample progress",
20208
- options: this.options
20209
- });
20210
- this.confirmedSampleNumber = args.sample;
20211
- this.sampleProgress = this.calculateSampleProgress();
20212
- this.updateOverallProgress();
20683
+ constructor(float32Array){
20684
+ _define_property(this, "float32Array", void 0);
20685
+ this.float32Array = float32Array;
20213
20686
  }
20214
- preview(args) {
20215
- if (!args) throw error_createError({
20216
- message: "Preview arguments are missing.",
20217
- options: this.options
20218
- });
20219
- const hasAudio = this.samplesCount > 0;
20220
- this.mediaTransmissionStarted = false;
20221
- this.recordingActive = false;
20222
- this.recordingTransportFailed = false;
20223
- this.confirmedFrameNumber = this.confirmedSampleNumber = this.samplesCount = this.framesCount = 0;
20224
- this.sampleProgress = this.frameProgress = void 0;
20225
- this.key = args.key;
20226
- if (args.mp4) this.replay.setMp4Source(`${args.mp4 + constants.WHITELIST_KEY_LABEL}/${this.options.whitelistKey}/videomail.mp4`, true);
20227
- if (args.webm) this.replay.setWebMSource(`${args.webm + constants.WHITELIST_KEY_LABEL}/${this.options.whitelistKey}/videomail.webm`, true);
20228
- this.hide();
20229
- const widthDimension = this.getRecorderWidth(true);
20230
- const heightDimension = this.getRecorderHeight(true);
20231
- const duration = args.duration ?? -1;
20232
- this.emit("PREVIEW", {
20233
- key: this.key,
20234
- width: widthDimension?.value,
20235
- height: heightDimension?.value,
20236
- hasAudio,
20237
- duration
20238
- });
20239
- if (this.stopTime) this.waitingTime = Date.now() - this.stopTime;
20240
- this.recordingStats ??= {};
20241
- this.recordingStats.waitingTime = this.waitingTime;
20687
+ }
20688
+ const esm_src = AudioSample;
20689
+ const esm = esm_src;
20690
+ var is_power_of_two = __webpack_require__("dZ");
20691
+ var is_power_of_two_default = /*#__PURE__*/ __webpack_require__.n(is_power_of_two);
20692
+ const CHANNELS = 1;
20693
+ function getAudioContextClass() {
20694
+ return window.AudioContext;
20695
+ }
20696
+ class AudioRecorder {
20697
+ scriptProcessor;
20698
+ audioInput;
20699
+ volume;
20700
+ vcAudioContext;
20701
+ userMedia;
20702
+ options;
20703
+ constructor(userMedia, options){
20704
+ this.options = options;
20705
+ this.userMedia = userMedia;
20242
20706
  }
20243
- initSocket(cb) {
20244
- if (this.connected) {
20245
- this.options.logger.debug("Recorder: already **connected**. Not going to initialize a new WebSocket connection.");
20246
- cb?.();
20247
- return;
20248
- }
20249
- if (this.connecting) return void this.options.logger.debug("Recorder: already **connecting**. Not going to initialize a new WebSocket connection.");
20250
- this.clearReconnectTimeout();
20251
- if (util_isAutomatedUserAgent()) {
20252
- this.connecting = false;
20253
- this.options.logger.debug("Recorder: skipping web socket connection for an automated crawler");
20254
- return;
20255
- }
20256
- this.connecting = true;
20257
- this.connectionFailed = false;
20258
- this.connectingStartedAt = Date.now();
20259
- this.lastCloseEvent = void 0;
20260
- this.lastSocketError = void 0;
20261
- this.emit("CONNECTING");
20262
- let url2Connect;
20263
- try {
20264
- const socketUrlObj = new URL(this.options.socketUrl);
20265
- socketUrlObj.searchParams.set(constants.WHITELIST_KEY_LABEL, this.options.whitelistKey);
20266
- url2Connect = socketUrlObj.toString();
20267
- } catch (exc) {
20268
- this.connecting = this.connected = false;
20269
- const err = error_createError({
20270
- message: "Invalid WebSocket URL",
20271
- explanation: `The configured socketUrl "${this.options.socketUrl}" is not a valid URL. Please check your videomail-client configuration.`,
20272
- options: this.options,
20273
- exc
20274
- });
20275
- this.emit("ERROR", {
20276
- err
20277
- });
20278
- return;
20707
+ hasAudioContext() {
20708
+ return Boolean(getAudioContextClass()) && Boolean(this.getAudioContext());
20709
+ }
20710
+ getAudioContext() {
20711
+ if (!this.vcAudioContext) {
20712
+ const AudioContext = getAudioContextClass();
20713
+ this.vcAudioContext = new AudioContext();
20279
20714
  }
20280
- this.options.logger.debug(`Recorder: initializing web socket stream to ${url2Connect}`);
20281
- let nativeSocket;
20282
- try {
20283
- nativeSocket = new WebSocket(url2Connect);
20284
- nativeSocket.addEventListener("close", (event)=>{
20285
- this.lastCloseEvent = {
20286
- code: event.code,
20287
- reason: event.reason,
20288
- wasClean: event.wasClean
20289
- };
20290
- });
20291
- nativeSocket.addEventListener("error", (event)=>{
20292
- this.lastSocketError = error_getEventDetails(event);
20293
- });
20294
- } catch (exc) {
20295
- this.connecting = this.connected = false;
20296
- const diagnostic = error_getWebSocketDiagnostic(url2Connect);
20297
- const message = diagnostic.looksAutomated ? "Automated crawler: WebSocket not supported in this environment" : `Failed to construct WebSocket to ${url2Connect}`;
20298
- const explanation = diagnostic.looksAutomated ? "Headless crawlers cannot use Videomail's WebSocket. No action needed." : `Please check your connection and try again. If the problem persists, contact us. Diagnostic: ${diagnostic.text}`;
20299
- const err = error_createError({
20300
- message,
20301
- explanation,
20302
- options: this.options,
20303
- exc
20304
- });
20305
- this.emit("ERROR", {
20306
- err
20307
- });
20308
- return;
20715
+ return this.vcAudioContext;
20716
+ }
20717
+ onAudioProcess(e, cb) {
20718
+ if (!this.userMedia.isRecording() || this.userMedia.isPaused()) return;
20719
+ const float32Array = e.inputBuffer.getChannelData(0);
20720
+ cb(new esm(float32Array));
20721
+ }
20722
+ disconnect() {
20723
+ if (this.scriptProcessor) {
20724
+ this.scriptProcessor.onaudioprocess = null;
20725
+ this.scriptProcessor.disconnect();
20309
20726
  }
20727
+ this.audioInput?.disconnect();
20728
+ this.volume?.disconnect();
20729
+ }
20730
+ init(localMediaStream) {
20731
+ this.options.logger.debug("AudioRecorder: init()");
20732
+ this.disconnect();
20733
+ const volume = this.getAudioContext().createGain();
20734
+ this.volume = volume;
20310
20735
  try {
20311
- this.stream = stream_default()(nativeSocket);
20736
+ this.audioInput = this.getAudioContext().createMediaStreamSource(localMediaStream);
20312
20737
  } catch (exc) {
20313
- this.connecting = this.connected = false;
20314
- const err = error_createError({
20315
- message: `Failed to create a stream to ${url2Connect}`,
20316
- explanation: "Please check your connection and try again. If the problem persists, contact us.",
20317
- options: this.options,
20318
- exc
20319
- });
20320
- this.emit("ERROR", {
20321
- err
20322
- });
20323
- }
20324
- const stream = this.stream;
20325
- if (stream) {
20326
- const connectionTimeoutMs = this.options.timeouts.connection;
20327
- this.connectionTimeout = window.setTimeout(()=>{
20328
- if (this.isOnline() && this.isUserMediaLoaded()) {
20329
- this.options.logger.debug(`${PIPE_SYMBOL}Reconnecting due to connection timeout.`);
20330
- this.reconnecting = true;
20331
- this.handleConnectionFailure({
20332
- url2Connect,
20333
- cause: "timeout"
20334
- });
20335
- } else this.handleConnectionFailure({
20336
- url2Connect,
20337
- cause: "timeout"
20338
- });
20339
- }, connectionTimeoutMs);
20340
- stream.on("close", ()=>{
20341
- this.handleStreamClose(url2Connect);
20342
- });
20343
- stream.on("connect", (args)=>{
20344
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *connect* event emitted with args: ${string_pretty(args)}`);
20345
- this.clearConnectionTimeout();
20346
- const isClosing = this.stream?.socket.readyState === WebSocket.CLOSING;
20347
- if (!this.connected && !isClosing && !this.unloaded) {
20348
- this.connected = true;
20349
- this.connecting = this.unloaded = false;
20350
- this.reconnecting = false;
20351
- this.reconnectAttempts = 0;
20352
- this.clearReconnectTimeout();
20353
- this.emit("CONNECTED");
20354
- cb?.();
20355
- }
20356
- });
20357
- stream.on("data", (data)=>{
20358
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *data* event emitted`);
20359
- try {
20360
- const command = JSON.parse(data.toString());
20361
- this.executeCommand(command);
20362
- } catch (exc) {
20363
- this.options.logger.error(`Failed to parse command: ${exc}`);
20364
- const err = error_createError({
20365
- message: "Invalid server command",
20366
- explanation: `Contact us. The invalid command was: ${data.toString()}.`,
20367
- options: this.options,
20368
- exc
20369
- });
20370
- this.emit("ERROR", {
20371
- err
20372
- });
20373
- }
20374
- });
20375
- stream.on("error", (err)=>{
20376
- this.handleStreamError(url2Connect, err);
20377
- });
20378
- stream.on("drain", ()=>{
20379
- this.handleStreamDrain(stream);
20380
- });
20381
- stream.on("preend", (args)=>{
20382
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *preend* event emitted with args: ${string_pretty(args)}`);
20383
- });
20384
- stream.on("end", ()=>{
20385
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *end* event emitted`);
20386
- });
20387
- stream.on("pipe", (src)=>{
20388
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *pipe* event emitted with src: ${string_pretty(src)}`);
20389
- });
20390
- stream.on("unpipe", (src)=>{
20391
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *unpipe* event emitted with src: ${string_pretty(src)}`);
20392
- });
20393
- stream.on("resume", (args)=>{
20394
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *resume* event emitted with args: ${string_pretty(args)}`);
20395
- });
20396
- stream.on("uncork", (args)=>{
20397
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *uncork* event emitted with args: ${string_pretty(args)}`);
20398
- });
20399
- stream.on("readable", (args)=>{
20400
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *readable* event emitted with args: ${string_pretty(args)}`);
20401
- });
20402
- stream.on("prefinish", (args)=>{
20403
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *prefinish* event emitted with args: ${string_pretty(args)}`);
20404
- });
20405
- stream.on("finish", (args)=>{
20406
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *finish* event emitted with args: ${string_pretty(args)}`);
20738
+ throw error_createError({
20739
+ message: "Webcam has no audio",
20740
+ exc,
20741
+ options: this.options
20407
20742
  });
20408
20743
  }
20744
+ let { bufferSize } = this.options.audio;
20745
+ if ("auto" === bufferSize) bufferSize = util_getBrowser(this.options).isFirefox() ? 512 : 2048;
20746
+ if (void 0 === bufferSize || !is_power_of_two_default()(bufferSize)) throw error_createError({
20747
+ message: `Audio buffer size must be a power of two. The current buffer size is ${bufferSize}.`,
20748
+ options: this.options
20749
+ });
20750
+ if (!Number.isFinite(this.options.audio.volume) || this.options.audio.volume < 0 || this.options.audio.volume > 1) throw error_createError({
20751
+ message: `Audio volume must be between zero and one. The current volume is ${this.options.audio.volume}.`,
20752
+ options: this.options
20753
+ });
20754
+ volume.gain.value = this.options.audio.volume;
20755
+ this.scriptProcessor = this.getAudioContext().createScriptProcessor(bufferSize, CHANNELS, CHANNELS);
20756
+ this.audioInput.connect(volume);
20757
+ volume.connect(this.scriptProcessor);
20758
+ this.scriptProcessor.connect(this.getAudioContext().destination);
20409
20759
  }
20410
- showUserMedia() {
20411
- if (!this.connected) return false;
20412
- const hidden = this.isHidden();
20413
- if (!hidden) return true;
20414
- const notifying = this.isNotifying();
20415
- if (notifying) return true;
20416
- return this.blocking;
20417
- }
20418
- userMediaErrorCallback(err, usedConstraints) {
20419
- if (this.cameraRecovery) return void this.failCameraRecovery(err);
20420
- if (this.unloaded) return void this.options.logger.debug(`Recorder: already unloaded. Not going to throw error ${string_pretty(err)}`);
20421
- this.userMediaLoading = false;
20422
- this.clearUserMediaTimeout();
20423
- const characteristics = this.userMedia?.getCharacteristics();
20424
- this.options.logger.debug(`Recorder: userMediaErrorCallback(), name: ${err.name}, message: ${err.message} and Webcam characteristics: ${characteristics ? string_pretty(characteristics) : "none"}`);
20425
- const errorListeners = this.getListeners("ERROR");
20426
- if (errorListeners?.length) if (err.name !== error_VideomailError.MEDIA_DEVICE_NOT_SUPPORTED) {
20427
- const videomailError = error_createError({
20428
- err,
20429
- options: this.options,
20430
- usedConstraints
20431
- });
20432
- this.emit("ERROR", {
20433
- err: videomailError
20434
- });
20435
- } else {
20436
- this.options.logger.debug(`Recorder: ignore user media error ${string_pretty(err)}`);
20437
- this.retryTimeout = window.setTimeout(()=>{
20438
- this.retryTimeout = void 0;
20439
- if (!this.unloaded) this.loadUserMedia();
20440
- }, this.options.timeouts.userMedia);
20441
- }
20442
- else {
20443
- this.options.logger.debug(`Recorder: no error listeners attached but throwing error ${string_pretty(err)}`);
20444
- throw error_createError({
20760
+ record(cb) {
20761
+ this.options.logger.debug("AudioRecorder: record()");
20762
+ if (this.scriptProcessor) this.scriptProcessor.onaudioprocess = (e)=>{
20763
+ this.onAudioProcess(e, cb);
20764
+ };
20765
+ }
20766
+ async resume() {
20767
+ const context = this.getAudioContext();
20768
+ if ("running" !== context.state) await context.resume();
20769
+ }
20770
+ stop() {
20771
+ this.options.logger.debug("AudioRecorder: stop()");
20772
+ this.disconnect();
20773
+ if (this.hasAudioContext()) this.getAudioContext().close().then(()=>{
20774
+ this.options.logger.debug("AudioRecorder: audio context is closed");
20775
+ this.vcAudioContext = void 0;
20776
+ }).catch((err)=>{
20777
+ if (err instanceof Error) return void this.options.logger.error(error_createError({
20445
20778
  err,
20446
- message: "Unable to process this error since there are no error listeners anymore.",
20447
20779
  options: this.options
20448
- });
20449
- }
20780
+ }));
20781
+ this.options.logger.error(err);
20782
+ });
20450
20783
  }
20451
- getUserMediaCallback(localStream, params, requestId = this.mediaRequestId) {
20452
- if (!this.userMedia) {
20453
- localStream.getTracks().forEach((track)=>{
20454
- track.stop();
20455
- });
20456
- throw error_createError({
20457
- message: "No user media is defined",
20458
- options: this.options
20459
- });
20460
- }
20461
- this.options.logger.debug(`Recorder: getUserMediaCallback(${params ? string_pretty(params) : ""})`);
20462
- if (this.showUserMedia()) try {
20463
- this.userMedia.init(localStream, ()=>{
20464
- if (!this.unloaded && requestId === this.mediaRequestId) this.onUserMediaReady(params);
20465
- }, this.onAudioSample.bind(this), (err)=>{
20466
- if (!this.unloaded && requestId === this.mediaRequestId) this.userMediaErrorCallback(err, {
20467
- audio: isAudioEnabled(this.options)
20468
- });
20469
- }, params);
20470
- } catch (exc) {
20471
- localStream.getTracks().forEach((track)=>{
20784
+ getSampleRate() {
20785
+ if (this.hasAudioContext()) return this.getAudioContext().sampleRate;
20786
+ return -1;
20787
+ }
20788
+ }
20789
+ const media_AudioRecorder = AudioRecorder;
20790
+ const VIRTUAL_KEYWORDS = [
20791
+ "obs",
20792
+ "virtual",
20793
+ "manycam",
20794
+ "vcam",
20795
+ "splitcam",
20796
+ "droidcam"
20797
+ ];
20798
+ function isVirtualCamera(videoTrack) {
20799
+ const capabilities = videoTrack.getCapabilities();
20800
+ if (!capabilities.frameRate) return true;
20801
+ const isVirtual = VIRTUAL_KEYWORDS.some((keyword)=>videoTrack.label.toLowerCase().includes(keyword));
20802
+ return isVirtual;
20803
+ }
20804
+ const util_isVirtualCamera = isVirtualCamera;
20805
+ const EVENT_ASCII = "|—O—|";
20806
+ class UserMedia extends util_Despot {
20807
+ recorder;
20808
+ rawVisualUserMedia;
20809
+ paused = false;
20810
+ recording = false;
20811
+ audioRecorder;
20812
+ currentVisualStream;
20813
+ onPlayReached = false;
20814
+ onLoadedMetaDataReached = false;
20815
+ playingPromiseReached = false;
20816
+ videoTrackLabel;
20817
+ clearInitializationListeners;
20818
+ initializationId = 0;
20819
+ outputEvent = (e)=>{
20820
+ this.logEvent(e.type, {
20821
+ readyState: this.rawVisualUserMedia?.readyState
20822
+ });
20823
+ this.rawVisualUserMedia?.removeEventListener(e.type, this.outputEvent);
20824
+ };
20825
+ handleMediaError = (event)=>{
20826
+ this.options.logger.warn(`Caught video element error event: ${string_pretty(event)}`);
20827
+ };
20828
+ constructor(recorder, options){
20829
+ super("UserMedia", options);
20830
+ this.recorder = recorder;
20831
+ this.rawVisualUserMedia = recorder.getRawVisualUserMedia();
20832
+ mediaEvents.forEach((eventName)=>{
20833
+ this.rawVisualUserMedia?.addEventListener(eventName, this.outputEvent, false);
20834
+ });
20835
+ }
20836
+ attachMediaStream(stream) {
20837
+ if (this.rawVisualUserMedia) {
20838
+ this.rawVisualUserMedia.srcObject = stream;
20839
+ const previousStream = this.currentVisualStream;
20840
+ this.currentVisualStream = stream;
20841
+ if (previousStream && previousStream !== stream) previousStream.getTracks().forEach((track)=>{
20472
20842
  track.stop();
20473
20843
  });
20474
- this.userMediaLoading = false;
20475
- this.clearUserMediaTimeout();
20476
- this.userMediaErrorCallback(exc, {
20477
- audio: isAudioEnabled(this.options)
20478
- });
20479
- }
20844
+ } else throw error_createError({
20845
+ message: "Error attaching stream to element.",
20846
+ explanation: "Contact the developer about this",
20847
+ options: this.options
20848
+ });
20849
+ }
20850
+ setVisualStream(localMediaStream) {
20851
+ if (localMediaStream) this.attachMediaStream(localMediaStream);
20480
20852
  else {
20481
- localStream.getTracks().forEach((track)=>{
20482
- track.stop();
20483
- });
20484
- this.userMediaLoading = false;
20485
- this.clearUserMediaTimeout();
20486
- this.cancelCameraRecovery();
20853
+ if (this.rawVisualUserMedia) {
20854
+ this.rawVisualUserMedia.srcObject = null;
20855
+ this.rawVisualUserMedia.removeAttribute("src");
20856
+ }
20857
+ this.currentVisualStream = void 0;
20487
20858
  }
20488
20859
  }
20489
- loadGenuineUserMedia(params) {
20490
- this.options.logger.debug(`Recorder: loadGenuineUserMedia(${params ? string_pretty(params) : ""})`);
20491
- this.emit("ASKING_WEBCAM_PERMISSION");
20492
- this.userMediaLoading = true;
20493
- const requestId = ++this.mediaRequestId;
20494
- const constraints = {
20495
- video: {
20496
- frameRate: {
20497
- ideal: this.options.video.fps
20860
+ async restorePlayback() {
20861
+ if (!this.rawVisualUserMedia) throw new Error("No camera preview is available.");
20862
+ await this.rawVisualUserMedia.play();
20863
+ await this.audioRecorder?.resume();
20864
+ }
20865
+ hasLiveStream() {
20866
+ const videoReady = this.currentVisualStream?.getVideoTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
20867
+ const audioReady = !isAudioEnabled(this.options) || this.currentVisualStream?.getAudioTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
20868
+ return Boolean(this.currentVisualStream?.active && videoReady && audioReady);
20869
+ }
20870
+ hasEnded() {
20871
+ if (this.rawVisualUserMedia?.ended) return this.rawVisualUserMedia.ended;
20872
+ return !this.currentVisualStream?.active;
20873
+ }
20874
+ hasInvalidDimensions() {
20875
+ if (!this.rawVisualUserMedia) return false;
20876
+ const { videoWidth, videoHeight } = this.rawVisualUserMedia;
20877
+ return videoWidth > 0 && videoWidth < 3 || videoHeight > 0 && videoHeight < 3;
20878
+ }
20879
+ logEvent(eventType, params) {
20880
+ this.options.logger.debug(`UserMedia: ... ${EVENT_ASCII} event ${eventType}: ${string_pretty(params)}`);
20881
+ }
20882
+ unloadRemainingEventListeners() {
20883
+ this.options.logger.debug("UserMedia: unloadRemainingEventListeners()");
20884
+ this.clearInitializationListeners?.();
20885
+ mediaEvents.forEach((eventName)=>{
20886
+ this.rawVisualUserMedia?.removeEventListener(eventName, this.outputEvent);
20887
+ });
20888
+ this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
20889
+ }
20890
+ audioRecord(audioCallback) {
20891
+ this.removeListener("SENDING_FIRST_FRAME");
20892
+ this.audioRecorder?.record(audioCallback);
20893
+ }
20894
+ init(localMediaStream, videoCallback, audioCallback, endedEarlyCallback, params) {
20895
+ this.clearInitializationListeners?.();
20896
+ this.stop(localMediaStream, {
20897
+ aboutToInitialize: true,
20898
+ preserveRecordingState: Boolean(params?.switchingFacingMode || params?.recovering)
20899
+ });
20900
+ const initializationId = ++this.initializationId;
20901
+ this.onPlayReached = false;
20902
+ this.onLoadedMetaDataReached = false;
20903
+ this.playingPromiseReached = false;
20904
+ this.videoTrackLabel = void 0;
20905
+ if (isAudioEnabled(this.options)) this.audioRecorder ??= new media_AudioRecorder(this, this.options);
20906
+ const unloadAllEventListeners = ()=>{
20907
+ this.options.logger.debug("UserMedia: unloadAllEventListeners()");
20908
+ this.unloadRemainingEventListeners();
20909
+ this.removeListener("SENDING_FIRST_FRAME");
20910
+ this.rawVisualUserMedia?.removeEventListener("play", onPlay);
20911
+ this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
20912
+ };
20913
+ this.clearInitializationListeners = ()=>{
20914
+ this.rawVisualUserMedia?.removeEventListener("play", onPlay);
20915
+ this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
20916
+ this.clearInitializationListeners = void 0;
20917
+ };
20918
+ const play = ()=>{
20919
+ try {
20920
+ this.rawVisualUserMedia?.load();
20921
+ if (this.rawVisualUserMedia?.paused) {
20922
+ this.options.logger.debug(`UserMedia: play(): media.readyState=${this.rawVisualUserMedia.readyState}, media.paused=${this.rawVisualUserMedia.paused}, media.ended=${this.rawVisualUserMedia.ended}, media.played=${string_pretty(this.rawVisualUserMedia.played)}`);
20923
+ this.rawVisualUserMedia.play().then(()=>{
20924
+ if (initializationId !== this.initializationId) return;
20925
+ if (!this.playingPromiseReached) {
20926
+ this.options.logger.debug("UserMedia: play promise successful. Playing now.");
20927
+ this.playingPromiseReached = true;
20928
+ }
20929
+ }).catch((exc)=>{
20930
+ if (initializationId !== this.initializationId) return;
20931
+ if (exc instanceof Error && !params?.recovering) this.options.logger.warn(`Caught pending user media promise exception: ${exc.toString()}`);
20932
+ else {
20933
+ unloadAllEventListeners();
20934
+ endedEarlyCallback(error_createError({
20935
+ message: "Failed to play user media upon play event.",
20936
+ exc,
20937
+ options: this.options
20938
+ }));
20939
+ }
20940
+ });
20941
+ }
20942
+ } catch (exc) {
20943
+ unloadAllEventListeners();
20944
+ endedEarlyCallback(exc);
20945
+ }
20946
+ };
20947
+ const fireCallbacks = ()=>{
20948
+ const readyState = this.rawVisualUserMedia?.readyState;
20949
+ this.options.logger.debug(`UserMedia: fireCallbacks(readyState=${readyState}, onPlayReached=${this.onPlayReached}, onLoadedMetaDataReached=${this.onLoadedMetaDataReached})`);
20950
+ if (this.onPlayReached && this.onLoadedMetaDataReached) if (this.audioRecorder) try {
20951
+ this.audioRecorder.init(localMediaStream);
20952
+ const onAudioReady = ()=>{
20953
+ if (initializationId !== this.initializationId || this.currentVisualStream !== localMediaStream) return;
20954
+ if (this.recording) this.audioRecord(audioCallback);
20955
+ else this.on("SENDING_FIRST_FRAME", ()=>{
20956
+ this.audioRecord(audioCallback);
20957
+ });
20958
+ this.clearInitializationListeners?.();
20959
+ videoCallback();
20960
+ };
20961
+ if (params?.recovering) this.audioRecorder.resume().then(onAudioReady).catch(endedEarlyCallback);
20962
+ else onAudioReady();
20963
+ } catch (exc) {
20964
+ unloadAllEventListeners();
20965
+ this.stop(void 0, {
20966
+ preserveRecordingState: Boolean(params?.recovering)
20967
+ });
20968
+ endedEarlyCallback(exc);
20969
+ }
20970
+ else {
20971
+ this.clearInitializationListeners?.();
20972
+ videoCallback();
20973
+ }
20974
+ };
20975
+ const onPlay = ()=>{
20976
+ try {
20977
+ this.logEvent("play", {
20978
+ readyState: this.rawVisualUserMedia?.readyState,
20979
+ audio: isAudioEnabled(this.options),
20980
+ width: this.rawVisualUserMedia?.width,
20981
+ height: this.rawVisualUserMedia?.height,
20982
+ videoWidth: this.rawVisualUserMedia?.videoWidth,
20983
+ videoHeight: this.rawVisualUserMedia?.videoHeight
20984
+ });
20985
+ this.rawVisualUserMedia?.removeEventListener("play", onPlay);
20986
+ if (this.hasEnded() || this.hasInvalidDimensions()) endedEarlyCallback(error_createError({
20987
+ message: "Already busy",
20988
+ explanation: "Probably another browser window is using your webcam?",
20989
+ options: this.options
20990
+ }));
20991
+ else {
20992
+ this.onPlayReached = true;
20993
+ fireCallbacks();
20498
20994
  }
20499
- },
20500
- audio: isAudioEnabled(this.options)
20995
+ } catch (exc) {
20996
+ unloadAllEventListeners();
20997
+ endedEarlyCallback(exc);
20998
+ }
20501
20999
  };
20502
- this.configureVideoConstraints(constraints, params);
20503
- this.options.logger.debug(string_summarize("Recorder: our webcam constraints are:", constraints));
20504
- this.options.logger.debug(string_summarize("Recorder: available webcam constraints are:", navigator.mediaDevices.getSupportedConstraints()));
20505
- const streamPromise = navigator.mediaDevices.getUserMedia(constraints);
20506
- streamPromise.then((localStream)=>{
20507
- if (this.unloaded || requestId !== this.mediaRequestId) return void localStream.getTracks().forEach((track)=>{
20508
- track.stop();
21000
+ const onLoadedMetaData = ()=>{
21001
+ this.logEvent("loadedmetadata", {
21002
+ readyState: this.rawVisualUserMedia?.readyState,
21003
+ paused: this.rawVisualUserMedia?.paused,
21004
+ width: this.rawVisualUserMedia?.width,
21005
+ height: this.rawVisualUserMedia?.height,
21006
+ videoWidth: this.rawVisualUserMedia?.videoWidth,
21007
+ videoHeight: this.rawVisualUserMedia?.videoHeight
20509
21008
  });
20510
- this.getUserMediaCallback(localStream, params, requestId);
20511
- }).catch((reason)=>{
20512
- if (!this.unloaded && requestId === this.mediaRequestId) this.userMediaErrorCallback(reason, constraints);
20513
- });
20514
- }
20515
- configureVideoConstraints(constraints, params) {
20516
- const videoConstraints = "object" == typeof constraints.video ? constraints.video : void 0;
20517
- if (!videoConstraints) return;
20518
- if (params?.switchingFacingMode || params?.recovering) videoConstraints.facingMode = params.switchingFacingMode || this.facingMode;
20519
- if (this.options.video.width) videoConstraints.width = {
20520
- ideal: this.options.video.width
20521
- };
20522
- else {
20523
- const limitedWidth = this.limitWidth();
20524
- if (limitedWidth?.value) videoConstraints.width = {
20525
- ideal: limitedWidth.value
20526
- };
20527
- }
20528
- if (this.options.video.height) videoConstraints.height = {
20529
- ideal: this.options.video.height
21009
+ this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
21010
+ if (!this.hasEnded() && !this.hasInvalidDimensions()) {
21011
+ this.emit("LOADED_META_DATA");
21012
+ this.onLoadedMetaDataReached = true;
21013
+ fireCallbacks();
21014
+ }
20530
21015
  };
20531
- }
20532
- loadUserMedia(params) {
20533
- if (this.userMediaLoaded) {
20534
- this.options.logger.debug("Recorder: skipping loadUserMedia() because it is already loaded");
20535
- this.onUserMediaReady(params);
20536
- return;
20537
- } else if (this.userMediaLoading) return void this.options.logger.debug("Recorder: skipping loadUserMedia() because it is already asking for permission");
20538
- this.options.logger.debug(`Recorder: loadUserMedia(${params ? string_pretty(params) : ""})`);
20539
- this.emit("LOADING_USER_MEDIA");
20540
21016
  try {
20541
- if (!navigator.mediaDevices) throw new Error("No media devices are available.");
20542
- this.userMediaTimeout = window.setTimeout(()=>{
20543
- if (!this.userMediaLoaded) {
20544
- this.userMediaLoading = false;
20545
- const err = util_getBrowser(this.options).getNoAccessIssue();
20546
- this.userMediaErrorCallback(err, {
20547
- audio: isAudioEnabled(this.options)
20548
- });
20549
- }
20550
- }, this.options.timeouts.userMedia);
20551
- this.userMediaLoading = true;
20552
- this.loadGenuineUserMedia(params);
20553
- } catch (exc) {
20554
- this.clearUserMediaTimeout();
20555
- this.options.logger.debug("Recorder: failed to load genuine user media");
20556
- this.userMediaLoading = false;
20557
- if (this.cameraRecovery) return void this.failCameraRecovery(exc);
20558
- const errorListeners = this.getListeners("ERROR");
20559
- if (errorListeners?.length) this.emit("ERROR", {
20560
- exc
21017
+ const videoTrack = media_getFirstVideoTrack(localMediaStream);
21018
+ this.videoTrackLabel = videoTrack?.label;
21019
+ if (videoTrack) if (videoTrack.enabled) if (util_isVirtualCamera(videoTrack)) throw error_createError({
21020
+ message: "Virtual cameras are not allowed.",
21021
+ explanation: `Please use a real camera, not "${videoTrack.label}".`,
21022
+ options: this.options
20561
21023
  });
20562
21024
  else {
20563
- this.options.logger.debug("Recorder: no error listeners attached but throwing exception further");
20564
- throw exc;
20565
- }
20566
- }
20567
- }
20568
- executeCommand(command) {
20569
- if (this.unloaded) return;
20570
- try {
20571
- if (command.args) {
20572
- const debugLine = string_summarize(`Server commanded: ${command.command} with:`, command.args);
20573
- this.options.logger.debug(debugLine);
20574
- } else this.options.logger.debug(`Server commanded: ${command.command}`);
20575
- switch(command.command){
20576
- case "ready":
20577
- this.emit("SERVER_READY");
20578
- if (!this.userMediaTimeout) if (this.options.loadUserMediaOnRecord) this.show();
20579
- else this.loadUserMedia();
20580
- break;
20581
- case "preview":
20582
- this.preview(command.args);
20583
- break;
20584
- case "error":
20585
- {
20586
- let explanation = "(No explanation given)";
20587
- if (command.args?.err?.message) explanation = command.args.err.message;
20588
- const err = error_createError({
20589
- message: "Websocket error from Videomail server",
20590
- explanation,
20591
- err: deserializeError(command.args?.err),
20592
- options: this.options
20593
- });
20594
- this.emit("ERROR", {
20595
- err
20596
- });
20597
- break;
20598
- }
20599
- case "confirmFrame":
20600
- this.updateFrameProgress(command.args);
20601
- break;
20602
- case "confirmSample":
20603
- this.updateSampleProgress(command.args);
20604
- break;
20605
- case "beginAudioEncoding":
20606
- this.emit("BEGIN_AUDIO_ENCODING");
20607
- break;
20608
- case "beginVideoEncoding":
20609
- this.emit("BEGIN_VIDEO_ENCODING");
20610
- break;
20611
- default:
20612
- {
20613
- const err = error_createError({
20614
- message: `Unknown server command: ${command.command}`,
20615
- options: this.options
20616
- });
20617
- this.emit("ERROR", {
20618
- err
20619
- });
20620
- break;
20621
- }
20622
- }
20623
- } catch (exc) {
20624
- this.emit("ERROR", {
20625
- exc
20626
- });
20627
- }
20628
- }
20629
- isNotifying() {
20630
- return this.visuals.isNotifying();
20631
- }
20632
- isHidden() {
20633
- return !this.recorderElement || html_isHidden(this.recorderElement);
20634
- }
20635
- writeCommand(command, args, cb) {
20636
- if (this.connected) {
20637
- if (this.stream) {
20638
- if (args) this.options.logger.debug(`$ ${command} with ${string_pretty(args)}`);
20639
- else this.options.logger.debug(`$ ${command}`);
20640
- const commandObj = {
20641
- command,
20642
- args
20643
- };
20644
- this.writeStream(Buffer.from(JSON.stringify(commandObj)), cb ? {
20645
- onFlushedCallback: ()=>{
20646
- queueMicrotask(cb);
20647
- }
20648
- } : void 0);
21025
+ let description = "";
21026
+ if (videoTrack.label.length > 0) description = description.concat(videoTrack.label);
21027
+ description = description.concat(` with enabled=${videoTrack.enabled}, muted=${videoTrack.muted}, readyState=${videoTrack.readyState}`);
21028
+ this.options.logger.debug(`UserMedia: ${videoTrack.kind} detected. ${description}`);
20649
21029
  }
20650
- } else {
20651
- this.options.logger.debug(`Reconnecting for the command ${command} …`);
20652
- this.initSocket(()=>{
20653
- this.writeCommand(command, args, cb);
21030
+ else throw error_createError({
21031
+ message: "Webcam is disabled",
21032
+ explanation: "The video track seems to be disabled. Enable it in your system.",
21033
+ options: this.options
20654
21034
  });
21035
+ else this.options.logger.debug("UserMedia: detected (but no video tracks exist");
21036
+ this.rawVisualUserMedia?.addEventListener("loadedmetadata", onLoadedMetaData);
21037
+ this.rawVisualUserMedia?.addEventListener("play", onPlay);
21038
+ this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
21039
+ this.rawVisualUserMedia?.addEventListener("error", this.handleMediaError);
21040
+ this.setVisualStream(localMediaStream);
21041
+ play();
21042
+ } catch (exc) {
21043
+ unloadAllEventListeners();
21044
+ endedEarlyCallback(exc);
20655
21045
  }
20656
21046
  }
20657
- cancelAnimationFrame() {
20658
- this.loop?.dispose();
20659
- }
20660
- getIntervalSum() {
20661
- if (!this.loop) return;
20662
- return this.loop.getElapsedTime();
20663
- }
20664
- getAvgInterval() {
20665
- const intervalSum = this.getIntervalSum() ?? 0;
20666
- if (!this.hasValidInterval(intervalSum)) return;
20667
- return intervalSum / this.framesCount;
20668
- }
20669
- getAvgFps() {
20670
- const intervalSum = this.getIntervalSum() ?? 0;
20671
- if (!this.hasValidInterval(intervalSum)) return;
20672
- return this.framesCount / intervalSum * 1000;
20673
- }
20674
- hasValidInterval(intervalSum) {
20675
- return intervalSum > 0;
20676
- }
20677
- getRecordingStats() {
20678
- return this.recordingStats;
21047
+ getVideoTrackLabel() {
21048
+ return this.videoTrackLabel;
20679
21049
  }
20680
- getAudioSampleRate() {
20681
- return this.recordingAudioSampleRate ?? this.userMedia?.getAudioSampleRate();
21050
+ isReady() {
21051
+ return Boolean(this.rawVisualUserMedia?.srcObject || this.rawVisualUserMedia?.currentSrc);
20682
21052
  }
20683
- stop(params) {
20684
- this.cancelCameraRecovery();
20685
- if (!this.userMedia) throw new Error("No user media defined, unable to stop");
20686
- this.options.logger.debug(`Recorder: stop(${params ? string_pretty(params) : ""})`);
20687
- this.recordingActive = false;
20688
- const limitReached = params?.limitReached;
20689
- this.emit("STOPPING", {
20690
- limitReached
20691
- });
20692
- this.loop?.complete();
20693
- this.stopTimeout = window.setTimeout(()=>{
20694
- this.stopTimeout = void 0;
20695
- this.stopTime = Date.now();
20696
- const videoType = this.replay.getVideoType();
20697
- if (!videoType) throw new Error("Unable to video record when no video type is defined.");
20698
- this.recordingStats = {
20699
- avgFps: this.getAvgFps(),
20700
- wantedFps: this.options.video.fps,
20701
- avgInterval: this.getAvgInterval(),
20702
- wantedInterval: 1e3 / this.options.video.fps,
20703
- intervalSum: this.getIntervalSum(),
20704
- framesCount: this.framesCount,
20705
- videoType
20706
- };
20707
- if (this.userMedia) this.recordingStats.videoTrackLabel = this.userMedia.getVideoTrackLabel();
20708
- if (isAudioEnabled(this.options) && this.userMedia) {
20709
- this.recordingStats.samplesCount = this.samplesCount;
20710
- this.recordingStats.sampleRate = this.recordingAudioSampleRate ?? this.userMedia.getAudioSampleRate();
20711
- }
20712
- this.writeCommand("stop", this.recordingStats, ()=>{
20713
- this.emit("STOPPED", {
20714
- recordingStats: this.recordingStats
21053
+ stop(visualStream, params) {
21054
+ try {
21055
+ let chosenStream = visualStream;
21056
+ if (!params?.aboutToInitialize) {
21057
+ this.initializationId++;
21058
+ this.clearInitializationListeners?.();
21059
+ chosenStream ??= this.currentVisualStream;
21060
+ const tracks = chosenStream?.getTracks();
21061
+ if (tracks) tracks.forEach((track)=>{
21062
+ track.stop();
20715
21063
  });
21064
+ this.setVisualStream();
21065
+ this.audioRecorder?.stop();
21066
+ this.audioRecorder = void 0;
21067
+ }
21068
+ if (!params?.preserveRecordingState) this.paused = this.recording = false;
21069
+ } catch (exc) {
21070
+ this.emit("ERROR", {
21071
+ exc
20716
21072
  });
20717
- this.reset();
20718
- }, 60);
21073
+ }
20719
21074
  }
20720
- back(cb) {
20721
- this.emit("GOING_BACK");
20722
- this.key = void 0;
20723
- this.recordingDimensions = void 0;
20724
- this.unloaded = false;
20725
- this.show();
20726
- this.writeCommand("back", void 0, cb);
21075
+ createCanvas() {
21076
+ const canvas = document.createElement("canvas");
21077
+ const rawWidthDimension = this.getRawWidth(true);
21078
+ if (rawWidthDimension.value) canvas.width = rawWidthDimension.value;
21079
+ const rawHeight = this.getRawHeight(true);
21080
+ if (rawHeight) canvas.height = rawHeight;
21081
+ return canvas;
20727
21082
  }
20728
- reInitializeAudio() {
20729
- this.options.logger.debug("Recorder: reInitializeAudio()");
20730
- this.clearUserMediaTimeout();
20731
- this.userMediaLoading = false;
20732
- this.userMedia?.stop();
20733
- this.recordingDimensions = void 0;
20734
- this.userMediaLoaded = this.key = this.canvas = this.ctx = void 0;
20735
- this.loadUserMedia();
21083
+ getVideoHeight() {
21084
+ if (!this.rawVisualUserMedia) return;
21085
+ return this.rawVisualUserMedia.videoHeight || this.rawVisualUserMedia.height;
20736
21086
  }
20737
- unload(params) {
20738
- if (this.unloaded || !this.built) return;
20739
- const e = params?.e;
20740
- let cause;
20741
- if (e) cause = e.type;
20742
- const prettyCause = cause ? `, cause: ${cause}` : "";
20743
- this.options.logger.debug(`Recorder: unload()${prettyCause}`);
20744
- this.recordingActive = false;
20745
- this.mediaTransmissionStarted = false;
20746
- this.reset();
20747
- this.recordingDimensions = void 0;
20748
- this.clearUserMediaTimeout();
20749
- this.clearConnectionTimeout();
20750
- this.clearReconnectTimeout();
20751
- this.clearRetryTimeout();
20752
- this.clearStopTimeout();
20753
- this.stopPings();
20754
- this.userMediaLoading = false;
20755
- this.connecting = false;
20756
- this.reconnecting = false;
20757
- this.reconnectAttempts = 0;
20758
- if (this.userMedia) this.userMedia.unloadRemainingEventListeners();
20759
- if (this.submitting) ;
20760
- else if (this.stream) this.discardSocket();
20761
- this.unloaded = true;
20762
- this.built = this.connecting = this.connected = false;
21087
+ getVideoWidth() {
21088
+ if (!this.rawVisualUserMedia) return;
21089
+ return this.rawVisualUserMedia.videoWidth || this.rawVisualUserMedia.width;
20763
21090
  }
20764
- reset() {
20765
- this.cancelCameraRecovery();
20766
- if (!this.unloaded) {
20767
- this.options.logger.debug("Recorder: reset()");
20768
- this.emit("RESETTING");
20769
- this.cancelAnimationFrame();
20770
- this.userMedia?.stop();
20771
- this.replay.reset();
20772
- this.userMediaLoaded = this.key = this.canvas = this.ctx = this.recordingBuffer = void 0;
20773
- }
21091
+ hasVideoWidth() {
21092
+ const videoWidth = this.getVideoWidth();
21093
+ return videoWidth && videoWidth > 0;
20774
21094
  }
20775
- clearUserMediaTimeout() {
20776
- if (void 0 !== this.userMediaTimeout) {
20777
- this.options.logger.debug("Recorder: clearUserMediaTimeout()");
20778
- window.clearTimeout(this.userMediaTimeout);
20779
- this.userMediaTimeout = void 0;
21095
+ getRawWidth(responsive) {
21096
+ let rawWidth;
21097
+ const widthDimension = {
21098
+ unit: "px"
21099
+ };
21100
+ rawWidth = responsive ? this.getVideoWidth() : this.options.video.width ? this.options.video.width : this.getVideoWidth();
21101
+ if (responsive) {
21102
+ const widthDimension = this.recorder.limitWidth(rawWidth);
21103
+ rawWidth = widthDimension?.value;
20780
21104
  }
21105
+ widthDimension.value = rawWidth;
21106
+ return widthDimension;
20781
21107
  }
20782
- clearStopTimeout() {
20783
- if (void 0 === this.stopTimeout) return;
20784
- window.clearTimeout(this.stopTimeout);
20785
- this.stopTimeout = void 0;
21108
+ getRawHeight(responsive) {
21109
+ let rawHeight;
21110
+ if (responsive) {
21111
+ rawHeight = this.options.video.height || this.getVideoHeight();
21112
+ const ratio = this.recorder.getRatio();
21113
+ const rawWidth = this.getRawWidth(responsive);
21114
+ if (void 0 !== ratio && rawWidth.value) rawHeight = rawWidth.value * ratio;
21115
+ } else rawHeight = this.options.video.height ? this.options.video.height : this.getVideoHeight();
21116
+ if (responsive) {
21117
+ const heightDimension = this.recorder.limitHeight(rawHeight);
21118
+ rawHeight = heightDimension.value;
21119
+ }
21120
+ return rawHeight;
20786
21121
  }
20787
- validate() {
20788
- return Boolean(this.key) && void 0 === this.canvas;
21122
+ getRawVisuals() {
21123
+ return this.rawVisualUserMedia;
20789
21124
  }
20790
- isReady() {
20791
- return this.userMedia?.isReady();
21125
+ pause() {
21126
+ this.paused = true;
20792
21127
  }
20793
- finishCameraRecovery(restored) {
20794
- this.clearUserMediaTimeout();
20795
- const resolve = this.resolveCameraRecovery;
20796
- const resumeRequested = restored && this.resumeAfterCameraRecovery;
20797
- this.cameraRecovery = void 0;
20798
- this.resolveCameraRecovery = void 0;
20799
- this.resumeAfterCameraRecovery = false;
20800
- resolve?.(restored);
20801
- if (resumeRequested && this.isPaused()) this.resume();
21128
+ isPaused() {
21129
+ return this.paused;
20802
21130
  }
20803
- cancelCameraRecovery() {
20804
- if (!this.cameraRecovery) return;
20805
- this.mediaRequestId++;
20806
- this.userMediaLoading = false;
20807
- this.userMediaLoaded = false;
20808
- this.clearUserMediaTimeout();
20809
- this.userMedia?.stop(void 0, {
20810
- preserveRecordingState: true
20811
- });
20812
- this.finishCameraRecovery(false);
21131
+ resume() {
21132
+ this.paused = false;
20813
21133
  }
20814
- failCameraRecovery(exc) {
20815
- this.cancelCameraRecovery();
20816
- const err = error_createError({
20817
- message: "Camera recovery failed",
20818
- explanation: "Your recording is still paused. Check camera permissions, then try Resume again or stop to keep the recorded part.",
20819
- exc,
20820
- options: this.options
20821
- });
20822
- this.emit("CAMERA_RECOVERY_FAILED", {
20823
- err
20824
- });
21134
+ record() {
21135
+ this.recording = true;
20825
21136
  }
20826
- recoverCamera() {
20827
- if (this.cameraRecovery) return this.cameraRecovery;
20828
- if (!this.built || this.unloaded || !this.userMedia || this.userMediaLoading || this.submitting || this.key || void 0 !== this.stopTimeout || this.recordingTransportFailed) return Promise.resolve(false);
20829
- const liveStream = this.userMedia.hasLiveStream();
20830
- if (liveStream && this.isRecording()) return Promise.resolve(true);
20831
- if (!liveStream && this.recordingActive && !this.isPaused()) this.pause();
20832
- const recovery = new Promise((resolve)=>{
20833
- this.resolveCameraRecovery = resolve;
20834
- });
20835
- this.cameraRecovery = recovery;
20836
- this.emit("CAMERA_RECOVERING");
20837
- if (liveStream) {
20838
- this.userMediaTimeout = window.setTimeout(()=>{
20839
- this.failCameraRecovery(new Error("Camera playback recovery timed out."));
20840
- }, this.options.timeouts.userMedia);
20841
- this.userMedia.restorePlayback().then(()=>{
20842
- if (this.cameraRecovery === recovery) this.finishCameraRecovery(true);
20843
- }).catch((exc)=>{
20844
- if (this.cameraRecovery === recovery) this.failCameraRecovery(exc);
20845
- });
20846
- } else {
20847
- this.userMediaLoaded = false;
20848
- this.loadUserMedia({
20849
- recovering: true
20850
- });
20851
- }
20852
- return recovery;
21137
+ isRecording() {
21138
+ return this.recording;
20853
21139
  }
20854
- pause(params) {
20855
- this.cancelCameraRecovery();
20856
- this.resumeAfterDrain = false;
20857
- if (params) this.options.logger.debug(`pause() at frame ${this.framesCount} with ${string_pretty(params)}`);
20858
- else this.options.logger.debug(`pause() at frame ${this.framesCount}`);
20859
- this.userMedia?.pause();
20860
- this.loop.stop();
20861
- this.emit("PAUSED");
20862
- this.sendPings();
21140
+ getAudioSampleRate() {
21141
+ if (this.audioRecorder) return this.audioRecorder.getSampleRate();
21142
+ return -1;
20863
21143
  }
20864
- resume() {
20865
- if (this.cameraRecovery || !this.userMedia?.hasLiveStream()) {
20866
- this.recoverCamera();
20867
- this.resumeAfterCameraRecovery = Boolean(this.cameraRecovery);
20868
- return;
20869
- }
20870
- this.options.logger.debug(`Recorder: resume() with frame ${this.framesCount}`);
20871
- this.stopPings();
20872
- this.emit("RESUMING");
20873
- this.userMedia.resume();
20874
- if (this.streamBackpressured) this.resumeAfterDrain = true;
20875
- else this.loop.start();
21144
+ getCharacteristics() {
21145
+ return {
21146
+ audioSampleRate: this.getAudioSampleRate(),
21147
+ muted: this.rawVisualUserMedia?.muted,
21148
+ width: this.rawVisualUserMedia?.width,
21149
+ height: this.rawVisualUserMedia?.height,
21150
+ videoWidth: this.rawVisualUserMedia?.videoWidth,
21151
+ videoHeight: this.rawVisualUserMedia?.videoHeight
21152
+ };
20876
21153
  }
20877
- onFlushed(opts) {
20878
- const frameNumber = opts.frameNumber;
20879
- if (1 === frameNumber) this.emit("FIRST_FRAME_SENT");
21154
+ }
21155
+ const visuals_userMedia = UserMedia;
21156
+ class Recorder extends capture {
21157
+ dimensions;
21158
+ constructor(visuals, replay, options){
21159
+ super(visuals, replay, options);
21160
+ this.dimensions = new recorder_dimensions({
21161
+ options,
21162
+ visuals,
21163
+ getRecorderElement: ()=>this.recorderElement,
21164
+ getUserMedia: ()=>this.userMedia
21165
+ });
20880
21166
  }
20881
- draw(_deltaTime, elapsedTime) {
20882
- if (this.recordingTransportFailed) return;
20883
- if (!this.userMedia) throw new Error("No user media defined, unable to draw on canvas");
21167
+ executeCommand(command) {
21168
+ if (this.unloaded) return;
20884
21169
  try {
20885
- if (!this.isPaused() && this.stream && this.ctx) {
20886
- if (0 === this.framesCount) this.emit("SENDING_FIRST_FRAME");
20887
- this.framesCount++;
20888
- const imageSource = this.userMedia.getRawVisuals();
20889
- if (this.canvas && imageSource) this.ctx.drawImage(imageSource, 0, 0, this.canvas.width, this.canvas.height);
20890
- else throw new Error("Unable to draw an image without a defined canvas");
20891
- this.recordingBuffer = this.frame?.toBuffer();
20892
- const recordingBufferLength = this.recordingBuffer?.length;
20893
- if (recordingBufferLength) {
20894
- if (this.recordingBuffer) {
20895
- const frameControlBuffer = Buffer.from(JSON.stringify({
20896
- frameNumber: this.framesCount
20897
- }));
20898
- const frameBuffer = Buffer.concat([
20899
- this.recordingBuffer,
20900
- frameControlBuffer
20901
- ]);
20902
- this.mediaTransmissionStarted = true;
20903
- this.writeStream(frameBuffer, {
20904
- frameNumber: this.framesCount,
20905
- onFlushedCallback: this.onFlushed.bind(this)
21170
+ if (command.args) {
21171
+ const debugLine = string_summarize(`Server commanded: ${command.command} with:`, command.args);
21172
+ this.options.logger.debug(debugLine);
21173
+ } else this.options.logger.debug(`Server commanded: ${command.command}`);
21174
+ switch(command.command){
21175
+ case "ready":
21176
+ this.emit("SERVER_READY");
21177
+ if (!this.userMediaTimeout) if (this.options.loadUserMediaOnRecord) this.show();
21178
+ else this.loadUserMedia();
21179
+ break;
21180
+ case "preview":
21181
+ this.preview(command.args);
21182
+ break;
21183
+ case "error":
21184
+ {
21185
+ let explanation = "(No explanation given)";
21186
+ if (command.args?.err?.message) explanation = command.args.err.message;
21187
+ const err = error_createError({
21188
+ message: "Websocket error from Videomail server",
21189
+ explanation,
21190
+ err: deserializeError(command.args?.err),
21191
+ options: this.options
20906
21192
  });
20907
- this.visuals.checkTimer(elapsedTime);
21193
+ this.emit("ERROR", {
21194
+ err
21195
+ });
21196
+ break;
21197
+ }
21198
+ case "confirmFrame":
21199
+ this.updateFrameProgress(command.args);
21200
+ break;
21201
+ case "confirmSample":
21202
+ this.updateSampleProgress(command.args);
21203
+ break;
21204
+ case "beginAudioEncoding":
21205
+ this.emit("BEGIN_AUDIO_ENCODING");
21206
+ break;
21207
+ case "beginVideoEncoding":
21208
+ this.emit("BEGIN_VIDEO_ENCODING");
21209
+ break;
21210
+ default:
21211
+ {
21212
+ const err = error_createError({
21213
+ message: `Unknown server command: ${command.command}`,
21214
+ options: this.options
21215
+ });
21216
+ this.emit("ERROR", {
21217
+ err
21218
+ });
21219
+ break;
20908
21220
  }
20909
- } else throw error_createError({
20910
- message: "Failed to extract webcam data.",
20911
- options: this.options
20912
- });
20913
21221
  }
20914
21222
  } catch (exc) {
20915
21223
  this.emit("ERROR", {
@@ -20917,156 +21225,34 @@
20917
21225
  });
20918
21226
  }
20919
21227
  }
20920
- createLoop() {
20921
- const newLoop = animitter_default()({
20922
- fps: this.options.video.fps
20923
- }, this.draw.bind(this));
20924
- this.originalAnimationFrameObject = newLoop.getRequestAnimationFrameObject();
20925
- return newLoop;
20926
- }
20927
- record() {
20928
- if (this.unloaded || this.cameraRecovery) return;
20929
- if (!this.connected) {
20930
- this.options.logger.debug("Recorder: reconnecting before recording …");
20931
- this.initSocket(()=>{
20932
- this.once("SERVER_READY", ()=>{
20933
- if (this.unloaded) return;
20934
- if (this.userMediaLoaded || this.options.loadUserMediaOnRecord) this.record();
20935
- else this.once("USER_MEDIA_READY", ()=>{
20936
- if (!this.unloaded) this.record();
20937
- });
20938
- });
20939
- });
20940
- return;
20941
- }
20942
- if (!this.userMediaLoaded) {
20943
- if (this.options.loadUserMediaOnRecord) this.loadUserMedia({
20944
- recordWhenReady: true
20945
- });
20946
- else {
20947
- const err = error_createError({
20948
- message: "Load and enable your camera first",
20949
- options: this.options
20950
- });
20951
- this.emit("ERROR", {
20952
- err
20953
- });
20954
- }
20955
- return;
20956
- }
20957
- this.recordingDimensions = void 0;
20958
- try {
20959
- if (!this.userMedia) throw new Error("No user media defined, unable to create canvas");
20960
- this.canvas = this.userMedia.createCanvas();
20961
- } catch (exc) {
20962
- const err = error_createError({
20963
- exc,
20964
- options: this.options
20965
- });
20966
- this.emit("ERROR", {
20967
- err
20968
- });
20969
- return;
20970
- }
20971
- this.ctx = this.canvas.getContext("2d");
20972
- if (!this.canvas.width) {
20973
- const err = error_createError({
20974
- message: "Canvas has an invalid width.",
20975
- options: this.options
20976
- });
20977
- this.emit("ERROR", {
20978
- err
20979
- });
20980
- return;
20981
- }
20982
- if (!this.canvas.height) {
20983
- const err = error_createError({
20984
- message: "Canvas has an invalid height.",
20985
- options: this.options
20986
- });
20987
- this.emit("ERROR", {
20988
- err
20989
- });
20990
- return;
20991
- }
20992
- this.recordingDimensions = {
20993
- width: this.canvas.width,
20994
- height: this.canvas.height
20995
- };
20996
- this.frame = new canvas_to_buffer_modern_r(this.canvas, this.options.image.types, this.options.image.quality);
20997
- this.options.logger.debug("Recorder: record()");
20998
- this.recordingActive = true;
20999
- this.recordingTransportFailed = false;
21228
+ unload(params) {
21229
+ if (this.unloaded || !this.built) return;
21230
+ const e = params?.e;
21231
+ let cause;
21232
+ if (e) cause = e.type;
21233
+ const prettyCause = cause ? `, cause: ${cause}` : "";
21234
+ this.options.logger.debug(`Recorder: unload()${prettyCause}`);
21235
+ this.recordingActive = false;
21000
21236
  this.mediaTransmissionStarted = false;
21001
- this.recordingAudioSampleRate = void 0;
21002
- this.userMedia.record();
21003
- this.emit("RECORDING", {
21004
- framesCount: this.framesCount
21005
- });
21006
- this.loop.on("update", (_deltaTime, elapsedTime)=>{
21007
- let avgFPS;
21008
- avgFPS = 0 !== elapsedTime ? Math.round(this.framesCount / elapsedTime * 1000) : void 0;
21009
- this.options.logger.debug(`Recorder updates avgFps = ${avgFPS} at frame ${this.framesCount}`);
21010
- });
21011
- this.loop.start();
21012
- }
21013
- setAnimationFrameObject(newObj) {
21014
- if (this.loop) {
21015
- const isRecording = this.isRecording();
21016
- this.loop.stop();
21017
- this.loop.setRequestAnimationFrameObject(newObj);
21018
- if (isRecording) this.loop.start();
21019
- }
21020
- }
21021
- restoreAnimationFrameObject() {
21022
- this.options.logger.debug("Recorder: restoreAnimationFrameObject()");
21023
- this.setAnimationFrameObject(this.originalAnimationFrameObject);
21024
- }
21025
- loopWithTimeouts() {
21026
- this.options.logger.debug("Recorder: loopWithTimeouts()");
21027
- const wantedInterval = 1e3 / this.options.video.fps;
21028
- let processingTime = 0;
21029
- let start;
21030
- const raf = (fn)=>setTimeout(()=>{
21031
- start = Date.now();
21032
- fn();
21033
- processingTime = Date.now() - start;
21034
- }, wantedInterval - processingTime);
21035
- const cancel = (id)=>{
21036
- window.clearTimeout(id);
21037
- };
21038
- this.setAnimationFrameObject({
21039
- requestAnimationFrame: raf,
21040
- cancelAnimationFrame: cancel
21041
- });
21237
+ this.reset();
21238
+ this.recordingDimensions = void 0;
21239
+ this.clearUserMediaTimeout();
21240
+ this.clearConnectionTimeout();
21241
+ this.clearReconnectTimeout();
21242
+ this.clearRetryTimeout();
21243
+ this.clearStopTimeout();
21244
+ this.stopPings();
21245
+ this.userMediaLoading = false;
21246
+ this.connecting = false;
21247
+ this.reconnecting = false;
21248
+ this.reconnectAttempts = 0;
21249
+ if (this.userMedia) this.userMedia.unloadRemainingEventListeners();
21250
+ this.unloaded = true;
21251
+ this.discardSocket();
21252
+ this.built = this.connecting = this.connected = false;
21042
21253
  }
21043
21254
  correctDimensions() {
21044
- if (!this.recorderElement) return;
21045
- const widthDimension = dimensions_useFullWidth(this.options.video.mobileBreakPoint);
21046
- if (this.options.video.stretch || widthDimension) {
21047
- this.recorderElement.style.width = "100%";
21048
- this.recorderElement.style.removeProperty("height");
21049
- } else {
21050
- if (this.options.video.width) {
21051
- const recorderWidth = this.getRecorderWidth(true);
21052
- if (recorderWidth?.value) this.recorderElement.width = recorderWidth.value;
21053
- else this.recorderElement.style.removeProperty("width");
21054
- }
21055
- if (this.options.video.height) {
21056
- const recorderHeight = this.getRecorderHeight(true);
21057
- if (recorderHeight?.value) this.recorderElement.height = recorderHeight.value;
21058
- else this.recorderElement.style.removeProperty("height");
21059
- }
21060
- }
21061
- }
21062
- switchFacingMode() {
21063
- if (this.cameraRecovery || !util_getBrowser(this.options).isMobile()) return;
21064
- if ("user" === this.facingMode) this.facingMode = "environment";
21065
- else if ("environment" === this.facingMode) this.facingMode = "user";
21066
- else this.options.logger.warn(`Recorder: unsupported facing mode ${string_pretty(this.facingMode)}`);
21067
- this.loadGenuineUserMedia({
21068
- switchingFacingMode: this.facingMode
21069
- });
21255
+ this.dimensions.correctDimensions();
21070
21256
  }
21071
21257
  initEvents() {
21072
21258
  this.options.logger.debug("Recorder: initEvents()");
@@ -21138,100 +21324,20 @@
21138
21324
  }
21139
21325
  this.built = true;
21140
21326
  }
21141
- isPaused() {
21142
- return this.userMedia?.isPaused() && !this.loop.isRunning();
21143
- }
21144
- isRecording() {
21145
- return Boolean(this.loop?.isRunning() && !this.isPaused() && !this.isNotifying() && this.stream && !this.stream.destroyed);
21146
- }
21147
- hide() {
21148
- if (!this.isHidden()) {
21149
- if (this.recorderElement) html_hideElement(this.recorderElement);
21150
- this.clearUserMediaTimeout();
21151
- this.clearRetryTimeout();
21152
- }
21153
- }
21154
- isUnloaded() {
21155
- return this.unloaded;
21156
- }
21157
21327
  getRecorderWidth(responsive) {
21158
- if (this.userMedia?.hasVideoWidth()) return this.userMedia.getRawWidth(responsive);
21159
- else if (responsive && this.options.video.width) return this.limitWidth(this.options.video.width);
21160
- const dimension = {
21161
- unit: "px",
21162
- value: this.options.video.width
21163
- };
21164
- return dimension;
21165
- }
21166
- getRecordingDimensions() {
21167
- return this.recordingDimensions && {
21168
- ...this.recordingDimensions
21169
- };
21328
+ return this.dimensions.getRecorderWidth(responsive);
21170
21329
  }
21171
21330
  getRecorderHeight(responsive, useBoundingClientRect) {
21172
- let recorderHeight;
21173
- if (this.recorderElement && useBoundingClientRect) {
21174
- const height = this.recorderElement.getBoundingClientRect().height;
21175
- recorderHeight = {
21176
- unit: "px",
21177
- value: height
21178
- };
21179
- } else if (this.userMedia) {
21180
- let height = this.userMedia.getRawHeight(responsive);
21181
- if (void 0 !== height && this.options.video.width) {
21182
- const ratio = this.getRatio();
21183
- if (void 0 !== ratio) {
21184
- const idealHeight = this.options.video.width * ratio;
21185
- height = Math.min(idealHeight, height);
21186
- }
21187
- }
21188
- recorderHeight = {
21189
- unit: "px",
21190
- value: height
21191
- };
21192
- } else if (responsive && this.options.video.height) recorderHeight = this.calculateHeight(responsive);
21193
- else if (this.options.video.height) {
21194
- const height = this.options.video.height;
21195
- recorderHeight = {
21196
- unit: "px",
21197
- value: height
21198
- };
21199
- }
21200
- return recorderHeight;
21331
+ return this.dimensions.getRecorderHeight(responsive, useBoundingClientRect);
21201
21332
  }
21202
21333
  getRatio() {
21203
- let ratio;
21204
- if (this.userMedia) {
21205
- const userMediaVideoWidth = this.userMedia.getVideoWidth();
21206
- const userMediaVideoHeight = this.userMedia.getVideoHeight();
21207
- if (!userMediaVideoWidth || userMediaVideoWidth < 1) ratio = this.visuals.getRatio();
21208
- else if (userMediaVideoHeight) ratio = userMediaVideoHeight / userMediaVideoWidth;
21209
- } else ratio = dimensions_getRatio(this.options);
21210
- return ratio;
21334
+ return this.dimensions.getRatio();
21211
21335
  }
21212
21336
  calculateWidth(responsive) {
21213
- let videoHeight;
21214
- if (this.userMedia) videoHeight = this.userMedia.getVideoHeight();
21215
- else if (this.recorderElement) videoHeight = this.recorderElement.videoHeight || this.recorderElement.height;
21216
- const ratio = this.getRatio();
21217
- return dimensions_calculateWidth(responsive, this.options, videoHeight, ratio);
21337
+ return this.dimensions.calculateWidth(responsive);
21218
21338
  }
21219
21339
  calculateHeight(responsive) {
21220
- let videoDimension;
21221
- if (this.userMedia) {
21222
- const videoHeight = this.userMedia.getVideoHeight();
21223
- videoDimension = {
21224
- value: videoHeight,
21225
- unit: "px"
21226
- };
21227
- } else if (this.recorderElement) {
21228
- const videoHeight = this.recorderElement.videoHeight || this.recorderElement.height;
21229
- videoDimension = {
21230
- value: videoHeight,
21231
- unit: "px"
21232
- };
21233
- } else videoDimension = dimensions_calculateHeight(responsive, void 0, this.options, this.getRatio(), this.recorderElement);
21234
- return videoDimension;
21340
+ return this.dimensions.calculateHeight(responsive);
21235
21341
  }
21236
21342
  getRawVisualUserMedia() {
21237
21343
  return this.recorderElement;
@@ -21248,9 +21354,6 @@
21248
21354
  limitHeight(height) {
21249
21355
  return this.visuals.limitHeight(height);
21250
21356
  }
21251
- isUserMediaLoaded() {
21252
- return this.userMediaLoaded;
21253
- }
21254
21357
  }
21255
21358
  const visuals_recorder = Recorder;
21256
21359
  class Replay extends util_Despot {
@@ -22162,7 +22265,8 @@
22162
22265
  }
22163
22266
  else runValidation = false;
22164
22267
  if (runValidation) {
22165
- const targetName = event?.target?.name;
22268
+ const target = event?.target;
22269
+ const targetName = target instanceof Element ? target.getAttribute("name") : void 0;
22166
22270
  if (targetName) this.emit("VALIDATING", {
22167
22271
  targetName
22168
22272
  });
@@ -22197,27 +22301,10 @@
22197
22301
  }
22198
22302
  }
22199
22303
  if (valid) {
22200
- const recipients = this.form.getRecipients();
22201
- const toIsConfigured = "to" in recipients;
22202
- const ccIsConfigured = "cc" in recipients;
22203
- const bccIsConfigured = "bcc" in recipients;
22204
- const hasTo = recipients.to && recipients.to.length > 0;
22205
- const hasCc = recipients.cc && recipients.cc.length > 0;
22206
- const hasBcc = recipients.bcc && recipients.bcc.length > 0;
22207
- if (toIsConfigured) {
22208
- if (!hasTo) if (ccIsConfigured && bccIsConfigured) {
22209
- if (!hasCc && !hasBcc) valid = false;
22210
- } else if (ccIsConfigured) {
22211
- if (!hasCc) valid = false;
22212
- } else if (bccIsConfigured) {
22213
- if (!hasBcc) valid = false;
22214
- } else valid = false;
22215
- } else if (ccIsConfigured) {
22216
- if (!hasCc) {
22217
- if (bccIsConfigured && !hasBcc) valid = false;
22218
- }
22304
+ if (!email_isRecipientSelectionValid(this.form.getRecipients())) {
22305
+ valid = false;
22306
+ whyInvalid = "At least one recipient is required";
22219
22307
  }
22220
- if (!valid) whyInvalid = "At least one recipient is required";
22221
22308
  }
22222
22309
  } else {
22223
22310
  valid = visualsValid;