videomail-client 16.1.1 → 16.4.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
@@ -5954,7 +5954,8 @@
5954
5954
  var percentTwenties = /%20/g;
5955
5955
  var Format = {
5956
5956
  RFC1738: 'RFC1738',
5957
- RFC3986: 'RFC3986'
5957
+ RFC3986: 'RFC3986',
5958
+ WHATWG: 'WHATWG'
5958
5959
  };
5959
5960
  module1.exports = {
5960
5961
  default: Format.RFC3986,
@@ -5964,10 +5965,14 @@
5964
5965
  },
5965
5966
  RFC3986: function(value) {
5966
5967
  return String(value);
5968
+ },
5969
+ WHATWG: function(value) {
5970
+ return replace.call(value, percentTwenties, '+');
5967
5971
  }
5968
5972
  },
5969
5973
  RFC1738: Format.RFC1738,
5970
- RFC3986: Format.RFC3986
5974
+ RFC3986: Format.RFC3986,
5975
+ WHATWG: Format.WHATWG
5971
5976
  };
5972
5977
  },
5973
5978
  b (module1, __unused_rspack_exports, __webpack_require__) {
@@ -6028,8 +6033,8 @@
6028
6033
  target: copy
6029
6034
  };
6030
6035
  }
6031
- target[keys[j]] = copies.get(value);
6032
- } else target[keys[j]] = value;
6036
+ utils.setProperty(target, keys[j], copies.get(value));
6037
+ } else utils.setProperty(target, keys[j], value);
6033
6038
  }
6034
6039
  }
6035
6040
  return result;
@@ -6076,7 +6081,7 @@
6076
6081
  }
6077
6082
  return val.split(',');
6078
6083
  }
6079
- if (options.throwOnLimitExceeded && currentArrayLength >= options.arrayLimit) throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (1 === options.arrayLimit ? '' : 's') + ' allowed in an array.');
6084
+ if (options.throwOnLimitExceeded && currentArrayLength >= options.arrayLimit && currentArrayLength > 0) throw new RangeError('Array limit exceeded. Only ' + options.arrayLimit + ' element' + (1 === options.arrayLimit ? '' : 's') + ' allowed in an array.');
6080
6085
  return val;
6081
6086
  };
6082
6087
  var isoSentinel = 'utf8=%26%2310003%3B';
@@ -6113,11 +6118,13 @@
6113
6118
  val = options.strictNullHandling ? null : '';
6114
6119
  } else {
6115
6120
  key = options.decoder(part.slice(0, pos), defaults.decoder, charset, 'key');
6116
- if (null !== key) val = utils.maybeMap(parseArrayValue(part.slice(pos + 1), options, isArray(obj[key]) ? obj[key].length : 0), function(encodedVal) {
6121
+ if (null !== key) val = utils.maybeMap(parseArrayValue(part.slice(pos + 1), options, isArray(obj[key]) && ('combine' === options.duplicates || isEmptyBracketKey) ? obj[key].length : 0), function(encodedVal) {
6117
6122
  return options.decoder(encodedVal, defaults.decoder, charset, 'value');
6118
6123
  });
6119
6124
  }
6120
- if (val && options.interpretNumericEntities && 'iso-8859-1' === charset) val = interpretNumericEntities(String(val));
6125
+ if (val && options.interpretNumericEntities && 'iso-8859-1' === charset) val = utils.maybeMap(val, function(value) {
6126
+ return value ? interpretNumericEntities(String(value)) : value;
6127
+ });
6121
6128
  if (isEmptyBracketKey) val = isArray(val) ? [
6122
6129
  val
6123
6130
  ] : val;
@@ -6146,7 +6153,7 @@
6146
6153
  __proto__: null
6147
6154
  } : {};
6148
6155
  var cleanRoot = '[' === root.charAt(0) && ']' === root.charAt(root.length - 1) ? root.slice(1, -1) : root;
6149
- var decodedRoot = options.decodeDotInKeys ? cleanRoot.replace(/%2E/g, '.') : cleanRoot;
6156
+ var decodedRoot = options.decodeDotInKeys ? cleanRoot.replace(/%2E/gi, '.') : cleanRoot;
6150
6157
  var index = parseInt(decodedRoot, 10);
6151
6158
  var isValidArrayIndex = !isNaN(index) && root !== decodedRoot && String(index) === decodedRoot && index >= 0 && options.parseArrays;
6152
6159
  if (options.parseArrays || '' !== decodedRoot) {
@@ -6408,7 +6415,7 @@
6408
6415
  }
6409
6416
  var encodedPrefix = String(prefix);
6410
6417
  var adjustedPrefix = commaRoundTrip && isArray(obj) && 1 === obj.length ? encodedPrefix + '[]' : encodedPrefix;
6411
- if (allowEmptyArrays && isArray(obj) && 0 === obj.length && 0 === Object.keys(obj).length) return adjustedPrefix + '[]';
6418
+ if (allowEmptyArrays && isArray(obj) && 0 === obj.length && 0 === Object.keys(obj).length) return formatter(encoder && !encodeValuesOnly ? encoder(adjustedPrefix, defaults.encoder, charset, 'key', format) : adjustedPrefix) + '[]';
6412
6419
  for(var j = 0; j < objKeys.length; ++j){
6413
6420
  var key = objKeys[j];
6414
6421
  var value = 'object' == typeof key && key && void 0 !== key.value ? key.value : obj[key];
@@ -6474,7 +6481,7 @@
6474
6481
  filter = options.filter;
6475
6482
  obj = filter('', obj);
6476
6483
  } else if (isArray(options.filter)) {
6477
- filter = options.filter;
6484
+ filter = options.filter.slice();
6478
6485
  objKeys = filter;
6479
6486
  }
6480
6487
  var keys = [];
@@ -6490,7 +6497,7 @@
6490
6497
  var value = obj[key];
6491
6498
  if (!options.skipNulls || null !== value) {
6492
6499
  var encodedKey = options.encodeDotInKeys ? String(key).replace(/\./g, '%2E') : String(key);
6493
- pushToArray(keys, stringify(value, encodedKey, generateArrayPrefix, commaRoundTrip, options.allowEmptyArrays, options.strictNullHandling, options.skipNulls, options.encodeDotInKeys, options.encode ? options.encoder : null, options.filter, options.sort, options.allowDots, options.serializeDate, options.format, options.formatter, options.encodeValuesOnly, options.charset, sideChannel, options.depth, 0));
6500
+ pushToArray(keys, stringify(value, encodedKey, generateArrayPrefix, commaRoundTrip, options.allowEmptyArrays, options.strictNullHandling, options.skipNulls, options.encodeDotInKeys, options.encode ? options.encoder : null, filter, options.sort, options.allowDots, options.serializeDate, options.format, options.formatter, options.encodeValuesOnly, options.charset, sideChannel, options.depth, 0));
6494
6501
  }
6495
6502
  }
6496
6503
  }
@@ -6739,7 +6746,7 @@
6739
6746
  var arr = [];
6740
6747
  for(var i = 0; i < segment.length; ++i){
6741
6748
  var c = segment.charCodeAt(i);
6742
- if (0x2D === c || 0x2E === c || 0x5F === c || 0x7E === c || c >= 0x30 && c <= 0x39 || c >= 0x41 && c <= 0x5A || c >= 0x61 && c <= 0x7A || format === formats.RFC1738 && (0x28 === c || 0x29 === c)) {
6749
+ if (0x2D === c || 0x2E === c || 0x5F === c || 0x7E === c && format !== formats.WHATWG || c >= 0x30 && c <= 0x39 || c >= 0x41 && c <= 0x5A || c >= 0x61 && c <= 0x7A || format === formats.RFC1738 && (0x28 === c || 0x29 === c) || format === formats.WHATWG && 0x2A === c) {
6743
6750
  arr[arr.length] = segment.charAt(i);
6744
6751
  continue;
6745
6752
  }
@@ -6755,9 +6762,12 @@
6755
6762
  arr[arr.length] = hexTable[0xE0 | c >> 12] + hexTable[0x80 | c >> 6 & 0x3F] + hexTable[0x80 | 0x3F & c];
6756
6763
  continue;
6757
6764
  }
6758
- i += 1;
6759
- c = 0x10000 + ((0x3FF & c) << 10 | 0x3FF & segment.charCodeAt(i));
6760
- arr[arr.length] = hexTable[0xF0 | c >> 18] + hexTable[0x80 | c >> 12 & 0x3F] + hexTable[0x80 | c >> 6 & 0x3F] + hexTable[0x80 | 0x3F & c];
6765
+ var next = segment.charCodeAt(i + 1);
6766
+ if (c <= 0xDBFF && next >= 0xDC00 && next <= 0xDFFF) {
6767
+ i += 1;
6768
+ c = 0x10000 + ((0x3FF & c) << 10 | 0x3FF & next);
6769
+ arr[arr.length] = hexTable[0xF0 | c >> 18] + hexTable[0x80 | c >> 12 & 0x3F] + hexTable[0x80 | c >> 6 & 0x3F] + hexTable[0x80 | 0x3F & c];
6770
+ } else arr[arr.length] = '%EF%BF%BD';
6761
6771
  }
6762
6772
  out += arr.join('');
6763
6773
  }
@@ -6843,6 +6853,7 @@
6843
6853
  markOverflow: markOverflow,
6844
6854
  maybeMap: maybeMap,
6845
6855
  merge: merge,
6856
+ setProperty: setProperty,
6846
6857
  toCoercible: toCoercible
6847
6858
  };
6848
6859
  },
@@ -7195,7 +7206,7 @@
7195
7206
  const qs = __webpack_require__("b");
7196
7207
  const RequestBase = __webpack_require__("d");
7197
7208
  const { isObject, mixin, hasOwn, isSafeKey } = __webpack_require__("Dp");
7198
- const ResponseBase = __webpack_require__("e");
7209
+ const ResponseBase = __webpack_require__("es");
7199
7210
  const Agent = __webpack_require__("pf");
7200
7211
  function noop() {}
7201
7212
  module1.exports = function(method, url) {
@@ -7909,7 +7920,7 @@
7909
7920
  }, this._responseTimeout);
7910
7921
  };
7911
7922
  },
7912
- e (module1, __unused_rspack_exports, __webpack_require__) {
7923
+ es (module1, __unused_rspack_exports, __webpack_require__) {
7913
7924
  "use strict";
7914
7925
  const utils = __webpack_require__("Dp");
7915
7926
  module1.exports = ResponseBase;
@@ -8254,7 +8265,7 @@
8254
8265
  });
8255
8266
  });
8256
8267
  },
8257
- Sy (__unused_rspack_module, exports1, __webpack_require__) {
8268
+ S (__unused_rspack_module, exports1, __webpack_require__) {
8258
8269
  var process = __webpack_require__("zT");
8259
8270
  var getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors || function(obj) {
8260
8271
  var keys = Object.keys(obj);
@@ -11121,7 +11132,7 @@
11121
11132
  var client = __webpack_require__("Q");
11122
11133
  var client_default = /*#__PURE__*/ __webpack_require__.n(client);
11123
11134
  var package_namespaceObject = {
11124
- rE: "16.1.1"
11135
+ rE: "16.4.0"
11125
11136
  };
11126
11137
  function isAudioEnabled(options) {
11127
11138
  return Boolean(options.audio.enabled);
@@ -11133,7 +11144,7 @@
11133
11144
  function isAutoPauseEnabled(options) {
11134
11145
  return options.enableAutoPause && options.enablePause;
11135
11146
  }
11136
- var util = __webpack_require__("Sy");
11147
+ var util = __webpack_require__("S");
11137
11148
  var util_default = /*#__PURE__*/ __webpack_require__.n(util);
11138
11149
  function getEventDetails(event) {
11139
11150
  const eventWithErrorDetails = event;
@@ -14996,6 +15007,14 @@
14996
15007
  }
14997
15008
  }
14998
15009
  const util_Despot = Despot;
15010
+ const EMAIL_SEPARATOR_REGEX = /[ ,]+/gu;
15011
+ function trimEmail(email) {
15012
+ return email.replace(EMAIL_SEPARATOR_REGEX, "");
15013
+ }
15014
+ function trimEmails(emails) {
15015
+ const trimmedEmails = emails.split(EMAIL_SEPARATOR_REGEX).map((item)=>item.trim()).filter(Boolean);
15016
+ return trimmedEmails;
15017
+ }
14999
15018
  function getValidity(element) {
15000
15019
  const validity = "validity" in element ? element.validity : void 0;
15001
15020
  return validity;
@@ -15015,14 +15034,6 @@
15015
15034
  element.style.removeProperty("display");
15016
15035
  }
15017
15036
  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
15037
  const FormMethod = {
15027
15038
  POST: "post",
15028
15039
  PUT: "put",
@@ -16705,11 +16716,11 @@
16705
16716
  const originalDocument = document1;
16706
16717
  const currentScript = originalDocument.currentScript;
16707
16718
  window1.DocumentFragment;
16708
- const HTMLTemplateElement = window1.HTMLTemplateElement, Node = window1.Node, Element = window1.Element, NodeFilter = window1.NodeFilter;
16719
+ const HTMLTemplateElement = window1.HTMLTemplateElement, Node = window1.Node, Element1 = window1.Element, NodeFilter = window1.NodeFilter;
16709
16720
  void 0 === window1.NamedNodeMap && (window1.NamedNodeMap || window1.MozNamedAttrMap);
16710
16721
  window1.HTMLFormElement;
16711
16722
  const DOMParser = window1.DOMParser, trustedTypes = window1.trustedTypes;
16712
- const ElementPrototype = Element.prototype;
16723
+ const ElementPrototype = Element1.prototype;
16713
16724
  const cloneNode = lookupGetter(ElementPrototype, "cloneNode");
16714
16725
  const remove = lookupGetter(ElementPrototype, "remove");
16715
16726
  const removeAttributeNode = lookupGetter(ElementPrototype, "removeAttributeNode");
@@ -17792,6 +17803,19 @@
17792
17803
  return DOMPurify;
17793
17804
  }
17794
17805
  var purify_default = createDOMPurify();
17806
+ function isRecipientSelectionValid(recipients) {
17807
+ const hasTo = Boolean(recipients.to?.length);
17808
+ const hasCc = Boolean(recipients.cc?.length);
17809
+ const hasBcc = Boolean(recipients.bcc?.length);
17810
+ const hasConfiguredTo = "to" in recipients;
17811
+ const hasConfiguredCc = "cc" in recipients;
17812
+ const hasConfiguredBcc = "bcc" in recipients;
17813
+ let isValid = true;
17814
+ if (hasConfiguredTo) isValid = hasTo || hasConfiguredCc && hasCc || hasConfiguredBcc && hasBcc;
17815
+ else if (hasConfiguredCc) isValid = hasCc || !hasConfiguredBcc || hasBcc;
17816
+ return isValid;
17817
+ }
17818
+ const email_isRecipientSelectionValid = isRecipientSelectionValid;
17795
17819
  function limitHeight(height, options, calledFrom) {
17796
17820
  const dimension = {
17797
17821
  unit: "px"
@@ -18388,7 +18412,7 @@
18388
18412
  html_showElement(this.facingModeElement);
18389
18413
  }
18390
18414
  }
18391
- const facingMode = FacingMode;
18415
+ const recorder_facingMode = FacingMode;
18392
18416
  class PausedNote {
18393
18417
  visuals;
18394
18418
  options;
@@ -18609,7 +18633,7 @@
18609
18633
  this.recordTimer = new recordTimer(visuals, this.recordNote, options);
18610
18634
  const browser = util_getBrowser(options);
18611
18635
  if (options.video.countdown) this.countdown = new countdown(visuals, options);
18612
- if (options.video.facingModeButton && browser.isMobile()) this.facingMode = new facingMode(visuals, options);
18636
+ if (options.video.facingModeButton && browser.isMobile()) this.facingMode = new recorder_facingMode(visuals, options);
18613
18637
  if (options.enablePause) this.pausedNote = new pausedNote(visuals, options);
18614
18638
  }
18615
18639
  startRecording() {
@@ -18942,6 +18966,18 @@
18942
18966
  }
18943
18967
  }
18944
18968
  const notifier = Notifier;
18969
+ function summarize(title, contents) {
18970
+ const lines = Object.entries(contents).sort(([keyA], [keyB])=>keyA.localeCompare(keyB)).map(([key, value])=>{
18971
+ const formattedValue = void 0 === value ? "undefined" : "object" == typeof value ? string_pretty(value) : value;
18972
+ return ` • ${key}: ${formattedValue}`;
18973
+ });
18974
+ const line = [
18975
+ `🔎 ${title}`,
18976
+ ...lines
18977
+ ].join("\n");
18978
+ return line;
18979
+ }
18980
+ const string_summarize = summarize;
18945
18981
  var animitter = __webpack_require__("b6");
18946
18982
  var animitter_default = /*#__PURE__*/ __webpack_require__.n(animitter);
18947
18983
  var typedarray_to_buffer = __webpack_require__("E");
@@ -19005,34 +19041,6 @@
19005
19041
  canvas_to_buffer_modern_r.atob = void 0;
19006
19042
  var websocket_stream_stream = __webpack_require__("U");
19007
19043
  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
19044
  function getEdgeCodes(value, amount, fromEnd) {
19037
19045
  const chars = fromEnd ? value.slice(-amount) : value.slice(0, amount);
19038
19046
  const codes = Array.from(chars, (char)=>String(char.codePointAt(0))).join(".");
@@ -19112,1804 +19120,2276 @@
19112
19120
  };
19113
19121
  }
19114
19122
  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
19123
  function isAutomatedUserAgent(userAgent = navigator.userAgent) {
19182
19124
  return /bot|crawler|spider|headlesschrome|phantomjs|puppeteer|playwright/iu.test(userAgent);
19183
19125
  }
19184
19126
  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);
