videomail-client 16.1.1 → 16.2.0

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