19242
- });
19243
- return typedarray_to_buffer(arr);
19127
+ const DEFAULT_MAX_QUEUED_BYTES = 1048576;
19128
+ class BoundedWriteQueue {
19129
+ maxBytes;
19130
+ writes = [];
19131
+ queuedBytes = 0;
19132
+ constructor(maxBytes = DEFAULT_MAX_QUEUED_BYTES){
19133
+ this.maxBytes = maxBytes;
19244
19134
  }
19245
- constructor(float32Array){
19246
- _define_property(this, "float32Array", void 0);
19247
- this.float32Array = float32Array;
19135
+ enqueue(buffer, params) {
19136
+ if (this.queuedBytes + buffer.length > this.maxBytes) return false;
19137
+ this.writes.push({
19138
+ buffer,
19139
+ params
19140
+ });
19141
+ this.queuedBytes += buffer.length;
19142
+ return true;
19248
19143
  }
19249
- }
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;
19144
+ dequeue() {
19145
+ const write = this.writes.shift();
19146
+ if (write) this.queuedBytes -= write.buffer.length;
19147
+ return write;
19268
19148
  }
19269
- hasAudioContext() {
19270
- return Boolean(getAudioContextClass()) && Boolean(this.getAudioContext());
19149
+ clear() {
19150
+ this.writes.length = 0;
19151
+ this.queuedBytes = 0;
19271
19152
  }
19272
- getAudioContext() {
19273
- if (!this.vcAudioContext) {
19274
- const AudioContext = getAudioContextClass();
19275
- this.vcAudioContext = new AudioContext();
19276
- }
19277
- return this.vcAudioContext;
19153
+ }
19154
+ const boundedWriteQueue = BoundedWriteQueue;
19155
+ const PIPE_SYMBOL = "°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸ ";
19156
+ class RecorderStreamWriter extends util_Despot {
19157
+ pendingWrites = new boundedWriteQueue();
19158
+ samplesCount = 0;
19159
+ framesCount = 0;
19160
+ recordingStats;
19161
+ confirmedFrameNumber = 0;
19162
+ confirmedSampleNumber = 0;
19163
+ backpressureTimeout;
19164
+ recordingDimensions;
19165
+ stream;
19166
+ streamBackpressured = false;
19167
+ resumeAfterDrain = false;
19168
+ mediaTransmissionStarted = false;
19169
+ recordingActive = false;
19170
+ recordingTransportFailed = false;
19171
+ connecting = false;
19172
+ connected = false;
19173
+ reconnecting = false;
19174
+ constructor(options){
19175
+ super("Recorder", options);
19278
19176
  }
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));
19177
+ hasExceededWebSocketSendBuffer(buffer, opts) {
19178
+ return buffer.length > 0 && !this.pendingWrites.enqueue(buffer, opts);
19283
19179
  }
19284
- disconnect() {
19285
- if (this.scriptProcessor) {
19286
- this.scriptProcessor.onaudioprocess = null;
19287
- this.scriptProcessor.disconnect();
19288
- }
19289
- this.audioInput?.disconnect();
19290
- this.volume?.disconnect();
19180
+ writeStream(buffer, opts) {
19181
+ if (this.stream) if (this.stream.destroyed) {
19182
+ this.stopPings();
19183
+ const err = error_createError({
19184
+ message: "Already disconnected",
19185
+ explanation: "Sorry, connection to the server has been destroyed. Please reload.",
19186
+ options: this.options
19187
+ });
19188
+ this.handleWriteFailure(err);
19189
+ } else if (this.streamBackpressured) {
19190
+ if (this.hasExceededWebSocketSendBuffer(buffer, opts)) this.failMediaTransmission(error_createError({
19191
+ message: "WebSocket send buffer limit exceeded",
19192
+ 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.",
19193
+ options: this.options
19194
+ }));
19195
+ } else this.writeToStream(this.stream, buffer, opts);
19291
19196
  }
19292
- init(localMediaStream) {
19293
- this.options.logger.debug("AudioRecorder: init()");
19294
- this.disconnect();
19295
- const volume = this.getAudioContext().createGain();
19296
- this.volume = volume;
19197
+ writeToStream(stream, buffer, opts) {
19198
+ const onFlushedCallback = opts?.onFlushedCallback;
19297
19199
  try {
19298
- this.audioInput = this.getAudioContext().createMediaStreamSource(localMediaStream);
19200
+ const canContinue = stream.write(buffer, ()=>{
19201
+ if (!onFlushedCallback) return;
19202
+ try {
19203
+ onFlushedCallback(opts);
19204
+ } catch (exc) {
19205
+ const err = error_createError({
19206
+ message: "Failed to write stream buffer",
19207
+ explanation: `stream.write() failed because of ${string_pretty(exc)}`,
19208
+ options: this.options,
19209
+ exc
19210
+ });
19211
+ this.emit("ERROR", {
19212
+ err
19213
+ });
19214
+ }
19215
+ });
19216
+ if (!canContinue) {
19217
+ this.streamBackpressured = true;
19218
+ this.startBackpressureTimeout();
19219
+ this.pauseForBackpressure();
19220
+ return false;
19221
+ }
19222
+ return true;
19299
19223
  } catch (exc) {
19300
- throw error_createError({
19301
- message: "Webcam has no audio",
19302
- exc,
19303
- options: this.options
19224
+ const err = error_createError({
19225
+ message: "Failed writing to server",
19226
+ explanation: `stream.write() failed because of ${string_pretty(exc)}`,
19227
+ options: this.options,
19228
+ exc
19304
19229
  });
19230
+ this.handleWriteFailure(err);
19231
+ return false;
19305
19232
  }
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
19233
  }
19322
- record(cb) {
19323
- this.options.logger.debug("AudioRecorder: record()");
19324
- if (this.scriptProcessor) this.scriptProcessor.onaudioprocess = (e)=>{
19325
- this.onAudioProcess(e, cb);
19326
- };
19234
+ flushPendingWrites(stream) {
19235
+ if (this.isStaleStream(stream)) return;
19236
+ this.streamBackpressured = false;
19237
+ this.clearBackpressureTimeout();
19238
+ let pendingWrite = this.pendingWrites.dequeue();
19239
+ while(pendingWrite){
19240
+ if (!this.writeToStream(stream, pendingWrite.buffer, pendingWrite.params)) return;
19241
+ pendingWrite = this.pendingWrites.dequeue();
19242
+ }
19243
+ this.resumeRecordingAfterDrain();
19327
19244
  }
19328
- async resume() {
19329
- const context = this.getAudioContext();
19330
- if ("running" !== context.state) await context.resume();
19245
+ isStaleStream(stream) {
19246
+ return this.stream !== stream || stream.destroyed;
19331
19247
  }
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
- });
19248
+ canPauseForBackpressure() {
19249
+ return this.recordingActive && !this.isUserMediaPaused() && this.isCaptureLoopRunning();
19345
19250
  }
19346
- getSampleRate() {
19347
- if (this.hasAudioContext()) return this.getAudioContext().sampleRate;
19348
- return -1;
19251
+ pauseForBackpressure() {
19252
+ if (!this.canPauseForBackpressure()) return;
19253
+ this.resumeAfterDrain = true;
19254
+ this.stopCaptureLoop();
19349
19255
  }
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
19256
+ resumeRecordingAfterDrain() {
19257
+ if (!this.resumeAfterDrain) return;
19258
+ this.resumeAfterDrain = false;
19259
+ if (this.canResumeAfterDrain()) this.startCaptureLoop();
19260
+ }
19261
+ canResumeAfterDrain() {
19262
+ return this.recordingActive && !this.isUserMediaPaused();
19263
+ }
19264
+ handleWriteFailure(err) {
19265
+ if (this.mediaTransmissionStarted) return void this.failMediaTransmission(err);
19266
+ this.emit("ERROR", {
19267
+ err
19384
19268
  });
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);
19269
+ }
19270
+ failMediaTransmission(err) {
19271
+ if (this.recordingTransportFailed) return;
19272
+ this.recordingTransportFailed = true;
19273
+ this.recordingActive = false;
19274
+ this.mediaTransmissionStarted = false;
19275
+ this.streamBackpressured = false;
19276
+ this.resumeAfterDrain = false;
19277
+ this.pendingWrites.clear();
19278
+ this.connected = false;
19279
+ this.connecting = false;
19280
+ this.reconnecting = false;
19281
+ this.clearReconnectTimeout();
19282
+ this.clearBackpressureTimeout();
19283
+ this.discardSocket();
19284
+ this.reset();
19285
+ this.framesCount = this.samplesCount = 0;
19286
+ this.confirmedFrameNumber = this.confirmedSampleNumber = 0;
19287
+ delete this.recordingStats;
19288
+ this.recordingDimensions = void 0;
19289
+ this.emit("ERROR", {
19290
+ err
19396
19291
  });
19397
19292
  }
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();
19293
+ startBackpressureTimeout() {
19294
+ if (!this.shouldStartBackpressureTimeout()) return;
19295
+ this.backpressureTimeout = window.setTimeout(()=>{
19296
+ this.backpressureTimeout = void 0;
19297
+ if (this.shouldIgnoreBackpressureTimeout()) return;
19298
+ const err = error_createError({
19299
+ message: "WebSocket write timed out",
19300
+ explanation: "The server stopped accepting recording data. The recording has been stopped; please check your connection and start a new one.",
19301
+ options: this.options
19405
19302
  });
19406
- } else throw error_createError({
19407
- message: "Error attaching stream to element.",
19408
- explanation: "Contact the developer about this",
19409
- options: this.options
19410
- });
19303
+ this.failMediaTransmission(err);
19304
+ }, this.options.timeouts.connection);
19411
19305
  }
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
- }
19306
+ shouldStartBackpressureTimeout() {
19307
+ return void 0 === this.backpressureTimeout && this.mediaTransmissionStarted;
19421
19308
  }
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();
19309
+ shouldIgnoreBackpressureTimeout() {
19310
+ return !this.streamBackpressured || this.recordingTransportFailed;
19426
19311
  }
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);
19312
+ failInterruptedRecording() {
19313
+ const unconfirmedFrames = Math.max(0, this.framesCount - this.confirmedFrameNumber);
19314
+ const unconfirmedSamples = Math.max(0, this.samplesCount - this.confirmedSampleNumber);
19315
+ const err = error_createError({
19316
+ message: "Recording interrupted",
19317
+ explanation: "The server connection closed before this recording completed. It cannot be safely resumed, so it has been stopped. Please start a new recording.",
19318
+ options: this.options,
19319
+ cause: {
19320
+ unconfirmedFrames,
19321
+ unconfirmedSamples
19322
+ }
19323
+ });
19324
+ this.failMediaTransmission(err);
19431
19325
  }
19432
- hasEnded() {
19433
- if (this.rawVisualUserMedia?.ended) return this.rawVisualUserMedia.ended;
19434
- return !this.currentVisualStream?.active;
19326
+ handleStreamDrain(stream) {
19327
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *drain* event emitted`);
19328
+ this.flushPendingWrites(stream);
19435
19329
  }
19436
- hasInvalidDimensions() {
19437
- if (!this.rawVisualUserMedia) return false;
19438
- const { videoWidth, videoHeight } = this.rawVisualUserMedia;
19439
- return videoWidth > 0 && videoWidth < 3 || videoHeight > 0 && videoHeight < 3;
19330
+ clearBackpressureTimeout() {
19331
+ if (void 0 === this.backpressureTimeout) return;
19332
+ window.clearTimeout(this.backpressureTimeout);
19333
+ this.backpressureTimeout = void 0;
19440
19334
  }
19441
- logEvent(eventType, params) {
19442
- this.options.logger.debug(`UserMedia: ... ${EVENT_ASCII} event ${eventType}: ${string_pretty(params)}`);
19335
+ discardSocket() {
19336
+ this.options.logger.debug("Recorder: discarding socket stream ...");
19337
+ this.clearBackpressureTimeout();
19338
+ this.pendingWrites.clear();
19339
+ this.streamBackpressured = false;
19340
+ this.resumeAfterDrain = false;
19341
+ const stream = this.stream;
19342
+ this.stream = void 0;
19343
+ if (stream && !stream.destroyed) stream.destroy();
19443
19344
  }
19444
- unloadRemainingEventListeners() {
19445
- this.options.logger.debug("UserMedia: unloadRemainingEventListeners()");
19446
- this.clearInitializationListeners?.();
19447
- mediaEvents.forEach((eventName)=>{
19448
- this.rawVisualUserMedia?.removeEventListener(eventName, this.outputEvent);
19345
+ }
19346
+ const streamWriter = RecorderStreamWriter;
19347
+ var Buffer = __webpack_require__("dY").Buffer;
19348
+ class RecorderConnection extends streamWriter {
19349
+ reconnectTimeout;
19350
+ connectionTimeout;
19351
+ userMediaLoaded;
19352
+ userMediaLoading = false;
19353
+ unloaded;
19354
+ connectionFailed = false;
19355
+ reconnectAttempts = 0;
19356
+ blocking = false;
19357
+ connectingStartedAt;
19358
+ lastCloseEvent;
19359
+ lastSocketError;
19360
+ pingInterval;
19361
+ constructor(options){
19362
+ super(options);
19363
+ }
19364
+ canReconnectAfterStreamClose() {
19365
+ return Boolean(this.connected && this.userMediaLoaded);
19366
+ }
19367
+ shouldReportStreamClose() {
19368
+ return !this.connecting && !this.blocking;
19369
+ }
19370
+ handleStreamClose(url2Connect) {
19371
+ const debugLine = string_summarize(`${PIPE_SYMBOL}Stream has closed:`, {
19372
+ connecting: this.connecting,
19373
+ connected: this.connected,
19374
+ userMediaLoaded: this.userMediaLoaded,
19375
+ blocking: this.blocking
19449
19376
  });
19450
- this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
19377
+ this.options.logger.debug(debugLine);
19378
+ const canReconnect = this.canReconnectAfterStreamClose();
19379
+ this.connected = false;
19380
+ this.stopPings();
19381
+ if (this.shouldIgnoreStreamClose()) return;
19382
+ if (this.mediaTransmissionStarted) this.failInterruptedRecording();
19383
+ else if (canReconnect) {
19384
+ this.pendingWrites.clear();
19385
+ this.streamBackpressured = false;
19386
+ this.resumeAfterDrain = false;
19387
+ this.reconnecting = true;
19388
+ this.initSocket();
19389
+ } else if (this.shouldReportStreamClose()) window.setTimeout(()=>{
19390
+ this.handleConnectionFailure({
19391
+ url2Connect,
19392
+ cause: "closed"
19393
+ });
19394
+ }, 0);
19451
19395
  }
19452
- audioRecord(audioCallback) {
19453
- this.removeListener("SENDING_FIRST_FRAME");
19454
- this.audioRecorder?.record(audioCallback);
19396
+ shouldIgnoreStreamClose() {
19397
+ return this.recordingTransportFailed || Boolean(this.unloaded) || this.validate();
19455
19398
  }
19456
- init(localMediaStream, videoCallback, audioCallback, endedEarlyCallback, params) {
19457
- this.clearInitializationListeners?.();
19458
- this.stop(localMediaStream, {
19459
- aboutToInitialize: true,
19460
- preserveRecordingState: Boolean(params?.switchingFacingMode || params?.recovering)
19399
+ handleStreamError(url2Connect, err) {
19400
+ if ("u" > typeof Event && err instanceof Event) this.lastSocketError = error_getEventDetails(err);
19401
+ else this.lastSocketError = serializeError(err);
19402
+ const debugLine = string_summarize(`${PIPE_SYMBOL}Stream *error* event emitted:`, {
19403
+ error: serializeError(err)
19461
19404
  });
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;
19405
+ this.options.logger.debug(debugLine);
19406
+ if (this.validate()) {
19407
+ this.connected = false;
19408
+ this.stopPings();
19409
+ this.discardSocket();
19410
+ return;
19411
+ }
19412
+ if (!this.connected) return void this.handleConnectionFailure({
19413
+ url2Connect,
19414
+ cause: "error"
19415
+ });
19416
+ const streamError = err instanceof Error ? err : new Error("WebSocket stream emitted an error event", {
19417
+ cause: this.lastSocketError ?? err
19418
+ });
19419
+ if (this.mediaTransmissionStarted) return void this.failInterruptedRecording();
19420
+ this.emit("ERROR", {
19421
+ err: error_createError({
19422
+ message: "WebSocket stream error",
19423
+ explanation: `The WebSocket stream emitted an error event. Details: ${string_pretty(err)}`,
19424
+ options: this.options,
19425
+ exc: streamError
19426
+ })
19427
+ });
19428
+ }
19429
+ sendPings() {
19430
+ this.stopPings();
19431
+ this.pingInterval = window.setInterval(()=>{
19432
+ this.options.logger.debug("Recorder: pinging...");
19433
+ this.writeStream(Buffer.from(""));
19434
+ }, this.options.timeouts.pingInterval);
19435
+ }
19436
+ stopPings() {
19437
+ if (void 0 === this.pingInterval) return;
19438
+ this.options.logger.debug("Recorder: stopping pings...");
19439
+ window.clearInterval(this.pingInterval);
19440
+ this.pingInterval = void 0;
19441
+ }
19442
+ clearConnectionTimeout() {
19443
+ if (void 0 === this.connectionTimeout) return;
19444
+ this.options.logger.debug("Recorder: clearConnectionTimeout()");
19445
+ window.clearTimeout(this.connectionTimeout);
19446
+ this.connectionTimeout = void 0;
19447
+ }
19448
+ clearReconnectTimeout() {
19449
+ if (void 0 === this.reconnectTimeout) return;
19450
+ this.options.logger.debug("Recorder: clearReconnectTimeout()");
19451
+ window.clearTimeout(this.reconnectTimeout);
19452
+ this.reconnectTimeout = void 0;
19453
+ }
19454
+ handleConnectionFailure(params) {
19455
+ if (this.retryConnection(params)) return;
19456
+ this.failConnection(params);
19457
+ }
19458
+ retryConnection(params) {
19459
+ if (!this.canRetryConnection()) return false;
19460
+ if (void 0 !== this.reconnectTimeout) return true;
19461
+ const delayMs = Math.min(1000 * 2 ** this.reconnectAttempts, 10000);
19462
+ this.reconnectAttempts++;
19463
+ this.connecting = false;
19464
+ this.clearConnectionTimeout();
19465
+ this.discardSocket();
19466
+ this.options.logger.debug(`Recorder: retrying WebSocket connection in ${delayMs}ms (cause ${params.cause}).`);
19467
+ this.reconnectTimeout = window.setTimeout(()=>{
19468
+ this.reconnectTimeout = void 0;
19469
+ if (this.shouldContinueReconnect()) this.initSocket();
19470
+ }, delayMs);
19471
+ return true;
19472
+ }
19473
+ canRetryConnection() {
19474
+ return Boolean(this.reconnecting && this.userMediaLoaded && this.isOnline() && !this.unloaded && !this.blocking);
19475
+ }
19476
+ shouldContinueReconnect() {
19477
+ return this.reconnecting && !this.unloaded && !this.blocking;
19478
+ }
19479
+ isOnline() {
19480
+ return navigator.onLine;
19481
+ }
19482
+ failConnection(params) {
19483
+ if (this.shouldIgnoreConnectionFailure()) return;
19484
+ this.connectionFailed = true;
19485
+ this.connecting = false;
19486
+ this.reconnecting = false;
19487
+ this.reconnectAttempts = 0;
19488
+ this.clearReconnectTimeout();
19489
+ this.clearConnectionTimeout();
19490
+ const { url2Connect, cause } = params;
19491
+ const online = this.isOnline();
19492
+ const elapsedMs = this.connectingStartedAt ? Date.now() - this.connectingStartedAt : void 0;
19493
+ const closeEvent = this.lastCloseEvent;
19494
+ const socketError = this.lastSocketError;
19495
+ const contents = {
19496
+ cause,
19497
+ closeCode: closeEvent?.code,
19498
+ closeReason: closeEvent?.reason,
19499
+ elapsedMs,
19500
+ online,
19501
+ secureContext: globalThis.isSecureContext,
19502
+ socketReadyState: this.stream?.socket.readyState,
19503
+ timeoutMs: this.options.timeouts.connection,
19504
+ url: url2Connect,
19505
+ unloaded: this.unloaded,
19506
+ userMediaLoaded: this.userMediaLoaded,
19507
+ userMediaLoading: this.userMediaLoading,
19508
+ wasClean: closeEvent?.wasClean,
19509
+ socketError,
19510
+ blocking: this.blocking,
19511
+ visibilityState: document.visibilityState
19479
19512
  };
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
- }
19513
+ const debugLine = string_summarize("Recorder: failConnection() diagnostic", contents);
19514
+ this.options.logger.debug(debugLine);
19515
+ let explanation;
19516
+ 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.`;
19517
+ 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.`;
19518
+ else {
19519
+ const closeSuffix = closeEvent ? ` (code ${closeEvent.code})` : "";
19520
+ explanation = `Connection to ${url2Connect} is closed${closeSuffix}. Please check your internet connection and try again. If the problem persists, contact us.`;
19521
+ }
19522
+ else explanation = "Your device appears to be offline. Please check your internet connection and try again.";
19523
+ this.discardSocket();
19524
+ const err = error_createError({
19525
+ message: "Unable to connect to the server",
19526
+ explanation,
19527
+ options: this.options,
19528
+ cause: contents,
19529
+ exc: socketError ? new Error("WebSocket connection error", {
19530
+ cause: socketError
19531
+ }) : void 0
19532
+ });
19533
+ this.emit("ERROR", {
19534
+ err
19535
+ });
19536
+ }
19537
+ shouldIgnoreConnectionFailure() {
19538
+ return this.connectionFailed || this.connected || Boolean(this.unloaded) || this.validate();
19539
+ }
19540
+ initSocket(cb) {
19541
+ if (this.connected) {
19542
+ this.options.logger.debug("Recorder: already **connected**. Not going to initialize a new WebSocket connection.");
19543
+ cb?.();
19544
+ return;
19545
+ }
19546
+ if (this.connecting) return void this.options.logger.debug("Recorder: already **connecting**. Not going to initialize a new WebSocket connection.");
19547
+ this.clearReconnectTimeout();
19548
+ if (util_isAutomatedUserAgent()) {
19549
+ this.connecting = false;
19550
+ this.options.logger.debug("Recorder: skipping web socket connection for an automated crawler");
19551
+ return;
19552
+ }
19553
+ this.connecting = true;
19554
+ this.connectionFailed = false;
19555
+ this.connectingStartedAt = Date.now();
19556
+ this.lastCloseEvent = void 0;
19557
+ this.lastSocketError = void 0;
19558
+ this.emit("CONNECTING");
19559
+ let url2Connect;
19560
+ try {
19561
+ const socketUrlObj = new URL(this.options.socketUrl);
19562
+ socketUrlObj.searchParams.set(constants.WHITELIST_KEY_LABEL, this.options.whitelistKey);
19563
+ url2Connect = socketUrlObj.toString();
19564
+ } catch (exc) {
19565
+ this.connecting = this.connected = false;
19566
+ const err = error_createError({
19567
+ message: "Invalid WebSocket URL",
19568
+ explanation: `The configured socketUrl "${this.options.socketUrl}" is not a valid URL. Please check your videomail-client configuration.`,
19569
+ options: this.options,
19570
+ exc
19571
+ });
19572
+ this.emit("ERROR", {
19573
+ err
19574
+ });
19575
+ return;
19576
+ }
19577
+ this.options.logger.debug(`Recorder: initializing web socket stream to ${url2Connect}`);
19578
+ let nativeSocket;
19579
+ try {
19580
+ nativeSocket = new WebSocket(url2Connect);
19581
+ nativeSocket.addEventListener("close", (event)=>{
19582
+ this.lastCloseEvent = {
19583
+ code: event.code,
19584
+ reason: event.reason,
19585
+ wasClean: event.wasClean
19586
+ };
19587
+ });
19588
+ nativeSocket.addEventListener("error", (event)=>{
19589
+ this.lastSocketError = error_getEventDetails(event);
19590
+ });
19591
+ } catch (exc) {
19592
+ this.connecting = this.connected = false;
19593
+ const diagnostic = error_getWebSocketDiagnostic(url2Connect);
19594
+ const message = diagnostic.looksAutomated ? "Automated crawler: WebSocket not supported in this environment" : `Failed to construct WebSocket to ${url2Connect}`;
19595
+ 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}`;
19596
+ const err = error_createError({
19597
+ message,
19598
+ explanation,
19599
+ options: this.options,
19600
+ exc
19601
+ });
19602
+ this.emit("ERROR", {
19603
+ err
19604
+ });
19605
+ return;
19606
+ }
19607
+ try {
19608
+ this.stream = stream_default()(nativeSocket);
19609
+ } catch (exc) {
19610
+ this.connecting = this.connected = false;
19611
+ const err = error_createError({
19612
+ message: `Failed to create a stream to ${url2Connect}`,
19613
+ explanation: "Please check your connection and try again. If the problem persists, contact us.",
19614
+ options: this.options,
19615
+ exc
19616
+ });
19617
+ this.emit("ERROR", {
19618
+ err
19619
+ });
19620
+ }
19621
+ const stream = this.stream;
19622
+ if (stream) {
19623
+ const connectionTimeoutMs = this.options.timeouts.connection;
19624
+ this.connectionTimeout = window.setTimeout(()=>{
19625
+ if (this.isOnline() && this.isUserMediaLoaded()) {
19626
+ this.options.logger.debug(`${PIPE_SYMBOL}Reconnecting due to connection timeout.`);
19627
+ this.reconnecting = true;
19628
+ this.handleConnectionFailure({
19629
+ url2Connect,
19630
+ cause: "timeout"
19502
19631
  });
19632
+ } else this.handleConnectionFailure({
19633
+ url2Connect,
19634
+ cause: "timeout"
19635
+ });
19636
+ }, connectionTimeoutMs);
19637
+ stream.on("close", ()=>{
19638
+ this.handleStreamClose(url2Connect);
19639
+ });
19640
+ stream.on("connect", (args)=>{
19641
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *connect* event emitted with args: ${string_pretty(args)}`);
19642
+ this.clearConnectionTimeout();
19643
+ const isClosing = this.stream?.socket.readyState === WebSocket.CLOSING;
19644
+ if (!this.connected && !isClosing && !this.unloaded) {
19645
+ this.connected = true;
19646
+ this.connecting = this.unloaded = false;
19647
+ this.reconnecting = false;
19648
+ this.reconnectAttempts = 0;
19649
+ this.clearReconnectTimeout();
19650
+ this.emit("CONNECTED");
19651
+ cb?.();
19503
19652
  }
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);
19653
+ });
19654
+ stream.on("data", (data)=>{
19655
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *data* event emitted`);
19656
+ try {
19657
+ const command = JSON.parse(data.toString());
19658
+ this.executeCommand(command);
19659
+ } catch (exc) {
19660
+ this.options.logger.error(`Failed to parse command: ${exc}`);
19661
+ const err = error_createError({
19662
+ message: "Invalid server command",
19663
+ explanation: `Contact us. The invalid command was: ${data.toString()}.`,
19664
+ options: this.options,
19665
+ exc
19666
+ });
19667
+ this.emit("ERROR", {
19668
+ err
19519
19669
  });
19520
- this.clearInitializationListeners?.();
19521
- videoCallback();
19522
- };
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
19546
- });
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();
19556
19670
  }
19557
- } catch (exc) {
19558
- unloadAllEventListeners();
19559
- endedEarlyCallback(exc);
19671
+ });
19672
+ stream.on("error", (err)=>{
19673
+ this.handleStreamError(url2Connect, err);
19674
+ });
19675
+ stream.on("drain", ()=>{
19676
+ this.handleStreamDrain(stream);
19677
+ });
19678
+ stream.on("preend", (args)=>{
19679
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *preend* event emitted with args: ${string_pretty(args)}`);
19680
+ });
19681
+ stream.on("end", ()=>{
19682
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *end* event emitted`);
19683
+ });
19684
+ stream.on("pipe", (src)=>{
19685
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *pipe* event emitted with src: ${string_pretty(src)}`);
19686
+ });
19687
+ stream.on("unpipe", (src)=>{
19688
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *unpipe* event emitted with src: ${string_pretty(src)}`);
19689
+ });
19690
+ stream.on("resume", (args)=>{
19691
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *resume* event emitted with args: ${string_pretty(args)}`);
19692
+ });
19693
+ stream.on("uncork", (args)=>{
19694
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *uncork* event emitted with args: ${string_pretty(args)}`);
19695
+ });
19696
+ stream.on("readable", (args)=>{
19697
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *readable* event emitted with args: ${string_pretty(args)}`);
19698
+ });
19699
+ stream.on("prefinish", (args)=>{
19700
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *prefinish* event emitted with args: ${string_pretty(args)}`);
19701
+ });
19702
+ stream.on("finish", (args)=>{
19703
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *finish* event emitted with args: ${string_pretty(args)}`);
19704
+ });
19705
+ }
19706
+ }
19707
+ writeCommand(command, args, cb) {
19708
+ if (this.connected) {
19709
+ if (this.stream) {
19710
+ if (args) this.options.logger.debug(`$ ${command} with ${string_pretty(args)}`);
19711
+ else this.options.logger.debug(`$ ${command}`);
19712
+ const commandObj = {
19713
+ command,
19714
+ args
19715
+ };
19716
+ this.writeStream(Buffer.from(JSON.stringify(commandObj)), cb ? {
19717
+ onFlushedCallback: ()=>{
19718
+ queueMicrotask(cb);
19719
+ }
19720
+ } : void 0);
19560
19721
  }
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
19722
+ } else {
19723
+ this.options.logger.debug(`Reconnecting for the command ${command} …`);
19724
+ this.initSocket(()=>{
19725
+ this.writeCommand(command, args, cb);
19726
+ });
19727
+ }
19728
+ }
19729
+ }
19730
+ const connection = RecorderConnection;
19731
+ function createUserMediaConstraints({ options, facingMode, limitedWidth, userMediaParams }) {
19732
+ const video = {
19733
+ frameRate: {
19734
+ ideal: options.video.fps
19735
+ }
19736
+ };
19737
+ if (userMediaParams?.switchingFacingMode || userMediaParams?.recovering) video.facingMode = userMediaParams.switchingFacingMode || facingMode;
19738
+ if (options.video.width) video.width = {
19739
+ ideal: options.video.width
19740
+ };
19741
+ else if (limitedWidth?.value) video.width = {
19742
+ ideal: limitedWidth.value
19743
+ };
19744
+ if (options.video.height) video.height = {
19745
+ ideal: options.video.height
19746
+ };
19747
+ return {
19748
+ audio: isAudioEnabled(options),
19749
+ video
19750
+ };
19751
+ }
19752
+ const mediaConstraints = createUserMediaConstraints;
19753
+ class RecorderMedia extends connection {
19754
+ visuals;
19755
+ recorderElement;
19756
+ userMedia;
19757
+ userMediaTimeout;
19758
+ retryTimeout;
19759
+ stopTimeout;
19760
+ mediaRequestId = 0;
19761
+ cameraRecovery;
19762
+ resolveCameraRecovery;
19763
+ resumeAfterCameraRecovery = false;
19764
+ submitting = false;
19765
+ built = false;
19766
+ key;
19767
+ facingMode;
19768
+ constructor(visuals, options){
19769
+ super(options);
19770
+ this.visuals = visuals;
19771
+ this.facingMode = options.video.facingMode;
19772
+ }
19773
+ show() {
19774
+ html_showElement(this.recorderElement);
19775
+ }
19776
+ clearRetryTimeout() {
19777
+ if (void 0 === this.retryTimeout) return;
19778
+ this.options.logger.debug("Recorder: clearRetryTimeout()");
19779
+ window.clearTimeout(this.retryTimeout);
19780
+ this.retryTimeout = void 0;
19781
+ }
19782
+ showUserMedia() {
19783
+ if (!this.connected) return false;
19784
+ const hidden = this.isHidden();
19785
+ if (!hidden) return true;
19786
+ const notifying = this.isNotifying();
19787
+ if (notifying) return true;
19788
+ return this.blocking;
19789
+ }
19790
+ userMediaErrorCallback(err, usedConstraints) {
19791
+ if (this.cameraRecovery) return void this.failCameraRecovery(err);
19792
+ if (this.unloaded) return void this.options.logger.debug(`Recorder: already unloaded. Not going to throw error ${string_pretty(err)}`);
19793
+ this.userMediaLoading = false;
19794
+ this.clearUserMediaTimeout();
19795
+ const characteristics = this.userMedia?.getCharacteristics();
19796
+ this.options.logger.debug(`Recorder: userMediaErrorCallback(), name: ${err.name}, message: ${err.message} and Webcam characteristics: ${characteristics ? string_pretty(characteristics) : "none"}`);
19797
+ const errorListeners = this.getListeners("ERROR");
19798
+ if (errorListeners?.length) if (err.name !== error_VideomailError.MEDIA_DEVICE_NOT_SUPPORTED) {
19799
+ const videomailError = error_createError({
19800
+ err,
19801
+ options: this.options,
19802
+ usedConstraints
19570
19803
  });
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}".`,
19804
+ this.emit("ERROR", {
19805
+ err: videomailError
19806
+ });
19807
+ } else {
19808
+ this.options.logger.debug(`Recorder: ignore user media error ${string_pretty(err)}`);
19809
+ this.retryTimeout = window.setTimeout(()=>{
19810
+ this.retryTimeout = void 0;
19811
+ if (!this.unloaded) this.loadUserMedia();
19812
+ }, this.options.timeouts.userMedia);
19813
+ }
19814
+ else {
19815
+ this.options.logger.debug(`Recorder: no error listeners attached but throwing error ${string_pretty(err)}`);
19816
+ throw error_createError({
19817
+ err,
19818
+ message: "Unable to process this error since there are no error listeners anymore.",
19584
19819
  options: this.options
19585
19820
  });
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.",
19821
+ }
19822
+ }
19823
+ getUserMediaCallback(localStream, params, requestId = this.mediaRequestId) {
19824
+ if (!this.userMedia) {
19825
+ localStream.getTracks().forEach((track)=>{
19826
+ track.stop();
19827
+ });
19828
+ throw error_createError({
19829
+ message: "No user media is defined",
19595
19830
  options: this.options
19596
19831
  });
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();
19832
+ }
19833
+ this.options.logger.debug(`Recorder: getUserMediaCallback(${params ? string_pretty(params) : ""})`);
19834
+ if (this.showUserMedia()) try {
19835
+ this.userMedia.init(localStream, ()=>{
19836
+ if (!this.unloaded && requestId === this.mediaRequestId) this.onUserMediaReady(params);
19837
+ }, this.onAudioSample.bind(this), (err)=>{
19838
+ if (!this.unloaded && requestId === this.mediaRequestId) this.userMediaErrorCallback(err, {
19839
+ audio: isAudioEnabled(this.options)
19840
+ });
19841
+ }, params);
19604
19842
  } catch (exc) {
19605
- unloadAllEventListeners();
19606
- endedEarlyCallback(exc);
19843
+ localStream.getTracks().forEach((track)=>{
19844
+ track.stop();
19845
+ });
19846
+ this.userMediaLoading = false;
19847
+ this.clearUserMediaTimeout();
19848
+ this.userMediaErrorCallback(exc, {
19849
+ audio: isAudioEnabled(this.options)
19850
+ });
19851
+ }
19852
+ else {
19853
+ localStream.getTracks().forEach((track)=>{
19854
+ track.stop();
19855
+ });
19856
+ this.userMediaLoading = false;
19857
+ this.clearUserMediaTimeout();
19858
+ this.cancelCameraRecovery();
19607
19859
  }
19608
19860
  }
19609
- getVideoTrackLabel() {
19610
- return this.videoTrackLabel;
19611
- }
19612
- isReady() {
19613
- return Boolean(this.rawVisualUserMedia?.srcObject || this.rawVisualUserMedia?.currentSrc);
19861
+ loadGenuineUserMedia(params) {
19862
+ this.options.logger.debug(`Recorder: loadGenuineUserMedia(${params ? string_pretty(params) : ""})`);
19863
+ this.emit("ASKING_WEBCAM_PERMISSION");
19864
+ this.userMediaLoading = true;
19865
+ const requestId = ++this.mediaRequestId;
19866
+ const limitedWidth = this.options.video.width ? void 0 : this.visuals.limitWidth();
19867
+ const constraints = mediaConstraints({
19868
+ facingMode: this.facingMode,
19869
+ limitedWidth,
19870
+ options: this.options,
19871
+ userMediaParams: params
19872
+ });
19873
+ this.options.logger.debug(string_summarize("Recorder: our webcam constraints are:", constraints));
19874
+ this.options.logger.debug(string_summarize("Recorder: available webcam constraints are:", navigator.mediaDevices.getSupportedConstraints()));
19875
+ const streamPromise = navigator.mediaDevices.getUserMedia(constraints);
19876
+ streamPromise.then((localStream)=>{
19877
+ if (this.unloaded || requestId !== this.mediaRequestId) return void localStream.getTracks().forEach((track)=>{
19878
+ track.stop();
19879
+ });
19880
+ this.getUserMediaCallback(localStream, params, requestId);
19881
+ }).catch((reason)=>{
19882
+ if (!this.unloaded && requestId === this.mediaRequestId) this.userMediaErrorCallback(reason, constraints);
19883
+ });
19614
19884
  }
19615
- stop(visualStream, params) {
19885
+ loadUserMedia(params) {
19886
+ if (this.userMediaLoaded) {
19887
+ this.options.logger.debug("Recorder: skipping loadUserMedia() because it is already loaded");
19888
+ this.onUserMediaReady(params);
19889
+ return;
19890
+ } else if (this.userMediaLoading) return void this.options.logger.debug("Recorder: skipping loadUserMedia() because it is already asking for permission");
19891
+ this.options.logger.debug(`Recorder: loadUserMedia(${params ? string_pretty(params) : ""})`);
19892
+ this.emit("LOADING_USER_MEDIA");
19616
19893
  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;
19894
+ if (!navigator.mediaDevices) throw new Error("No media devices are available.");
19895
+ this.userMediaTimeout = window.setTimeout(()=>{
19896
+ if (!this.userMediaLoaded) {
19897
+ this.userMediaLoading = false;
19898
+ const err = util_getBrowser(this.options).getNoAccessIssue();
19899
+ this.userMediaErrorCallback(err, {
19900
+ audio: isAudioEnabled(this.options)
19901
+ });
19902
+ }
19903
+ }, this.options.timeouts.userMedia);
19904
+ this.userMediaLoading = true;
19905
+ this.loadGenuineUserMedia(params);
19631
19906
  } catch (exc) {
19632
- this.emit("ERROR", {
19907
+ this.clearUserMediaTimeout();
19908
+ this.options.logger.debug("Recorder: failed to load genuine user media");
19909
+ this.userMediaLoading = false;
19910
+ if (this.cameraRecovery) return void this.failCameraRecovery(exc);
19911
+ const errorListeners = this.getListeners("ERROR");
19912
+ if (errorListeners?.length) this.emit("ERROR", {
19633
19913
  exc
19634
19914
  });
19915
+ else {
19916
+ this.options.logger.debug("Recorder: no error listeners attached but throwing exception further");
19917
+ throw exc;
19918
+ }
19635
19919
  }
19636
19920
  }
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;
19921
+ isNotifying() {
19922
+ return this.visuals.isNotifying();
19652
19923
  }
19653
- hasVideoWidth() {
19654
- const videoWidth = this.getVideoWidth();
19655
- return videoWidth && videoWidth > 0;
19924
+ isHidden() {
19925
+ return !this.recorderElement || html_isHidden(this.recorderElement);
19656
19926
  }
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;
19927
+ clearUserMediaTimeout() {
19928
+ if (void 0 !== this.userMediaTimeout) {
19929
+ this.options.logger.debug("Recorder: clearUserMediaTimeout()");
19930
+ window.clearTimeout(this.userMediaTimeout);
19931
+ this.userMediaTimeout = void 0;
19666
19932
  }
19667
- widthDimension.value = rawWidth;
19668
- return widthDimension;
19669
19933
  }
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;
19934
+ clearStopTimeout() {
19935
+ if (void 0 === this.stopTimeout) return;
19936
+ window.clearTimeout(this.stopTimeout);
19937
+ this.stopTimeout = void 0;
19683
19938
  }
19684
- getRawVisuals() {
19685
- return this.rawVisualUserMedia;
19939
+ isReady() {
19940
+ return this.userMedia?.isReady();
19686
19941
  }
19687
- pause() {
19688
- this.paused = true;
19942
+ finishCameraRecovery(restored) {
19943
+ this.clearUserMediaTimeout();
19944
+ const resolve = this.resolveCameraRecovery;
19945
+ const resumeRequested = restored && this.resumeAfterCameraRecovery;
19946
+ this.cameraRecovery = void 0;
19947
+ this.resolveCameraRecovery = void 0;
19948
+ this.resumeAfterCameraRecovery = false;
19949
+ resolve?.(restored);
19950
+ if (resumeRequested && this.isPaused()) this.resume();
19689
19951
  }
19690
- isPaused() {
19691
- return this.paused;
19952
+ cancelCameraRecovery() {
19953
+ if (!this.cameraRecovery) return;
19954
+ this.mediaRequestId++;
19955
+ this.userMediaLoading = false;
19956
+ this.userMediaLoaded = false;
19957
+ this.clearUserMediaTimeout();
19958
+ this.userMedia?.stop(void 0, {
19959
+ preserveRecordingState: true
19960
+ });
19961
+ this.finishCameraRecovery(false);
19692
19962
  }
19693
- resume() {
19694
- this.paused = false;
19963
+ failCameraRecovery(exc) {
19964
+ this.cancelCameraRecovery();
19965
+ const err = error_createError({
19966
+ message: "Camera recovery failed",
19967
+ explanation: "Your recording is still paused. Check camera permissions, then try Resume again or stop to keep the recorded part.",
19968
+ exc,
19969
+ options: this.options
19970
+ });
19971
+ this.emit("CAMERA_RECOVERY_FAILED", {
19972
+ err
19973
+ });
19695
19974
  }
19696
- record() {
19697
- this.recording = true;
19975
+ recoverCamera() {
19976
+ if (this.cameraRecovery) return this.cameraRecovery;
19977
+ if (!this.built || this.unloaded || !this.userMedia || this.userMediaLoading || this.submitting || this.key || void 0 !== this.stopTimeout || this.recordingTransportFailed) return Promise.resolve(false);
19978
+ const liveStream = this.userMedia.hasLiveStream();
19979
+ if (liveStream && this.isRecording()) return Promise.resolve(true);
19980
+ if (!liveStream && this.recordingActive && !this.isPaused()) this.pause();
19981
+ const recovery = new Promise((resolve)=>{
19982
+ this.resolveCameraRecovery = resolve;
19983
+ });
19984
+ this.cameraRecovery = recovery;
19985
+ this.emit("CAMERA_RECOVERING");
19986
+ if (liveStream) {
19987
+ this.userMediaTimeout = window.setTimeout(()=>{
19988
+ this.failCameraRecovery(new Error("Camera playback recovery timed out."));
19989
+ }, this.options.timeouts.userMedia);
19990
+ this.userMedia.restorePlayback().then(()=>{
19991
+ if (this.cameraRecovery === recovery) this.finishCameraRecovery(true);
19992
+ }).catch((exc)=>{
19993
+ if (this.cameraRecovery === recovery) this.failCameraRecovery(exc);
19994
+ });
19995
+ } else {
19996
+ this.userMediaLoaded = false;
19997
+ this.loadUserMedia({
19998
+ recovering: true
19999
+ });
20000
+ }
20001
+ return recovery;
19698
20002
  }
19699
- isRecording() {
19700
- return this.recording;
20003
+ switchFacingMode() {
20004
+ if (this.cameraRecovery || !util_getBrowser(this.options).isMobile()) return;
20005
+ if ("user" === this.facingMode) this.facingMode = "environment";
20006
+ else if ("environment" === this.facingMode) this.facingMode = "user";
20007
+ else this.options.logger.warn(`Recorder: unsupported facing mode ${string_pretty(this.facingMode)}`);
20008
+ this.loadGenuineUserMedia({
20009
+ switchingFacingMode: this.facingMode
20010
+ });
19701
20011
  }
19702
- getAudioSampleRate() {
19703
- if (this.audioRecorder) return this.audioRecorder.getSampleRate();
19704
- return -1;
20012
+ hide() {
20013
+ if (!this.isHidden()) {
20014
+ if (this.recorderElement) html_hideElement(this.recorderElement);
20015
+ this.clearUserMediaTimeout();
20016
+ this.clearRetryTimeout();
20017
+ }
19705
20018
  }
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
- };
20019
+ isUnloaded() {
20020
+ return this.unloaded;
20021
+ }
20022
+ isUserMediaLoaded() {
20023
+ return this.userMediaLoaded;
19715
20024
  }
19716
20025
  }
19717
- const visuals_userMedia = UserMedia;
19718
- var Buffer = __webpack_require__("dY").Buffer;
19719
- const PIPE_SYMBOL = "°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸ ";
19720
- class Recorder extends util_Despot {
19721
- visuals;
20026
+ const media = RecorderMedia;
20027
+ function calculateProgress(confirmedCount, totalCount) {
20028
+ return `${(confirmedCount / (totalCount || 1) * 100).toFixed(2)}%`;
20029
+ }
20030
+ function getConfirmedProgressNumber({ args, kind, options }) {
20031
+ const label = "frame" === kind ? "frame" : "audio sample";
20032
+ if (!args) throw error_createError({
20033
+ message: `Arguments are missing for updating the ${label} progress`,
20034
+ options
20035
+ });
20036
+ const confirmedNumber = args[kind];
20037
+ if (!confirmedNumber) throw error_createError({
20038
+ message: `The ${label} number is missing`,
20039
+ options
20040
+ });
20041
+ return confirmedNumber;
20042
+ }
20043
+ var capture_Buffer = __webpack_require__("dY").Buffer;
20044
+ class RecorderCapture extends media {
19722
20045
  replay;
19723
- pendingWrites = new boundedWriteQueue();
19724
20046
  loop;
19725
20047
  originalAnimationFrameObject;
19726
- samplesCount = 0;
19727
- framesCount = 0;
19728
20048
  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
20049
  frameProgress;
19741
20050
  sampleProgress;
19742
20051
  canvas;
19743
- recordingDimensions;
19744
20052
  ctx;
19745
- userMediaLoaded;
19746
- userMediaLoading = false;
19747
- mediaRequestId = 0;
19748
- cameraRecovery;
19749
- resolveCameraRecovery;
19750
- resumeAfterCameraRecovery = false;
19751
- submitting = false;
19752
- unloaded;
19753
20053
  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
20054
  waitingTime;
19769
- connectingStartedAt;
19770
- lastCloseEvent;
19771
- lastSocketError;
19772
- pingInterval;
19773
20055
  frame;
19774
20056
  recordingBuffer;
19775
- facingMode;
19776
20057
  constructor(visuals, replay, options){
19777
- super("Recorder", options);
19778
- this.visuals = visuals;
20058
+ super(visuals, options);
19779
20059
  this.replay = replay;
19780
- this.facingMode = options.video.facingMode;
19781
20060
  }
19782
- hasExceededWebSocketSendBuffer(buffer, opts) {
19783
- return buffer.length > 0 && !this.pendingWrites.enqueue(buffer, opts);
20061
+ onAudioSample(audioSample) {
20062
+ if (this.recordingTransportFailed || this.cameraRecovery) return;
20063
+ const sampleRate = this.userMedia?.getAudioSampleRate();
20064
+ if (sampleRate && sampleRate > 0) this.recordingAudioSampleRate ??= sampleRate;
20065
+ this.samplesCount++;
20066
+ this.mediaTransmissionStarted = true;
20067
+ const audioBuffer = audioSample.toBuffer();
20068
+ this.writeStream(audioBuffer);
20069
+ }
20070
+ onUserMediaReady(params) {
20071
+ try {
20072
+ this.options.logger.debug(`Recorder: onUserMediaReady(${params ? string_pretty(params) : ""})`);
20073
+ const switchingFacingMode = params?.switchingFacingMode;
20074
+ if (params?.recovering) {
20075
+ if (!this.userMedia?.hasLiveStream()) return void this.failCameraRecovery(new Error("The restored camera has no live tracks."));
20076
+ if (void 0 !== this.recordingAudioSampleRate && this.userMedia.getAudioSampleRate() !== this.recordingAudioSampleRate) return void this.failCameraRecovery(new Error("The audio sample rate changed during recovery."));
20077
+ }
20078
+ this.userMediaLoading = this.blocking = this.unloaded = this.submitting = false;
20079
+ this.userMediaLoaded = true;
20080
+ this.clearUserMediaTimeout();
20081
+ if (!switchingFacingMode && !params?.recovering) this.loop = this.createLoop();
20082
+ this.show();
20083
+ if (params?.recordWhenReady) this.record();
20084
+ this.emit("USER_MEDIA_READY", {
20085
+ switchingFacingMode: params?.switchingFacingMode,
20086
+ recovering: params?.recovering,
20087
+ paused: this.isPaused(),
20088
+ recordWhenReady: params?.recordWhenReady
20089
+ });
20090
+ if (params?.recovering) this.finishCameraRecovery(true);
20091
+ } catch (exc) {
20092
+ if (params?.recovering) this.failCameraRecovery(exc);
20093
+ else this.emit("ERROR", {
20094
+ exc
20095
+ });
20096
+ }
20097
+ }
20098
+ calculateFrameProgress() {
20099
+ return calculateProgress(this.confirmedFrameNumber, this.framesCount);
20100
+ }
20101
+ calculateSampleProgress() {
20102
+ return calculateProgress(this.confirmedSampleNumber, this.samplesCount);
20103
+ }
20104
+ updateOverallProgress() {
20105
+ this.frameProgress = this.calculateFrameProgress();
20106
+ if (isAudioEnabled(this.options)) this.sampleProgress = this.calculateSampleProgress();
20107
+ this.emit("PROGRESS", {
20108
+ frameProgress: this.frameProgress,
20109
+ sampleProgress: this.sampleProgress
20110
+ });
20111
+ }
20112
+ updateFrameProgress(args) {
20113
+ this.confirmedFrameNumber = getConfirmedProgressNumber({
20114
+ args,
20115
+ kind: "frame",
20116
+ options: this.options
20117
+ });
20118
+ this.frameProgress = this.calculateFrameProgress();
20119
+ this.updateOverallProgress();
20120
+ }
20121
+ updateSampleProgress(args) {
20122
+ this.confirmedSampleNumber = getConfirmedProgressNumber({
20123
+ args,
20124
+ kind: "sample",
20125
+ options: this.options
20126
+ });
20127
+ this.sampleProgress = this.calculateSampleProgress();
20128
+ this.updateOverallProgress();
20129
+ }
20130
+ preview(args) {
20131
+ if (!args) throw error_createError({
20132
+ message: "Preview arguments are missing.",
20133
+ options: this.options
20134
+ });
20135
+ const hasAudio = this.samplesCount > 0;
20136
+ this.mediaTransmissionStarted = false;
20137
+ this.recordingActive = false;
20138
+ this.recordingTransportFailed = false;
20139
+ this.confirmedFrameNumber = this.confirmedSampleNumber = this.samplesCount = this.framesCount = 0;
20140
+ this.sampleProgress = this.frameProgress = void 0;
20141
+ this.key = args.key;
20142
+ if (args.mp4) this.replay.setMp4Source(`${args.mp4 + constants.WHITELIST_KEY_LABEL}/${this.options.whitelistKey}/videomail.mp4`, true);
20143
+ if (args.webm) this.replay.setWebMSource(`${args.webm + constants.WHITELIST_KEY_LABEL}/${this.options.whitelistKey}/videomail.webm`, true);
20144
+ this.hide();
20145
+ const widthDimension = this.getRecorderWidth(true);
20146
+ const heightDimension = this.getRecorderHeight(true);
20147
+ const duration = args.duration ?? -1;
20148
+ this.emit("PREVIEW", {
20149
+ key: this.key,
20150
+ width: widthDimension?.value,
20151
+ height: heightDimension?.value,
20152
+ hasAudio,
20153
+ duration
20154
+ });
20155
+ if (this.stopTime) this.waitingTime = Date.now() - this.stopTime;
20156
+ this.recordingStats ??= {};
20157
+ this.recordingStats.waitingTime = this.waitingTime;
20158
+ }
20159
+ cancelAnimationFrame() {
20160
+ this.loop?.dispose();
20161
+ }
20162
+ getIntervalSum() {
20163
+ if (!this.loop) return;
20164
+ return this.loop.getElapsedTime();
20165
+ }
20166
+ getAvgInterval() {
20167
+ const intervalSum = this.getIntervalSum() ?? 0;
20168
+ if (!this.hasValidInterval(intervalSum)) return;
20169
+ return intervalSum / this.framesCount;
20170
+ }
20171
+ getAvgFps() {
20172
+ const intervalSum = this.getIntervalSum() ?? 0;
20173
+ if (!this.hasValidInterval(intervalSum)) return;
20174
+ return this.framesCount / intervalSum * 1000;
19784
20175
  }
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);
20176
+ hasValidInterval(intervalSum) {
20177
+ return intervalSum > 0;
19801
20178
  }
19802
- writeToStream(stream, buffer, opts) {
19803
- const onFlushedCallback = opts?.onFlushedCallback;
19804
- 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;
20179
+ getRecordingStats() {
20180
+ return this.recordingStats;
20181
+ }
20182
+ getAudioSampleRate() {
20183
+ return this.recordingAudioSampleRate ?? this.userMedia?.getAudioSampleRate();
20184
+ }
20185
+ stop(params) {
20186
+ this.cancelCameraRecovery();
20187
+ if (!this.userMedia) throw new Error("No user media defined, unable to stop");
20188
+ this.options.logger.debug(`Recorder: stop(${params ? string_pretty(params) : ""})`);
20189
+ this.recordingActive = false;
20190
+ const limitReached = params?.limitReached;
20191
+ this.emit("STOPPING", {
20192
+ limitReached
20193
+ });
20194
+ this.loop?.complete?.();
20195
+ this.stopTimeout = window.setTimeout(()=>{
20196
+ this.stopTimeout = void 0;
20197
+ this.stopTime = Date.now();
20198
+ const videoType = this.replay.getVideoType();
20199
+ if (!videoType) throw new Error("Unable to video record when no video type is defined.");
20200
+ this.recordingStats = {
20201
+ avgFps: this.getAvgFps(),
20202
+ wantedFps: this.options.video.fps,
20203
+ avgInterval: this.getAvgInterval(),
20204
+ wantedInterval: 1e3 / this.options.video.fps,
20205
+ intervalSum: this.getIntervalSum(),
20206
+ framesCount: this.framesCount,
20207
+ videoType
20208
+ };
20209
+ if (this.userMedia) this.recordingStats.videoTrackLabel = this.userMedia.getVideoTrackLabel();
20210
+ if (isAudioEnabled(this.options) && this.userMedia) {
20211
+ this.recordingStats.samplesCount = this.samplesCount;
20212
+ this.recordingStats.sampleRate = this.recordingAudioSampleRate ?? this.userMedia.getAudioSampleRate();
19826
20213
  }
19827
- return true;
19828
- } 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,
19833
- exc
20214
+ this.writeCommand("stop", this.recordingStats, ()=>{
20215
+ this.emit("STOPPED", {
20216
+ recordingStats: this.recordingStats
20217
+ });
19834
20218
  });
19835
- this.handleWriteFailure(err);
19836
- return false;
19837
- }
20219
+ this.reset();
20220
+ }, 60);
19838
20221
  }
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();
20222
+ back(cb) {
20223
+ this.emit("GOING_BACK");
20224
+ this.key = void 0;
20225
+ this.recordingDimensions = void 0;
20226
+ this.unloaded = false;
20227
+ this.show();
20228
+ this.writeCommand("back", void 0, cb);
19849
20229
  }
19850
- isStaleStream(stream) {
19851
- return this.stream !== stream || stream.destroyed;
20230
+ reInitializeAudio() {
20231
+ this.options.logger.debug("Recorder: reInitializeAudio()");
20232
+ this.clearUserMediaTimeout();
20233
+ this.userMediaLoading = false;
20234
+ this.userMedia?.stop();
20235
+ this.recordingDimensions = void 0;
20236
+ this.userMediaLoaded = this.key = this.canvas = this.ctx = void 0;
20237
+ this.loadUserMedia();
19852
20238
  }
19853
- canPauseForBackpressure() {
19854
- return Boolean(this.recordingActive && !this.userMedia?.isPaused() && this.loop?.isRunning());
20239
+ reset() {
20240
+ this.cancelCameraRecovery();
20241
+ if (!this.unloaded) {
20242
+ this.options.logger.debug("Recorder: reset()");
20243
+ this.emit("RESETTING");
20244
+ this.cancelAnimationFrame();
20245
+ this.userMedia?.stop();
20246
+ this.replay.reset();
20247
+ this.userMediaLoaded = this.key = this.canvas = this.ctx = this.recordingBuffer = void 0;
20248
+ }
19855
20249
  }
19856
- pauseForBackpressure() {
19857
- if (!this.canPauseForBackpressure()) return;
19858
- this.resumeAfterDrain = true;
19859
- this.loop.stop();
20250
+ validate() {
20251
+ return Boolean(this.key) && void 0 === this.canvas;
19860
20252
  }
19861
- resumeRecordingAfterDrain() {
19862
- if (!this.resumeAfterDrain) return;
20253
+ pause(params) {
20254
+ this.cancelCameraRecovery();
19863
20255
  this.resumeAfterDrain = false;
19864
- if (this.canResumeAfterDrain()) this.loop?.start();
20256
+ const loop = this.loop;
20257
+ if (!loop) throw new Error("No recording loop defined");
20258
+ if (params) this.options.logger.debug(`pause() at frame ${this.framesCount} with ${string_pretty(params)}`);
20259
+ else this.options.logger.debug(`pause() at frame ${this.framesCount}`);
20260
+ this.userMedia?.pause();
20261
+ loop.stop();
20262
+ this.emit("PAUSED");
20263
+ this.sendPings();
19865
20264
  }
19866
- canResumeAfterDrain() {
19867
- return this.recordingActive && !this.userMedia?.isPaused();
20265
+ resume() {
20266
+ if (this.cameraRecovery || !this.userMedia?.hasLiveStream()) {
20267
+ this.recoverCamera();
20268
+ this.resumeAfterCameraRecovery = Boolean(this.cameraRecovery);
20269
+ return;
20270
+ }
20271
+ const loop = this.loop;
20272
+ if (!loop) throw new Error("No recording loop defined");
20273
+ this.options.logger.debug(`Recorder: resume() with frame ${this.framesCount}`);
20274
+ this.stopPings();
20275
+ this.emit("RESUMING");
20276
+ this.userMedia.resume();
20277
+ if (this.streamBackpressured) this.resumeAfterDrain = true;
20278
+ else loop.start();
19868
20279
  }
19869
- handleWriteFailure(err) {
19870
- if (this.mediaTransmissionStarted) return void this.failMediaTransmission(err);
19871
- this.emit("ERROR", {
19872
- err
19873
- });
20280
+ onFlushed(opts) {
20281
+ const frameNumber = opts.frameNumber;
20282
+ if (1 === frameNumber) this.emit("FIRST_FRAME_SENT");
19874
20283
  }
19875
- failMediaTransmission(err) {
20284
+ draw(_deltaTime, elapsedTime) {
19876
20285
  if (this.recordingTransportFailed) return;
19877
- this.recordingTransportFailed = true;
19878
- 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;
20286
+ if (!this.userMedia) throw new Error("No user media defined, unable to draw on canvas");
20287
+ try {
20288
+ if (!this.isPaused() && this.stream && this.ctx) {
20289
+ if (0 === this.framesCount) this.emit("SENDING_FIRST_FRAME");
20290
+ this.framesCount++;
20291
+ const imageSource = this.userMedia.getRawVisuals();
20292
+ if (this.canvas && imageSource) this.ctx.drawImage(imageSource, 0, 0, this.canvas.width, this.canvas.height);
20293
+ else throw new Error("Unable to draw an image without a defined canvas");
20294
+ this.recordingBuffer = this.frame?.toBuffer();
20295
+ const recordingBufferLength = this.recordingBuffer?.length;
20296
+ if (recordingBufferLength) {
20297
+ if (this.recordingBuffer) {
20298
+ const frameControlBuffer = capture_Buffer.from(JSON.stringify({
20299
+ frameNumber: this.framesCount
20300
+ }));
20301
+ const frameBuffer = capture_Buffer.concat([
20302
+ this.recordingBuffer,
20303
+ frameControlBuffer
20304
+ ]);
20305
+ this.mediaTransmissionStarted = true;
20306
+ this.writeStream(frameBuffer, {
20307
+ frameNumber: this.framesCount,
20308
+ onFlushedCallback: this.onFlushed.bind(this)
20309
+ });
20310
+ this.visuals.checkTimer(elapsedTime);
20311
+ }
20312
+ } else throw error_createError({
20313
+ message: "Failed to extract webcam data.",
20314
+ options: this.options
20315
+ });
20316
+ }
20317
+ } catch (exc) {
20318
+ this.emit("ERROR", {
20319
+ exc
20320
+ });
20321
+ }
20322
+ }
20323
+ createLoop() {
20324
+ const newLoop = animitter_default()({
20325
+ fps: this.options.video.fps
20326
+ }, this.draw.bind(this));
20327
+ this.originalAnimationFrameObject = newLoop.getRequestAnimationFrameObject();
20328
+ return newLoop;
20329
+ }
20330
+ record() {
20331
+ if (this.unloaded || this.cameraRecovery) return;
20332
+ if (!this.connected) {
20333
+ this.options.logger.debug("Recorder: reconnecting before recording …");
20334
+ this.initSocket(()=>{
20335
+ this.once("SERVER_READY", ()=>{
20336
+ if (this.unloaded) return;
20337
+ if (this.userMediaLoaded || this.options.loadUserMediaOnRecord) this.record();
20338
+ else this.once("USER_MEDIA_READY", ()=>{
20339
+ if (!this.unloaded) this.record();
20340
+ });
20341
+ });
20342
+ });
20343
+ return;
20344
+ }
20345
+ if (!this.userMediaLoaded) {
20346
+ if (this.options.loadUserMediaOnRecord) this.loadUserMedia({
20347
+ recordWhenReady: true
20348
+ });
20349
+ else {
20350
+ const err = error_createError({
20351
+ message: "Load and enable your camera first",
20352
+ options: this.options
20353
+ });
20354
+ this.emit("ERROR", {
20355
+ err
20356
+ });
20357
+ }
20358
+ return;
20359
+ }
20360
+ const loop = this.loop;
20361
+ if (!loop) throw new Error("No recording loop defined");
19893
20362
  this.recordingDimensions = void 0;
19894
- this.emit("ERROR", {
19895
- err
19896
- });
19897
- }
19898
- startBackpressureTimeout() {
19899
- if (!this.shouldStartBackpressureTimeout()) return;
19900
- this.backpressureTimeout = window.setTimeout(()=>{
19901
- this.backpressureTimeout = void 0;
19902
- if (this.shouldIgnoreBackpressureTimeout()) return;
20363
+ try {
20364
+ if (!this.userMedia) throw new Error("No user media defined, unable to create canvas");
20365
+ this.canvas = this.userMedia.createCanvas();
20366
+ } catch (exc) {
19903
20367
  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.",
20368
+ exc,
19906
20369
  options: this.options
19907
20370
  });
19908
- this.failMediaTransmission(err);
19909
- }, this.options.timeouts.connection);
19910
- }
19911
- shouldStartBackpressureTimeout() {
19912
- return void 0 === this.backpressureTimeout && this.mediaTransmissionStarted;
19913
- }
19914
- shouldIgnoreBackpressureTimeout() {
19915
- return !this.streamBackpressured || this.recordingTransportFailed;
19916
- }
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);
19930
- }
19931
- handleStreamDrain(stream) {
19932
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *drain* event emitted`);
19933
- this.flushPendingWrites(stream);
19934
- }
19935
- canReconnectAfterStreamClose() {
19936
- return Boolean(this.connected && this.userMediaLoaded);
19937
- }
19938
- shouldReportStreamClose() {
19939
- return !this.connecting && !this.blocking;
19940
- }
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;
19951
- 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"
20371
+ this.emit("ERROR", {
20372
+ err
19964
20373
  });
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
20374
  return;
19982
20375
  }
19983
- if (!this.connected) return void this.handleConnectionFailure({
19984
- url2Connect,
19985
- cause: "error"
20376
+ this.ctx = this.canvas.getContext("2d");
20377
+ if (!this.canvas.width) {
20378
+ const err = error_createError({
20379
+ message: "Canvas has an invalid width.",
20380
+ options: this.options
20381
+ });
20382
+ this.emit("ERROR", {
20383
+ err
20384
+ });
20385
+ return;
20386
+ }
20387
+ if (!this.canvas.height) {
20388
+ const err = error_createError({
20389
+ message: "Canvas has an invalid height.",
20390
+ options: this.options
20391
+ });
20392
+ this.emit("ERROR", {
20393
+ err
20394
+ });
20395
+ return;
20396
+ }
20397
+ this.recordingDimensions = {
20398
+ width: this.canvas.width,
20399
+ height: this.canvas.height
20400
+ };
20401
+ this.frame = new canvas_to_buffer_modern_r(this.canvas, this.options.image.types, this.options.image.quality);
20402
+ this.options.logger.debug("Recorder: record()");
20403
+ this.recordingActive = true;
20404
+ this.recordingTransportFailed = false;
20405
+ this.mediaTransmissionStarted = false;
20406
+ this.recordingAudioSampleRate = void 0;
20407
+ this.userMedia.record();
20408
+ this.emit("RECORDING", {
20409
+ framesCount: this.framesCount
19986
20410
  });
19987
- const streamError = err instanceof Error ? err : new Error("WebSocket stream emitted an error event", {
19988
- cause: this.lastSocketError ?? err
20411
+ loop.on("update", (_deltaTime, elapsedTime)=>{
20412
+ let avgFPS;
20413
+ avgFPS = 0 !== elapsedTime ? Math.round(this.framesCount / elapsedTime * 1000) : void 0;
20414
+ this.options.logger.debug(`Recorder updates avgFps = ${avgFPS} at frame ${this.framesCount}`);
19989
20415
  });
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
- })
20416
+ loop.start();
20417
+ }
20418
+ setAnimationFrameObject(newObj) {
20419
+ if (this.loop) {
20420
+ const isRecording = this.isRecording();
20421
+ this.loop.stop();
20422
+ this.loop.setRequestAnimationFrameObject(newObj);
20423
+ if (isRecording) this.loop.start();
20424
+ }
20425
+ }
20426
+ restoreAnimationFrameObject() {
20427
+ this.options.logger.debug("Recorder: restoreAnimationFrameObject()");
20428
+ if (this.originalAnimationFrameObject) this.setAnimationFrameObject(this.originalAnimationFrameObject);
20429
+ }
20430
+ loopWithTimeouts() {
20431
+ this.options.logger.debug("Recorder: loopWithTimeouts()");
20432
+ const wantedInterval = 1e3 / this.options.video.fps;
20433
+ let processingTime = 0;
20434
+ const raf = (fn)=>window.setTimeout(()=>{
20435
+ const start = Date.now();
20436
+ fn();
20437
+ processingTime = Date.now() - start;
20438
+ }, wantedInterval - processingTime);
20439
+ const cancel = (id)=>{
20440
+ window.clearTimeout(id);
20441
+ };
20442
+ this.setAnimationFrameObject({
20443
+ requestAnimationFrame: raf,
20444
+ cancelAnimationFrame: cancel
19998
20445
  });
19999
20446
  }
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);
20447
+ isPaused() {
20448
+ return this.userMedia?.isPaused() && !this.loop?.isRunning();
20006
20449
  }
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;
20450
+ isUserMediaPaused() {
20451
+ return this.userMedia?.isPaused() ?? false;
20012
20452
  }
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);
20453
+ isCaptureLoopRunning() {
20454
+ return this.loop?.isRunning() ?? false;
20021
20455
  }
20022
- show() {
20023
- html_showElement(this.recorderElement);
20456
+ stopCaptureLoop() {
20457
+ this.loop?.stop();
20024
20458
  }
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
- }
20459
+ startCaptureLoop() {
20460
+ this.loop?.start();
20052
20461
  }
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;
20462
+ isRecording() {
20463
+ return Boolean(this.loop?.isRunning() && !this.isPaused() && !this.isNotifying() && this.stream && !this.stream.destroyed);
20058
20464
  }
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;
20465
+ getRecordingDimensions() {
20466
+ return this.recordingDimensions && {
20467
+ ...this.recordingDimensions
20468
+ };
20469
+ }
20470
+ }
20471
+ const capture = RecorderCapture;
20472
+ function figureMinHeight(height, options) {
20473
+ let minHeight;
20474
+ if (options.video.height) {
20475
+ minHeight = height ? Math.min(options.video.height, height) : options.video.height;
20476
+ if (minHeight < 1) throw error_createError({
20477
+ message: `Got a min height less than 1 (${minHeight})!`,
20478
+ options
20479
+ });
20480
+ } else minHeight = height;
20481
+ return minHeight;
20482
+ }
20483
+ const dimensions_figureMinHeight = figureMinHeight;
20484
+ function getRatio(options, videoHeight, videoWidth) {
20485
+ let ratio = 1;
20486
+ const hasVideoDimensions = videoHeight && videoWidth;
20487
+ const desiredHeight = options.video.height;
20488
+ const desiredWidth = options.video.width;
20489
+ const hasDesiredDimensions = desiredHeight && desiredWidth;
20490
+ if (hasDesiredDimensions) ratio = hasVideoDimensions ? videoHeight < desiredHeight || videoWidth < desiredWidth ? videoHeight / videoWidth : desiredHeight / desiredWidth : desiredHeight / desiredWidth;
20491
+ else if (hasVideoDimensions) ratio = videoHeight / videoWidth;
20492
+ return ratio;
20493
+ }
20494
+ const dimensions_getRatio = getRatio;
20495
+ function calculateHeight(responsive, videoWidth, options, ratio, element) {
20496
+ const dimension = {
20497
+ unit: "px"
20498
+ };
20499
+ let width = videoWidth;
20500
+ if (responsive && element) {
20501
+ const limitedDimension = dimensions_limitWidth(element, options, width);
20502
+ width = limitedDimension.value;
20064
20503
  }
20065
- clearBackpressureTimeout() {
20066
- if (void 0 === this.backpressureTimeout) return;
20067
- window.clearTimeout(this.backpressureTimeout);
20068
- this.backpressureTimeout = void 0;
20504
+ const chosenRatio = ratio ?? dimensions_getRatio(options, void 0, videoWidth);
20505
+ const height = width ? Math.round(width * chosenRatio) : void 0;
20506
+ if (height && Number.isInteger(height) && height < 1) throw error_createError({
20507
+ message: "Just calculated a height less than 1 which is wrong.",
20508
+ options
20509
+ });
20510
+ const minHeight = dimensions_figureMinHeight(height, options);
20511
+ dimension.value = minHeight;
20512
+ return dimension;
20513
+ }
20514
+ const dimensions_calculateHeight = calculateHeight;
20515
+ function calculateWidth(responsive, options, videoHeight, ratio) {
20516
+ const dimension = {
20517
+ unit: "px"
20518
+ };
20519
+ let height = dimensions_figureMinHeight(videoHeight, options);
20520
+ if (responsive) {
20521
+ const limitedDimension = dimensions_limitHeight(height, options, "calculateWidth");
20522
+ height = limitedDimension.value;
20069
20523
  }
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;
20524
+ if (!height || height < 1) throw error_createError({
20525
+ message: `Height ${height} cannot be smaller than 1 when calculating width.`,
20526
+ options
20527
+ });
20528
+ const chosenRatio = ratio ?? dimensions_getRatio(options, videoHeight);
20529
+ const calculatedWidth = Math.round(height / chosenRatio);
20530
+ if (calculatedWidth < 1) throw error_createError({
20531
+ message: "Calculated width cannot be smaller than 1!",
20532
+ options
20533
+ });
20534
+ dimension.value = calculatedWidth;
20535
+ return dimension;
20536
+ }
20537
+ const dimensions_calculateWidth = calculateWidth;
20538
+ class RecorderDimensions {
20539
+ context;
20540
+ constructor(context){
20541
+ this.context = context;
20075
20542
  }
20076
- handleConnectionFailure(params) {
20077
- if (this.retryConnection(params)) return;
20078
- this.failConnection(params);
20543
+ correctDimensions() {
20544
+ const recorderElement = this.context.getRecorderElement();
20545
+ if (!recorderElement) return;
20546
+ const widthDimension = dimensions_useFullWidth(this.context.options.video.mobileBreakPoint);
20547
+ if (this.context.options.video.stretch || widthDimension) {
20548
+ recorderElement.style.width = "100%";
20549
+ recorderElement.style.removeProperty("height");
20550
+ } else {
20551
+ if (this.context.options.video.width) {
20552
+ const recorderWidth = this.getRecorderWidth(true);
20553
+ if (recorderWidth?.value) recorderElement.width = recorderWidth.value;
20554
+ else recorderElement.style.removeProperty("width");
20555
+ }
20556
+ if (this.context.options.video.height) {
20557
+ const recorderHeight = this.getRecorderHeight(true);
20558
+ if (recorderHeight?.value) recorderElement.height = recorderHeight.value;
20559
+ else recorderElement.style.removeProperty("height");
20560
+ }
20561
+ }
20079
20562
  }
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;
20563
+ getRecorderWidth(responsive) {
20564
+ const userMedia = this.context.getUserMedia();
20565
+ if (userMedia?.hasVideoWidth()) return userMedia.getRawWidth(responsive);
20566
+ else if (responsive && this.context.options.video.width) return this.context.visuals.limitWidth(this.context.options.video.width);
20567
+ const dimension = {
20568
+ unit: "px",
20569
+ value: this.context.options.video.width
20570
+ };
20571
+ return dimension;
20094
20572
  }
20095
- canRetryConnection() {
20096
- return Boolean(this.reconnecting && this.userMediaLoaded && this.isOnline() && !this.unloaded && !this.blocking);
20573
+ getRecorderHeight(responsive, useBoundingClientRect) {
20574
+ const recorderElement = this.context.getRecorderElement();
20575
+ const userMedia = this.context.getUserMedia();
20576
+ let recorderHeight;
20577
+ if (recorderElement && useBoundingClientRect) {
20578
+ const height = recorderElement.getBoundingClientRect().height;
20579
+ recorderHeight = {
20580
+ unit: "px",
20581
+ value: height
20582
+ };
20583
+ } else if (userMedia) {
20584
+ let height = userMedia.getRawHeight(responsive);
20585
+ if (void 0 !== height && this.context.options.video.width) {
20586
+ const ratio = this.getRatio();
20587
+ if (void 0 !== ratio) {
20588
+ const idealHeight = this.context.options.video.width * ratio;
20589
+ height = Math.min(idealHeight, height);
20590
+ }
20591
+ }
20592
+ recorderHeight = {
20593
+ unit: "px",
20594
+ value: height
20595
+ };
20596
+ } else if (responsive && this.context.options.video.height) recorderHeight = this.calculateHeight(responsive);
20597
+ else if (this.context.options.video.height) {
20598
+ const height = this.context.options.video.height;
20599
+ recorderHeight = {
20600
+ unit: "px",
20601
+ value: height
20602
+ };
20603
+ }
20604
+ return recorderHeight;
20097
20605
  }
20098
- shouldContinueReconnect() {
20099
- return this.reconnecting && !this.unloaded && !this.blocking;
20606
+ getRatio() {
20607
+ const userMedia = this.context.getUserMedia();
20608
+ let ratio;
20609
+ if (userMedia) {
20610
+ const userMediaVideoWidth = userMedia.getVideoWidth();
20611
+ const userMediaVideoHeight = userMedia.getVideoHeight();
20612
+ if (!userMediaVideoWidth || userMediaVideoWidth < 1) ratio = this.context.visuals.getRatio();
20613
+ else if (userMediaVideoHeight) ratio = userMediaVideoHeight / userMediaVideoWidth;
20614
+ } else ratio = dimensions_getRatio(this.context.options);
20615
+ return ratio;
20100
20616
  }
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();
20617
+ calculateWidth(responsive) {
20618
+ const userMedia = this.context.getUserMedia();
20619
+ const recorderElement = this.context.getRecorderElement();
20620
+ let videoHeight;
20621
+ if (userMedia) videoHeight = userMedia.getVideoHeight();
20622
+ else if (recorderElement) videoHeight = recorderElement.videoHeight || recorderElement.height;
20623
+ return dimensions_calculateWidth(responsive, this.context.options, videoHeight, this.getRatio());
20110
20624
  }
20111
- isOnline() {
20112
- return navigator.onLine;
20625
+ calculateHeight(responsive) {
20626
+ const userMedia = this.context.getUserMedia();
20627
+ const recorderElement = this.context.getRecorderElement();
20628
+ let videoDimension;
20629
+ if (userMedia) {
20630
+ const videoHeight = userMedia.getVideoHeight();
20631
+ videoDimension = {
20632
+ value: videoHeight,
20633
+ unit: "px"
20634
+ };
20635
+ } else if (recorderElement) {
20636
+ const videoHeight = recorderElement.videoHeight || recorderElement.height;
20637
+ videoDimension = {
20638
+ value: videoHeight,
20639
+ unit: "px"
20640
+ };
20641
+ } else videoDimension = dimensions_calculateHeight(responsive, void 0, this.context.options, this.getRatio(), recorderElement);
20642
+ return videoDimension;
20113
20643
  }
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
20144
- };
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
20644
+ }
20645
+ const recorder_dimensions = RecorderDimensions;
20646
+ function getFirstVideoTrack(localMediaStream) {
20647
+ const videoTracks = localMediaStream.getVideoTracks();
20648
+ let videoTrack;
20649
+ if (videoTracks[0]) videoTrack = videoTracks[0];
20650
+ return videoTrack;
20651
+ }
20652
+ const media_getFirstVideoTrack = getFirstVideoTrack;
20653
+ const MEDIA_EVENTS = [
20654
+ "loadstart",
20655
+ "suspend",
20656
+ "progress",
20657
+ "abort",
20658
+ "emptied",
20659
+ "stalled",
20660
+ "pause",
20661
+ "loadeddata",
20662
+ "waiting",
20663
+ "playing",
20664
+ "canplay",
20665
+ "canplaythrough",
20666
+ "seeking",
20667
+ "seeked",
20668
+ "ended",
20669
+ "ratechange",
20670
+ "durationchange",
20671
+ "volumechange"
20672
+ ];
20673
+ const mediaEvents = MEDIA_EVENTS;
20674
+ function _define_property(obj, key, value) {
20675
+ if (key in obj) Object.defineProperty(obj, key, {
20676
+ value: value,
20677
+ enumerable: true,
20678
+ configurable: true,
20679
+ writable: true
20680
+ });
20681
+ else obj[key] = value;
20682
+ return obj;
20683
+ }
20684
+ class AudioSample {
20685
+ toBuffer() {
20686
+ const l = this.float32Array.length;
20687
+ const arr = new Int16Array(l);
20688
+ this.float32Array.forEach((sample, i)=>{
20689
+ const clampedSample = Number.isFinite(sample) ? Math.max(-1, sample) : sample;
20690
+ arr[i] = 0x7fff * Math.min(1, clampedSample);
20167
20691
  });
20692
+ return typedarray_to_buffer(arr);
20168
20693
  }
20169
- shouldIgnoreConnectionFailure() {
20170
- return this.connectionFailed || this.connected || Boolean(this.unloaded) || this.validate();
20694
+ constructor(float32Array){
20695
+ _define_property(this, "float32Array", void 0);
20696
+ this.float32Array = float32Array;
20171
20697
  }
20172
- calculateFrameProgress() {
20173
- return `${(this.confirmedFrameNumber / (this.framesCount || 1) * 100).toFixed(2)}%`;
20698
+ }
20699
+ const esm_src = AudioSample;
20700
+ const esm = esm_src;
20701
+ var is_power_of_two = __webpack_require__("dZ");
20702
+ var is_power_of_two_default = /*#__PURE__*/ __webpack_require__.n(is_power_of_two);
20703
+ const AUDIO_RECORDER_PROCESSOR_NAME = "videomail-pcm-recorder";
20704
+ function registerAudioRecorderProcessor(name, Processor, register) {
20705
+ class PcmRecorderProcessor extends Processor {
20706
+ constructor(options){
20707
+ super();
20708
+ this.samples = new Float32Array(options.processorOptions.bufferSize);
20709
+ this.offset = 0;
20710
+ this.recordingId = 0;
20711
+ this.port.onmessage = (event)=>{
20712
+ const recordingId = event.data;
20713
+ if ("number" != typeof recordingId) return;
20714
+ if (!Number.isSafeInteger(recordingId) || recordingId < -1) return;
20715
+ this.recordingId = recordingId;
20716
+ this.offset = 0;
20717
+ };
20718
+ }
20719
+ process(inputs) {
20720
+ if (this.recordingId < 0) return false;
20721
+ const channel = inputs[0]?.[0];
20722
+ if (!channel || 0 === this.recordingId) return true;
20723
+ let inputOffset = 0;
20724
+ while(inputOffset < channel.length){
20725
+ const count = Math.min(this.samples.length - this.offset, channel.length - inputOffset);
20726
+ this.samples.set(channel.subarray(inputOffset, inputOffset + count), this.offset);
20727
+ this.offset += count;
20728
+ inputOffset += count;
20729
+ if (this.offset === this.samples.length) {
20730
+ const samples = this.samples;
20731
+ this.port.postMessage({
20732
+ recordingId: this.recordingId,
20733
+ samples
20734
+ }, [
20735
+ samples.buffer
20736
+ ]);
20737
+ this.samples = new Float32Array(this.offset);
20738
+ this.offset = 0;
20739
+ }
20740
+ }
20741
+ return true;
20742
+ }
20174
20743
  }
20175
- calculateSampleProgress() {
20176
- return `${(this.confirmedSampleNumber / (this.samplesCount || 1) * 100).toFixed(2)}%`;
20744
+ register(name, PcmRecorderProcessor);
20745
+ }
20746
+ const CHANNELS = 1;
20747
+ const DEFAULT_BUFFER_SIZE = 2048;
20748
+ const MIN_BUFFER_SIZE = 256;
20749
+ const MAX_BUFFER_SIZE = 16384;
20750
+ class AudioRecorder extends util_Despot {
20751
+ userMedia;
20752
+ worklet;
20753
+ audioInput;
20754
+ volume;
20755
+ vcAudioContext;
20756
+ workletReady;
20757
+ moduleUrl;
20758
+ audioCallback;
20759
+ initializationId = 0;
20760
+ recordingSequence = 0;
20761
+ recordingId = 0;
20762
+ constructor(userMedia, options){
20763
+ super("AudioRecorder", options);
20764
+ this.userMedia = userMedia;
20177
20765
  }
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
- });
20766
+ getAudioContext() {
20767
+ if (!this.vcAudioContext) this.vcAudioContext = new window.AudioContext();
20768
+ return this.vcAudioContext;
20185
20769
  }
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",
20770
+ getBufferSize() {
20771
+ const configuredSize = this.options.audio.bufferSize;
20772
+ const bufferSize = "auto" === configuredSize ? DEFAULT_BUFFER_SIZE : configuredSize;
20773
+ if (void 0 === bufferSize) throw error_createError({
20774
+ message: "Audio buffer size is required.",
20194
20775
  options: this.options
20195
20776
  });
20196
- this.confirmedFrameNumber = args.frame;
20197
- this.frameProgress = this.calculateFrameProgress();
20198
- this.updateOverallProgress();
20199
- }
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",
20777
+ const validBufferSize = Number.isInteger(bufferSize) && bufferSize >= MIN_BUFFER_SIZE && bufferSize <= MAX_BUFFER_SIZE && is_power_of_two_default()(bufferSize);
20778
+ if (!validBufferSize) throw error_createError({
20779
+ message: `Audio buffer size must be a power of two between ${MIN_BUFFER_SIZE} and ${MAX_BUFFER_SIZE}. The current buffer size is ${bufferSize}.`,
20208
20780
  options: this.options
20209
20781
  });
20210
- this.confirmedSampleNumber = args.sample;
20211
- this.sampleProgress = this.calculateSampleProgress();
20212
- this.updateOverallProgress();
20782
+ return bufferSize;
20213
20783
  }
20214
- preview(args) {
20215
- if (!args) throw error_createError({
20216
- message: "Preview arguments are missing.",
20784
+ validateVolume() {
20785
+ const { volume } = this.options.audio;
20786
+ const validVolume = Number.isFinite(volume) && volume >= 0 && volume <= 1;
20787
+ if (!validVolume) throw error_createError({
20788
+ message: `Audio volume must be between zero and one. The current volume is ${volume}.`,
20217
20789
  options: this.options
20218
20790
  });
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;
20242
20791
  }
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;
20279
- }
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;
20792
+ revokeModuleUrl() {
20793
+ if (this.moduleUrl) {
20794
+ URL.revokeObjectURL(this.moduleUrl);
20795
+ this.moduleUrl = void 0;
20309
20796
  }
20797
+ }
20798
+ async loadWorklet(context) {
20799
+ const source = `(${registerAudioRecorderProcessor.toString()})(${JSON.stringify(AUDIO_RECORDER_PROCESSOR_NAME)}, AudioWorkletProcessor, registerProcessor);`;
20800
+ const moduleUrl = URL.createObjectURL(new Blob([
20801
+ source
20802
+ ], {
20803
+ type: "text/javascript"
20804
+ }));
20805
+ this.moduleUrl = moduleUrl;
20310
20806
  try {
20311
- this.stream = stream_default()(nativeSocket);
20312
- } 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)}`);
20407
- });
20807
+ await context.audioWorklet.addModule(moduleUrl);
20808
+ } finally{
20809
+ if (this.moduleUrl === moduleUrl) this.revokeModuleUrl();
20408
20810
  }
20409
20811
  }
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;
20812
+ onAudioProcess(event) {
20813
+ const callback = this.audioCallback;
20814
+ if (!callback) return;
20815
+ if (!this.userMedia.isRecording() || this.userMedia.isPaused()) return;
20816
+ const message = event.data;
20817
+ if ("object" != typeof message || null === message) return;
20818
+ if (!("recordingId" in message) || message.recordingId !== this.recordingId) return;
20819
+ if (!("samples" in message) || !(message.samples instanceof Float32Array)) return;
20820
+ if (0 === message.samples.length) return;
20821
+ callback(new esm(message.samples));
20417
20822
  }
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);
20823
+ disconnect() {
20824
+ if (this.worklet) {
20825
+ this.worklet.port.onmessage = null;
20826
+ this.worklet.onprocessorerror = null;
20827
+ this.worklet.port.postMessage(-1);
20828
+ this.worklet.port.close();
20829
+ this.worklet.disconnect();
20830
+ this.worklet = void 0;
20441
20831
  }
20442
- else {
20443
- this.options.logger.debug(`Recorder: no error listeners attached but throwing error ${string_pretty(err)}`);
20832
+ this.audioInput?.disconnect();
20833
+ this.volume?.disconnect();
20834
+ this.audioInput = void 0;
20835
+ this.volume = void 0;
20836
+ this.audioCallback = void 0;
20837
+ this.recordingId = 0;
20838
+ }
20839
+ async init(localMediaStream) {
20840
+ this.options.logger.debug("AudioRecorder: init()");
20841
+ const initializationId = ++this.initializationId;
20842
+ this.disconnect();
20843
+ const bufferSize = this.getBufferSize();
20844
+ this.validateVolume();
20845
+ if ("function" != typeof window.AudioContext) throw error_createError({
20846
+ message: "Audio recording requires Web Audio support.",
20847
+ options: this.options
20848
+ });
20849
+ const context = this.getAudioContext();
20850
+ if (void 0 === context.audioWorklet || "function" != typeof window.AudioWorkletNode) throw error_createError({
20851
+ message: "Audio recording requires AudioWorklet support in a secure context.",
20852
+ options: this.options
20853
+ });
20854
+ const workletReady = this.workletReady ??= this.loadWorklet(context);
20855
+ try {
20856
+ await workletReady;
20857
+ } catch (exc) {
20858
+ if (this.workletReady === workletReady) this.workletReady = void 0;
20859
+ if (initializationId !== this.initializationId) return;
20444
20860
  throw error_createError({
20445
- err,
20446
- message: "Unable to process this error since there are no error listeners anymore.",
20861
+ message: "Unable to load the audio recorder worklet. Check AudioWorklet and Content Security Policy support for blob: scripts.",
20862
+ cause: exc,
20447
20863
  options: this.options
20448
20864
  });
20449
20865
  }
20450
- }
20451
- getUserMediaCallback(localStream, params, requestId = this.mediaRequestId) {
20452
- if (!this.userMedia) {
20453
- localStream.getTracks().forEach((track)=>{
20454
- track.stop();
20455
- });
20866
+ if (initializationId !== this.initializationId) return;
20867
+ try {
20868
+ this.audioInput = context.createMediaStreamSource(localMediaStream);
20869
+ } catch (exc) {
20456
20870
  throw error_createError({
20457
- message: "No user media is defined",
20871
+ message: "Webcam has no audio",
20872
+ exc,
20458
20873
  options: this.options
20459
20874
  });
20460
20875
  }
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)
20876
+ try {
20877
+ this.volume = context.createGain();
20878
+ this.volume.gain.value = this.options.audio.volume;
20879
+ this.worklet = new window.AudioWorkletNode(context, AUDIO_RECORDER_PROCESSOR_NAME, {
20880
+ channelCount: CHANNELS,
20881
+ channelCountMode: "explicit",
20882
+ outputChannelCount: [
20883
+ CHANNELS
20884
+ ],
20885
+ processorOptions: {
20886
+ bufferSize
20887
+ }
20888
+ });
20889
+ this.worklet.port.onmessage = (event)=>{
20890
+ this.onAudioProcess(event);
20891
+ };
20892
+ this.worklet.onprocessorerror = ()=>{
20893
+ this.stop();
20894
+ this.emit("ERROR", {
20895
+ err: error_createError({
20896
+ message: "Audio recording processor failed.",
20897
+ options: this.options
20898
+ })
20468
20899
  });
20469
- }, params);
20900
+ };
20901
+ this.audioInput.connect(this.volume);
20902
+ this.volume.connect(this.worklet);
20903
+ this.worklet.connect(context.destination);
20470
20904
  } catch (exc) {
20471
- localStream.getTracks().forEach((track)=>{
20472
- track.stop();
20473
- });
20474
- this.userMediaLoading = false;
20475
- this.clearUserMediaTimeout();
20476
- this.userMediaErrorCallback(exc, {
20477
- audio: isAudioEnabled(this.options)
20478
- });
20479
- }
20480
- else {
20481
- localStream.getTracks().forEach((track)=>{
20482
- track.stop();
20905
+ this.disconnect();
20906
+ throw error_createError({
20907
+ message: "Failed to connect the audio recorder.",
20908
+ exc,
20909
+ options: this.options
20483
20910
  });
20484
- this.userMediaLoading = false;
20485
- this.clearUserMediaTimeout();
20486
- this.cancelCameraRecovery();
20487
20911
  }
20488
20912
  }
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
20498
- }
20499
- },
20500
- audio: isAudioEnabled(this.options)
20501
- };
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)=>{
20913
+ record(cb) {
20914
+ this.options.logger.debug("AudioRecorder: record()");
20915
+ this.audioCallback = cb;
20916
+ this.updateRecordingState();
20917
+ }
20918
+ updateRecordingState() {
20919
+ if (!this.worklet) return;
20920
+ const shouldRecord = Boolean(this.audioCallback) && this.userMedia.isRecording() && !this.userMedia.isPaused();
20921
+ this.recordingId = shouldRecord ? ++this.recordingSequence : 0;
20922
+ this.worklet.port.postMessage(this.recordingId);
20923
+ }
20924
+ async resume() {
20925
+ const context = this.vcAudioContext;
20926
+ if (!context) return;
20927
+ if ("running" !== context.state) await context.resume();
20928
+ }
20929
+ stop() {
20930
+ this.options.logger.debug("AudioRecorder: stop()");
20931
+ this.initializationId++;
20932
+ this.disconnect();
20933
+ this.revokeModuleUrl();
20934
+ const context = this.vcAudioContext;
20935
+ this.vcAudioContext = void 0;
20936
+ this.workletReady = void 0;
20937
+ if (context && "closed" !== context.state) context.close().then(()=>{
20938
+ this.options.logger.debug("AudioRecorder: audio context is closed");
20939
+ }).catch((err)=>{
20940
+ if (err instanceof Error) return void this.options.logger.error(error_createError({
20941
+ err,
20942
+ options: this.options
20943
+ }));
20944
+ this.options.logger.error(err);
20945
+ });
20946
+ }
20947
+ getSampleRate() {
20948
+ if ("function" == typeof window.AudioContext) return this.getAudioContext().sampleRate;
20949
+ return -1;
20950
+ }
20951
+ }
20952
+ const media_AudioRecorder = AudioRecorder;
20953
+ const VIRTUAL_KEYWORDS = [
20954
+ "obs",
20955
+ "virtual",
20956
+ "manycam",
20957
+ "vcam",
20958
+ "splitcam",
20959
+ "droidcam"
20960
+ ];
20961
+ function isVirtualCamera(videoTrack) {
20962
+ const capabilities = videoTrack.getCapabilities();
20963
+ if (!capabilities.frameRate) return true;
20964
+ const isVirtual = VIRTUAL_KEYWORDS.some((keyword)=>videoTrack.label.toLowerCase().includes(keyword));
20965
+ return isVirtual;
20966
+ }
20967
+ const util_isVirtualCamera = isVirtualCamera;
20968
+ const EVENT_ASCII = "|—O—|";
20969
+ class UserMedia extends util_Despot {
20970
+ recorder;
20971
+ rawVisualUserMedia;
20972
+ paused = false;
20973
+ recording = false;
20974
+ audioRecorder;
20975
+ currentVisualStream;
20976
+ onPlayReached = false;
20977
+ onLoadedMetaDataReached = false;
20978
+ playingPromiseReached = false;
20979
+ videoTrackLabel;
20980
+ clearInitializationListeners;
20981
+ initializationId = 0;
20982
+ outputEvent = (e)=>{
20983
+ this.logEvent(e.type, {
20984
+ readyState: this.rawVisualUserMedia?.readyState
20985
+ });
20986
+ this.rawVisualUserMedia?.removeEventListener(e.type, this.outputEvent);
20987
+ };
20988
+ handleMediaError = (event)=>{
20989
+ this.options.logger.warn(`Caught video element error event: ${string_pretty(event)}`);
20990
+ };
20991
+ constructor(recorder, options){
20992
+ super("UserMedia", options);
20993
+ this.recorder = recorder;
20994
+ this.rawVisualUserMedia = recorder.getRawVisualUserMedia();
20995
+ mediaEvents.forEach((eventName)=>{
20996
+ this.rawVisualUserMedia?.addEventListener(eventName, this.outputEvent, false);
20997
+ });
20998
+ }
20999
+ attachMediaStream(stream) {
21000
+ if (this.rawVisualUserMedia) {
21001
+ this.rawVisualUserMedia.srcObject = stream;
21002
+ const previousStream = this.currentVisualStream;
21003
+ this.currentVisualStream = stream;
21004
+ if (previousStream && previousStream !== stream) previousStream.getTracks().forEach((track)=>{
20508
21005
  track.stop();
20509
21006
  });
20510
- this.getUserMediaCallback(localStream, params, requestId);
20511
- }).catch((reason)=>{
20512
- if (!this.unloaded && requestId === this.mediaRequestId) this.userMediaErrorCallback(reason, constraints);
21007
+ } else throw error_createError({
21008
+ message: "Error attaching stream to element.",
21009
+ explanation: "Contact the developer about this",
21010
+ options: this.options
20513
21011
  });
20514
21012
  }
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
- };
21013
+ setVisualStream(localMediaStream) {
21014
+ if (localMediaStream) this.attachMediaStream(localMediaStream);
20522
21015
  else {
20523
- const limitedWidth = this.limitWidth();
20524
- if (limitedWidth?.value) videoConstraints.width = {
20525
- ideal: limitedWidth.value
20526
- };
21016
+ if (this.rawVisualUserMedia) {
21017
+ this.rawVisualUserMedia.srcObject = null;
21018
+ this.rawVisualUserMedia.removeAttribute("src");
21019
+ }
21020
+ this.currentVisualStream = void 0;
20527
21021
  }
20528
- if (this.options.video.height) videoConstraints.height = {
20529
- ideal: this.options.video.height
20530
- };
20531
21022
  }
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
- 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)
21023
+ async restorePlayback() {
21024
+ if (!this.rawVisualUserMedia) throw new Error("No camera preview is available.");
21025
+ await this.rawVisualUserMedia.play();
21026
+ await this.audioRecorder?.resume();
21027
+ }
21028
+ hasLiveStream() {
21029
+ const videoReady = this.currentVisualStream?.getVideoTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
21030
+ const audioReady = !isAudioEnabled(this.options) || this.currentVisualStream?.getAudioTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
21031
+ return Boolean(this.currentVisualStream?.active && videoReady && audioReady);
21032
+ }
21033
+ hasEnded() {
21034
+ if (this.rawVisualUserMedia?.ended) return this.rawVisualUserMedia.ended;
21035
+ return !this.currentVisualStream?.active;
21036
+ }
21037
+ hasInvalidDimensions() {
21038
+ if (!this.rawVisualUserMedia) return false;
21039
+ const { videoWidth, videoHeight } = this.rawVisualUserMedia;
21040
+ return videoWidth > 0 && videoWidth < 3 || videoHeight > 0 && videoHeight < 3;
21041
+ }
21042
+ logEvent(eventType, params) {
21043
+ this.options.logger.debug(`UserMedia: ... ${EVENT_ASCII} event ${eventType}: ${string_pretty(params)}`);
21044
+ }
21045
+ unloadRemainingEventListeners() {
21046
+ this.options.logger.debug("UserMedia: unloadRemainingEventListeners()");
21047
+ this.clearInitializationListeners?.();
21048
+ mediaEvents.forEach((eventName)=>{
21049
+ this.rawVisualUserMedia?.removeEventListener(eventName, this.outputEvent);
21050
+ });
21051
+ this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
21052
+ }
21053
+ audioRecord(audioCallback) {
21054
+ this.removeListener("SENDING_FIRST_FRAME");
21055
+ this.audioRecorder?.record(audioCallback);
21056
+ }
21057
+ init(localMediaStream, videoCallback, audioCallback, endedEarlyCallback, params) {
21058
+ this.clearInitializationListeners?.();
21059
+ this.stop(localMediaStream, {
21060
+ aboutToInitialize: true,
21061
+ preserveRecordingState: Boolean(params?.switchingFacingMode || params?.recovering)
21062
+ });
21063
+ const initializationId = ++this.initializationId;
21064
+ this.onPlayReached = false;
21065
+ this.onLoadedMetaDataReached = false;
21066
+ this.playingPromiseReached = false;
21067
+ this.videoTrackLabel = void 0;
21068
+ if (isAudioEnabled(this.options)) this.audioRecorder ??= new media_AudioRecorder(this, this.options);
21069
+ const unloadAllEventListeners = ()=>{
21070
+ this.options.logger.debug("UserMedia: unloadAllEventListeners()");
21071
+ this.unloadRemainingEventListeners();
21072
+ this.removeListener("SENDING_FIRST_FRAME");
21073
+ this.rawVisualUserMedia?.removeEventListener("play", onPlay);
21074
+ this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
21075
+ };
21076
+ this.clearInitializationListeners = ()=>{
21077
+ this.rawVisualUserMedia?.removeEventListener("play", onPlay);
21078
+ this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
21079
+ this.clearInitializationListeners = void 0;
21080
+ };
21081
+ const play = ()=>{
21082
+ try {
21083
+ this.rawVisualUserMedia?.load();
21084
+ if (this.rawVisualUserMedia?.paused) {
21085
+ 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)}`);
21086
+ this.rawVisualUserMedia.play().then(()=>{
21087
+ if (initializationId !== this.initializationId) return;
21088
+ if (!this.playingPromiseReached) {
21089
+ this.options.logger.debug("UserMedia: play promise successful. Playing now.");
21090
+ this.playingPromiseReached = true;
21091
+ }
21092
+ }).catch((exc)=>{
21093
+ if (initializationId !== this.initializationId) return;
21094
+ if (exc instanceof Error && !params?.recovering) this.options.logger.warn(`Caught pending user media promise exception: ${exc.toString()}`);
21095
+ else {
21096
+ unloadAllEventListeners();
21097
+ endedEarlyCallback(error_createError({
21098
+ message: "Failed to play user media upon play event.",
21099
+ exc,
21100
+ options: this.options
21101
+ }));
21102
+ }
20548
21103
  });
20549
21104
  }
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
20561
- });
20562
- else {
20563
- this.options.logger.debug("Recorder: no error listeners attached but throwing exception further");
20564
- throw exc;
21105
+ } catch (exc) {
21106
+ unloadAllEventListeners();
21107
+ endedEarlyCallback(exc);
20565
21108
  }
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
21109
+ };
21110
+ const fireCallbacks = ()=>{
21111
+ const readyState = this.rawVisualUserMedia?.readyState;
21112
+ this.options.logger.debug(`UserMedia: fireCallbacks(readyState=${readyState}, onPlayReached=${this.onPlayReached}, onLoadedMetaDataReached=${this.onLoadedMetaDataReached})`);
21113
+ if (this.onPlayReached && this.onLoadedMetaDataReached) if (this.audioRecorder) {
21114
+ const initializeAudio = async (audioRecorder)=>{
21115
+ try {
21116
+ await audioRecorder.init(localMediaStream);
21117
+ if (initializationId !== this.initializationId || this.currentVisualStream !== localMediaStream) return;
21118
+ const onAudioReady = ()=>{
21119
+ if (initializationId !== this.initializationId || this.currentVisualStream !== localMediaStream) return;
21120
+ if (this.recording) this.audioRecord(audioCallback);
21121
+ else this.on("SENDING_FIRST_FRAME", ()=>{
21122
+ this.audioRecord(audioCallback);
21123
+ });
21124
+ this.clearInitializationListeners?.();
21125
+ videoCallback();
21126
+ };
21127
+ if (params?.recovering) await audioRecorder.resume();
21128
+ onAudioReady();
21129
+ } catch (exc) {
21130
+ if (initializationId !== this.initializationId) return;
21131
+ unloadAllEventListeners();
21132
+ this.stop(void 0, {
21133
+ preserveRecordingState: Boolean(params?.recovering)
20619
21134
  });
20620
- break;
21135
+ endedEarlyCallback(exc);
20621
21136
  }
21137
+ };
21138
+ initializeAudio(this.audioRecorder);
21139
+ } else {
21140
+ this.clearInitializationListeners?.();
21141
+ videoCallback();
20622
21142
  }
20623
- } catch (exc) {
20624
- this.emit("ERROR", {
20625
- exc
21143
+ };
21144
+ const onPlay = ()=>{
21145
+ try {
21146
+ this.logEvent("play", {
21147
+ readyState: this.rawVisualUserMedia?.readyState,
21148
+ audio: isAudioEnabled(this.options),
21149
+ width: this.rawVisualUserMedia?.width,
21150
+ height: this.rawVisualUserMedia?.height,
21151
+ videoWidth: this.rawVisualUserMedia?.videoWidth,
21152
+ videoHeight: this.rawVisualUserMedia?.videoHeight
21153
+ });
21154
+ this.rawVisualUserMedia?.removeEventListener("play", onPlay);
21155
+ if (this.hasEnded() || this.hasInvalidDimensions()) endedEarlyCallback(error_createError({
21156
+ message: "Already busy",
21157
+ explanation: "Probably another browser window is using your webcam?",
21158
+ options: this.options
21159
+ }));
21160
+ else {
21161
+ this.onPlayReached = true;
21162
+ fireCallbacks();
21163
+ }
21164
+ } catch (exc) {
21165
+ unloadAllEventListeners();
21166
+ endedEarlyCallback(exc);
21167
+ }
21168
+ };
21169
+ const onLoadedMetaData = ()=>{
21170
+ this.logEvent("loadedmetadata", {
21171
+ readyState: this.rawVisualUserMedia?.readyState,
21172
+ paused: this.rawVisualUserMedia?.paused,
21173
+ width: this.rawVisualUserMedia?.width,
21174
+ height: this.rawVisualUserMedia?.height,
21175
+ videoWidth: this.rawVisualUserMedia?.videoWidth,
21176
+ videoHeight: this.rawVisualUserMedia?.videoHeight
20626
21177
  });
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);
21178
+ this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
21179
+ if (!this.hasEnded() && !this.hasInvalidDimensions()) {
21180
+ this.emit("LOADED_META_DATA");
21181
+ this.onLoadedMetaDataReached = true;
21182
+ fireCallbacks();
20649
21183
  }
20650
- } else {
20651
- this.options.logger.debug(`Reconnecting for the command ${command} …`);
20652
- this.initSocket(()=>{
20653
- this.writeCommand(command, args, cb);
21184
+ };
21185
+ try {
21186
+ const videoTrack = media_getFirstVideoTrack(localMediaStream);
21187
+ this.videoTrackLabel = videoTrack?.label;
21188
+ if (videoTrack) if (videoTrack.enabled) if (util_isVirtualCamera(videoTrack)) throw error_createError({
21189
+ message: "Virtual cameras are not allowed.",
21190
+ explanation: `Please use a real camera, not "${videoTrack.label}".`,
21191
+ options: this.options
21192
+ });
21193
+ else {
21194
+ let description = "";
21195
+ if (videoTrack.label.length > 0) description = description.concat(videoTrack.label);
21196
+ description = description.concat(` with enabled=${videoTrack.enabled}, muted=${videoTrack.muted}, readyState=${videoTrack.readyState}`);
21197
+ this.options.logger.debug(`UserMedia: ${videoTrack.kind} detected. ${description}`);
21198
+ }
21199
+ else throw error_createError({
21200
+ message: "Webcam is disabled",
21201
+ explanation: "The video track seems to be disabled. Enable it in your system.",
21202
+ options: this.options
20654
21203
  });
21204
+ else this.options.logger.debug("UserMedia: detected (but no video tracks exist");
21205
+ this.rawVisualUserMedia?.addEventListener("loadedmetadata", onLoadedMetaData);
21206
+ this.rawVisualUserMedia?.addEventListener("play", onPlay);
21207
+ this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
21208
+ this.rawVisualUserMedia?.addEventListener("error", this.handleMediaError);
21209
+ this.setVisualStream(localMediaStream);
21210
+ play();
21211
+ } catch (exc) {
21212
+ unloadAllEventListeners();
21213
+ endedEarlyCallback(exc);
20655
21214
  }
20656
21215
  }
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;
21216
+ getVideoTrackLabel() {
21217
+ return this.videoTrackLabel;
20679
21218
  }
20680
- getAudioSampleRate() {
20681
- return this.recordingAudioSampleRate ?? this.userMedia?.getAudioSampleRate();
21219
+ isReady() {
21220
+ return Boolean(this.rawVisualUserMedia?.srcObject || this.rawVisualUserMedia?.currentSrc);
20682
21221
  }
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
21222
+ stop(visualStream, params) {
21223
+ try {
21224
+ let chosenStream = visualStream;
21225
+ if (!params?.aboutToInitialize) {
21226
+ this.initializationId++;
21227
+ this.clearInitializationListeners?.();
21228
+ chosenStream ??= this.currentVisualStream;
21229
+ const tracks = chosenStream?.getTracks();
21230
+ if (tracks) tracks.forEach((track)=>{
21231
+ track.stop();
20715
21232
  });
21233
+ this.setVisualStream();
21234
+ this.audioRecorder?.stop();
21235
+ this.audioRecorder = void 0;
21236
+ }
21237
+ if (!params?.preserveRecordingState) this.paused = this.recording = false;
21238
+ } catch (exc) {
21239
+ this.emit("ERROR", {
21240
+ exc
20716
21241
  });
20717
- this.reset();
20718
- }, 60);
21242
+ }
20719
21243
  }
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);
21244
+ createCanvas() {
21245
+ const canvas = document.createElement("canvas");
21246
+ const rawWidthDimension = this.getRawWidth(true);
21247
+ if (rawWidthDimension.value) canvas.width = rawWidthDimension.value;
21248
+ const rawHeight = this.getRawHeight(true);
21249
+ if (rawHeight) canvas.height = rawHeight;
21250
+ return canvas;
20727
21251
  }
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();
21252
+ getVideoHeight() {
21253
+ if (!this.rawVisualUserMedia) return;
21254
+ return this.rawVisualUserMedia.videoHeight || this.rawVisualUserMedia.height;
20736
21255
  }
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;
21256
+ getVideoWidth() {
21257
+ if (!this.rawVisualUserMedia) return;
21258
+ return this.rawVisualUserMedia.videoWidth || this.rawVisualUserMedia.width;
20763
21259
  }
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
- }
21260
+ hasVideoWidth() {
21261
+ const videoWidth = this.getVideoWidth();
21262
+ return videoWidth && videoWidth > 0;
20774
21263
  }
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;
21264
+ getRawWidth(responsive) {
21265
+ let rawWidth;
21266
+ const widthDimension = {
21267
+ unit: "px"
21268
+ };
21269
+ rawWidth = responsive ? this.getVideoWidth() : this.options.video.width ? this.options.video.width : this.getVideoWidth();
21270
+ if (responsive) {
21271
+ const widthDimension = this.recorder.limitWidth(rawWidth);
21272
+ rawWidth = widthDimension?.value;
20780
21273
  }
21274
+ widthDimension.value = rawWidth;
21275
+ return widthDimension;
20781
21276
  }
20782
- clearStopTimeout() {
20783
- if (void 0 === this.stopTimeout) return;
20784
- window.clearTimeout(this.stopTimeout);
20785
- this.stopTimeout = void 0;
21277
+ getRawHeight(responsive) {
21278
+ let rawHeight;
21279
+ if (responsive) {
21280
+ rawHeight = this.options.video.height || this.getVideoHeight();
21281
+ const ratio = this.recorder.getRatio();
21282
+ const rawWidth = this.getRawWidth(responsive);
21283
+ if (void 0 !== ratio && rawWidth.value) rawHeight = rawWidth.value * ratio;
21284
+ } else rawHeight = this.options.video.height ? this.options.video.height : this.getVideoHeight();
21285
+ if (responsive) {
21286
+ const heightDimension = this.recorder.limitHeight(rawHeight);
21287
+ rawHeight = heightDimension.value;
21288
+ }
21289
+ return rawHeight;
20786
21290
  }
20787
- validate() {
20788
- return Boolean(this.key) && void 0 === this.canvas;
21291
+ getRawVisuals() {
21292
+ return this.rawVisualUserMedia;
20789
21293
  }
20790
- isReady() {
20791
- return this.userMedia?.isReady();
21294
+ pause() {
21295
+ this.paused = true;
21296
+ this.audioRecorder?.updateRecordingState();
20792
21297
  }
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();
21298
+ isPaused() {
21299
+ return this.paused;
20802
21300
  }
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);
21301
+ resume() {
21302
+ this.paused = false;
21303
+ this.audioRecorder?.updateRecordingState();
20813
21304
  }
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
- });
21305
+ record() {
21306
+ this.recording = true;
21307
+ this.audioRecorder?.updateRecordingState();
20825
21308
  }
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;
21309
+ isRecording() {
21310
+ return this.recording;
20853
21311
  }
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();
21312
+ getAudioSampleRate() {
21313
+ if (this.audioRecorder) return this.audioRecorder.getSampleRate();
21314
+ return -1;
20863
21315
  }
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();
21316
+ getCharacteristics() {
21317
+ return {
21318
+ audioSampleRate: this.getAudioSampleRate(),
21319
+ muted: this.rawVisualUserMedia?.muted,
21320
+ width: this.rawVisualUserMedia?.width,
21321
+ height: this.rawVisualUserMedia?.height,
21322
+ videoWidth: this.rawVisualUserMedia?.videoWidth,
21323
+ videoHeight: this.rawVisualUserMedia?.videoHeight
21324
+ };
20876
21325
  }
20877
- onFlushed(opts) {
20878
- const frameNumber = opts.frameNumber;
20879
- if (1 === frameNumber) this.emit("FIRST_FRAME_SENT");
21326
+ }
21327
+ const visuals_userMedia = UserMedia;
21328
+ class Recorder extends capture {
21329
+ dimensions;
21330
+ constructor(visuals, replay, options){
21331
+ super(visuals, replay, options);
21332
+ this.dimensions = new recorder_dimensions({
21333
+ options,
21334
+ visuals,
21335
+ getRecorderElement: ()=>this.recorderElement,
21336
+ getUserMedia: ()=>this.userMedia
21337
+ });
20880
21338
  }
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");
21339
+ executeCommand(command) {
21340
+ if (this.unloaded) return;
20884
21341
  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)
21342
+ if (command.args) {
21343
+ const debugLine = string_summarize(`Server commanded: ${command.command} with:`, command.args);
21344
+ this.options.logger.debug(debugLine);
21345
+ } else this.options.logger.debug(`Server commanded: ${command.command}`);
21346
+ switch(command.command){
21347
+ case "ready":
21348
+ this.emit("SERVER_READY");
21349
+ if (!this.userMediaTimeout) if (this.options.loadUserMediaOnRecord) this.show();
21350
+ else this.loadUserMedia();
21351
+ break;
21352
+ case "preview":
21353
+ this.preview(command.args);
21354
+ break;
21355
+ case "error":
21356
+ {
21357
+ let explanation = "(No explanation given)";
21358
+ if (command.args?.err?.message) explanation = command.args.err.message;
21359
+ const err = error_createError({
21360
+ message: "Websocket error from Videomail server",
21361
+ explanation,
21362
+ err: deserializeError(command.args?.err),
21363
+ options: this.options
20906
21364
  });
20907
- this.visuals.checkTimer(elapsedTime);
21365
+ this.emit("ERROR", {
21366
+ err
21367
+ });
21368
+ break;
21369
+ }
21370
+ case "confirmFrame":
21371
+ this.updateFrameProgress(command.args);
21372
+ break;
21373
+ case "confirmSample":
21374
+ this.updateSampleProgress(command.args);
21375
+ break;
21376
+ case "beginAudioEncoding":
21377
+ this.emit("BEGIN_AUDIO_ENCODING");
21378
+ break;
21379
+ case "beginVideoEncoding":
21380
+ this.emit("BEGIN_VIDEO_ENCODING");
21381
+ break;
21382
+ default:
21383
+ {
21384
+ const err = error_createError({
21385
+ message: `Unknown server command: ${command.command}`,
21386
+ options: this.options
21387
+ });
21388
+ this.emit("ERROR", {
21389
+ err
21390
+ });
21391
+ break;
20908
21392
  }
20909
- } else throw error_createError({
20910
- message: "Failed to extract webcam data.",
20911
- options: this.options
20912
- });
20913
21393
  }
20914
21394
  } catch (exc) {
20915
21395
  this.emit("ERROR", {
@@ -20917,156 +21397,34 @@
20917
21397
  });
20918
21398
  }
20919
21399
  }
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;
21400
+ unload(params) {
21401
+ if (this.unloaded || !this.built) return;
21402
+ const e = params?.e;
21403
+ let cause;
21404
+ if (e) cause = e.type;
21405
+ const prettyCause = cause ? `, cause: ${cause}` : "";
21406
+ this.options.logger.debug(`Recorder: unload()${prettyCause}`);
21407
+ this.recordingActive = false;
21000
21408
  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
- });
21409
+ this.reset();
21410
+ this.recordingDimensions = void 0;
21411
+ this.clearUserMediaTimeout();
21412
+ this.clearConnectionTimeout();
21413
+ this.clearReconnectTimeout();
21414
+ this.clearRetryTimeout();
21415
+ this.clearStopTimeout();
21416
+ this.stopPings();
21417
+ this.userMediaLoading = false;
21418
+ this.connecting = false;
21419
+ this.reconnecting = false;
21420
+ this.reconnectAttempts = 0;
21421
+ if (this.userMedia) this.userMedia.unloadRemainingEventListeners();
21422
+ this.unloaded = true;
21423
+ this.discardSocket();
21424
+ this.built = this.connecting = this.connected = false;
21042
21425
  }
21043
21426
  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
- });
21427
+ this.dimensions.correctDimensions();
21070
21428
  }
21071
21429
  initEvents() {
21072
21430
  this.options.logger.debug("Recorder: initEvents()");
@@ -21138,100 +21496,20 @@
21138
21496
  }
21139
21497
  this.built = true;
21140
21498
  }
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
21499
  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
- };
21500
+ return this.dimensions.getRecorderWidth(responsive);
21170
21501
  }
21171
21502
  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;
21503
+ return this.dimensions.getRecorderHeight(responsive, useBoundingClientRect);
21201
21504
  }
21202
21505
  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;
21506
+ return this.dimensions.getRatio();
21211
21507
  }
21212
21508
  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);
21509
+ return this.dimensions.calculateWidth(responsive);
21218
21510
  }
21219
21511
  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;
21512
+ return this.dimensions.calculateHeight(responsive);
21235
21513
  }
21236
21514
  getRawVisualUserMedia() {
21237
21515
  return this.recorderElement;
@@ -21248,9 +21526,6 @@
21248
21526
  limitHeight(height) {
21249
21527
  return this.visuals.limitHeight(height);
21250
21528
  }
21251
- isUserMediaLoaded() {
21252
- return this.userMediaLoaded;
21253
- }
21254
21529
  }
21255
21530
  const visuals_recorder = Recorder;
21256
21531
  class Replay extends util_Despot {
@@ -22162,7 +22437,8 @@
22162
22437
  }
22163
22438
  else runValidation = false;
22164
22439
  if (runValidation) {
22165
- const targetName = event?.target?.name;
22440
+ const target = event?.target;
22441
+ const targetName = target instanceof Element ? target.getAttribute("name") : void 0;
22166
22442
  if (targetName) this.emit("VALIDATING", {
22167
22443
  targetName
22168
22444
  });
@@ -22197,27 +22473,10 @@
22197
22473
  }
22198
22474
  }
22199
22475
  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
- }
22476
+ if (!email_isRecipientSelectionValid(this.form.getRecipients())) {
22477
+ valid = false;
22478
+ whyInvalid = "At least one recipient is required";
22219
22479
  }
22220
- if (!valid) whyInvalid = "At least one recipient is required";
22221
22480
  }
22222
22481
  } else {
22223
22482
  valid = visualsValid;