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/esm/index.js CHANGED
@@ -3620,7 +3620,7 @@ const constants = {
3620
3620
  }
3621
3621
  };
3622
3622
  var package_namespaceObject = {
3623
- rE: "16.1.1"
3623
+ rE: "16.2.0"
3624
3624
  };
3625
3625
  function isAudioEnabled(options) {
3626
3626
  return Boolean(options.audio.enabled);
@@ -4142,6 +4142,14 @@ class Despot {
4142
4142
  }
4143
4143
  Despot_define_property(Despot, "emitters", new WeakMap());
4144
4144
  const util_Despot = Despot;
4145
+ const EMAIL_SEPARATOR_REGEX = /[ ,]+/gu;
4146
+ function trimEmail(email) {
4147
+ return email.replace(EMAIL_SEPARATOR_REGEX, "");
4148
+ }
4149
+ function trimEmails(emails) {
4150
+ const trimmedEmails = emails.split(EMAIL_SEPARATOR_REGEX).map((item)=>item.trim()).filter(Boolean);
4151
+ return trimmedEmails;
4152
+ }
4145
4153
  function getValidity(element) {
4146
4154
  const validity = "validity" in element ? element.validity : void 0;
4147
4155
  return validity;
@@ -4161,14 +4169,6 @@ function showElement(element) {
4161
4169
  element.style.removeProperty("display");
4162
4170
  }
4163
4171
  const html_showElement = showElement;
4164
- const EMAIL_SEPARATOR_REGEX = /[ ,]+/gu;
4165
- function trimEmail(email) {
4166
- return email.replace(EMAIL_SEPARATOR_REGEX, "");
4167
- }
4168
- function trimEmails(emails) {
4169
- const trimmedEmails = emails.split(EMAIL_SEPARATOR_REGEX).map((item)=>item.trim()).filter(Boolean);
4170
- return trimmedEmails;
4171
- }
4172
4172
  function form_define_property(obj, key, value) {
4173
4173
  if (key in obj) Object.defineProperty(obj, key, {
4174
4174
  value: value,
@@ -4876,6 +4876,19 @@ main_options.domAPI = styleDomAPI_default();
4876
4876
  main_options.insertStyleElement = insertStyleElement_default();
4877
4877
  injectStylesIntoStyleTag_default()(main_0.A, main_options);
4878
4878
  main_0.A && main_0.A.locals && main_0.A.locals;
4879
+ function isRecipientSelectionValid(recipients) {
4880
+ const hasTo = Boolean(recipients.to?.length);
4881
+ const hasCc = Boolean(recipients.cc?.length);
4882
+ const hasBcc = Boolean(recipients.bcc?.length);
4883
+ const hasConfiguredTo = "to" in recipients;
4884
+ const hasConfiguredCc = "cc" in recipients;
4885
+ const hasConfiguredBcc = "bcc" in recipients;
4886
+ let isValid = true;
4887
+ if (hasConfiguredTo) isValid = hasTo || hasConfiguredCc && hasCc || hasConfiguredBcc && hasBcc;
4888
+ else if (hasConfiguredCc) isValid = hasCc || !hasConfiguredBcc || hasBcc;
4889
+ return isValid;
4890
+ }
4891
+ const email_isRecipientSelectionValid = isRecipientSelectionValid;
4879
4892
  function limitHeight(height, options, calledFrom) {
4880
4893
  const dimension = {
4881
4894
  unit: "px"
@@ -5487,7 +5500,7 @@ class FacingMode extends util_Despot {
5487
5500
  this.visuals = visuals;
5488
5501
  }
5489
5502
  }
5490
- const facingMode = FacingMode;
5503
+ const recorder_facingMode = FacingMode;
5491
5504
  function pausedNote_define_property(obj, key, value) {
5492
5505
  if (key in obj) Object.defineProperty(obj, key, {
5493
5506
  value: value,
@@ -5831,7 +5844,7 @@ class RecorderInsides extends util_Despot {
5831
5844
  this.recordTimer = new recordTimer(visuals, this.recordNote, options);
5832
5845
  const browser = util_getBrowser(options);
5833
5846
  if (options.video.countdown) this.countdown = new countdown(visuals, options);
5834
- if (options.video.facingModeButton && browser.isMobile()) this.facingMode = new facingMode(visuals, options);
5847
+ if (options.video.facingModeButton && browser.isMobile()) this.facingMode = new recorder_facingMode(visuals, options);
5835
5848
  if (options.enablePause) this.pausedNote = new pausedNote(visuals, options);
5836
5849
  }
5837
5850
  }
@@ -6078,46 +6091,18 @@ class Notifier extends util_Despot {
6078
6091
  }
6079
6092
  }
6080
6093
  const notifier = Notifier;
6081
- function boundedWriteQueue_define_property(obj, key, value) {
6082
- if (key in obj) Object.defineProperty(obj, key, {
6083
- value: value,
6084
- enumerable: true,
6085
- configurable: true,
6086
- writable: true
6094
+ function summarize(title, contents) {
6095
+ const lines = Object.entries(contents).sort(([keyA], [keyB])=>keyA.localeCompare(keyB)).map(([key, value])=>{
6096
+ const formattedValue = void 0 === value ? "undefined" : "object" == typeof value ? string_pretty(value) : value;
6097
+ return ` • ${key}: ${formattedValue}`;
6087
6098
  });
6088
- else obj[key] = value;
6089
- return obj;
6090
- }
6091
- const DEFAULT_MAX_QUEUED_BYTES = 1048576;
6092
- class BoundedWriteQueue {
6093
- enqueue(buffer, params) {
6094
- if (this.queuedBytes + buffer.length > this.maxBytes) return false;
6095
- this.writes.push({
6096
- buffer,
6097
- params
6098
- });
6099
- this.queuedBytes += buffer.length;
6100
- return true;
6101
- }
6102
- dequeue() {
6103
- const write = this.writes.shift();
6104
- if (write) this.queuedBytes -= write.buffer.length;
6105
- return write;
6106
- }
6107
- clear() {
6108
- this.writes.length = 0;
6109
- this.queuedBytes = 0;
6110
- }
6111
- constructor(maxBytes = DEFAULT_MAX_QUEUED_BYTES){
6112
- boundedWriteQueue_define_property(this, "maxBytes", void 0);
6113
- boundedWriteQueue_define_property(this, "writes", void 0);
6114
- boundedWriteQueue_define_property(this, "queuedBytes", void 0);
6115
- this.maxBytes = maxBytes;
6116
- this.writes = [];
6117
- this.queuedBytes = 0;
6118
- }
6099
+ const line = [
6100
+ `🔎 ${title}`,
6101
+ ...lines
6102
+ ].join("\n");
6103
+ return line;
6119
6104
  }
6120
- const boundedWriteQueue = BoundedWriteQueue;
6105
+ const string_summarize = summarize;
6121
6106
  function getEdgeCodes(value, amount, fromEnd) {
6122
6107
  const chars = fromEnd ? value.slice(-amount) : value.slice(0, amount);
6123
6108
  const codes = Array.from(chars, (char)=>String(char.codePointAt(0))).join(".");
@@ -6197,117 +6182,11 @@ function getWebSocketDiagnostic(url2Connect) {
6197
6182
  };
6198
6183
  }
6199
6184
  const error_getWebSocketDiagnostic = getWebSocketDiagnostic;
6200
- function figureMinHeight(height, options) {
6201
- let minHeight;
6202
- if (options.video.height) {
6203
- minHeight = height ? Math.min(options.video.height, height) : options.video.height;
6204
- if (minHeight < 1) throw error_createError({
6205
- message: `Got a min height less than 1 (${minHeight})!`,
6206
- options
6207
- });
6208
- } else minHeight = height;
6209
- return minHeight;
6210
- }
6211
- const dimensions_figureMinHeight = figureMinHeight;
6212
- function getRatio(options, videoHeight, videoWidth) {
6213
- let ratio = 1;
6214
- const hasVideoDimensions = videoHeight && videoWidth;
6215
- const desiredHeight = options.video.height;
6216
- const desiredWidth = options.video.width;
6217
- const hasDesiredDimensions = desiredHeight && desiredWidth;
6218
- if (hasDesiredDimensions) ratio = hasVideoDimensions ? videoHeight < desiredHeight || videoWidth < desiredWidth ? videoHeight / videoWidth : desiredHeight / desiredWidth : desiredHeight / desiredWidth;
6219
- else if (hasVideoDimensions) ratio = videoHeight / videoWidth;
6220
- return ratio;
6221
- }
6222
- const dimensions_getRatio = getRatio;
6223
- function calculateHeight(responsive, videoWidth, options, ratio, element) {
6224
- const dimension = {
6225
- unit: "px"
6226
- };
6227
- let width = videoWidth;
6228
- if (responsive && element) {
6229
- const limitedDimension = dimensions_limitWidth(element, options, width);
6230
- width = limitedDimension.value;
6231
- }
6232
- const chosenRatio = ratio ?? dimensions_getRatio(options, void 0, videoWidth);
6233
- const height = width ? Math.round(width * chosenRatio) : void 0;
6234
- if (height && Number.isInteger(height) && height < 1) throw error_createError({
6235
- message: "Just calculated a height less than 1 which is wrong.",
6236
- options
6237
- });
6238
- const minHeight = dimensions_figureMinHeight(height, options);
6239
- dimension.value = minHeight;
6240
- return dimension;
6241
- }
6242
- const dimensions_calculateHeight = calculateHeight;
6243
- function calculateWidth(responsive, options, videoHeight, ratio) {
6244
- const dimension = {
6245
- unit: "px"
6246
- };
6247
- let height = dimensions_figureMinHeight(videoHeight, options);
6248
- if (responsive) {
6249
- const limitedDimension = dimensions_limitHeight(height, options, "calculateWidth");
6250
- height = limitedDimension.value;
6251
- }
6252
- if (!height || height < 1) throw error_createError({
6253
- message: `Height ${height} cannot be smaller than 1 when calculating width.`,
6254
- options
6255
- });
6256
- const chosenRatio = ratio ?? dimensions_getRatio(options, videoHeight);
6257
- const calculatedWidth = Math.round(height / chosenRatio);
6258
- if (calculatedWidth < 1) throw error_createError({
6259
- message: "Calculated width cannot be smaller than 1!",
6260
- options
6261
- });
6262
- dimension.value = calculatedWidth;
6263
- return dimension;
6264
- }
6265
- const dimensions_calculateWidth = calculateWidth;
6266
6185
  function isAutomatedUserAgent(userAgent = navigator.userAgent) {
6267
6186
  return /bot|crawler|spider|headlesschrome|phantomjs|puppeteer|playwright/iu.test(userAgent);
6268
6187
  }
6269
6188
  const util_isAutomatedUserAgent = isAutomatedUserAgent;
6270
- function summarize(title, contents) {
6271
- const lines = Object.entries(contents).sort(([keyA], [keyB])=>keyA.localeCompare(keyB)).map(([key, value])=>{
6272
- const formattedValue = void 0 === value ? "undefined" : "object" == typeof value ? string_pretty(value) : value;
6273
- return ` • ${key}: ${formattedValue}`;
6274
- });
6275
- const line = [
6276
- `🔎 ${title}`,
6277
- ...lines
6278
- ].join("\n");
6279
- return line;
6280
- }
6281
- const string_summarize = summarize;
6282
- function getFirstVideoTrack(localMediaStream) {
6283
- const videoTracks = localMediaStream.getVideoTracks();
6284
- let videoTrack;
6285
- if (videoTracks[0]) videoTrack = videoTracks[0];
6286
- return videoTrack;
6287
- }
6288
- const media_getFirstVideoTrack = getFirstVideoTrack;
6289
- const MEDIA_EVENTS = [
6290
- "loadstart",
6291
- "suspend",
6292
- "progress",
6293
- "abort",
6294
- "emptied",
6295
- "stalled",
6296
- "pause",
6297
- "loadeddata",
6298
- "waiting",
6299
- "playing",
6300
- "canplay",
6301
- "canplaythrough",
6302
- "seeking",
6303
- "seeked",
6304
- "ended",
6305
- "ratechange",
6306
- "durationchange",
6307
- "volumechange"
6308
- ];
6309
- const mediaEvents = MEDIA_EVENTS;
6310
- function AudioRecorder_define_property(obj, key, value) {
6189
+ function boundedWriteQueue_define_property(obj, key, value) {
6311
6190
  if (key in obj) Object.defineProperty(obj, key, {
6312
6191
  value: value,
6313
6192
  enumerable: true,
@@ -6317,120 +6196,223 @@ function AudioRecorder_define_property(obj, key, value) {
6317
6196
  else obj[key] = value;
6318
6197
  return obj;
6319
6198
  }
6320
- const CHANNELS = 1;
6321
- function getAudioContextClass() {
6322
- return window.AudioContext;
6323
- }
6324
- class AudioRecorder {
6325
- hasAudioContext() {
6326
- return Boolean(getAudioContextClass()) && Boolean(this.getAudioContext());
6199
+ const DEFAULT_MAX_QUEUED_BYTES = 1048576;
6200
+ class BoundedWriteQueue {
6201
+ enqueue(buffer, params) {
6202
+ if (this.queuedBytes + buffer.length > this.maxBytes) return false;
6203
+ this.writes.push({
6204
+ buffer,
6205
+ params
6206
+ });
6207
+ this.queuedBytes += buffer.length;
6208
+ return true;
6327
6209
  }
6328
- getAudioContext() {
6329
- if (!this.vcAudioContext) {
6330
- const AudioContext = getAudioContextClass();
6331
- this.vcAudioContext = new AudioContext();
6332
- }
6333
- return this.vcAudioContext;
6210
+ dequeue() {
6211
+ const write = this.writes.shift();
6212
+ if (write) this.queuedBytes -= write.buffer.length;
6213
+ return write;
6334
6214
  }
6335
- onAudioProcess(e, cb) {
6336
- if (!this.userMedia.isRecording() || this.userMedia.isPaused()) return;
6337
- const float32Array = e.inputBuffer.getChannelData(0);
6338
- cb(new audio_sample(float32Array));
6215
+ clear() {
6216
+ this.writes.length = 0;
6217
+ this.queuedBytes = 0;
6339
6218
  }
6340
- disconnect() {
6341
- if (this.scriptProcessor) {
6342
- this.scriptProcessor.onaudioprocess = null;
6343
- this.scriptProcessor.disconnect();
6344
- }
6345
- this.audioInput?.disconnect();
6346
- this.volume?.disconnect();
6219
+ constructor(maxBytes = DEFAULT_MAX_QUEUED_BYTES){
6220
+ boundedWriteQueue_define_property(this, "maxBytes", void 0);
6221
+ boundedWriteQueue_define_property(this, "writes", void 0);
6222
+ boundedWriteQueue_define_property(this, "queuedBytes", void 0);
6223
+ this.maxBytes = maxBytes;
6224
+ this.writes = [];
6225
+ this.queuedBytes = 0;
6347
6226
  }
6348
- init(localMediaStream) {
6349
- this.options.logger.debug("AudioRecorder: init()");
6350
- this.disconnect();
6351
- const volume = this.getAudioContext().createGain();
6352
- this.volume = volume;
6353
- try {
6354
- this.audioInput = this.getAudioContext().createMediaStreamSource(localMediaStream);
6355
- } catch (exc) {
6356
- throw error_createError({
6357
- message: "Webcam has no audio",
6358
- exc,
6227
+ }
6228
+ const boundedWriteQueue = BoundedWriteQueue;
6229
+ function streamWriter_define_property(obj, key, value) {
6230
+ if (key in obj) Object.defineProperty(obj, key, {
6231
+ value: value,
6232
+ enumerable: true,
6233
+ configurable: true,
6234
+ writable: true
6235
+ });
6236
+ else obj[key] = value;
6237
+ return obj;
6238
+ }
6239
+ const PIPE_SYMBOL = "°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸ ";
6240
+ class RecorderStreamWriter extends util_Despot {
6241
+ hasExceededWebSocketSendBuffer(buffer, opts) {
6242
+ return buffer.length > 0 && !this.pendingWrites.enqueue(buffer, opts);
6243
+ }
6244
+ writeStream(buffer, opts) {
6245
+ if (this.stream) if (this.stream.destroyed) {
6246
+ this.stopPings();
6247
+ const err = error_createError({
6248
+ message: "Already disconnected",
6249
+ explanation: "Sorry, connection to the server has been destroyed. Please reload.",
6359
6250
  options: this.options
6360
6251
  });
6361
- }
6362
- let { bufferSize } = this.options.audio;
6363
- if ("auto" === bufferSize) bufferSize = util_getBrowser(this.options).isFirefox() ? 512 : 2048;
6364
- if (void 0 === bufferSize || !is_power_of_two(bufferSize)) throw error_createError({
6365
- message: `Audio buffer size must be a power of two. The current buffer size is ${bufferSize}.`,
6366
- options: this.options
6252
+ this.handleWriteFailure(err);
6253
+ } else if (this.streamBackpressured) {
6254
+ if (this.hasExceededWebSocketSendBuffer(buffer, opts)) this.failMediaTransmission(error_createError({
6255
+ message: "WebSocket send buffer limit exceeded",
6256
+ 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.",
6257
+ options: this.options
6258
+ }));
6259
+ } else this.writeToStream(this.stream, buffer, opts);
6260
+ }
6261
+ writeToStream(stream, buffer, opts) {
6262
+ const onFlushedCallback = opts?.onFlushedCallback;
6263
+ try {
6264
+ const canContinue = stream.write(buffer, ()=>{
6265
+ if (!onFlushedCallback) return;
6266
+ try {
6267
+ onFlushedCallback(opts);
6268
+ } catch (exc) {
6269
+ const err = error_createError({
6270
+ message: "Failed to write stream buffer",
6271
+ explanation: `stream.write() failed because of ${string_pretty(exc)}`,
6272
+ options: this.options,
6273
+ exc
6274
+ });
6275
+ this.emit("ERROR", {
6276
+ err
6277
+ });
6278
+ }
6279
+ });
6280
+ if (!canContinue) {
6281
+ this.streamBackpressured = true;
6282
+ this.startBackpressureTimeout();
6283
+ this.pauseForBackpressure();
6284
+ return false;
6285
+ }
6286
+ return true;
6287
+ } catch (exc) {
6288
+ const err = error_createError({
6289
+ message: "Failed writing to server",
6290
+ explanation: `stream.write() failed because of ${string_pretty(exc)}`,
6291
+ options: this.options,
6292
+ exc
6293
+ });
6294
+ this.handleWriteFailure(err);
6295
+ return false;
6296
+ }
6297
+ }
6298
+ flushPendingWrites(stream) {
6299
+ if (this.isStaleStream(stream)) return;
6300
+ this.streamBackpressured = false;
6301
+ this.clearBackpressureTimeout();
6302
+ let pendingWrite = this.pendingWrites.dequeue();
6303
+ while(pendingWrite){
6304
+ if (!this.writeToStream(stream, pendingWrite.buffer, pendingWrite.params)) return;
6305
+ pendingWrite = this.pendingWrites.dequeue();
6306
+ }
6307
+ this.resumeRecordingAfterDrain();
6308
+ }
6309
+ isStaleStream(stream) {
6310
+ return this.stream !== stream || stream.destroyed;
6311
+ }
6312
+ canPauseForBackpressure() {
6313
+ return this.recordingActive && !this.isUserMediaPaused() && this.isCaptureLoopRunning();
6314
+ }
6315
+ pauseForBackpressure() {
6316
+ if (!this.canPauseForBackpressure()) return;
6317
+ this.resumeAfterDrain = true;
6318
+ this.stopCaptureLoop();
6319
+ }
6320
+ resumeRecordingAfterDrain() {
6321
+ if (!this.resumeAfterDrain) return;
6322
+ this.resumeAfterDrain = false;
6323
+ if (this.canResumeAfterDrain()) this.startCaptureLoop();
6324
+ }
6325
+ canResumeAfterDrain() {
6326
+ return this.recordingActive && !this.isUserMediaPaused();
6327
+ }
6328
+ handleWriteFailure(err) {
6329
+ if (this.mediaTransmissionStarted) return void this.failMediaTransmission(err);
6330
+ this.emit("ERROR", {
6331
+ err
6367
6332
  });
6368
- if (!Number.isFinite(this.options.audio.volume) || this.options.audio.volume < 0 || this.options.audio.volume > 1) throw error_createError({
6369
- message: `Audio volume must be between zero and one. The current volume is ${this.options.audio.volume}.`,
6370
- options: this.options
6333
+ }
6334
+ failMediaTransmission(err) {
6335
+ if (this.recordingTransportFailed) return;
6336
+ this.recordingTransportFailed = true;
6337
+ this.recordingActive = false;
6338
+ this.mediaTransmissionStarted = false;
6339
+ this.streamBackpressured = false;
6340
+ this.resumeAfterDrain = false;
6341
+ this.pendingWrites.clear();
6342
+ this.connected = false;
6343
+ this.connecting = false;
6344
+ this.reconnecting = false;
6345
+ this.clearReconnectTimeout();
6346
+ this.clearBackpressureTimeout();
6347
+ this.discardSocket();
6348
+ this.reset();
6349
+ this.framesCount = this.samplesCount = 0;
6350
+ this.confirmedFrameNumber = this.confirmedSampleNumber = 0;
6351
+ delete this.recordingStats;
6352
+ this.recordingDimensions = void 0;
6353
+ this.emit("ERROR", {
6354
+ err
6371
6355
  });
6372
- volume.gain.value = this.options.audio.volume;
6373
- this.scriptProcessor = this.getAudioContext().createScriptProcessor(bufferSize, CHANNELS, CHANNELS);
6374
- this.audioInput.connect(volume);
6375
- volume.connect(this.scriptProcessor);
6376
- this.scriptProcessor.connect(this.getAudioContext().destination);
6377
6356
  }
6378
- record(cb) {
6379
- this.options.logger.debug("AudioRecorder: record()");
6380
- if (this.scriptProcessor) this.scriptProcessor.onaudioprocess = (e)=>{
6381
- this.onAudioProcess(e, cb);
6382
- };
6357
+ startBackpressureTimeout() {
6358
+ if (!this.shouldStartBackpressureTimeout()) return;
6359
+ this.backpressureTimeout = window.setTimeout(()=>{
6360
+ this.backpressureTimeout = void 0;
6361
+ if (this.shouldIgnoreBackpressureTimeout()) return;
6362
+ const err = error_createError({
6363
+ message: "WebSocket write timed out",
6364
+ explanation: "The server stopped accepting recording data. The recording has been stopped; please check your connection and start a new one.",
6365
+ options: this.options
6366
+ });
6367
+ this.failMediaTransmission(err);
6368
+ }, this.options.timeouts.connection);
6383
6369
  }
6384
- async resume() {
6385
- const context = this.getAudioContext();
6386
- if ("running" !== context.state) await context.resume();
6370
+ shouldStartBackpressureTimeout() {
6371
+ return void 0 === this.backpressureTimeout && this.mediaTransmissionStarted;
6387
6372
  }
6388
- stop() {
6389
- this.options.logger.debug("AudioRecorder: stop()");
6390
- this.disconnect();
6391
- if (this.hasAudioContext()) this.getAudioContext().close().then(()=>{
6392
- this.options.logger.debug("AudioRecorder: audio context is closed");
6393
- this.vcAudioContext = void 0;
6394
- }).catch((err)=>{
6395
- if (err instanceof Error) return void this.options.logger.error(error_createError({
6396
- err,
6397
- options: this.options
6398
- }));
6399
- this.options.logger.error(err);
6373
+ shouldIgnoreBackpressureTimeout() {
6374
+ return !this.streamBackpressured || this.recordingTransportFailed;
6375
+ }
6376
+ failInterruptedRecording() {
6377
+ const unconfirmedFrames = Math.max(0, this.framesCount - this.confirmedFrameNumber);
6378
+ const unconfirmedSamples = Math.max(0, this.samplesCount - this.confirmedSampleNumber);
6379
+ const err = error_createError({
6380
+ message: "Recording interrupted",
6381
+ explanation: "The server connection closed before this recording completed. It cannot be safely resumed, so it has been stopped. Please start a new recording.",
6382
+ options: this.options,
6383
+ cause: {
6384
+ unconfirmedFrames,
6385
+ unconfirmedSamples
6386
+ }
6400
6387
  });
6388
+ this.failMediaTransmission(err);
6401
6389
  }
6402
- getSampleRate() {
6403
- if (this.hasAudioContext()) return this.getAudioContext().sampleRate;
6404
- return -1;
6390
+ handleStreamDrain(stream) {
6391
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *drain* event emitted`);
6392
+ this.flushPendingWrites(stream);
6405
6393
  }
6406
- constructor(userMedia, options){
6407
- AudioRecorder_define_property(this, "scriptProcessor", void 0);
6408
- AudioRecorder_define_property(this, "audioInput", void 0);
6409
- AudioRecorder_define_property(this, "volume", void 0);
6410
- AudioRecorder_define_property(this, "vcAudioContext", void 0);
6411
- AudioRecorder_define_property(this, "userMedia", void 0);
6412
- AudioRecorder_define_property(this, "options", void 0);
6413
- this.options = options;
6414
- this.userMedia = userMedia;
6394
+ clearBackpressureTimeout() {
6395
+ if (void 0 === this.backpressureTimeout) return;
6396
+ window.clearTimeout(this.backpressureTimeout);
6397
+ this.backpressureTimeout = void 0;
6398
+ }
6399
+ discardSocket() {
6400
+ this.options.logger.debug("Recorder: discarding socket stream ...");
6401
+ this.clearBackpressureTimeout();
6402
+ this.pendingWrites.clear();
6403
+ this.streamBackpressured = false;
6404
+ this.resumeAfterDrain = false;
6405
+ const stream = this.stream;
6406
+ this.stream = void 0;
6407
+ if (stream && !stream.destroyed) stream.destroy();
6408
+ }
6409
+ constructor(options){
6410
+ super("Recorder", options), streamWriter_define_property(this, "pendingWrites", new boundedWriteQueue()), streamWriter_define_property(this, "samplesCount", 0), streamWriter_define_property(this, "framesCount", 0), streamWriter_define_property(this, "recordingStats", void 0), streamWriter_define_property(this, "confirmedFrameNumber", 0), streamWriter_define_property(this, "confirmedSampleNumber", 0), streamWriter_define_property(this, "backpressureTimeout", void 0), streamWriter_define_property(this, "recordingDimensions", void 0), streamWriter_define_property(this, "stream", void 0), streamWriter_define_property(this, "streamBackpressured", false), streamWriter_define_property(this, "resumeAfterDrain", false), streamWriter_define_property(this, "mediaTransmissionStarted", false), streamWriter_define_property(this, "recordingActive", false), streamWriter_define_property(this, "recordingTransportFailed", false), streamWriter_define_property(this, "connecting", false), streamWriter_define_property(this, "connected", false), streamWriter_define_property(this, "reconnecting", false);
6415
6411
  }
6416
6412
  }
6417
- const media_AudioRecorder = AudioRecorder;
6418
- const VIRTUAL_KEYWORDS = [
6419
- "obs",
6420
- "virtual",
6421
- "manycam",
6422
- "vcam",
6423
- "splitcam",
6424
- "droidcam"
6425
- ];
6426
- function isVirtualCamera(videoTrack) {
6427
- const capabilities = videoTrack.getCapabilities();
6428
- if (!capabilities.frameRate) return true;
6429
- const isVirtual = VIRTUAL_KEYWORDS.some((keyword)=>videoTrack.label.toLowerCase().includes(keyword));
6430
- return isVirtual;
6431
- }
6432
- const util_isVirtualCamera = isVirtualCamera;
6433
- function userMedia_define_property(obj, key, value) {
6413
+ const streamWriter = RecorderStreamWriter;
6414
+ var Buffer = __webpack_require__("dY").hp;
6415
+ function connection_define_property(obj, key, value) {
6434
6416
  if (key in obj) Object.defineProperty(obj, key, {
6435
6417
  value: value,
6436
6418
  enumerable: true,
@@ -6440,1487 +6422,1859 @@ function userMedia_define_property(obj, key, value) {
6440
6422
  else obj[key] = value;
6441
6423
  return obj;
6442
6424
  }
6443
- const EVENT_ASCII = "|—O—|";
6444
- class UserMedia extends util_Despot {
6445
- attachMediaStream(stream) {
6446
- if (this.rawVisualUserMedia) {
6447
- this.rawVisualUserMedia.srcObject = stream;
6448
- const previousStream = this.currentVisualStream;
6449
- this.currentVisualStream = stream;
6450
- if (previousStream && previousStream !== stream) previousStream.getTracks().forEach((track)=>{
6451
- track.stop();
6452
- });
6453
- } else throw error_createError({
6454
- message: "Error attaching stream to element.",
6455
- explanation: "Contact the developer about this",
6456
- options: this.options
6457
- });
6458
- }
6459
- setVisualStream(localMediaStream) {
6460
- if (localMediaStream) this.attachMediaStream(localMediaStream);
6461
- else {
6462
- if (this.rawVisualUserMedia) {
6463
- this.rawVisualUserMedia.srcObject = null;
6464
- this.rawVisualUserMedia.removeAttribute("src");
6465
- }
6466
- this.currentVisualStream = void 0;
6467
- }
6468
- }
6469
- async restorePlayback() {
6470
- if (!this.rawVisualUserMedia) throw new Error("No camera preview is available.");
6471
- await this.rawVisualUserMedia.play();
6472
- await this.audioRecorder?.resume();
6473
- }
6474
- hasLiveStream() {
6475
- const videoReady = this.currentVisualStream?.getVideoTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
6476
- const audioReady = !isAudioEnabled(this.options) || this.currentVisualStream?.getAudioTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
6477
- return Boolean(this.currentVisualStream?.active && videoReady && audioReady);
6478
- }
6479
- hasEnded() {
6480
- if (this.rawVisualUserMedia?.ended) return this.rawVisualUserMedia.ended;
6481
- return !this.currentVisualStream?.active;
6425
+ class RecorderConnection extends streamWriter {
6426
+ canReconnectAfterStreamClose() {
6427
+ return Boolean(this.connected && this.userMediaLoaded);
6482
6428
  }
6483
- hasInvalidDimensions() {
6484
- if (!this.rawVisualUserMedia) return false;
6485
- const { videoWidth, videoHeight } = this.rawVisualUserMedia;
6486
- return videoWidth > 0 && videoWidth < 3 || videoHeight > 0 && videoHeight < 3;
6429
+ shouldReportStreamClose() {
6430
+ return !this.connecting && !this.blocking;
6487
6431
  }
6488
- logEvent(eventType, params) {
6489
- this.options.logger.debug(`UserMedia: ... ${EVENT_ASCII} event ${eventType}: ${string_pretty(params)}`);
6490
- }
6491
- unloadRemainingEventListeners() {
6492
- this.options.logger.debug("UserMedia: unloadRemainingEventListeners()");
6493
- this.clearInitializationListeners?.();
6494
- mediaEvents.forEach((eventName)=>{
6495
- this.rawVisualUserMedia?.removeEventListener(eventName, this.outputEvent);
6432
+ handleStreamClose(url2Connect) {
6433
+ const debugLine = string_summarize(`${PIPE_SYMBOL}Stream has closed:`, {
6434
+ connecting: this.connecting,
6435
+ connected: this.connected,
6436
+ userMediaLoaded: this.userMediaLoaded,
6437
+ blocking: this.blocking
6496
6438
  });
6497
- this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
6439
+ this.options.logger.debug(debugLine);
6440
+ const canReconnect = this.canReconnectAfterStreamClose();
6441
+ this.connected = false;
6442
+ this.stopPings();
6443
+ if (this.shouldIgnoreStreamClose()) return;
6444
+ if (this.mediaTransmissionStarted) this.failInterruptedRecording();
6445
+ else if (canReconnect) {
6446
+ this.pendingWrites.clear();
6447
+ this.streamBackpressured = false;
6448
+ this.resumeAfterDrain = false;
6449
+ this.reconnecting = true;
6450
+ this.initSocket();
6451
+ } else if (this.shouldReportStreamClose()) window.setTimeout(()=>{
6452
+ this.handleConnectionFailure({
6453
+ url2Connect,
6454
+ cause: "closed"
6455
+ });
6456
+ }, 0);
6498
6457
  }
6499
- audioRecord(audioCallback) {
6500
- this.removeListener("SENDING_FIRST_FRAME");
6501
- this.audioRecorder?.record(audioCallback);
6458
+ shouldIgnoreStreamClose() {
6459
+ return this.recordingTransportFailed || Boolean(this.unloaded) || this.validate();
6502
6460
  }
6503
- init(localMediaStream, videoCallback, audioCallback, endedEarlyCallback, params) {
6504
- this.clearInitializationListeners?.();
6505
- this.stop(localMediaStream, {
6506
- aboutToInitialize: true,
6507
- preserveRecordingState: Boolean(params?.switchingFacingMode || params?.recovering)
6461
+ handleStreamError(url2Connect, err) {
6462
+ if ("u" > typeof Event && err instanceof Event) this.lastSocketError = error_getEventDetails(err);
6463
+ else this.lastSocketError = serializeError(err);
6464
+ const debugLine = string_summarize(`${PIPE_SYMBOL}Stream *error* event emitted:`, {
6465
+ error: serializeError(err)
6508
6466
  });
6509
- const initializationId = ++this.initializationId;
6510
- this.onPlayReached = false;
6511
- this.onLoadedMetaDataReached = false;
6512
- this.playingPromiseReached = false;
6513
- this.videoTrackLabel = void 0;
6514
- if (isAudioEnabled(this.options)) this.audioRecorder ??= new media_AudioRecorder(this, this.options);
6515
- const unloadAllEventListeners = ()=>{
6516
- this.options.logger.debug("UserMedia: unloadAllEventListeners()");
6517
- this.unloadRemainingEventListeners();
6518
- this.removeListener("SENDING_FIRST_FRAME");
6519
- this.rawVisualUserMedia?.removeEventListener("play", onPlay);
6520
- this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
6521
- };
6522
- this.clearInitializationListeners = ()=>{
6523
- this.rawVisualUserMedia?.removeEventListener("play", onPlay);
6524
- this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
6525
- this.clearInitializationListeners = void 0;
6467
+ this.options.logger.debug(debugLine);
6468
+ if (this.validate()) {
6469
+ this.connected = false;
6470
+ this.stopPings();
6471
+ this.discardSocket();
6472
+ return;
6473
+ }
6474
+ if (!this.connected) return void this.handleConnectionFailure({
6475
+ url2Connect,
6476
+ cause: "error"
6477
+ });
6478
+ const streamError = err instanceof Error ? err : new Error("WebSocket stream emitted an error event", {
6479
+ cause: this.lastSocketError ?? err
6480
+ });
6481
+ if (this.mediaTransmissionStarted) return void this.failInterruptedRecording();
6482
+ this.emit("ERROR", {
6483
+ err: error_createError({
6484
+ message: "WebSocket stream error",
6485
+ explanation: `The WebSocket stream emitted an error event. Details: ${string_pretty(err)}`,
6486
+ options: this.options,
6487
+ exc: streamError
6488
+ })
6489
+ });
6490
+ }
6491
+ sendPings() {
6492
+ this.stopPings();
6493
+ this.pingInterval = window.setInterval(()=>{
6494
+ this.options.logger.debug("Recorder: pinging...");
6495
+ this.writeStream(Buffer.from(""));
6496
+ }, this.options.timeouts.pingInterval);
6497
+ }
6498
+ stopPings() {
6499
+ if (void 0 === this.pingInterval) return;
6500
+ this.options.logger.debug("Recorder: stopping pings...");
6501
+ window.clearInterval(this.pingInterval);
6502
+ this.pingInterval = void 0;
6503
+ }
6504
+ clearConnectionTimeout() {
6505
+ if (void 0 === this.connectionTimeout) return;
6506
+ this.options.logger.debug("Recorder: clearConnectionTimeout()");
6507
+ window.clearTimeout(this.connectionTimeout);
6508
+ this.connectionTimeout = void 0;
6509
+ }
6510
+ clearReconnectTimeout() {
6511
+ if (void 0 === this.reconnectTimeout) return;
6512
+ this.options.logger.debug("Recorder: clearReconnectTimeout()");
6513
+ window.clearTimeout(this.reconnectTimeout);
6514
+ this.reconnectTimeout = void 0;
6515
+ }
6516
+ handleConnectionFailure(params) {
6517
+ if (this.retryConnection(params)) return;
6518
+ this.failConnection(params);
6519
+ }
6520
+ retryConnection(params) {
6521
+ if (!this.canRetryConnection()) return false;
6522
+ if (void 0 !== this.reconnectTimeout) return true;
6523
+ const delayMs = Math.min(1000 * 2 ** this.reconnectAttempts, 10000);
6524
+ this.reconnectAttempts++;
6525
+ this.connecting = false;
6526
+ this.clearConnectionTimeout();
6527
+ this.discardSocket();
6528
+ this.options.logger.debug(`Recorder: retrying WebSocket connection in ${delayMs}ms (cause ${params.cause}).`);
6529
+ this.reconnectTimeout = window.setTimeout(()=>{
6530
+ this.reconnectTimeout = void 0;
6531
+ if (this.shouldContinueReconnect()) this.initSocket();
6532
+ }, delayMs);
6533
+ return true;
6534
+ }
6535
+ canRetryConnection() {
6536
+ return Boolean(this.reconnecting && this.userMediaLoaded && this.isOnline() && !this.unloaded && !this.blocking);
6537
+ }
6538
+ shouldContinueReconnect() {
6539
+ return this.reconnecting && !this.unloaded && !this.blocking;
6540
+ }
6541
+ isOnline() {
6542
+ return navigator.onLine;
6543
+ }
6544
+ failConnection(params) {
6545
+ if (this.shouldIgnoreConnectionFailure()) return;
6546
+ this.connectionFailed = true;
6547
+ this.connecting = false;
6548
+ this.reconnecting = false;
6549
+ this.reconnectAttempts = 0;
6550
+ this.clearReconnectTimeout();
6551
+ this.clearConnectionTimeout();
6552
+ const { url2Connect, cause } = params;
6553
+ const online = this.isOnline();
6554
+ const elapsedMs = this.connectingStartedAt ? Date.now() - this.connectingStartedAt : void 0;
6555
+ const closeEvent = this.lastCloseEvent;
6556
+ const socketError = this.lastSocketError;
6557
+ const contents = {
6558
+ cause,
6559
+ closeCode: closeEvent?.code,
6560
+ closeReason: closeEvent?.reason,
6561
+ elapsedMs,
6562
+ online,
6563
+ secureContext: globalThis.isSecureContext,
6564
+ socketReadyState: this.stream?.socket.readyState,
6565
+ timeoutMs: this.options.timeouts.connection,
6566
+ url: url2Connect,
6567
+ unloaded: this.unloaded,
6568
+ userMediaLoaded: this.userMediaLoaded,
6569
+ userMediaLoading: this.userMediaLoading,
6570
+ wasClean: closeEvent?.wasClean,
6571
+ socketError,
6572
+ blocking: this.blocking,
6573
+ visibilityState: document.visibilityState
6526
6574
  };
6527
- const play = ()=>{
6528
- try {
6529
- this.rawVisualUserMedia?.load();
6530
- if (this.rawVisualUserMedia?.paused) {
6531
- 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)}`);
6532
- this.rawVisualUserMedia.play().then(()=>{
6533
- if (initializationId !== this.initializationId) return;
6534
- if (!this.playingPromiseReached) {
6535
- this.options.logger.debug("UserMedia: play promise successful. Playing now.");
6536
- this.playingPromiseReached = true;
6537
- }
6538
- }).catch((exc)=>{
6539
- if (initializationId !== this.initializationId) return;
6540
- if (exc instanceof Error && !params?.recovering) this.options.logger.warn(`Caught pending user media promise exception: ${exc.toString()}`);
6541
- else {
6542
- unloadAllEventListeners();
6543
- endedEarlyCallback(error_createError({
6544
- message: "Failed to play user media upon play event.",
6545
- exc,
6546
- options: this.options
6547
- }));
6548
- }
6575
+ const debugLine = string_summarize("Recorder: failConnection() diagnostic", contents);
6576
+ this.options.logger.debug(debugLine);
6577
+ let explanation;
6578
+ 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.`;
6579
+ 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.`;
6580
+ else {
6581
+ const closeSuffix = closeEvent ? ` (code ${closeEvent.code})` : "";
6582
+ explanation = `Connection to ${url2Connect} is closed${closeSuffix}. Please check your internet connection and try again. If the problem persists, contact us.`;
6583
+ }
6584
+ else explanation = "Your device appears to be offline. Please check your internet connection and try again.";
6585
+ this.discardSocket();
6586
+ const err = error_createError({
6587
+ message: "Unable to connect to the server",
6588
+ explanation,
6589
+ options: this.options,
6590
+ cause: contents,
6591
+ exc: socketError ? new Error("WebSocket connection error", {
6592
+ cause: socketError
6593
+ }) : void 0
6594
+ });
6595
+ this.emit("ERROR", {
6596
+ err
6597
+ });
6598
+ }
6599
+ shouldIgnoreConnectionFailure() {
6600
+ return this.connectionFailed || this.connected || Boolean(this.unloaded) || this.validate();
6601
+ }
6602
+ initSocket(cb) {
6603
+ if (this.connected) {
6604
+ this.options.logger.debug("Recorder: already **connected**. Not going to initialize a new WebSocket connection.");
6605
+ cb?.();
6606
+ return;
6607
+ }
6608
+ if (this.connecting) return void this.options.logger.debug("Recorder: already **connecting**. Not going to initialize a new WebSocket connection.");
6609
+ this.clearReconnectTimeout();
6610
+ if (util_isAutomatedUserAgent()) {
6611
+ this.connecting = false;
6612
+ this.options.logger.debug("Recorder: skipping web socket connection for an automated crawler");
6613
+ return;
6614
+ }
6615
+ this.connecting = true;
6616
+ this.connectionFailed = false;
6617
+ this.connectingStartedAt = Date.now();
6618
+ this.lastCloseEvent = void 0;
6619
+ this.lastSocketError = void 0;
6620
+ this.emit("CONNECTING");
6621
+ let url2Connect;
6622
+ try {
6623
+ const socketUrlObj = new URL(this.options.socketUrl);
6624
+ socketUrlObj.searchParams.set(constants.WHITELIST_KEY_LABEL, this.options.whitelistKey);
6625
+ url2Connect = socketUrlObj.toString();
6626
+ } catch (exc) {
6627
+ this.connecting = this.connected = false;
6628
+ const err = error_createError({
6629
+ message: "Invalid WebSocket URL",
6630
+ explanation: `The configured socketUrl "${this.options.socketUrl}" is not a valid URL. Please check your videomail-client configuration.`,
6631
+ options: this.options,
6632
+ exc
6633
+ });
6634
+ this.emit("ERROR", {
6635
+ err
6636
+ });
6637
+ return;
6638
+ }
6639
+ this.options.logger.debug(`Recorder: initializing web socket stream to ${url2Connect}`);
6640
+ let nativeSocket;
6641
+ try {
6642
+ nativeSocket = new WebSocket(url2Connect);
6643
+ nativeSocket.addEventListener("close", (event)=>{
6644
+ this.lastCloseEvent = {
6645
+ code: event.code,
6646
+ reason: event.reason,
6647
+ wasClean: event.wasClean
6648
+ };
6649
+ });
6650
+ nativeSocket.addEventListener("error", (event)=>{
6651
+ this.lastSocketError = error_getEventDetails(event);
6652
+ });
6653
+ } catch (exc) {
6654
+ this.connecting = this.connected = false;
6655
+ const diagnostic = error_getWebSocketDiagnostic(url2Connect);
6656
+ const message = diagnostic.looksAutomated ? "Automated crawler: WebSocket not supported in this environment" : `Failed to construct WebSocket to ${url2Connect}`;
6657
+ 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}`;
6658
+ const err = error_createError({
6659
+ message,
6660
+ explanation,
6661
+ options: this.options,
6662
+ exc
6663
+ });
6664
+ this.emit("ERROR", {
6665
+ err
6666
+ });
6667
+ return;
6668
+ }
6669
+ try {
6670
+ this.stream = websocket_stream(nativeSocket);
6671
+ } catch (exc) {
6672
+ this.connecting = this.connected = false;
6673
+ const err = error_createError({
6674
+ message: `Failed to create a stream to ${url2Connect}`,
6675
+ explanation: "Please check your connection and try again. If the problem persists, contact us.",
6676
+ options: this.options,
6677
+ exc
6678
+ });
6679
+ this.emit("ERROR", {
6680
+ err
6681
+ });
6682
+ }
6683
+ const stream = this.stream;
6684
+ if (stream) {
6685
+ const connectionTimeoutMs = this.options.timeouts.connection;
6686
+ this.connectionTimeout = window.setTimeout(()=>{
6687
+ if (this.isOnline() && this.isUserMediaLoaded()) {
6688
+ this.options.logger.debug(`${PIPE_SYMBOL}Reconnecting due to connection timeout.`);
6689
+ this.reconnecting = true;
6690
+ this.handleConnectionFailure({
6691
+ url2Connect,
6692
+ cause: "timeout"
6549
6693
  });
6694
+ } else this.handleConnectionFailure({
6695
+ url2Connect,
6696
+ cause: "timeout"
6697
+ });
6698
+ }, connectionTimeoutMs);
6699
+ stream.on("close", ()=>{
6700
+ this.handleStreamClose(url2Connect);
6701
+ });
6702
+ stream.on("connect", (args)=>{
6703
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *connect* event emitted with args: ${string_pretty(args)}`);
6704
+ this.clearConnectionTimeout();
6705
+ const isClosing = this.stream?.socket.readyState === WebSocket.CLOSING;
6706
+ if (!this.connected && !isClosing && !this.unloaded) {
6707
+ this.connected = true;
6708
+ this.connecting = this.unloaded = false;
6709
+ this.reconnecting = false;
6710
+ this.reconnectAttempts = 0;
6711
+ this.clearReconnectTimeout();
6712
+ this.emit("CONNECTED");
6713
+ cb?.();
6550
6714
  }
6551
- } catch (exc) {
6552
- unloadAllEventListeners();
6553
- endedEarlyCallback(exc);
6554
- }
6555
- };
6556
- const fireCallbacks = ()=>{
6557
- const readyState = this.rawVisualUserMedia?.readyState;
6558
- this.options.logger.debug(`UserMedia: fireCallbacks(readyState=${readyState}, onPlayReached=${this.onPlayReached}, onLoadedMetaDataReached=${this.onLoadedMetaDataReached})`);
6559
- if (this.onPlayReached && this.onLoadedMetaDataReached) if (this.audioRecorder) try {
6560
- this.audioRecorder.init(localMediaStream);
6561
- const onAudioReady = ()=>{
6562
- if (initializationId !== this.initializationId || this.currentVisualStream !== localMediaStream) return;
6563
- if (this.recording) this.audioRecord(audioCallback);
6564
- else this.on("SENDING_FIRST_FRAME", ()=>{
6565
- this.audioRecord(audioCallback);
6715
+ });
6716
+ stream.on("data", (data)=>{
6717
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *data* event emitted`);
6718
+ try {
6719
+ const command = JSON.parse(data.toString());
6720
+ this.executeCommand(command);
6721
+ } catch (exc) {
6722
+ this.options.logger.error(`Failed to parse command: ${exc}`);
6723
+ const err = error_createError({
6724
+ message: "Invalid server command",
6725
+ explanation: `Contact us. The invalid command was: ${data.toString()}.`,
6726
+ options: this.options,
6727
+ exc
6566
6728
  });
6567
- this.clearInitializationListeners?.();
6568
- videoCallback();
6729
+ this.emit("ERROR", {
6730
+ err
6731
+ });
6732
+ }
6733
+ });
6734
+ stream.on("error", (err)=>{
6735
+ this.handleStreamError(url2Connect, err);
6736
+ });
6737
+ stream.on("drain", ()=>{
6738
+ this.handleStreamDrain(stream);
6739
+ });
6740
+ stream.on("preend", (args)=>{
6741
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *preend* event emitted with args: ${string_pretty(args)}`);
6742
+ });
6743
+ stream.on("end", ()=>{
6744
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *end* event emitted`);
6745
+ });
6746
+ stream.on("pipe", (src)=>{
6747
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *pipe* event emitted with src: ${string_pretty(src)}`);
6748
+ });
6749
+ stream.on("unpipe", (src)=>{
6750
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *unpipe* event emitted with src: ${string_pretty(src)}`);
6751
+ });
6752
+ stream.on("resume", (args)=>{
6753
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *resume* event emitted with args: ${string_pretty(args)}`);
6754
+ });
6755
+ stream.on("uncork", (args)=>{
6756
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *uncork* event emitted with args: ${string_pretty(args)}`);
6757
+ });
6758
+ stream.on("readable", (args)=>{
6759
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *readable* event emitted with args: ${string_pretty(args)}`);
6760
+ });
6761
+ stream.on("prefinish", (args)=>{
6762
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *prefinish* event emitted with args: ${string_pretty(args)}`);
6763
+ });
6764
+ stream.on("finish", (args)=>{
6765
+ this.options.logger.debug(`${PIPE_SYMBOL}Stream *finish* event emitted with args: ${string_pretty(args)}`);
6766
+ });
6767
+ }
6768
+ }
6769
+ writeCommand(command, args, cb) {
6770
+ if (this.connected) {
6771
+ if (this.stream) {
6772
+ if (args) this.options.logger.debug(`$ ${command} with ${string_pretty(args)}`);
6773
+ else this.options.logger.debug(`$ ${command}`);
6774
+ const commandObj = {
6775
+ command,
6776
+ args
6569
6777
  };
6570
- if (params?.recovering) this.audioRecorder.resume().then(onAudioReady).catch(endedEarlyCallback);
6571
- else onAudioReady();
6572
- } catch (exc) {
6573
- unloadAllEventListeners();
6574
- this.stop(void 0, {
6575
- preserveRecordingState: Boolean(params?.recovering)
6576
- });
6577
- endedEarlyCallback(exc);
6578
- }
6579
- else {
6580
- this.clearInitializationListeners?.();
6581
- videoCallback();
6778
+ this.writeStream(Buffer.from(JSON.stringify(commandObj)), cb ? {
6779
+ onFlushedCallback: ()=>{
6780
+ queueMicrotask(cb);
6781
+ }
6782
+ } : void 0);
6582
6783
  }
6583
- };
6584
- const onPlay = ()=>{
6585
- try {
6586
- this.logEvent("play", {
6587
- readyState: this.rawVisualUserMedia?.readyState,
6588
- audio: isAudioEnabled(this.options),
6589
- width: this.rawVisualUserMedia?.width,
6590
- height: this.rawVisualUserMedia?.height,
6591
- videoWidth: this.rawVisualUserMedia?.videoWidth,
6592
- videoHeight: this.rawVisualUserMedia?.videoHeight
6784
+ } else {
6785
+ this.options.logger.debug(`Reconnecting for the command ${command} …`);
6786
+ this.initSocket(()=>{
6787
+ this.writeCommand(command, args, cb);
6788
+ });
6789
+ }
6790
+ }
6791
+ constructor(options){
6792
+ super(options), connection_define_property(this, "reconnectTimeout", void 0), connection_define_property(this, "connectionTimeout", void 0), connection_define_property(this, "userMediaLoaded", void 0), connection_define_property(this, "userMediaLoading", false), connection_define_property(this, "unloaded", void 0), connection_define_property(this, "connectionFailed", false), connection_define_property(this, "reconnectAttempts", 0), connection_define_property(this, "blocking", false), connection_define_property(this, "connectingStartedAt", void 0), connection_define_property(this, "lastCloseEvent", void 0), connection_define_property(this, "lastSocketError", void 0), connection_define_property(this, "pingInterval", void 0);
6793
+ }
6794
+ }
6795
+ const connection = RecorderConnection;
6796
+ function createUserMediaConstraints({ options, facingMode, limitedWidth, userMediaParams }) {
6797
+ const video = {
6798
+ frameRate: {
6799
+ ideal: options.video.fps
6800
+ }
6801
+ };
6802
+ if (userMediaParams?.switchingFacingMode || userMediaParams?.recovering) video.facingMode = userMediaParams.switchingFacingMode || facingMode;
6803
+ if (options.video.width) video.width = {
6804
+ ideal: options.video.width
6805
+ };
6806
+ else if (limitedWidth?.value) video.width = {
6807
+ ideal: limitedWidth.value
6808
+ };
6809
+ if (options.video.height) video.height = {
6810
+ ideal: options.video.height
6811
+ };
6812
+ return {
6813
+ audio: isAudioEnabled(options),
6814
+ video
6815
+ };
6816
+ }
6817
+ const mediaConstraints = createUserMediaConstraints;
6818
+ function media_define_property(obj, key, value) {
6819
+ if (key in obj) Object.defineProperty(obj, key, {
6820
+ value: value,
6821
+ enumerable: true,
6822
+ configurable: true,
6823
+ writable: true
6824
+ });
6825
+ else obj[key] = value;
6826
+ return obj;
6827
+ }
6828
+ class RecorderMedia extends connection {
6829
+ show() {
6830
+ html_showElement(this.recorderElement);
6831
+ }
6832
+ clearRetryTimeout() {
6833
+ if (void 0 === this.retryTimeout) return;
6834
+ this.options.logger.debug("Recorder: clearRetryTimeout()");
6835
+ window.clearTimeout(this.retryTimeout);
6836
+ this.retryTimeout = void 0;
6837
+ }
6838
+ showUserMedia() {
6839
+ if (!this.connected) return false;
6840
+ const hidden = this.isHidden();
6841
+ if (!hidden) return true;
6842
+ const notifying = this.isNotifying();
6843
+ if (notifying) return true;
6844
+ return this.blocking;
6845
+ }
6846
+ userMediaErrorCallback(err, usedConstraints) {
6847
+ if (this.cameraRecovery) return void this.failCameraRecovery(err);
6848
+ if (this.unloaded) return void this.options.logger.debug(`Recorder: already unloaded. Not going to throw error ${string_pretty(err)}`);
6849
+ this.userMediaLoading = false;
6850
+ this.clearUserMediaTimeout();
6851
+ const characteristics = this.userMedia?.getCharacteristics();
6852
+ this.options.logger.debug(`Recorder: userMediaErrorCallback(), name: ${err.name}, message: ${err.message} and Webcam characteristics: ${characteristics ? string_pretty(characteristics) : "none"}`);
6853
+ const errorListeners = this.getListeners("ERROR");
6854
+ if (errorListeners?.length) if (err.name !== error_VideomailError.MEDIA_DEVICE_NOT_SUPPORTED) {
6855
+ const videomailError = error_createError({
6856
+ err,
6857
+ options: this.options,
6858
+ usedConstraints
6859
+ });
6860
+ this.emit("ERROR", {
6861
+ err: videomailError
6862
+ });
6863
+ } else {
6864
+ this.options.logger.debug(`Recorder: ignore user media error ${string_pretty(err)}`);
6865
+ this.retryTimeout = window.setTimeout(()=>{
6866
+ this.retryTimeout = void 0;
6867
+ if (!this.unloaded) this.loadUserMedia();
6868
+ }, this.options.timeouts.userMedia);
6869
+ }
6870
+ else {
6871
+ this.options.logger.debug(`Recorder: no error listeners attached but throwing error ${string_pretty(err)}`);
6872
+ throw error_createError({
6873
+ err,
6874
+ message: "Unable to process this error since there are no error listeners anymore.",
6875
+ options: this.options
6876
+ });
6877
+ }
6878
+ }
6879
+ getUserMediaCallback(localStream, params, requestId = this.mediaRequestId) {
6880
+ if (!this.userMedia) {
6881
+ localStream.getTracks().forEach((track)=>{
6882
+ track.stop();
6883
+ });
6884
+ throw error_createError({
6885
+ message: "No user media is defined",
6886
+ options: this.options
6887
+ });
6888
+ }
6889
+ this.options.logger.debug(`Recorder: getUserMediaCallback(${params ? string_pretty(params) : ""})`);
6890
+ if (this.showUserMedia()) try {
6891
+ this.userMedia.init(localStream, ()=>{
6892
+ if (!this.unloaded && requestId === this.mediaRequestId) this.onUserMediaReady(params);
6893
+ }, this.onAudioSample.bind(this), (err)=>{
6894
+ if (!this.unloaded && requestId === this.mediaRequestId) this.userMediaErrorCallback(err, {
6895
+ audio: isAudioEnabled(this.options)
6593
6896
  });
6594
- this.rawVisualUserMedia?.removeEventListener("play", onPlay);
6595
- if (this.hasEnded() || this.hasInvalidDimensions()) endedEarlyCallback(error_createError({
6596
- message: "Already busy",
6597
- explanation: "Probably another browser window is using your webcam?",
6598
- options: this.options
6599
- }));
6600
- else {
6601
- this.onPlayReached = true;
6602
- fireCallbacks();
6603
- }
6604
- } catch (exc) {
6605
- unloadAllEventListeners();
6606
- endedEarlyCallback(exc);
6607
- }
6608
- };
6609
- const onLoadedMetaData = ()=>{
6610
- this.logEvent("loadedmetadata", {
6611
- readyState: this.rawVisualUserMedia?.readyState,
6612
- paused: this.rawVisualUserMedia?.paused,
6613
- width: this.rawVisualUserMedia?.width,
6614
- height: this.rawVisualUserMedia?.height,
6615
- videoWidth: this.rawVisualUserMedia?.videoWidth,
6616
- videoHeight: this.rawVisualUserMedia?.videoHeight
6897
+ }, params);
6898
+ } catch (exc) {
6899
+ localStream.getTracks().forEach((track)=>{
6900
+ track.stop();
6617
6901
  });
6618
- this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
6619
- if (!this.hasEnded() && !this.hasInvalidDimensions()) {
6620
- this.emit("LOADED_META_DATA");
6621
- this.onLoadedMetaDataReached = true;
6622
- fireCallbacks();
6623
- }
6624
- };
6902
+ this.userMediaLoading = false;
6903
+ this.clearUserMediaTimeout();
6904
+ this.userMediaErrorCallback(exc, {
6905
+ audio: isAudioEnabled(this.options)
6906
+ });
6907
+ }
6908
+ else {
6909
+ localStream.getTracks().forEach((track)=>{
6910
+ track.stop();
6911
+ });
6912
+ this.userMediaLoading = false;
6913
+ this.clearUserMediaTimeout();
6914
+ this.cancelCameraRecovery();
6915
+ }
6916
+ }
6917
+ loadGenuineUserMedia(params) {
6918
+ this.options.logger.debug(`Recorder: loadGenuineUserMedia(${params ? string_pretty(params) : ""})`);
6919
+ this.emit("ASKING_WEBCAM_PERMISSION");
6920
+ this.userMediaLoading = true;
6921
+ const requestId = ++this.mediaRequestId;
6922
+ const limitedWidth = this.options.video.width ? void 0 : this.visuals.limitWidth();
6923
+ const constraints = mediaConstraints({
6924
+ facingMode: this.facingMode,
6925
+ limitedWidth,
6926
+ options: this.options,
6927
+ userMediaParams: params
6928
+ });
6929
+ this.options.logger.debug(string_summarize("Recorder: our webcam constraints are:", constraints));
6930
+ this.options.logger.debug(string_summarize("Recorder: available webcam constraints are:", navigator.mediaDevices.getSupportedConstraints()));
6931
+ const streamPromise = navigator.mediaDevices.getUserMedia(constraints);
6932
+ streamPromise.then((localStream)=>{
6933
+ if (this.unloaded || requestId !== this.mediaRequestId) return void localStream.getTracks().forEach((track)=>{
6934
+ track.stop();
6935
+ });
6936
+ this.getUserMediaCallback(localStream, params, requestId);
6937
+ }).catch((reason)=>{
6938
+ if (!this.unloaded && requestId === this.mediaRequestId) this.userMediaErrorCallback(reason, constraints);
6939
+ });
6940
+ }
6941
+ loadUserMedia(params) {
6942
+ if (this.userMediaLoaded) {
6943
+ this.options.logger.debug("Recorder: skipping loadUserMedia() because it is already loaded");
6944
+ this.onUserMediaReady(params);
6945
+ return;
6946
+ } else if (this.userMediaLoading) return void this.options.logger.debug("Recorder: skipping loadUserMedia() because it is already asking for permission");
6947
+ this.options.logger.debug(`Recorder: loadUserMedia(${params ? string_pretty(params) : ""})`);
6948
+ this.emit("LOADING_USER_MEDIA");
6625
6949
  try {
6626
- const videoTrack = media_getFirstVideoTrack(localMediaStream);
6627
- this.videoTrackLabel = videoTrack?.label;
6628
- if (videoTrack) if (videoTrack.enabled) if (util_isVirtualCamera(videoTrack)) throw error_createError({
6629
- message: "Virtual cameras are not allowed.",
6630
- explanation: `Please use a real camera, not "${videoTrack.label}".`,
6631
- options: this.options
6950
+ if (!navigator.mediaDevices) throw new Error("No media devices are available.");
6951
+ this.userMediaTimeout = window.setTimeout(()=>{
6952
+ if (!this.userMediaLoaded) {
6953
+ this.userMediaLoading = false;
6954
+ const err = util_getBrowser(this.options).getNoAccessIssue();
6955
+ this.userMediaErrorCallback(err, {
6956
+ audio: isAudioEnabled(this.options)
6957
+ });
6958
+ }
6959
+ }, this.options.timeouts.userMedia);
6960
+ this.userMediaLoading = true;
6961
+ this.loadGenuineUserMedia(params);
6962
+ } catch (exc) {
6963
+ this.clearUserMediaTimeout();
6964
+ this.options.logger.debug("Recorder: failed to load genuine user media");
6965
+ this.userMediaLoading = false;
6966
+ if (this.cameraRecovery) return void this.failCameraRecovery(exc);
6967
+ const errorListeners = this.getListeners("ERROR");
6968
+ if (errorListeners?.length) this.emit("ERROR", {
6969
+ exc
6632
6970
  });
6633
6971
  else {
6634
- let description = "";
6635
- if (videoTrack.label.length > 0) description = description.concat(videoTrack.label);
6636
- description = description.concat(` with enabled=${videoTrack.enabled}, muted=${videoTrack.muted}, readyState=${videoTrack.readyState}`);
6637
- this.options.logger.debug(`UserMedia: ${videoTrack.kind} detected. ${description}`);
6972
+ this.options.logger.debug("Recorder: no error listeners attached but throwing exception further");
6973
+ throw exc;
6638
6974
  }
6639
- else throw error_createError({
6640
- message: "Webcam is disabled",
6641
- explanation: "The video track seems to be disabled. Enable it in your system.",
6642
- options: this.options
6975
+ }
6976
+ }
6977
+ isNotifying() {
6978
+ return this.visuals.isNotifying();
6979
+ }
6980
+ isHidden() {
6981
+ return !this.recorderElement || html_isHidden(this.recorderElement);
6982
+ }
6983
+ clearUserMediaTimeout() {
6984
+ if (void 0 !== this.userMediaTimeout) {
6985
+ this.options.logger.debug("Recorder: clearUserMediaTimeout()");
6986
+ window.clearTimeout(this.userMediaTimeout);
6987
+ this.userMediaTimeout = void 0;
6988
+ }
6989
+ }
6990
+ clearStopTimeout() {
6991
+ if (void 0 === this.stopTimeout) return;
6992
+ window.clearTimeout(this.stopTimeout);
6993
+ this.stopTimeout = void 0;
6994
+ }
6995
+ isReady() {
6996
+ return this.userMedia?.isReady();
6997
+ }
6998
+ finishCameraRecovery(restored) {
6999
+ this.clearUserMediaTimeout();
7000
+ const resolve = this.resolveCameraRecovery;
7001
+ const resumeRequested = restored && this.resumeAfterCameraRecovery;
7002
+ this.cameraRecovery = void 0;
7003
+ this.resolveCameraRecovery = void 0;
7004
+ this.resumeAfterCameraRecovery = false;
7005
+ resolve?.(restored);
7006
+ if (resumeRequested && this.isPaused()) this.resume();
7007
+ }
7008
+ cancelCameraRecovery() {
7009
+ if (!this.cameraRecovery) return;
7010
+ this.mediaRequestId++;
7011
+ this.userMediaLoading = false;
7012
+ this.userMediaLoaded = false;
7013
+ this.clearUserMediaTimeout();
7014
+ this.userMedia?.stop(void 0, {
7015
+ preserveRecordingState: true
7016
+ });
7017
+ this.finishCameraRecovery(false);
7018
+ }
7019
+ failCameraRecovery(exc) {
7020
+ this.cancelCameraRecovery();
7021
+ const err = error_createError({
7022
+ message: "Camera recovery failed",
7023
+ explanation: "Your recording is still paused. Check camera permissions, then try Resume again or stop to keep the recorded part.",
7024
+ exc,
7025
+ options: this.options
7026
+ });
7027
+ this.emit("CAMERA_RECOVERY_FAILED", {
7028
+ err
7029
+ });
7030
+ }
7031
+ recoverCamera() {
7032
+ if (this.cameraRecovery) return this.cameraRecovery;
7033
+ if (!this.built || this.unloaded || !this.userMedia || this.userMediaLoading || this.submitting || this.key || void 0 !== this.stopTimeout || this.recordingTransportFailed) return Promise.resolve(false);
7034
+ const liveStream = this.userMedia.hasLiveStream();
7035
+ if (liveStream && this.isRecording()) return Promise.resolve(true);
7036
+ if (!liveStream && this.recordingActive && !this.isPaused()) this.pause();
7037
+ const recovery = new Promise((resolve)=>{
7038
+ this.resolveCameraRecovery = resolve;
7039
+ });
7040
+ this.cameraRecovery = recovery;
7041
+ this.emit("CAMERA_RECOVERING");
7042
+ if (liveStream) {
7043
+ this.userMediaTimeout = window.setTimeout(()=>{
7044
+ this.failCameraRecovery(new Error("Camera playback recovery timed out."));
7045
+ }, this.options.timeouts.userMedia);
7046
+ this.userMedia.restorePlayback().then(()=>{
7047
+ if (this.cameraRecovery === recovery) this.finishCameraRecovery(true);
7048
+ }).catch((exc)=>{
7049
+ if (this.cameraRecovery === recovery) this.failCameraRecovery(exc);
6643
7050
  });
6644
- else this.options.logger.debug("UserMedia: detected (but no video tracks exist");
6645
- this.rawVisualUserMedia?.addEventListener("loadedmetadata", onLoadedMetaData);
6646
- this.rawVisualUserMedia?.addEventListener("play", onPlay);
6647
- this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
6648
- this.rawVisualUserMedia?.addEventListener("error", this.handleMediaError);
6649
- this.setVisualStream(localMediaStream);
6650
- play();
6651
- } catch (exc) {
6652
- unloadAllEventListeners();
6653
- endedEarlyCallback(exc);
7051
+ } else {
7052
+ this.userMediaLoaded = false;
7053
+ this.loadUserMedia({
7054
+ recovering: true
7055
+ });
7056
+ }
7057
+ return recovery;
7058
+ }
7059
+ switchFacingMode() {
7060
+ if (this.cameraRecovery || !util_getBrowser(this.options).isMobile()) return;
7061
+ if ("user" === this.facingMode) this.facingMode = "environment";
7062
+ else if ("environment" === this.facingMode) this.facingMode = "user";
7063
+ else this.options.logger.warn(`Recorder: unsupported facing mode ${string_pretty(this.facingMode)}`);
7064
+ this.loadGenuineUserMedia({
7065
+ switchingFacingMode: this.facingMode
7066
+ });
7067
+ }
7068
+ hide() {
7069
+ if (!this.isHidden()) {
7070
+ if (this.recorderElement) html_hideElement(this.recorderElement);
7071
+ this.clearUserMediaTimeout();
7072
+ this.clearRetryTimeout();
6654
7073
  }
6655
7074
  }
6656
- getVideoTrackLabel() {
6657
- return this.videoTrackLabel;
7075
+ isUnloaded() {
7076
+ return this.unloaded;
7077
+ }
7078
+ isUserMediaLoaded() {
7079
+ return this.userMediaLoaded;
6658
7080
  }
6659
- isReady() {
6660
- return Boolean(this.rawVisualUserMedia?.srcObject || this.rawVisualUserMedia?.currentSrc);
7081
+ constructor(visuals, options){
7082
+ super(options), media_define_property(this, "visuals", void 0), media_define_property(this, "recorderElement", void 0), media_define_property(this, "userMedia", void 0), media_define_property(this, "userMediaTimeout", void 0), media_define_property(this, "retryTimeout", void 0), media_define_property(this, "stopTimeout", void 0), media_define_property(this, "mediaRequestId", 0), media_define_property(this, "cameraRecovery", void 0), media_define_property(this, "resolveCameraRecovery", void 0), media_define_property(this, "resumeAfterCameraRecovery", false), media_define_property(this, "submitting", false), media_define_property(this, "built", false), media_define_property(this, "key", void 0), media_define_property(this, "facingMode", void 0);
7083
+ this.visuals = visuals;
7084
+ this.facingMode = options.video.facingMode;
6661
7085
  }
6662
- stop(visualStream, params) {
7086
+ }
7087
+ const media = RecorderMedia;
7088
+ function calculateProgress(confirmedCount, totalCount) {
7089
+ return `${(confirmedCount / (totalCount || 1) * 100).toFixed(2)}%`;
7090
+ }
7091
+ function getConfirmedProgressNumber({ args, kind, options }) {
7092
+ const label = "frame" === kind ? "frame" : "audio sample";
7093
+ if (!args) throw error_createError({
7094
+ message: `Arguments are missing for updating the ${label} progress`,
7095
+ options
7096
+ });
7097
+ const confirmedNumber = args[kind];
7098
+ if (!confirmedNumber) throw error_createError({
7099
+ message: `The ${label} number is missing`,
7100
+ options
7101
+ });
7102
+ return confirmedNumber;
7103
+ }
7104
+ var capture_Buffer = __webpack_require__("dY").hp;
7105
+ function capture_define_property(obj, key, value) {
7106
+ if (key in obj) Object.defineProperty(obj, key, {
7107
+ value: value,
7108
+ enumerable: true,
7109
+ configurable: true,
7110
+ writable: true
7111
+ });
7112
+ else obj[key] = value;
7113
+ return obj;
7114
+ }
7115
+ class RecorderCapture extends media {
7116
+ onAudioSample(audioSample) {
7117
+ if (this.recordingTransportFailed || this.cameraRecovery) return;
7118
+ const sampleRate = this.userMedia?.getAudioSampleRate();
7119
+ if (sampleRate && sampleRate > 0) this.recordingAudioSampleRate ??= sampleRate;
7120
+ this.samplesCount++;
7121
+ this.mediaTransmissionStarted = true;
7122
+ const audioBuffer = audioSample.toBuffer();
7123
+ this.writeStream(audioBuffer);
7124
+ }
7125
+ onUserMediaReady(params) {
6663
7126
  try {
6664
- let chosenStream = visualStream;
6665
- if (!params?.aboutToInitialize) {
6666
- this.initializationId++;
6667
- this.clearInitializationListeners?.();
6668
- chosenStream ??= this.currentVisualStream;
6669
- const tracks = chosenStream?.getTracks();
6670
- if (tracks) tracks.forEach((track)=>{
6671
- track.stop();
6672
- });
6673
- this.setVisualStream();
6674
- this.audioRecorder?.stop();
6675
- this.audioRecorder = void 0;
7127
+ this.options.logger.debug(`Recorder: onUserMediaReady(${params ? string_pretty(params) : ""})`);
7128
+ const switchingFacingMode = params?.switchingFacingMode;
7129
+ if (params?.recovering) {
7130
+ if (!this.userMedia?.hasLiveStream()) return void this.failCameraRecovery(new Error("The restored camera has no live tracks."));
7131
+ if (void 0 !== this.recordingAudioSampleRate && this.userMedia.getAudioSampleRate() !== this.recordingAudioSampleRate) return void this.failCameraRecovery(new Error("The audio sample rate changed during recovery."));
6676
7132
  }
6677
- if (!params?.preserveRecordingState) this.paused = this.recording = false;
7133
+ this.userMediaLoading = this.blocking = this.unloaded = this.submitting = false;
7134
+ this.userMediaLoaded = true;
7135
+ this.clearUserMediaTimeout();
7136
+ if (!switchingFacingMode && !params?.recovering) this.loop = this.createLoop();
7137
+ this.show();
7138
+ if (params?.recordWhenReady) this.record();
7139
+ this.emit("USER_MEDIA_READY", {
7140
+ switchingFacingMode: params?.switchingFacingMode,
7141
+ recovering: params?.recovering,
7142
+ paused: this.isPaused(),
7143
+ recordWhenReady: params?.recordWhenReady
7144
+ });
7145
+ if (params?.recovering) this.finishCameraRecovery(true);
6678
7146
  } catch (exc) {
6679
- this.emit("ERROR", {
7147
+ if (params?.recovering) this.failCameraRecovery(exc);
7148
+ else this.emit("ERROR", {
6680
7149
  exc
6681
7150
  });
6682
7151
  }
6683
7152
  }
6684
- createCanvas() {
6685
- const canvas = document.createElement("canvas");
6686
- const rawWidthDimension = this.getRawWidth(true);
6687
- if (rawWidthDimension.value) canvas.width = rawWidthDimension.value;
6688
- const rawHeight = this.getRawHeight(true);
6689
- if (rawHeight) canvas.height = rawHeight;
6690
- return canvas;
7153
+ calculateFrameProgress() {
7154
+ return calculateProgress(this.confirmedFrameNumber, this.framesCount);
6691
7155
  }
6692
- getVideoHeight() {
6693
- if (!this.rawVisualUserMedia) return;
6694
- return this.rawVisualUserMedia.videoHeight || this.rawVisualUserMedia.height;
7156
+ calculateSampleProgress() {
7157
+ return calculateProgress(this.confirmedSampleNumber, this.samplesCount);
6695
7158
  }
6696
- getVideoWidth() {
6697
- if (!this.rawVisualUserMedia) return;
6698
- return this.rawVisualUserMedia.videoWidth || this.rawVisualUserMedia.width;
7159
+ updateOverallProgress() {
7160
+ this.frameProgress = this.calculateFrameProgress();
7161
+ if (isAudioEnabled(this.options)) this.sampleProgress = this.calculateSampleProgress();
7162
+ this.emit("PROGRESS", {
7163
+ frameProgress: this.frameProgress,
7164
+ sampleProgress: this.sampleProgress
7165
+ });
6699
7166
  }
6700
- hasVideoWidth() {
6701
- const videoWidth = this.getVideoWidth();
6702
- return videoWidth && videoWidth > 0;
7167
+ updateFrameProgress(args) {
7168
+ this.confirmedFrameNumber = getConfirmedProgressNumber({
7169
+ args,
7170
+ kind: "frame",
7171
+ options: this.options
7172
+ });
7173
+ this.frameProgress = this.calculateFrameProgress();
7174
+ this.updateOverallProgress();
6703
7175
  }
6704
- getRawWidth(responsive) {
6705
- let rawWidth;
6706
- const widthDimension = {
6707
- unit: "px"
6708
- };
6709
- rawWidth = responsive ? this.getVideoWidth() : this.options.video.width ? this.options.video.width : this.getVideoWidth();
6710
- if (responsive) {
6711
- const widthDimension = this.recorder.limitWidth(rawWidth);
6712
- rawWidth = widthDimension?.value;
6713
- }
6714
- widthDimension.value = rawWidth;
6715
- return widthDimension;
7176
+ updateSampleProgress(args) {
7177
+ this.confirmedSampleNumber = getConfirmedProgressNumber({
7178
+ args,
7179
+ kind: "sample",
7180
+ options: this.options
7181
+ });
7182
+ this.sampleProgress = this.calculateSampleProgress();
7183
+ this.updateOverallProgress();
6716
7184
  }
6717
- getRawHeight(responsive) {
6718
- let rawHeight;
6719
- if (responsive) {
6720
- rawHeight = this.options.video.height || this.getVideoHeight();
6721
- const ratio = this.recorder.getRatio();
6722
- const rawWidth = this.getRawWidth(responsive);
6723
- if (void 0 !== ratio && rawWidth.value) rawHeight = rawWidth.value * ratio;
6724
- } else rawHeight = this.options.video.height ? this.options.video.height : this.getVideoHeight();
6725
- if (responsive) {
6726
- const heightDimension = this.recorder.limitHeight(rawHeight);
6727
- rawHeight = heightDimension.value;
6728
- }
6729
- return rawHeight;
7185
+ preview(args) {
7186
+ if (!args) throw error_createError({
7187
+ message: "Preview arguments are missing.",
7188
+ options: this.options
7189
+ });
7190
+ const hasAudio = this.samplesCount > 0;
7191
+ this.mediaTransmissionStarted = false;
7192
+ this.recordingActive = false;
7193
+ this.recordingTransportFailed = false;
7194
+ this.confirmedFrameNumber = this.confirmedSampleNumber = this.samplesCount = this.framesCount = 0;
7195
+ this.sampleProgress = this.frameProgress = void 0;
7196
+ this.key = args.key;
7197
+ if (args.mp4) this.replay.setMp4Source(`${args.mp4 + constants.WHITELIST_KEY_LABEL}/${this.options.whitelistKey}/videomail.mp4`, true);
7198
+ if (args.webm) this.replay.setWebMSource(`${args.webm + constants.WHITELIST_KEY_LABEL}/${this.options.whitelistKey}/videomail.webm`, true);
7199
+ this.hide();
7200
+ const widthDimension = this.getRecorderWidth(true);
7201
+ const heightDimension = this.getRecorderHeight(true);
7202
+ const duration = args.duration ?? -1;
7203
+ this.emit("PREVIEW", {
7204
+ key: this.key,
7205
+ width: widthDimension?.value,
7206
+ height: heightDimension?.value,
7207
+ hasAudio,
7208
+ duration
7209
+ });
7210
+ if (this.stopTime) this.waitingTime = Date.now() - this.stopTime;
7211
+ this.recordingStats ??= {};
7212
+ this.recordingStats.waitingTime = this.waitingTime;
6730
7213
  }
6731
- getRawVisuals() {
6732
- return this.rawVisualUserMedia;
7214
+ cancelAnimationFrame() {
7215
+ this.loop?.dispose();
6733
7216
  }
6734
- pause() {
6735
- this.paused = true;
7217
+ getIntervalSum() {
7218
+ if (!this.loop) return;
7219
+ return this.loop.getElapsedTime();
6736
7220
  }
6737
- isPaused() {
6738
- return this.paused;
7221
+ getAvgInterval() {
7222
+ const intervalSum = this.getIntervalSum() ?? 0;
7223
+ if (!this.hasValidInterval(intervalSum)) return;
7224
+ return intervalSum / this.framesCount;
6739
7225
  }
6740
- resume() {
6741
- this.paused = false;
7226
+ getAvgFps() {
7227
+ const intervalSum = this.getIntervalSum() ?? 0;
7228
+ if (!this.hasValidInterval(intervalSum)) return;
7229
+ return this.framesCount / intervalSum * 1000;
6742
7230
  }
6743
- record() {
6744
- this.recording = true;
7231
+ hasValidInterval(intervalSum) {
7232
+ return intervalSum > 0;
6745
7233
  }
6746
- isRecording() {
6747
- return this.recording;
7234
+ getRecordingStats() {
7235
+ return this.recordingStats;
6748
7236
  }
6749
7237
  getAudioSampleRate() {
6750
- if (this.audioRecorder) return this.audioRecorder.getSampleRate();
6751
- return -1;
7238
+ return this.recordingAudioSampleRate ?? this.userMedia?.getAudioSampleRate();
7239
+ }
7240
+ stop(params) {
7241
+ this.cancelCameraRecovery();
7242
+ if (!this.userMedia) throw new Error("No user media defined, unable to stop");
7243
+ this.options.logger.debug(`Recorder: stop(${params ? string_pretty(params) : ""})`);
7244
+ this.recordingActive = false;
7245
+ const limitReached = params?.limitReached;
7246
+ this.emit("STOPPING", {
7247
+ limitReached
7248
+ });
7249
+ this.loop?.complete?.();
7250
+ this.stopTimeout = window.setTimeout(()=>{
7251
+ this.stopTimeout = void 0;
7252
+ this.stopTime = Date.now();
7253
+ const videoType = this.replay.getVideoType();
7254
+ if (!videoType) throw new Error("Unable to video record when no video type is defined.");
7255
+ this.recordingStats = {
7256
+ avgFps: this.getAvgFps(),
7257
+ wantedFps: this.options.video.fps,
7258
+ avgInterval: this.getAvgInterval(),
7259
+ wantedInterval: 1e3 / this.options.video.fps,
7260
+ intervalSum: this.getIntervalSum(),
7261
+ framesCount: this.framesCount,
7262
+ videoType
7263
+ };
7264
+ if (this.userMedia) this.recordingStats.videoTrackLabel = this.userMedia.getVideoTrackLabel();
7265
+ if (isAudioEnabled(this.options) && this.userMedia) {
7266
+ this.recordingStats.samplesCount = this.samplesCount;
7267
+ this.recordingStats.sampleRate = this.recordingAudioSampleRate ?? this.userMedia.getAudioSampleRate();
7268
+ }
7269
+ this.writeCommand("stop", this.recordingStats, ()=>{
7270
+ this.emit("STOPPED", {
7271
+ recordingStats: this.recordingStats
7272
+ });
7273
+ });
7274
+ this.reset();
7275
+ }, 60);
7276
+ }
7277
+ back(cb) {
7278
+ this.emit("GOING_BACK");
7279
+ this.key = void 0;
7280
+ this.recordingDimensions = void 0;
7281
+ this.unloaded = false;
7282
+ this.show();
7283
+ this.writeCommand("back", void 0, cb);
7284
+ }
7285
+ reInitializeAudio() {
7286
+ this.options.logger.debug("Recorder: reInitializeAudio()");
7287
+ this.clearUserMediaTimeout();
7288
+ this.userMediaLoading = false;
7289
+ this.userMedia?.stop();
7290
+ this.recordingDimensions = void 0;
7291
+ this.userMediaLoaded = this.key = this.canvas = this.ctx = void 0;
7292
+ this.loadUserMedia();
7293
+ }
7294
+ reset() {
7295
+ this.cancelCameraRecovery();
7296
+ if (!this.unloaded) {
7297
+ this.options.logger.debug("Recorder: reset()");
7298
+ this.emit("RESETTING");
7299
+ this.cancelAnimationFrame();
7300
+ this.userMedia?.stop();
7301
+ this.replay.reset();
7302
+ this.userMediaLoaded = this.key = this.canvas = this.ctx = this.recordingBuffer = void 0;
7303
+ }
7304
+ }
7305
+ validate() {
7306
+ return Boolean(this.key) && void 0 === this.canvas;
6752
7307
  }
6753
- getCharacteristics() {
6754
- return {
6755
- audioSampleRate: this.getAudioSampleRate(),
6756
- muted: this.rawVisualUserMedia?.muted,
6757
- width: this.rawVisualUserMedia?.width,
6758
- height: this.rawVisualUserMedia?.height,
6759
- videoWidth: this.rawVisualUserMedia?.videoWidth,
6760
- videoHeight: this.rawVisualUserMedia?.videoHeight
6761
- };
7308
+ pause(params) {
7309
+ this.cancelCameraRecovery();
7310
+ this.resumeAfterDrain = false;
7311
+ const loop = this.loop;
7312
+ if (!loop) throw new Error("No recording loop defined");
7313
+ if (params) this.options.logger.debug(`pause() at frame ${this.framesCount} with ${string_pretty(params)}`);
7314
+ else this.options.logger.debug(`pause() at frame ${this.framesCount}`);
7315
+ this.userMedia?.pause();
7316
+ loop.stop();
7317
+ this.emit("PAUSED");
7318
+ this.sendPings();
6762
7319
  }
6763
- constructor(recorder, options){
6764
- super("UserMedia", options), userMedia_define_property(this, "recorder", void 0), userMedia_define_property(this, "rawVisualUserMedia", void 0), userMedia_define_property(this, "paused", false), userMedia_define_property(this, "recording", false), userMedia_define_property(this, "audioRecorder", void 0), userMedia_define_property(this, "currentVisualStream", void 0), userMedia_define_property(this, "onPlayReached", false), userMedia_define_property(this, "onLoadedMetaDataReached", false), userMedia_define_property(this, "playingPromiseReached", false), userMedia_define_property(this, "videoTrackLabel", void 0), userMedia_define_property(this, "clearInitializationListeners", void 0), userMedia_define_property(this, "initializationId", 0), userMedia_define_property(this, "outputEvent", (e)=>{
6765
- this.logEvent(e.type, {
6766
- readyState: this.rawVisualUserMedia?.readyState
6767
- });
6768
- this.rawVisualUserMedia?.removeEventListener(e.type, this.outputEvent);
6769
- }), userMedia_define_property(this, "handleMediaError", (event)=>{
6770
- this.options.logger.warn(`Caught video element error event: ${string_pretty(event)}`);
6771
- });
6772
- this.recorder = recorder;
6773
- this.rawVisualUserMedia = recorder.getRawVisualUserMedia();
6774
- mediaEvents.forEach((eventName)=>{
6775
- this.rawVisualUserMedia?.addEventListener(eventName, this.outputEvent, false);
6776
- });
7320
+ resume() {
7321
+ if (this.cameraRecovery || !this.userMedia?.hasLiveStream()) {
7322
+ this.recoverCamera();
7323
+ this.resumeAfterCameraRecovery = Boolean(this.cameraRecovery);
7324
+ return;
7325
+ }
7326
+ const loop = this.loop;
7327
+ if (!loop) throw new Error("No recording loop defined");
7328
+ this.options.logger.debug(`Recorder: resume() with frame ${this.framesCount}`);
7329
+ this.stopPings();
7330
+ this.emit("RESUMING");
7331
+ this.userMedia.resume();
7332
+ if (this.streamBackpressured) this.resumeAfterDrain = true;
7333
+ else loop.start();
6777
7334
  }
6778
- }
6779
- const visuals_userMedia = UserMedia;
6780
- var Buffer = __webpack_require__("dY").hp;
6781
- function recorder_define_property(obj, key, value) {
6782
- if (key in obj) Object.defineProperty(obj, key, {
6783
- value: value,
6784
- enumerable: true,
6785
- configurable: true,
6786
- writable: true
6787
- });
6788
- else obj[key] = value;
6789
- return obj;
6790
- }
6791
- const PIPE_SYMBOL = "°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸ ";
6792
- class Recorder extends util_Despot {
6793
- hasExceededWebSocketSendBuffer(buffer, opts) {
6794
- return buffer.length > 0 && !this.pendingWrites.enqueue(buffer, opts);
7335
+ onFlushed(opts) {
7336
+ const frameNumber = opts.frameNumber;
7337
+ if (1 === frameNumber) this.emit("FIRST_FRAME_SENT");
6795
7338
  }
6796
- writeStream(buffer, opts) {
6797
- if (this.stream) if (this.stream.destroyed) {
6798
- this.stopPings();
6799
- const err = error_createError({
6800
- message: "Already disconnected",
6801
- explanation: "Sorry, connection to the server has been destroyed. Please reload.",
6802
- options: this.options
7339
+ draw(_deltaTime, elapsedTime) {
7340
+ if (this.recordingTransportFailed) return;
7341
+ if (!this.userMedia) throw new Error("No user media defined, unable to draw on canvas");
7342
+ try {
7343
+ if (!this.isPaused() && this.stream && this.ctx) {
7344
+ if (0 === this.framesCount) this.emit("SENDING_FIRST_FRAME");
7345
+ this.framesCount++;
7346
+ const imageSource = this.userMedia.getRawVisuals();
7347
+ if (this.canvas && imageSource) this.ctx.drawImage(imageSource, 0, 0, this.canvas.width, this.canvas.height);
7348
+ else throw new Error("Unable to draw an image without a defined canvas");
7349
+ this.recordingBuffer = this.frame?.toBuffer();
7350
+ const recordingBufferLength = this.recordingBuffer?.length;
7351
+ if (recordingBufferLength) {
7352
+ if (this.recordingBuffer) {
7353
+ const frameControlBuffer = capture_Buffer.from(JSON.stringify({
7354
+ frameNumber: this.framesCount
7355
+ }));
7356
+ const frameBuffer = capture_Buffer.concat([
7357
+ this.recordingBuffer,
7358
+ frameControlBuffer
7359
+ ]);
7360
+ this.mediaTransmissionStarted = true;
7361
+ this.writeStream(frameBuffer, {
7362
+ frameNumber: this.framesCount,
7363
+ onFlushedCallback: this.onFlushed.bind(this)
7364
+ });
7365
+ this.visuals.checkTimer(elapsedTime);
7366
+ }
7367
+ } else throw error_createError({
7368
+ message: "Failed to extract webcam data.",
7369
+ options: this.options
7370
+ });
7371
+ }
7372
+ } catch (exc) {
7373
+ this.emit("ERROR", {
7374
+ exc
6803
7375
  });
6804
- this.handleWriteFailure(err);
6805
- } else if (this.streamBackpressured) {
6806
- if (this.hasExceededWebSocketSendBuffer(buffer, opts)) this.failMediaTransmission(error_createError({
6807
- message: "WebSocket send buffer limit exceeded",
6808
- 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.",
6809
- options: this.options
6810
- }));
6811
- } else this.writeToStream(this.stream, buffer, opts);
7376
+ }
6812
7377
  }
6813
- writeToStream(stream, buffer, opts) {
6814
- const onFlushedCallback = opts?.onFlushedCallback;
6815
- try {
6816
- const canContinue = stream.write(buffer, ()=>{
6817
- if (!onFlushedCallback) return;
6818
- try {
6819
- onFlushedCallback(opts);
6820
- } catch (exc) {
6821
- const err = error_createError({
6822
- message: "Failed to write stream buffer",
6823
- explanation: `stream.write() failed because of ${string_pretty(exc)}`,
6824
- options: this.options,
6825
- exc
6826
- });
6827
- this.emit("ERROR", {
6828
- err
7378
+ createLoop() {
7379
+ const newLoop = animitter({
7380
+ fps: this.options.video.fps
7381
+ }, this.draw.bind(this));
7382
+ this.originalAnimationFrameObject = newLoop.getRequestAnimationFrameObject();
7383
+ return newLoop;
7384
+ }
7385
+ record() {
7386
+ if (this.unloaded || this.cameraRecovery) return;
7387
+ if (!this.connected) {
7388
+ this.options.logger.debug("Recorder: reconnecting before recording …");
7389
+ this.initSocket(()=>{
7390
+ this.once("SERVER_READY", ()=>{
7391
+ if (this.unloaded) return;
7392
+ if (this.userMediaLoaded || this.options.loadUserMediaOnRecord) this.record();
7393
+ else this.once("USER_MEDIA_READY", ()=>{
7394
+ if (!this.unloaded) this.record();
6829
7395
  });
6830
- }
7396
+ });
6831
7397
  });
6832
- if (!canContinue) {
6833
- this.streamBackpressured = true;
6834
- this.startBackpressureTimeout();
6835
- this.pauseForBackpressure();
6836
- return false;
7398
+ return;
7399
+ }
7400
+ if (!this.userMediaLoaded) {
7401
+ if (this.options.loadUserMediaOnRecord) this.loadUserMedia({
7402
+ recordWhenReady: true
7403
+ });
7404
+ else {
7405
+ const err = error_createError({
7406
+ message: "Load and enable your camera first",
7407
+ options: this.options
7408
+ });
7409
+ this.emit("ERROR", {
7410
+ err
7411
+ });
6837
7412
  }
6838
- return true;
7413
+ return;
7414
+ }
7415
+ const loop = this.loop;
7416
+ if (!loop) throw new Error("No recording loop defined");
7417
+ this.recordingDimensions = void 0;
7418
+ try {
7419
+ if (!this.userMedia) throw new Error("No user media defined, unable to create canvas");
7420
+ this.canvas = this.userMedia.createCanvas();
6839
7421
  } catch (exc) {
6840
7422
  const err = error_createError({
6841
- message: "Failed writing to server",
6842
- explanation: `stream.write() failed because of ${string_pretty(exc)}`,
6843
- options: this.options,
6844
- exc
7423
+ exc,
7424
+ options: this.options
6845
7425
  });
6846
- this.handleWriteFailure(err);
6847
- return false;
7426
+ this.emit("ERROR", {
7427
+ err
7428
+ });
7429
+ return;
6848
7430
  }
6849
- }
6850
- flushPendingWrites(stream) {
6851
- if (this.isStaleStream(stream)) return;
6852
- this.streamBackpressured = false;
6853
- this.clearBackpressureTimeout();
6854
- let pendingWrite = this.pendingWrites.dequeue();
6855
- while(pendingWrite){
6856
- if (!this.writeToStream(stream, pendingWrite.buffer, pendingWrite.params)) return;
6857
- pendingWrite = this.pendingWrites.dequeue();
7431
+ this.ctx = this.canvas.getContext("2d");
7432
+ if (!this.canvas.width) {
7433
+ const err = error_createError({
7434
+ message: "Canvas has an invalid width.",
7435
+ options: this.options
7436
+ });
7437
+ this.emit("ERROR", {
7438
+ err
7439
+ });
7440
+ return;
6858
7441
  }
6859
- this.resumeRecordingAfterDrain();
6860
- }
6861
- isStaleStream(stream) {
6862
- return this.stream !== stream || stream.destroyed;
6863
- }
6864
- canPauseForBackpressure() {
6865
- return Boolean(this.recordingActive && !this.userMedia?.isPaused() && this.loop?.isRunning());
6866
- }
6867
- pauseForBackpressure() {
6868
- if (!this.canPauseForBackpressure()) return;
6869
- this.resumeAfterDrain = true;
6870
- this.loop.stop();
6871
- }
6872
- resumeRecordingAfterDrain() {
6873
- if (!this.resumeAfterDrain) return;
6874
- this.resumeAfterDrain = false;
6875
- if (this.canResumeAfterDrain()) this.loop?.start();
6876
- }
6877
- canResumeAfterDrain() {
6878
- return this.recordingActive && !this.userMedia?.isPaused();
6879
- }
6880
- handleWriteFailure(err) {
6881
- if (this.mediaTransmissionStarted) return void this.failMediaTransmission(err);
6882
- this.emit("ERROR", {
6883
- err
6884
- });
6885
- }
6886
- failMediaTransmission(err) {
6887
- if (this.recordingTransportFailed) return;
6888
- this.recordingTransportFailed = true;
6889
- this.recordingActive = false;
6890
- this.mediaTransmissionStarted = false;
6891
- this.streamBackpressured = false;
6892
- this.resumeAfterDrain = false;
6893
- this.pendingWrites.clear();
6894
- this.connected = false;
6895
- this.connecting = false;
6896
- this.reconnecting = false;
6897
- this.clearReconnectTimeout();
6898
- this.clearBackpressureTimeout();
6899
- this.discardSocket();
6900
- this.reset();
6901
- this.framesCount = this.samplesCount = 0;
6902
- this.confirmedFrameNumber = this.confirmedSampleNumber = 0;
6903
- delete this.recordingStats;
6904
- this.recordingDimensions = void 0;
6905
- this.emit("ERROR", {
6906
- err
6907
- });
6908
- }
6909
- startBackpressureTimeout() {
6910
- if (!this.shouldStartBackpressureTimeout()) return;
6911
- this.backpressureTimeout = window.setTimeout(()=>{
6912
- this.backpressureTimeout = void 0;
6913
- if (this.shouldIgnoreBackpressureTimeout()) return;
7442
+ if (!this.canvas.height) {
6914
7443
  const err = error_createError({
6915
- message: "WebSocket write timed out",
6916
- explanation: "The server stopped accepting recording data. The recording has been stopped; please check your connection and start a new one.",
7444
+ message: "Canvas has an invalid height.",
6917
7445
  options: this.options
6918
7446
  });
6919
- this.failMediaTransmission(err);
6920
- }, this.options.timeouts.connection);
6921
- }
6922
- shouldStartBackpressureTimeout() {
6923
- return void 0 === this.backpressureTimeout && this.mediaTransmissionStarted;
6924
- }
6925
- shouldIgnoreBackpressureTimeout() {
6926
- return !this.streamBackpressured || this.recordingTransportFailed;
6927
- }
6928
- failInterruptedRecording() {
6929
- const unconfirmedFrames = Math.max(0, this.framesCount - this.confirmedFrameNumber);
6930
- const unconfirmedSamples = Math.max(0, this.samplesCount - this.confirmedSampleNumber);
6931
- const err = error_createError({
6932
- message: "Recording interrupted",
6933
- explanation: "The server connection closed before this recording completed. It cannot be safely resumed, so it has been stopped. Please start a new recording.",
6934
- options: this.options,
6935
- cause: {
6936
- unconfirmedFrames,
6937
- unconfirmedSamples
6938
- }
6939
- });
6940
- this.failMediaTransmission(err);
6941
- }
6942
- handleStreamDrain(stream) {
6943
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *drain* event emitted`);
6944
- this.flushPendingWrites(stream);
6945
- }
6946
- canReconnectAfterStreamClose() {
6947
- return Boolean(this.connected && this.userMediaLoaded);
6948
- }
6949
- shouldReportStreamClose() {
6950
- return !this.connecting && !this.blocking;
6951
- }
6952
- handleStreamClose(url2Connect) {
6953
- const debugLine = string_summarize(`${PIPE_SYMBOL}Stream has closed:`, {
6954
- connecting: this.connecting,
6955
- connected: this.connected,
6956
- userMediaLoaded: this.userMediaLoaded,
6957
- blocking: this.blocking
6958
- });
6959
- this.options.logger.debug(debugLine);
6960
- const canReconnect = this.canReconnectAfterStreamClose();
6961
- this.connected = false;
6962
- this.stopPings();
6963
- if (this.shouldIgnoreStreamClose()) return;
6964
- if (this.mediaTransmissionStarted) this.failInterruptedRecording();
6965
- else if (canReconnect) {
6966
- this.pendingWrites.clear();
6967
- this.streamBackpressured = false;
6968
- this.resumeAfterDrain = false;
6969
- this.reconnecting = true;
6970
- this.initSocket();
6971
- } else if (this.shouldReportStreamClose()) window.setTimeout(()=>{
6972
- this.handleConnectionFailure({
6973
- url2Connect,
6974
- cause: "closed"
7447
+ this.emit("ERROR", {
7448
+ err
6975
7449
  });
6976
- }, 0);
6977
- }
6978
- shouldIgnoreStreamClose() {
6979
- return this.recordingTransportFailed || Boolean(this.unloaded) || this.validate();
6980
- }
6981
- handleStreamError(url2Connect, err) {
6982
- if ("u" > typeof Event && err instanceof Event) this.lastSocketError = error_getEventDetails(err);
6983
- else this.lastSocketError = serializeError(err);
6984
- const debugLine = string_summarize(`${PIPE_SYMBOL}Stream *error* event emitted:`, {
6985
- error: serializeError(err)
6986
- });
6987
- this.options.logger.debug(debugLine);
6988
- if (this.validate()) {
6989
- this.connected = false;
6990
- this.stopPings();
6991
- this.discardSocket();
6992
7450
  return;
6993
7451
  }
6994
- if (!this.connected) return void this.handleConnectionFailure({
6995
- url2Connect,
6996
- cause: "error"
6997
- });
6998
- const streamError = err instanceof Error ? err : new Error("WebSocket stream emitted an error event", {
6999
- cause: this.lastSocketError ?? err
7452
+ this.recordingDimensions = {
7453
+ width: this.canvas.width,
7454
+ height: this.canvas.height
7455
+ };
7456
+ this.frame = new canvas_to_buffer(this.canvas, this.options.image.types, this.options.image.quality);
7457
+ this.options.logger.debug("Recorder: record()");
7458
+ this.recordingActive = true;
7459
+ this.recordingTransportFailed = false;
7460
+ this.mediaTransmissionStarted = false;
7461
+ this.recordingAudioSampleRate = void 0;
7462
+ this.userMedia.record();
7463
+ this.emit("RECORDING", {
7464
+ framesCount: this.framesCount
7000
7465
  });
7001
- if (this.mediaTransmissionStarted) return void this.failInterruptedRecording();
7002
- this.emit("ERROR", {
7003
- err: error_createError({
7004
- message: "WebSocket stream error",
7005
- explanation: `The WebSocket stream emitted an error event. Details: ${string_pretty(err)}`,
7006
- options: this.options,
7007
- exc: streamError
7008
- })
7466
+ loop.on("update", (_deltaTime, elapsedTime)=>{
7467
+ let avgFPS;
7468
+ avgFPS = 0 !== elapsedTime ? Math.round(this.framesCount / elapsedTime * 1000) : void 0;
7469
+ this.options.logger.debug(`Recorder updates avgFps = ${avgFPS} at frame ${this.framesCount}`);
7009
7470
  });
7471
+ loop.start();
7010
7472
  }
7011
- sendPings() {
7012
- this.stopPings();
7013
- this.pingInterval = window.setInterval(()=>{
7014
- this.options.logger.debug("Recorder: pinging...");
7015
- this.writeStream(Buffer.from(""));
7016
- }, this.options.timeouts.pingInterval);
7017
- }
7018
- stopPings() {
7019
- if (void 0 === this.pingInterval) return;
7020
- this.options.logger.debug("Recorder: stopping pings...");
7021
- window.clearInterval(this.pingInterval);
7022
- this.pingInterval = void 0;
7023
- }
7024
- onAudioSample(audioSample) {
7025
- if (this.recordingTransportFailed || this.cameraRecovery) return;
7026
- const sampleRate = this.userMedia?.getAudioSampleRate();
7027
- if (sampleRate && sampleRate > 0) this.recordingAudioSampleRate ??= sampleRate;
7028
- this.samplesCount++;
7029
- this.mediaTransmissionStarted = true;
7030
- const audioBuffer = audioSample.toBuffer();
7031
- this.writeStream(audioBuffer);
7473
+ setAnimationFrameObject(newObj) {
7474
+ if (this.loop) {
7475
+ const isRecording = this.isRecording();
7476
+ this.loop.stop();
7477
+ this.loop.setRequestAnimationFrameObject(newObj);
7478
+ if (isRecording) this.loop.start();
7479
+ }
7032
7480
  }
7033
- show() {
7034
- html_showElement(this.recorderElement);
7481
+ restoreAnimationFrameObject() {
7482
+ this.options.logger.debug("Recorder: restoreAnimationFrameObject()");
7483
+ if (this.originalAnimationFrameObject) this.setAnimationFrameObject(this.originalAnimationFrameObject);
7035
7484
  }
7036
- onUserMediaReady(params) {
7037
- try {
7038
- this.options.logger.debug(`Recorder: onUserMediaReady(${params ? string_pretty(params) : ""})`);
7039
- const switchingFacingMode = params?.switchingFacingMode;
7040
- if (params?.recovering) {
7041
- if (!this.userMedia?.hasLiveStream()) return void this.failCameraRecovery(new Error("The restored camera has no live tracks."));
7042
- if (void 0 !== this.recordingAudioSampleRate && this.userMedia.getAudioSampleRate() !== this.recordingAudioSampleRate) return void this.failCameraRecovery(new Error("The audio sample rate changed during recovery."));
7043
- }
7044
- this.userMediaLoading = this.blocking = this.unloaded = this.submitting = false;
7045
- this.userMediaLoaded = true;
7046
- this.clearUserMediaTimeout();
7047
- if (!switchingFacingMode && !params?.recovering) this.loop = this.createLoop();
7048
- this.show();
7049
- if (params?.recordWhenReady) this.record();
7050
- this.emit("USER_MEDIA_READY", {
7051
- switchingFacingMode: params?.switchingFacingMode,
7052
- recovering: params?.recovering,
7053
- paused: this.isPaused(),
7054
- recordWhenReady: params?.recordWhenReady
7055
- });
7056
- if (params?.recovering) this.finishCameraRecovery(true);
7057
- } catch (exc) {
7058
- if (params?.recovering) this.failCameraRecovery(exc);
7059
- else this.emit("ERROR", {
7060
- exc
7061
- });
7062
- }
7485
+ loopWithTimeouts() {
7486
+ this.options.logger.debug("Recorder: loopWithTimeouts()");
7487
+ const wantedInterval = 1e3 / this.options.video.fps;
7488
+ let processingTime = 0;
7489
+ const raf = (fn)=>window.setTimeout(()=>{
7490
+ const start = Date.now();
7491
+ fn();
7492
+ processingTime = Date.now() - start;
7493
+ }, wantedInterval - processingTime);
7494
+ const cancel = (id)=>{
7495
+ window.clearTimeout(id);
7496
+ };
7497
+ this.setAnimationFrameObject({
7498
+ requestAnimationFrame: raf,
7499
+ cancelAnimationFrame: cancel
7500
+ });
7063
7501
  }
7064
- clearRetryTimeout() {
7065
- if (void 0 === this.retryTimeout) return;
7066
- this.options.logger.debug("Recorder: clearRetryTimeout()");
7067
- window.clearTimeout(this.retryTimeout);
7068
- this.retryTimeout = void 0;
7502
+ isPaused() {
7503
+ return this.userMedia?.isPaused() && !this.loop?.isRunning();
7069
7504
  }
7070
- clearConnectionTimeout() {
7071
- if (void 0 === this.connectionTimeout) return;
7072
- this.options.logger.debug("Recorder: clearConnectionTimeout()");
7073
- window.clearTimeout(this.connectionTimeout);
7074
- this.connectionTimeout = void 0;
7505
+ isUserMediaPaused() {
7506
+ return this.userMedia?.isPaused() ?? false;
7075
7507
  }
7076
- clearBackpressureTimeout() {
7077
- if (void 0 === this.backpressureTimeout) return;
7078
- window.clearTimeout(this.backpressureTimeout);
7079
- this.backpressureTimeout = void 0;
7508
+ isCaptureLoopRunning() {
7509
+ return this.loop?.isRunning() ?? false;
7080
7510
  }
7081
- clearReconnectTimeout() {
7082
- if (void 0 === this.reconnectTimeout) return;
7083
- this.options.logger.debug("Recorder: clearReconnectTimeout()");
7084
- window.clearTimeout(this.reconnectTimeout);
7085
- this.reconnectTimeout = void 0;
7511
+ stopCaptureLoop() {
7512
+ this.loop?.stop();
7086
7513
  }
7087
- handleConnectionFailure(params) {
7088
- if (this.retryConnection(params)) return;
7089
- this.failConnection(params);
7514
+ startCaptureLoop() {
7515
+ this.loop?.start();
7090
7516
  }
7091
- retryConnection(params) {
7092
- if (!this.canRetryConnection()) return false;
7093
- if (void 0 !== this.reconnectTimeout) return true;
7094
- const delayMs = Math.min(1000 * 2 ** this.reconnectAttempts, 10000);
7095
- this.reconnectAttempts++;
7096
- this.connecting = false;
7097
- this.clearConnectionTimeout();
7098
- this.discardSocket();
7099
- this.options.logger.debug(`Recorder: retrying WebSocket connection in ${delayMs}ms (cause ${params.cause}).`);
7100
- this.reconnectTimeout = window.setTimeout(()=>{
7101
- this.reconnectTimeout = void 0;
7102
- if (this.shouldContinueReconnect()) this.initSocket();
7103
- }, delayMs);
7104
- return true;
7517
+ isRecording() {
7518
+ return Boolean(this.loop?.isRunning() && !this.isPaused() && !this.isNotifying() && this.stream && !this.stream.destroyed);
7105
7519
  }
7106
- canRetryConnection() {
7107
- return Boolean(this.reconnecting && this.userMediaLoaded && this.isOnline() && !this.unloaded && !this.blocking);
7520
+ getRecordingDimensions() {
7521
+ return this.recordingDimensions && {
7522
+ ...this.recordingDimensions
7523
+ };
7108
7524
  }
7109
- shouldContinueReconnect() {
7110
- return this.reconnecting && !this.unloaded && !this.blocking;
7525
+ constructor(visuals, replay, options){
7526
+ super(visuals, options), capture_define_property(this, "replay", void 0), capture_define_property(this, "loop", void 0), capture_define_property(this, "originalAnimationFrameObject", void 0), capture_define_property(this, "recordingAudioSampleRate", void 0), capture_define_property(this, "frameProgress", void 0), capture_define_property(this, "sampleProgress", void 0), capture_define_property(this, "canvas", void 0), capture_define_property(this, "ctx", void 0), capture_define_property(this, "stopTime", void 0), capture_define_property(this, "waitingTime", void 0), capture_define_property(this, "frame", void 0), capture_define_property(this, "recordingBuffer", void 0);
7527
+ this.replay = replay;
7111
7528
  }
7112
- discardSocket() {
7113
- this.options.logger.debug("Recorder: discarding socket stream ...");
7114
- this.clearBackpressureTimeout();
7115
- this.pendingWrites.clear();
7116
- this.streamBackpressured = false;
7117
- this.resumeAfterDrain = false;
7118
- const stream = this.stream;
7119
- this.stream = void 0;
7120
- if (stream && !stream.destroyed) stream.destroy();
7529
+ }
7530
+ const capture = RecorderCapture;
7531
+ function figureMinHeight(height, options) {
7532
+ let minHeight;
7533
+ if (options.video.height) {
7534
+ minHeight = height ? Math.min(options.video.height, height) : options.video.height;
7535
+ if (minHeight < 1) throw error_createError({
7536
+ message: `Got a min height less than 1 (${minHeight})!`,
7537
+ options
7538
+ });
7539
+ } else minHeight = height;
7540
+ return minHeight;
7541
+ }
7542
+ const dimensions_figureMinHeight = figureMinHeight;
7543
+ function getRatio(options, videoHeight, videoWidth) {
7544
+ let ratio = 1;
7545
+ const hasVideoDimensions = videoHeight && videoWidth;
7546
+ const desiredHeight = options.video.height;
7547
+ const desiredWidth = options.video.width;
7548
+ const hasDesiredDimensions = desiredHeight && desiredWidth;
7549
+ if (hasDesiredDimensions) ratio = hasVideoDimensions ? videoHeight < desiredHeight || videoWidth < desiredWidth ? videoHeight / videoWidth : desiredHeight / desiredWidth : desiredHeight / desiredWidth;
7550
+ else if (hasVideoDimensions) ratio = videoHeight / videoWidth;
7551
+ return ratio;
7552
+ }
7553
+ const dimensions_getRatio = getRatio;
7554
+ function calculateHeight(responsive, videoWidth, options, ratio, element) {
7555
+ const dimension = {
7556
+ unit: "px"
7557
+ };
7558
+ let width = videoWidth;
7559
+ if (responsive && element) {
7560
+ const limitedDimension = dimensions_limitWidth(element, options, width);
7561
+ width = limitedDimension.value;
7121
7562
  }
7122
- isOnline() {
7123
- return navigator.onLine;
7563
+ const chosenRatio = ratio ?? dimensions_getRatio(options, void 0, videoWidth);
7564
+ const height = width ? Math.round(width * chosenRatio) : void 0;
7565
+ if (height && Number.isInteger(height) && height < 1) throw error_createError({
7566
+ message: "Just calculated a height less than 1 which is wrong.",
7567
+ options
7568
+ });
7569
+ const minHeight = dimensions_figureMinHeight(height, options);
7570
+ dimension.value = minHeight;
7571
+ return dimension;
7572
+ }
7573
+ const dimensions_calculateHeight = calculateHeight;
7574
+ function calculateWidth(responsive, options, videoHeight, ratio) {
7575
+ const dimension = {
7576
+ unit: "px"
7577
+ };
7578
+ let height = dimensions_figureMinHeight(videoHeight, options);
7579
+ if (responsive) {
7580
+ const limitedDimension = dimensions_limitHeight(height, options, "calculateWidth");
7581
+ height = limitedDimension.value;
7124
7582
  }
7125
- failConnection(params) {
7126
- if (this.shouldIgnoreConnectionFailure()) return;
7127
- this.connectionFailed = true;
7128
- this.connecting = false;
7129
- this.reconnecting = false;
7130
- this.reconnectAttempts = 0;
7131
- this.clearReconnectTimeout();
7132
- this.clearConnectionTimeout();
7133
- const { url2Connect, cause } = params;
7134
- const online = this.isOnline();
7135
- const elapsedMs = this.connectingStartedAt ? Date.now() - this.connectingStartedAt : void 0;
7136
- const closeEvent = this.lastCloseEvent;
7137
- const socketError = this.lastSocketError;
7138
- const contents = {
7139
- cause,
7140
- closeCode: closeEvent?.code,
7141
- closeReason: closeEvent?.reason,
7142
- elapsedMs,
7143
- online,
7144
- secureContext: globalThis.isSecureContext,
7145
- socketReadyState: this.stream?.socket.readyState,
7146
- timeoutMs: this.options.timeouts.connection,
7147
- url: url2Connect,
7148
- unloaded: this.unloaded,
7149
- userMediaLoaded: this.userMediaLoaded,
7150
- userMediaLoading: this.userMediaLoading,
7151
- wasClean: closeEvent?.wasClean,
7152
- socketError,
7153
- blocking: this.blocking,
7154
- visibilityState: document.visibilityState
7155
- };
7156
- const debugLine = string_summarize("Recorder: failConnection() diagnostic", contents);
7157
- this.options.logger.debug(debugLine);
7158
- let explanation;
7159
- 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.`;
7160
- 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.`;
7161
- else {
7162
- const closeSuffix = closeEvent ? ` (code ${closeEvent.code})` : "";
7163
- explanation = `Connection to ${url2Connect} is closed${closeSuffix}. Please check your internet connection and try again. If the problem persists, contact us.`;
7583
+ if (!height || height < 1) throw error_createError({
7584
+ message: `Height ${height} cannot be smaller than 1 when calculating width.`,
7585
+ options
7586
+ });
7587
+ const chosenRatio = ratio ?? dimensions_getRatio(options, videoHeight);
7588
+ const calculatedWidth = Math.round(height / chosenRatio);
7589
+ if (calculatedWidth < 1) throw error_createError({
7590
+ message: "Calculated width cannot be smaller than 1!",
7591
+ options
7592
+ });
7593
+ dimension.value = calculatedWidth;
7594
+ return dimension;
7595
+ }
7596
+ const dimensions_calculateWidth = calculateWidth;
7597
+ function dimensions_define_property(obj, key, value) {
7598
+ if (key in obj) Object.defineProperty(obj, key, {
7599
+ value: value,
7600
+ enumerable: true,
7601
+ configurable: true,
7602
+ writable: true
7603
+ });
7604
+ else obj[key] = value;
7605
+ return obj;
7606
+ }
7607
+ class RecorderDimensions {
7608
+ correctDimensions() {
7609
+ const recorderElement = this.context.getRecorderElement();
7610
+ if (!recorderElement) return;
7611
+ const widthDimension = dimensions_useFullWidth(this.context.options.video.mobileBreakPoint);
7612
+ if (this.context.options.video.stretch || widthDimension) {
7613
+ recorderElement.style.width = "100%";
7614
+ recorderElement.style.removeProperty("height");
7615
+ } else {
7616
+ if (this.context.options.video.width) {
7617
+ const recorderWidth = this.getRecorderWidth(true);
7618
+ if (recorderWidth?.value) recorderElement.width = recorderWidth.value;
7619
+ else recorderElement.style.removeProperty("width");
7620
+ }
7621
+ if (this.context.options.video.height) {
7622
+ const recorderHeight = this.getRecorderHeight(true);
7623
+ if (recorderHeight?.value) recorderElement.height = recorderHeight.value;
7624
+ else recorderElement.style.removeProperty("height");
7625
+ }
7164
7626
  }
7165
- else explanation = "Your device appears to be offline. Please check your internet connection and try again.";
7166
- this.discardSocket();
7167
- const err = error_createError({
7168
- message: "Unable to connect to the server",
7169
- explanation,
7170
- options: this.options,
7171
- cause: contents,
7172
- exc: socketError ? new Error("WebSocket connection error", {
7173
- cause: socketError
7174
- }) : void 0
7175
- });
7176
- this.emit("ERROR", {
7177
- err
7178
- });
7179
7627
  }
7180
- shouldIgnoreConnectionFailure() {
7181
- return this.connectionFailed || this.connected || Boolean(this.unloaded) || this.validate();
7628
+ getRecorderWidth(responsive) {
7629
+ const userMedia = this.context.getUserMedia();
7630
+ if (userMedia?.hasVideoWidth()) return userMedia.getRawWidth(responsive);
7631
+ else if (responsive && this.context.options.video.width) return this.context.visuals.limitWidth(this.context.options.video.width);
7632
+ const dimension = {
7633
+ unit: "px",
7634
+ value: this.context.options.video.width
7635
+ };
7636
+ return dimension;
7182
7637
  }
7183
- calculateFrameProgress() {
7184
- return `${(this.confirmedFrameNumber / (this.framesCount || 1) * 100).toFixed(2)}%`;
7638
+ getRecorderHeight(responsive, useBoundingClientRect) {
7639
+ const recorderElement = this.context.getRecorderElement();
7640
+ const userMedia = this.context.getUserMedia();
7641
+ let recorderHeight;
7642
+ if (recorderElement && useBoundingClientRect) {
7643
+ const height = recorderElement.getBoundingClientRect().height;
7644
+ recorderHeight = {
7645
+ unit: "px",
7646
+ value: height
7647
+ };
7648
+ } else if (userMedia) {
7649
+ let height = userMedia.getRawHeight(responsive);
7650
+ if (void 0 !== height && this.context.options.video.width) {
7651
+ const ratio = this.getRatio();
7652
+ if (void 0 !== ratio) {
7653
+ const idealHeight = this.context.options.video.width * ratio;
7654
+ height = Math.min(idealHeight, height);
7655
+ }
7656
+ }
7657
+ recorderHeight = {
7658
+ unit: "px",
7659
+ value: height
7660
+ };
7661
+ } else if (responsive && this.context.options.video.height) recorderHeight = this.calculateHeight(responsive);
7662
+ else if (this.context.options.video.height) {
7663
+ const height = this.context.options.video.height;
7664
+ recorderHeight = {
7665
+ unit: "px",
7666
+ value: height
7667
+ };
7668
+ }
7669
+ return recorderHeight;
7185
7670
  }
7186
- calculateSampleProgress() {
7187
- return `${(this.confirmedSampleNumber / (this.samplesCount || 1) * 100).toFixed(2)}%`;
7671
+ getRatio() {
7672
+ const userMedia = this.context.getUserMedia();
7673
+ let ratio;
7674
+ if (userMedia) {
7675
+ const userMediaVideoWidth = userMedia.getVideoWidth();
7676
+ const userMediaVideoHeight = userMedia.getVideoHeight();
7677
+ if (!userMediaVideoWidth || userMediaVideoWidth < 1) ratio = this.context.visuals.getRatio();
7678
+ else if (userMediaVideoHeight) ratio = userMediaVideoHeight / userMediaVideoWidth;
7679
+ } else ratio = dimensions_getRatio(this.context.options);
7680
+ return ratio;
7188
7681
  }
7189
- updateOverallProgress() {
7190
- this.frameProgress = this.calculateFrameProgress();
7191
- if (isAudioEnabled(this.options)) this.sampleProgress = this.calculateSampleProgress();
7192
- this.emit("PROGRESS", {
7193
- frameProgress: this.frameProgress,
7194
- sampleProgress: this.sampleProgress
7195
- });
7682
+ calculateWidth(responsive) {
7683
+ const userMedia = this.context.getUserMedia();
7684
+ const recorderElement = this.context.getRecorderElement();
7685
+ let videoHeight;
7686
+ if (userMedia) videoHeight = userMedia.getVideoHeight();
7687
+ else if (recorderElement) videoHeight = recorderElement.videoHeight || recorderElement.height;
7688
+ return dimensions_calculateWidth(responsive, this.context.options, videoHeight, this.getRatio());
7196
7689
  }
7197
- updateFrameProgress(args) {
7198
- if (args) {
7199
- if (!args.frame) throw error_createError({
7200
- message: "The frame number is missing",
7201
- options: this.options
7202
- });
7203
- } else throw error_createError({
7204
- message: "Arguments are missing for updating the frame progress",
7205
- options: this.options
7206
- });
7207
- this.confirmedFrameNumber = args.frame;
7208
- this.frameProgress = this.calculateFrameProgress();
7209
- this.updateOverallProgress();
7690
+ calculateHeight(responsive) {
7691
+ const userMedia = this.context.getUserMedia();
7692
+ const recorderElement = this.context.getRecorderElement();
7693
+ let videoDimension;
7694
+ if (userMedia) {
7695
+ const videoHeight = userMedia.getVideoHeight();
7696
+ videoDimension = {
7697
+ value: videoHeight,
7698
+ unit: "px"
7699
+ };
7700
+ } else if (recorderElement) {
7701
+ const videoHeight = recorderElement.videoHeight || recorderElement.height;
7702
+ videoDimension = {
7703
+ value: videoHeight,
7704
+ unit: "px"
7705
+ };
7706
+ } else videoDimension = dimensions_calculateHeight(responsive, void 0, this.context.options, this.getRatio(), recorderElement);
7707
+ return videoDimension;
7210
7708
  }
7211
- updateSampleProgress(args) {
7212
- if (args) {
7213
- if (!args.sample) throw error_createError({
7214
- message: "The audio sample number is missing",
7215
- options: this.options
7216
- });
7217
- } else throw error_createError({
7218
- message: "Arguments are missing for updating the audio sample progress",
7219
- options: this.options
7220
- });
7221
- this.confirmedSampleNumber = args.sample;
7222
- this.sampleProgress = this.calculateSampleProgress();
7223
- this.updateOverallProgress();
7709
+ constructor(context){
7710
+ dimensions_define_property(this, "context", void 0);
7711
+ this.context = context;
7224
7712
  }
7225
- preview(args) {
7226
- if (!args) throw error_createError({
7227
- message: "Preview arguments are missing.",
7228
- options: this.options
7229
- });
7230
- const hasAudio = this.samplesCount > 0;
7231
- this.mediaTransmissionStarted = false;
7232
- this.recordingActive = false;
7233
- this.recordingTransportFailed = false;
7234
- this.confirmedFrameNumber = this.confirmedSampleNumber = this.samplesCount = this.framesCount = 0;
7235
- this.sampleProgress = this.frameProgress = void 0;
7236
- this.key = args.key;
7237
- if (args.mp4) this.replay.setMp4Source(`${args.mp4 + constants.WHITELIST_KEY_LABEL}/${this.options.whitelistKey}/videomail.mp4`, true);
7238
- if (args.webm) this.replay.setWebMSource(`${args.webm + constants.WHITELIST_KEY_LABEL}/${this.options.whitelistKey}/videomail.webm`, true);
7239
- this.hide();
7240
- const widthDimension = this.getRecorderWidth(true);
7241
- const heightDimension = this.getRecorderHeight(true);
7242
- const duration = args.duration ?? -1;
7243
- this.emit("PREVIEW", {
7244
- key: this.key,
7245
- width: widthDimension?.value,
7246
- height: heightDimension?.value,
7247
- hasAudio,
7248
- duration
7249
- });
7250
- if (this.stopTime) this.waitingTime = Date.now() - this.stopTime;
7251
- this.recordingStats ??= {};
7252
- this.recordingStats.waitingTime = this.waitingTime;
7713
+ }
7714
+ const recorder_dimensions = RecorderDimensions;
7715
+ function getFirstVideoTrack(localMediaStream) {
7716
+ const videoTracks = localMediaStream.getVideoTracks();
7717
+ let videoTrack;
7718
+ if (videoTracks[0]) videoTrack = videoTracks[0];
7719
+ return videoTrack;
7720
+ }
7721
+ const media_getFirstVideoTrack = getFirstVideoTrack;
7722
+ const MEDIA_EVENTS = [
7723
+ "loadstart",
7724
+ "suspend",
7725
+ "progress",
7726
+ "abort",
7727
+ "emptied",
7728
+ "stalled",
7729
+ "pause",
7730
+ "loadeddata",
7731
+ "waiting",
7732
+ "playing",
7733
+ "canplay",
7734
+ "canplaythrough",
7735
+ "seeking",
7736
+ "seeked",
7737
+ "ended",
7738
+ "ratechange",
7739
+ "durationchange",
7740
+ "volumechange"
7741
+ ];
7742
+ const mediaEvents = MEDIA_EVENTS;
7743
+ function AudioRecorder_define_property(obj, key, value) {
7744
+ if (key in obj) Object.defineProperty(obj, key, {
7745
+ value: value,
7746
+ enumerable: true,
7747
+ configurable: true,
7748
+ writable: true
7749
+ });
7750
+ else obj[key] = value;
7751
+ return obj;
7752
+ }
7753
+ const CHANNELS = 1;
7754
+ function getAudioContextClass() {
7755
+ return window.AudioContext;
7756
+ }
7757
+ class AudioRecorder {
7758
+ hasAudioContext() {
7759
+ return Boolean(getAudioContextClass()) && Boolean(this.getAudioContext());
7253
7760
  }
7254
- initSocket(cb) {
7255
- if (this.connected) {
7256
- this.options.logger.debug("Recorder: already **connected**. Not going to initialize a new WebSocket connection.");
7257
- cb?.();
7258
- return;
7259
- }
7260
- if (this.connecting) return void this.options.logger.debug("Recorder: already **connecting**. Not going to initialize a new WebSocket connection.");
7261
- this.clearReconnectTimeout();
7262
- if (util_isAutomatedUserAgent()) {
7263
- this.connecting = false;
7264
- this.options.logger.debug("Recorder: skipping web socket connection for an automated crawler");
7265
- return;
7266
- }
7267
- this.connecting = true;
7268
- this.connectionFailed = false;
7269
- this.connectingStartedAt = Date.now();
7270
- this.lastCloseEvent = void 0;
7271
- this.lastSocketError = void 0;
7272
- this.emit("CONNECTING");
7273
- let url2Connect;
7274
- try {
7275
- const socketUrlObj = new URL(this.options.socketUrl);
7276
- socketUrlObj.searchParams.set(constants.WHITELIST_KEY_LABEL, this.options.whitelistKey);
7277
- url2Connect = socketUrlObj.toString();
7278
- } catch (exc) {
7279
- this.connecting = this.connected = false;
7280
- const err = error_createError({
7281
- message: "Invalid WebSocket URL",
7282
- explanation: `The configured socketUrl "${this.options.socketUrl}" is not a valid URL. Please check your videomail-client configuration.`,
7283
- options: this.options,
7284
- exc
7285
- });
7286
- this.emit("ERROR", {
7287
- err
7288
- });
7289
- return;
7761
+ getAudioContext() {
7762
+ if (!this.vcAudioContext) {
7763
+ const AudioContext = getAudioContextClass();
7764
+ this.vcAudioContext = new AudioContext();
7290
7765
  }
7291
- this.options.logger.debug(`Recorder: initializing web socket stream to ${url2Connect}`);
7292
- let nativeSocket;
7293
- try {
7294
- nativeSocket = new WebSocket(url2Connect);
7295
- nativeSocket.addEventListener("close", (event)=>{
7296
- this.lastCloseEvent = {
7297
- code: event.code,
7298
- reason: event.reason,
7299
- wasClean: event.wasClean
7300
- };
7301
- });
7302
- nativeSocket.addEventListener("error", (event)=>{
7303
- this.lastSocketError = error_getEventDetails(event);
7304
- });
7305
- } catch (exc) {
7306
- this.connecting = this.connected = false;
7307
- const diagnostic = error_getWebSocketDiagnostic(url2Connect);
7308
- const message = diagnostic.looksAutomated ? "Automated crawler: WebSocket not supported in this environment" : `Failed to construct WebSocket to ${url2Connect}`;
7309
- 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}`;
7310
- const err = error_createError({
7311
- message,
7312
- explanation,
7313
- options: this.options,
7314
- exc
7315
- });
7316
- this.emit("ERROR", {
7317
- err
7318
- });
7319
- return;
7766
+ return this.vcAudioContext;
7767
+ }
7768
+ onAudioProcess(e, cb) {
7769
+ if (!this.userMedia.isRecording() || this.userMedia.isPaused()) return;
7770
+ const float32Array = e.inputBuffer.getChannelData(0);
7771
+ cb(new audio_sample(float32Array));
7772
+ }
7773
+ disconnect() {
7774
+ if (this.scriptProcessor) {
7775
+ this.scriptProcessor.onaudioprocess = null;
7776
+ this.scriptProcessor.disconnect();
7320
7777
  }
7778
+ this.audioInput?.disconnect();
7779
+ this.volume?.disconnect();
7780
+ }
7781
+ init(localMediaStream) {
7782
+ this.options.logger.debug("AudioRecorder: init()");
7783
+ this.disconnect();
7784
+ const volume = this.getAudioContext().createGain();
7785
+ this.volume = volume;
7321
7786
  try {
7322
- this.stream = websocket_stream(nativeSocket);
7787
+ this.audioInput = this.getAudioContext().createMediaStreamSource(localMediaStream);
7323
7788
  } catch (exc) {
7324
- this.connecting = this.connected = false;
7325
- const err = error_createError({
7326
- message: `Failed to create a stream to ${url2Connect}`,
7327
- explanation: "Please check your connection and try again. If the problem persists, contact us.",
7328
- options: this.options,
7329
- exc
7330
- });
7331
- this.emit("ERROR", {
7332
- err
7333
- });
7334
- }
7335
- const stream = this.stream;
7336
- if (stream) {
7337
- const connectionTimeoutMs = this.options.timeouts.connection;
7338
- this.connectionTimeout = window.setTimeout(()=>{
7339
- if (this.isOnline() && this.isUserMediaLoaded()) {
7340
- this.options.logger.debug(`${PIPE_SYMBOL}Reconnecting due to connection timeout.`);
7341
- this.reconnecting = true;
7342
- this.handleConnectionFailure({
7343
- url2Connect,
7344
- cause: "timeout"
7345
- });
7346
- } else this.handleConnectionFailure({
7347
- url2Connect,
7348
- cause: "timeout"
7349
- });
7350
- }, connectionTimeoutMs);
7351
- stream.on("close", ()=>{
7352
- this.handleStreamClose(url2Connect);
7353
- });
7354
- stream.on("connect", (args)=>{
7355
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *connect* event emitted with args: ${string_pretty(args)}`);
7356
- this.clearConnectionTimeout();
7357
- const isClosing = this.stream?.socket.readyState === WebSocket.CLOSING;
7358
- if (!this.connected && !isClosing && !this.unloaded) {
7359
- this.connected = true;
7360
- this.connecting = this.unloaded = false;
7361
- this.reconnecting = false;
7362
- this.reconnectAttempts = 0;
7363
- this.clearReconnectTimeout();
7364
- this.emit("CONNECTED");
7365
- cb?.();
7366
- }
7367
- });
7368
- stream.on("data", (data)=>{
7369
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *data* event emitted`);
7370
- try {
7371
- const command = JSON.parse(data.toString());
7372
- this.executeCommand(command);
7373
- } catch (exc) {
7374
- this.options.logger.error(`Failed to parse command: ${exc}`);
7375
- const err = error_createError({
7376
- message: "Invalid server command",
7377
- explanation: `Contact us. The invalid command was: ${data.toString()}.`,
7378
- options: this.options,
7379
- exc
7380
- });
7381
- this.emit("ERROR", {
7382
- err
7383
- });
7384
- }
7385
- });
7386
- stream.on("error", (err)=>{
7387
- this.handleStreamError(url2Connect, err);
7388
- });
7389
- stream.on("drain", ()=>{
7390
- this.handleStreamDrain(stream);
7391
- });
7392
- stream.on("preend", (args)=>{
7393
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *preend* event emitted with args: ${string_pretty(args)}`);
7394
- });
7395
- stream.on("end", ()=>{
7396
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *end* event emitted`);
7397
- });
7398
- stream.on("pipe", (src)=>{
7399
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *pipe* event emitted with src: ${string_pretty(src)}`);
7400
- });
7401
- stream.on("unpipe", (src)=>{
7402
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *unpipe* event emitted with src: ${string_pretty(src)}`);
7403
- });
7404
- stream.on("resume", (args)=>{
7405
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *resume* event emitted with args: ${string_pretty(args)}`);
7406
- });
7407
- stream.on("uncork", (args)=>{
7408
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *uncork* event emitted with args: ${string_pretty(args)}`);
7409
- });
7410
- stream.on("readable", (args)=>{
7411
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *readable* event emitted with args: ${string_pretty(args)}`);
7412
- });
7413
- stream.on("prefinish", (args)=>{
7414
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *prefinish* event emitted with args: ${string_pretty(args)}`);
7415
- });
7416
- stream.on("finish", (args)=>{
7417
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *finish* event emitted with args: ${string_pretty(args)}`);
7789
+ throw error_createError({
7790
+ message: "Webcam has no audio",
7791
+ exc,
7792
+ options: this.options
7418
7793
  });
7419
7794
  }
7795
+ let { bufferSize } = this.options.audio;
7796
+ if ("auto" === bufferSize) bufferSize = util_getBrowser(this.options).isFirefox() ? 512 : 2048;
7797
+ if (void 0 === bufferSize || !is_power_of_two(bufferSize)) throw error_createError({
7798
+ message: `Audio buffer size must be a power of two. The current buffer size is ${bufferSize}.`,
7799
+ options: this.options
7800
+ });
7801
+ if (!Number.isFinite(this.options.audio.volume) || this.options.audio.volume < 0 || this.options.audio.volume > 1) throw error_createError({
7802
+ message: `Audio volume must be between zero and one. The current volume is ${this.options.audio.volume}.`,
7803
+ options: this.options
7804
+ });
7805
+ volume.gain.value = this.options.audio.volume;
7806
+ this.scriptProcessor = this.getAudioContext().createScriptProcessor(bufferSize, CHANNELS, CHANNELS);
7807
+ this.audioInput.connect(volume);
7808
+ volume.connect(this.scriptProcessor);
7809
+ this.scriptProcessor.connect(this.getAudioContext().destination);
7420
7810
  }
7421
- showUserMedia() {
7422
- if (!this.connected) return false;
7423
- const hidden = this.isHidden();
7424
- if (!hidden) return true;
7425
- const notifying = this.isNotifying();
7426
- if (notifying) return true;
7427
- return this.blocking;
7811
+ record(cb) {
7812
+ this.options.logger.debug("AudioRecorder: record()");
7813
+ if (this.scriptProcessor) this.scriptProcessor.onaudioprocess = (e)=>{
7814
+ this.onAudioProcess(e, cb);
7815
+ };
7428
7816
  }
7429
- userMediaErrorCallback(err, usedConstraints) {
7430
- if (this.cameraRecovery) return void this.failCameraRecovery(err);
7431
- if (this.unloaded) return void this.options.logger.debug(`Recorder: already unloaded. Not going to throw error ${string_pretty(err)}`);
7432
- this.userMediaLoading = false;
7433
- this.clearUserMediaTimeout();
7434
- const characteristics = this.userMedia?.getCharacteristics();
7435
- this.options.logger.debug(`Recorder: userMediaErrorCallback(), name: ${err.name}, message: ${err.message} and Webcam characteristics: ${characteristics ? string_pretty(characteristics) : "none"}`);
7436
- const errorListeners = this.getListeners("ERROR");
7437
- if (errorListeners?.length) if (err.name !== error_VideomailError.MEDIA_DEVICE_NOT_SUPPORTED) {
7438
- const videomailError = error_createError({
7439
- err,
7440
- options: this.options,
7441
- usedConstraints
7442
- });
7443
- this.emit("ERROR", {
7444
- err: videomailError
7445
- });
7446
- } else {
7447
- this.options.logger.debug(`Recorder: ignore user media error ${string_pretty(err)}`);
7448
- this.retryTimeout = window.setTimeout(()=>{
7449
- this.retryTimeout = void 0;
7450
- if (!this.unloaded) this.loadUserMedia();
7451
- }, this.options.timeouts.userMedia);
7452
- }
7453
- else {
7454
- this.options.logger.debug(`Recorder: no error listeners attached but throwing error ${string_pretty(err)}`);
7455
- throw error_createError({
7817
+ async resume() {
7818
+ const context = this.getAudioContext();
7819
+ if ("running" !== context.state) await context.resume();
7820
+ }
7821
+ stop() {
7822
+ this.options.logger.debug("AudioRecorder: stop()");
7823
+ this.disconnect();
7824
+ if (this.hasAudioContext()) this.getAudioContext().close().then(()=>{
7825
+ this.options.logger.debug("AudioRecorder: audio context is closed");
7826
+ this.vcAudioContext = void 0;
7827
+ }).catch((err)=>{
7828
+ if (err instanceof Error) return void this.options.logger.error(error_createError({
7456
7829
  err,
7457
- message: "Unable to process this error since there are no error listeners anymore.",
7458
7830
  options: this.options
7459
- });
7460
- }
7831
+ }));
7832
+ this.options.logger.error(err);
7833
+ });
7461
7834
  }
7462
- getUserMediaCallback(localStream, params, requestId = this.mediaRequestId) {
7463
- if (!this.userMedia) {
7464
- localStream.getTracks().forEach((track)=>{
7465
- track.stop();
7466
- });
7467
- throw error_createError({
7468
- message: "No user media is defined",
7469
- options: this.options
7470
- });
7471
- }
7472
- this.options.logger.debug(`Recorder: getUserMediaCallback(${params ? string_pretty(params) : ""})`);
7473
- if (this.showUserMedia()) try {
7474
- this.userMedia.init(localStream, ()=>{
7475
- if (!this.unloaded && requestId === this.mediaRequestId) this.onUserMediaReady(params);
7476
- }, this.onAudioSample.bind(this), (err)=>{
7477
- if (!this.unloaded && requestId === this.mediaRequestId) this.userMediaErrorCallback(err, {
7478
- audio: isAudioEnabled(this.options)
7479
- });
7480
- }, params);
7481
- } catch (exc) {
7482
- localStream.getTracks().forEach((track)=>{
7835
+ getSampleRate() {
7836
+ if (this.hasAudioContext()) return this.getAudioContext().sampleRate;
7837
+ return -1;
7838
+ }
7839
+ constructor(userMedia, options){
7840
+ AudioRecorder_define_property(this, "scriptProcessor", void 0);
7841
+ AudioRecorder_define_property(this, "audioInput", void 0);
7842
+ AudioRecorder_define_property(this, "volume", void 0);
7843
+ AudioRecorder_define_property(this, "vcAudioContext", void 0);
7844
+ AudioRecorder_define_property(this, "userMedia", void 0);
7845
+ AudioRecorder_define_property(this, "options", void 0);
7846
+ this.options = options;
7847
+ this.userMedia = userMedia;
7848
+ }
7849
+ }
7850
+ const media_AudioRecorder = AudioRecorder;
7851
+ const VIRTUAL_KEYWORDS = [
7852
+ "obs",
7853
+ "virtual",
7854
+ "manycam",
7855
+ "vcam",
7856
+ "splitcam",
7857
+ "droidcam"
7858
+ ];
7859
+ function isVirtualCamera(videoTrack) {
7860
+ const capabilities = videoTrack.getCapabilities();
7861
+ if (!capabilities.frameRate) return true;
7862
+ const isVirtual = VIRTUAL_KEYWORDS.some((keyword)=>videoTrack.label.toLowerCase().includes(keyword));
7863
+ return isVirtual;
7864
+ }
7865
+ const util_isVirtualCamera = isVirtualCamera;
7866
+ function userMedia_define_property(obj, key, value) {
7867
+ if (key in obj) Object.defineProperty(obj, key, {
7868
+ value: value,
7869
+ enumerable: true,
7870
+ configurable: true,
7871
+ writable: true
7872
+ });
7873
+ else obj[key] = value;
7874
+ return obj;
7875
+ }
7876
+ const EVENT_ASCII = "|—O—|";
7877
+ class UserMedia extends util_Despot {
7878
+ attachMediaStream(stream) {
7879
+ if (this.rawVisualUserMedia) {
7880
+ this.rawVisualUserMedia.srcObject = stream;
7881
+ const previousStream = this.currentVisualStream;
7882
+ this.currentVisualStream = stream;
7883
+ if (previousStream && previousStream !== stream) previousStream.getTracks().forEach((track)=>{
7483
7884
  track.stop();
7484
7885
  });
7485
- this.userMediaLoading = false;
7486
- this.clearUserMediaTimeout();
7487
- this.userMediaErrorCallback(exc, {
7488
- audio: isAudioEnabled(this.options)
7489
- });
7490
- }
7886
+ } else throw error_createError({
7887
+ message: "Error attaching stream to element.",
7888
+ explanation: "Contact the developer about this",
7889
+ options: this.options
7890
+ });
7891
+ }
7892
+ setVisualStream(localMediaStream) {
7893
+ if (localMediaStream) this.attachMediaStream(localMediaStream);
7491
7894
  else {
7492
- localStream.getTracks().forEach((track)=>{
7493
- track.stop();
7494
- });
7495
- this.userMediaLoading = false;
7496
- this.clearUserMediaTimeout();
7497
- this.cancelCameraRecovery();
7895
+ if (this.rawVisualUserMedia) {
7896
+ this.rawVisualUserMedia.srcObject = null;
7897
+ this.rawVisualUserMedia.removeAttribute("src");
7898
+ }
7899
+ this.currentVisualStream = void 0;
7498
7900
  }
7499
7901
  }
7500
- loadGenuineUserMedia(params) {
7501
- this.options.logger.debug(`Recorder: loadGenuineUserMedia(${params ? string_pretty(params) : ""})`);
7502
- this.emit("ASKING_WEBCAM_PERMISSION");
7503
- this.userMediaLoading = true;
7504
- const requestId = ++this.mediaRequestId;
7505
- const constraints = {
7506
- video: {
7507
- frameRate: {
7508
- ideal: this.options.video.fps
7509
- }
7510
- },
7511
- audio: isAudioEnabled(this.options)
7512
- };
7513
- this.configureVideoConstraints(constraints, params);
7514
- this.options.logger.debug(string_summarize("Recorder: our webcam constraints are:", constraints));
7515
- this.options.logger.debug(string_summarize("Recorder: available webcam constraints are:", navigator.mediaDevices.getSupportedConstraints()));
7516
- const streamPromise = navigator.mediaDevices.getUserMedia(constraints);
7517
- streamPromise.then((localStream)=>{
7518
- if (this.unloaded || requestId !== this.mediaRequestId) return void localStream.getTracks().forEach((track)=>{
7519
- track.stop();
7520
- });
7521
- this.getUserMediaCallback(localStream, params, requestId);
7522
- }).catch((reason)=>{
7523
- if (!this.unloaded && requestId === this.mediaRequestId) this.userMediaErrorCallback(reason, constraints);
7902
+ async restorePlayback() {
7903
+ if (!this.rawVisualUserMedia) throw new Error("No camera preview is available.");
7904
+ await this.rawVisualUserMedia.play();
7905
+ await this.audioRecorder?.resume();
7906
+ }
7907
+ hasLiveStream() {
7908
+ const videoReady = this.currentVisualStream?.getVideoTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
7909
+ const audioReady = !isAudioEnabled(this.options) || this.currentVisualStream?.getAudioTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
7910
+ return Boolean(this.currentVisualStream?.active && videoReady && audioReady);
7911
+ }
7912
+ hasEnded() {
7913
+ if (this.rawVisualUserMedia?.ended) return this.rawVisualUserMedia.ended;
7914
+ return !this.currentVisualStream?.active;
7915
+ }
7916
+ hasInvalidDimensions() {
7917
+ if (!this.rawVisualUserMedia) return false;
7918
+ const { videoWidth, videoHeight } = this.rawVisualUserMedia;
7919
+ return videoWidth > 0 && videoWidth < 3 || videoHeight > 0 && videoHeight < 3;
7920
+ }
7921
+ logEvent(eventType, params) {
7922
+ this.options.logger.debug(`UserMedia: ... ${EVENT_ASCII} event ${eventType}: ${string_pretty(params)}`);
7923
+ }
7924
+ unloadRemainingEventListeners() {
7925
+ this.options.logger.debug("UserMedia: unloadRemainingEventListeners()");
7926
+ this.clearInitializationListeners?.();
7927
+ mediaEvents.forEach((eventName)=>{
7928
+ this.rawVisualUserMedia?.removeEventListener(eventName, this.outputEvent);
7524
7929
  });
7930
+ this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
7931
+ }
7932
+ audioRecord(audioCallback) {
7933
+ this.removeListener("SENDING_FIRST_FRAME");
7934
+ this.audioRecorder?.record(audioCallback);
7525
7935
  }
7526
- configureVideoConstraints(constraints, params) {
7527
- const videoConstraints = "object" == typeof constraints.video ? constraints.video : void 0;
7528
- if (!videoConstraints) return;
7529
- if (params?.switchingFacingMode || params?.recovering) videoConstraints.facingMode = params.switchingFacingMode || this.facingMode;
7530
- if (this.options.video.width) videoConstraints.width = {
7531
- ideal: this.options.video.width
7936
+ init(localMediaStream, videoCallback, audioCallback, endedEarlyCallback, params) {
7937
+ this.clearInitializationListeners?.();
7938
+ this.stop(localMediaStream, {
7939
+ aboutToInitialize: true,
7940
+ preserveRecordingState: Boolean(params?.switchingFacingMode || params?.recovering)
7941
+ });
7942
+ const initializationId = ++this.initializationId;
7943
+ this.onPlayReached = false;
7944
+ this.onLoadedMetaDataReached = false;
7945
+ this.playingPromiseReached = false;
7946
+ this.videoTrackLabel = void 0;
7947
+ if (isAudioEnabled(this.options)) this.audioRecorder ??= new media_AudioRecorder(this, this.options);
7948
+ const unloadAllEventListeners = ()=>{
7949
+ this.options.logger.debug("UserMedia: unloadAllEventListeners()");
7950
+ this.unloadRemainingEventListeners();
7951
+ this.removeListener("SENDING_FIRST_FRAME");
7952
+ this.rawVisualUserMedia?.removeEventListener("play", onPlay);
7953
+ this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
7954
+ };
7955
+ this.clearInitializationListeners = ()=>{
7956
+ this.rawVisualUserMedia?.removeEventListener("play", onPlay);
7957
+ this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
7958
+ this.clearInitializationListeners = void 0;
7959
+ };
7960
+ const play = ()=>{
7961
+ try {
7962
+ this.rawVisualUserMedia?.load();
7963
+ if (this.rawVisualUserMedia?.paused) {
7964
+ 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)}`);
7965
+ this.rawVisualUserMedia.play().then(()=>{
7966
+ if (initializationId !== this.initializationId) return;
7967
+ if (!this.playingPromiseReached) {
7968
+ this.options.logger.debug("UserMedia: play promise successful. Playing now.");
7969
+ this.playingPromiseReached = true;
7970
+ }
7971
+ }).catch((exc)=>{
7972
+ if (initializationId !== this.initializationId) return;
7973
+ if (exc instanceof Error && !params?.recovering) this.options.logger.warn(`Caught pending user media promise exception: ${exc.toString()}`);
7974
+ else {
7975
+ unloadAllEventListeners();
7976
+ endedEarlyCallback(error_createError({
7977
+ message: "Failed to play user media upon play event.",
7978
+ exc,
7979
+ options: this.options
7980
+ }));
7981
+ }
7982
+ });
7983
+ }
7984
+ } catch (exc) {
7985
+ unloadAllEventListeners();
7986
+ endedEarlyCallback(exc);
7987
+ }
7988
+ };
7989
+ const fireCallbacks = ()=>{
7990
+ const readyState = this.rawVisualUserMedia?.readyState;
7991
+ this.options.logger.debug(`UserMedia: fireCallbacks(readyState=${readyState}, onPlayReached=${this.onPlayReached}, onLoadedMetaDataReached=${this.onLoadedMetaDataReached})`);
7992
+ if (this.onPlayReached && this.onLoadedMetaDataReached) if (this.audioRecorder) try {
7993
+ this.audioRecorder.init(localMediaStream);
7994
+ const onAudioReady = ()=>{
7995
+ if (initializationId !== this.initializationId || this.currentVisualStream !== localMediaStream) return;
7996
+ if (this.recording) this.audioRecord(audioCallback);
7997
+ else this.on("SENDING_FIRST_FRAME", ()=>{
7998
+ this.audioRecord(audioCallback);
7999
+ });
8000
+ this.clearInitializationListeners?.();
8001
+ videoCallback();
8002
+ };
8003
+ if (params?.recovering) this.audioRecorder.resume().then(onAudioReady).catch(endedEarlyCallback);
8004
+ else onAudioReady();
8005
+ } catch (exc) {
8006
+ unloadAllEventListeners();
8007
+ this.stop(void 0, {
8008
+ preserveRecordingState: Boolean(params?.recovering)
8009
+ });
8010
+ endedEarlyCallback(exc);
8011
+ }
8012
+ else {
8013
+ this.clearInitializationListeners?.();
8014
+ videoCallback();
8015
+ }
8016
+ };
8017
+ const onPlay = ()=>{
8018
+ try {
8019
+ this.logEvent("play", {
8020
+ readyState: this.rawVisualUserMedia?.readyState,
8021
+ audio: isAudioEnabled(this.options),
8022
+ width: this.rawVisualUserMedia?.width,
8023
+ height: this.rawVisualUserMedia?.height,
8024
+ videoWidth: this.rawVisualUserMedia?.videoWidth,
8025
+ videoHeight: this.rawVisualUserMedia?.videoHeight
8026
+ });
8027
+ this.rawVisualUserMedia?.removeEventListener("play", onPlay);
8028
+ if (this.hasEnded() || this.hasInvalidDimensions()) endedEarlyCallback(error_createError({
8029
+ message: "Already busy",
8030
+ explanation: "Probably another browser window is using your webcam?",
8031
+ options: this.options
8032
+ }));
8033
+ else {
8034
+ this.onPlayReached = true;
8035
+ fireCallbacks();
8036
+ }
8037
+ } catch (exc) {
8038
+ unloadAllEventListeners();
8039
+ endedEarlyCallback(exc);
8040
+ }
7532
8041
  };
7533
- else {
7534
- const limitedWidth = this.limitWidth();
7535
- if (limitedWidth?.value) videoConstraints.width = {
7536
- ideal: limitedWidth.value
7537
- };
7538
- }
7539
- if (this.options.video.height) videoConstraints.height = {
7540
- ideal: this.options.video.height
8042
+ const onLoadedMetaData = ()=>{
8043
+ this.logEvent("loadedmetadata", {
8044
+ readyState: this.rawVisualUserMedia?.readyState,
8045
+ paused: this.rawVisualUserMedia?.paused,
8046
+ width: this.rawVisualUserMedia?.width,
8047
+ height: this.rawVisualUserMedia?.height,
8048
+ videoWidth: this.rawVisualUserMedia?.videoWidth,
8049
+ videoHeight: this.rawVisualUserMedia?.videoHeight
8050
+ });
8051
+ this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
8052
+ if (!this.hasEnded() && !this.hasInvalidDimensions()) {
8053
+ this.emit("LOADED_META_DATA");
8054
+ this.onLoadedMetaDataReached = true;
8055
+ fireCallbacks();
8056
+ }
7541
8057
  };
7542
- }
7543
- loadUserMedia(params) {
7544
- if (this.userMediaLoaded) {
7545
- this.options.logger.debug("Recorder: skipping loadUserMedia() because it is already loaded");
7546
- this.onUserMediaReady(params);
7547
- return;
7548
- } else if (this.userMediaLoading) return void this.options.logger.debug("Recorder: skipping loadUserMedia() because it is already asking for permission");
7549
- this.options.logger.debug(`Recorder: loadUserMedia(${params ? string_pretty(params) : ""})`);
7550
- this.emit("LOADING_USER_MEDIA");
7551
8058
  try {
7552
- if (!navigator.mediaDevices) throw new Error("No media devices are available.");
7553
- this.userMediaTimeout = window.setTimeout(()=>{
7554
- if (!this.userMediaLoaded) {
7555
- this.userMediaLoading = false;
7556
- const err = util_getBrowser(this.options).getNoAccessIssue();
7557
- this.userMediaErrorCallback(err, {
7558
- audio: isAudioEnabled(this.options)
7559
- });
7560
- }
7561
- }, this.options.timeouts.userMedia);
7562
- this.userMediaLoading = true;
7563
- this.loadGenuineUserMedia(params);
7564
- } catch (exc) {
7565
- this.clearUserMediaTimeout();
7566
- this.options.logger.debug("Recorder: failed to load genuine user media");
7567
- this.userMediaLoading = false;
7568
- if (this.cameraRecovery) return void this.failCameraRecovery(exc);
7569
- const errorListeners = this.getListeners("ERROR");
7570
- if (errorListeners?.length) this.emit("ERROR", {
7571
- exc
8059
+ const videoTrack = media_getFirstVideoTrack(localMediaStream);
8060
+ this.videoTrackLabel = videoTrack?.label;
8061
+ if (videoTrack) if (videoTrack.enabled) if (util_isVirtualCamera(videoTrack)) throw error_createError({
8062
+ message: "Virtual cameras are not allowed.",
8063
+ explanation: `Please use a real camera, not "${videoTrack.label}".`,
8064
+ options: this.options
7572
8065
  });
7573
8066
  else {
7574
- this.options.logger.debug("Recorder: no error listeners attached but throwing exception further");
7575
- throw exc;
8067
+ let description = "";
8068
+ if (videoTrack.label.length > 0) description = description.concat(videoTrack.label);
8069
+ description = description.concat(` with enabled=${videoTrack.enabled}, muted=${videoTrack.muted}, readyState=${videoTrack.readyState}`);
8070
+ this.options.logger.debug(`UserMedia: ${videoTrack.kind} detected. ${description}`);
7576
8071
  }
8072
+ else throw error_createError({
8073
+ message: "Webcam is disabled",
8074
+ explanation: "The video track seems to be disabled. Enable it in your system.",
8075
+ options: this.options
8076
+ });
8077
+ else this.options.logger.debug("UserMedia: detected (but no video tracks exist");
8078
+ this.rawVisualUserMedia?.addEventListener("loadedmetadata", onLoadedMetaData);
8079
+ this.rawVisualUserMedia?.addEventListener("play", onPlay);
8080
+ this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
8081
+ this.rawVisualUserMedia?.addEventListener("error", this.handleMediaError);
8082
+ this.setVisualStream(localMediaStream);
8083
+ play();
8084
+ } catch (exc) {
8085
+ unloadAllEventListeners();
8086
+ endedEarlyCallback(exc);
7577
8087
  }
7578
8088
  }
7579
- executeCommand(command) {
7580
- if (this.unloaded) return;
8089
+ getVideoTrackLabel() {
8090
+ return this.videoTrackLabel;
8091
+ }
8092
+ isReady() {
8093
+ return Boolean(this.rawVisualUserMedia?.srcObject || this.rawVisualUserMedia?.currentSrc);
8094
+ }
8095
+ stop(visualStream, params) {
7581
8096
  try {
7582
- if (command.args) {
7583
- const debugLine = string_summarize(`Server commanded: ${command.command} with:`, command.args);
7584
- this.options.logger.debug(debugLine);
7585
- } else this.options.logger.debug(`Server commanded: ${command.command}`);
7586
- switch(command.command){
7587
- case "ready":
7588
- this.emit("SERVER_READY");
7589
- if (!this.userMediaTimeout) if (this.options.loadUserMediaOnRecord) this.show();
7590
- else this.loadUserMedia();
7591
- break;
7592
- case "preview":
7593
- this.preview(command.args);
7594
- break;
7595
- case "error":
7596
- {
7597
- let explanation = "(No explanation given)";
7598
- if (command.args?.err?.message) explanation = command.args.err.message;
7599
- const err = error_createError({
7600
- message: "Websocket error from Videomail server",
7601
- explanation,
7602
- err: deserializeError(command.args?.err),
7603
- options: this.options
7604
- });
7605
- this.emit("ERROR", {
7606
- err
7607
- });
7608
- break;
7609
- }
7610
- case "confirmFrame":
7611
- this.updateFrameProgress(command.args);
7612
- break;
7613
- case "confirmSample":
7614
- this.updateSampleProgress(command.args);
7615
- break;
7616
- case "beginAudioEncoding":
7617
- this.emit("BEGIN_AUDIO_ENCODING");
7618
- break;
7619
- case "beginVideoEncoding":
7620
- this.emit("BEGIN_VIDEO_ENCODING");
7621
- break;
7622
- default:
7623
- {
7624
- const err = error_createError({
7625
- message: `Unknown server command: ${command.command}`,
7626
- options: this.options
7627
- });
7628
- this.emit("ERROR", {
7629
- err
7630
- });
7631
- break;
7632
- }
8097
+ let chosenStream = visualStream;
8098
+ if (!params?.aboutToInitialize) {
8099
+ this.initializationId++;
8100
+ this.clearInitializationListeners?.();
8101
+ chosenStream ??= this.currentVisualStream;
8102
+ const tracks = chosenStream?.getTracks();
8103
+ if (tracks) tracks.forEach((track)=>{
8104
+ track.stop();
8105
+ });
8106
+ this.setVisualStream();
8107
+ this.audioRecorder?.stop();
8108
+ this.audioRecorder = void 0;
7633
8109
  }
8110
+ if (!params?.preserveRecordingState) this.paused = this.recording = false;
7634
8111
  } catch (exc) {
7635
8112
  this.emit("ERROR", {
7636
8113
  exc
7637
8114
  });
7638
8115
  }
7639
8116
  }
7640
- isNotifying() {
7641
- return this.visuals.isNotifying();
7642
- }
7643
- isHidden() {
7644
- return !this.recorderElement || html_isHidden(this.recorderElement);
7645
- }
7646
- writeCommand(command, args, cb) {
7647
- if (this.connected) {
7648
- if (this.stream) {
7649
- if (args) this.options.logger.debug(`$ ${command} with ${string_pretty(args)}`);
7650
- else this.options.logger.debug(`$ ${command}`);
7651
- const commandObj = {
7652
- command,
7653
- args
7654
- };
7655
- this.writeStream(Buffer.from(JSON.stringify(commandObj)), cb ? {
7656
- onFlushedCallback: ()=>{
7657
- queueMicrotask(cb);
7658
- }
7659
- } : void 0);
7660
- }
7661
- } else {
7662
- this.options.logger.debug(`Reconnecting for the command ${command} …`);
7663
- this.initSocket(()=>{
7664
- this.writeCommand(command, args, cb);
7665
- });
7666
- }
7667
- }
7668
- cancelAnimationFrame() {
7669
- this.loop?.dispose();
7670
- }
7671
- getIntervalSum() {
7672
- if (!this.loop) return;
7673
- return this.loop.getElapsedTime();
7674
- }
7675
- getAvgInterval() {
7676
- const intervalSum = this.getIntervalSum() ?? 0;
7677
- if (!this.hasValidInterval(intervalSum)) return;
7678
- return intervalSum / this.framesCount;
7679
- }
7680
- getAvgFps() {
7681
- const intervalSum = this.getIntervalSum() ?? 0;
7682
- if (!this.hasValidInterval(intervalSum)) return;
7683
- return this.framesCount / intervalSum * 1000;
7684
- }
7685
- hasValidInterval(intervalSum) {
7686
- return intervalSum > 0;
7687
- }
7688
- getRecordingStats() {
7689
- return this.recordingStats;
7690
- }
7691
- getAudioSampleRate() {
7692
- return this.recordingAudioSampleRate ?? this.userMedia?.getAudioSampleRate();
7693
- }
7694
- stop(params) {
7695
- this.cancelCameraRecovery();
7696
- if (!this.userMedia) throw new Error("No user media defined, unable to stop");
7697
- this.options.logger.debug(`Recorder: stop(${params ? string_pretty(params) : ""})`);
7698
- this.recordingActive = false;
7699
- const limitReached = params?.limitReached;
7700
- this.emit("STOPPING", {
7701
- limitReached
7702
- });
7703
- this.loop?.complete();
7704
- this.stopTimeout = window.setTimeout(()=>{
7705
- this.stopTimeout = void 0;
7706
- this.stopTime = Date.now();
7707
- const videoType = this.replay.getVideoType();
7708
- if (!videoType) throw new Error("Unable to video record when no video type is defined.");
7709
- this.recordingStats = {
7710
- avgFps: this.getAvgFps(),
7711
- wantedFps: this.options.video.fps,
7712
- avgInterval: this.getAvgInterval(),
7713
- wantedInterval: 1e3 / this.options.video.fps,
7714
- intervalSum: this.getIntervalSum(),
7715
- framesCount: this.framesCount,
7716
- videoType
7717
- };
7718
- if (this.userMedia) this.recordingStats.videoTrackLabel = this.userMedia.getVideoTrackLabel();
7719
- if (isAudioEnabled(this.options) && this.userMedia) {
7720
- this.recordingStats.samplesCount = this.samplesCount;
7721
- this.recordingStats.sampleRate = this.recordingAudioSampleRate ?? this.userMedia.getAudioSampleRate();
7722
- }
7723
- this.writeCommand("stop", this.recordingStats, ()=>{
7724
- this.emit("STOPPED", {
7725
- recordingStats: this.recordingStats
7726
- });
7727
- });
7728
- this.reset();
7729
- }, 60);
8117
+ createCanvas() {
8118
+ const canvas = document.createElement("canvas");
8119
+ const rawWidthDimension = this.getRawWidth(true);
8120
+ if (rawWidthDimension.value) canvas.width = rawWidthDimension.value;
8121
+ const rawHeight = this.getRawHeight(true);
8122
+ if (rawHeight) canvas.height = rawHeight;
8123
+ return canvas;
7730
8124
  }
7731
- back(cb) {
7732
- this.emit("GOING_BACK");
7733
- this.key = void 0;
7734
- this.recordingDimensions = void 0;
7735
- this.unloaded = false;
7736
- this.show();
7737
- this.writeCommand("back", void 0, cb);
8125
+ getVideoHeight() {
8126
+ if (!this.rawVisualUserMedia) return;
8127
+ return this.rawVisualUserMedia.videoHeight || this.rawVisualUserMedia.height;
7738
8128
  }
7739
- reInitializeAudio() {
7740
- this.options.logger.debug("Recorder: reInitializeAudio()");
7741
- this.clearUserMediaTimeout();
7742
- this.userMediaLoading = false;
7743
- this.userMedia?.stop();
7744
- this.recordingDimensions = void 0;
7745
- this.userMediaLoaded = this.key = this.canvas = this.ctx = void 0;
7746
- this.loadUserMedia();
8129
+ getVideoWidth() {
8130
+ if (!this.rawVisualUserMedia) return;
8131
+ return this.rawVisualUserMedia.videoWidth || this.rawVisualUserMedia.width;
7747
8132
  }
7748
- unload(params) {
7749
- if (this.unloaded || !this.built) return;
7750
- const e = params?.e;
7751
- let cause;
7752
- if (e) cause = e.type;
7753
- const prettyCause = cause ? `, cause: ${cause}` : "";
7754
- this.options.logger.debug(`Recorder: unload()${prettyCause}`);
7755
- this.recordingActive = false;
7756
- this.mediaTransmissionStarted = false;
7757
- this.reset();
7758
- this.recordingDimensions = void 0;
7759
- this.clearUserMediaTimeout();
7760
- this.clearConnectionTimeout();
7761
- this.clearReconnectTimeout();
7762
- this.clearRetryTimeout();
7763
- this.clearStopTimeout();
7764
- this.stopPings();
7765
- this.userMediaLoading = false;
7766
- this.connecting = false;
7767
- this.reconnecting = false;
7768
- this.reconnectAttempts = 0;
7769
- if (this.userMedia) this.userMedia.unloadRemainingEventListeners();
7770
- if (this.submitting) ;
7771
- else if (this.stream) this.discardSocket();
7772
- this.unloaded = true;
7773
- this.built = this.connecting = this.connected = false;
8133
+ hasVideoWidth() {
8134
+ const videoWidth = this.getVideoWidth();
8135
+ return videoWidth && videoWidth > 0;
7774
8136
  }
7775
- reset() {
7776
- this.cancelCameraRecovery();
7777
- if (!this.unloaded) {
7778
- this.options.logger.debug("Recorder: reset()");
7779
- this.emit("RESETTING");
7780
- this.cancelAnimationFrame();
7781
- this.userMedia?.stop();
7782
- this.replay.reset();
7783
- this.userMediaLoaded = this.key = this.canvas = this.ctx = this.recordingBuffer = void 0;
8137
+ getRawWidth(responsive) {
8138
+ let rawWidth;
8139
+ const widthDimension = {
8140
+ unit: "px"
8141
+ };
8142
+ rawWidth = responsive ? this.getVideoWidth() : this.options.video.width ? this.options.video.width : this.getVideoWidth();
8143
+ if (responsive) {
8144
+ const widthDimension = this.recorder.limitWidth(rawWidth);
8145
+ rawWidth = widthDimension?.value;
7784
8146
  }
8147
+ widthDimension.value = rawWidth;
8148
+ return widthDimension;
7785
8149
  }
7786
- clearUserMediaTimeout() {
7787
- if (void 0 !== this.userMediaTimeout) {
7788
- this.options.logger.debug("Recorder: clearUserMediaTimeout()");
7789
- window.clearTimeout(this.userMediaTimeout);
7790
- this.userMediaTimeout = void 0;
8150
+ getRawHeight(responsive) {
8151
+ let rawHeight;
8152
+ if (responsive) {
8153
+ rawHeight = this.options.video.height || this.getVideoHeight();
8154
+ const ratio = this.recorder.getRatio();
8155
+ const rawWidth = this.getRawWidth(responsive);
8156
+ if (void 0 !== ratio && rawWidth.value) rawHeight = rawWidth.value * ratio;
8157
+ } else rawHeight = this.options.video.height ? this.options.video.height : this.getVideoHeight();
8158
+ if (responsive) {
8159
+ const heightDimension = this.recorder.limitHeight(rawHeight);
8160
+ rawHeight = heightDimension.value;
7791
8161
  }
8162
+ return rawHeight;
7792
8163
  }
7793
- clearStopTimeout() {
7794
- if (void 0 === this.stopTimeout) return;
7795
- window.clearTimeout(this.stopTimeout);
7796
- this.stopTimeout = void 0;
7797
- }
7798
- validate() {
7799
- return Boolean(this.key) && void 0 === this.canvas;
8164
+ getRawVisuals() {
8165
+ return this.rawVisualUserMedia;
7800
8166
  }
7801
- isReady() {
7802
- return this.userMedia?.isReady();
8167
+ pause() {
8168
+ this.paused = true;
7803
8169
  }
7804
- finishCameraRecovery(restored) {
7805
- this.clearUserMediaTimeout();
7806
- const resolve = this.resolveCameraRecovery;
7807
- const resumeRequested = restored && this.resumeAfterCameraRecovery;
7808
- this.cameraRecovery = void 0;
7809
- this.resolveCameraRecovery = void 0;
7810
- this.resumeAfterCameraRecovery = false;
7811
- resolve?.(restored);
7812
- if (resumeRequested && this.isPaused()) this.resume();
8170
+ isPaused() {
8171
+ return this.paused;
7813
8172
  }
7814
- cancelCameraRecovery() {
7815
- if (!this.cameraRecovery) return;
7816
- this.mediaRequestId++;
7817
- this.userMediaLoading = false;
7818
- this.userMediaLoaded = false;
7819
- this.clearUserMediaTimeout();
7820
- this.userMedia?.stop(void 0, {
7821
- preserveRecordingState: true
7822
- });
7823
- this.finishCameraRecovery(false);
8173
+ resume() {
8174
+ this.paused = false;
7824
8175
  }
7825
- failCameraRecovery(exc) {
7826
- this.cancelCameraRecovery();
7827
- const err = error_createError({
7828
- message: "Camera recovery failed",
7829
- explanation: "Your recording is still paused. Check camera permissions, then try Resume again or stop to keep the recorded part.",
7830
- exc,
7831
- options: this.options
7832
- });
7833
- this.emit("CAMERA_RECOVERY_FAILED", {
7834
- err
7835
- });
8176
+ record() {
8177
+ this.recording = true;
7836
8178
  }
7837
- recoverCamera() {
7838
- if (this.cameraRecovery) return this.cameraRecovery;
7839
- if (!this.built || this.unloaded || !this.userMedia || this.userMediaLoading || this.submitting || this.key || void 0 !== this.stopTimeout || this.recordingTransportFailed) return Promise.resolve(false);
7840
- const liveStream = this.userMedia.hasLiveStream();
7841
- if (liveStream && this.isRecording()) return Promise.resolve(true);
7842
- if (!liveStream && this.recordingActive && !this.isPaused()) this.pause();
7843
- const recovery = new Promise((resolve)=>{
7844
- this.resolveCameraRecovery = resolve;
7845
- });
7846
- this.cameraRecovery = recovery;
7847
- this.emit("CAMERA_RECOVERING");
7848
- if (liveStream) {
7849
- this.userMediaTimeout = window.setTimeout(()=>{
7850
- this.failCameraRecovery(new Error("Camera playback recovery timed out."));
7851
- }, this.options.timeouts.userMedia);
7852
- this.userMedia.restorePlayback().then(()=>{
7853
- if (this.cameraRecovery === recovery) this.finishCameraRecovery(true);
7854
- }).catch((exc)=>{
7855
- if (this.cameraRecovery === recovery) this.failCameraRecovery(exc);
7856
- });
7857
- } else {
7858
- this.userMediaLoaded = false;
7859
- this.loadUserMedia({
7860
- recovering: true
7861
- });
7862
- }
7863
- return recovery;
8179
+ isRecording() {
8180
+ return this.recording;
7864
8181
  }
7865
- pause(params) {
7866
- this.cancelCameraRecovery();
7867
- this.resumeAfterDrain = false;
7868
- if (params) this.options.logger.debug(`pause() at frame ${this.framesCount} with ${string_pretty(params)}`);
7869
- else this.options.logger.debug(`pause() at frame ${this.framesCount}`);
7870
- this.userMedia?.pause();
7871
- this.loop.stop();
7872
- this.emit("PAUSED");
7873
- this.sendPings();
8182
+ getAudioSampleRate() {
8183
+ if (this.audioRecorder) return this.audioRecorder.getSampleRate();
8184
+ return -1;
7874
8185
  }
7875
- resume() {
7876
- if (this.cameraRecovery || !this.userMedia?.hasLiveStream()) {
7877
- this.recoverCamera();
7878
- this.resumeAfterCameraRecovery = Boolean(this.cameraRecovery);
7879
- return;
7880
- }
7881
- this.options.logger.debug(`Recorder: resume() with frame ${this.framesCount}`);
7882
- this.stopPings();
7883
- this.emit("RESUMING");
7884
- this.userMedia.resume();
7885
- if (this.streamBackpressured) this.resumeAfterDrain = true;
7886
- else this.loop.start();
8186
+ getCharacteristics() {
8187
+ return {
8188
+ audioSampleRate: this.getAudioSampleRate(),
8189
+ muted: this.rawVisualUserMedia?.muted,
8190
+ width: this.rawVisualUserMedia?.width,
8191
+ height: this.rawVisualUserMedia?.height,
8192
+ videoWidth: this.rawVisualUserMedia?.videoWidth,
8193
+ videoHeight: this.rawVisualUserMedia?.videoHeight
8194
+ };
7887
8195
  }
7888
- onFlushed(opts) {
7889
- const frameNumber = opts.frameNumber;
7890
- if (1 === frameNumber) this.emit("FIRST_FRAME_SENT");
8196
+ constructor(recorder, options){
8197
+ super("UserMedia", options), userMedia_define_property(this, "recorder", void 0), userMedia_define_property(this, "rawVisualUserMedia", void 0), userMedia_define_property(this, "paused", false), userMedia_define_property(this, "recording", false), userMedia_define_property(this, "audioRecorder", void 0), userMedia_define_property(this, "currentVisualStream", void 0), userMedia_define_property(this, "onPlayReached", false), userMedia_define_property(this, "onLoadedMetaDataReached", false), userMedia_define_property(this, "playingPromiseReached", false), userMedia_define_property(this, "videoTrackLabel", void 0), userMedia_define_property(this, "clearInitializationListeners", void 0), userMedia_define_property(this, "initializationId", 0), userMedia_define_property(this, "outputEvent", (e)=>{
8198
+ this.logEvent(e.type, {
8199
+ readyState: this.rawVisualUserMedia?.readyState
8200
+ });
8201
+ this.rawVisualUserMedia?.removeEventListener(e.type, this.outputEvent);
8202
+ }), userMedia_define_property(this, "handleMediaError", (event)=>{
8203
+ this.options.logger.warn(`Caught video element error event: ${string_pretty(event)}`);
8204
+ });
8205
+ this.recorder = recorder;
8206
+ this.rawVisualUserMedia = recorder.getRawVisualUserMedia();
8207
+ mediaEvents.forEach((eventName)=>{
8208
+ this.rawVisualUserMedia?.addEventListener(eventName, this.outputEvent, false);
8209
+ });
7891
8210
  }
7892
- draw(_deltaTime, elapsedTime) {
7893
- if (this.recordingTransportFailed) return;
7894
- if (!this.userMedia) throw new Error("No user media defined, unable to draw on canvas");
8211
+ }
8212
+ const visuals_userMedia = UserMedia;
8213
+ function recorder_define_property(obj, key, value) {
8214
+ if (key in obj) Object.defineProperty(obj, key, {
8215
+ value: value,
8216
+ enumerable: true,
8217
+ configurable: true,
8218
+ writable: true
8219
+ });
8220
+ else obj[key] = value;
8221
+ return obj;
8222
+ }
8223
+ class Recorder extends capture {
8224
+ executeCommand(command) {
8225
+ if (this.unloaded) return;
7895
8226
  try {
7896
- if (!this.isPaused() && this.stream && this.ctx) {
7897
- if (0 === this.framesCount) this.emit("SENDING_FIRST_FRAME");
7898
- this.framesCount++;
7899
- const imageSource = this.userMedia.getRawVisuals();
7900
- if (this.canvas && imageSource) this.ctx.drawImage(imageSource, 0, 0, this.canvas.width, this.canvas.height);
7901
- else throw new Error("Unable to draw an image without a defined canvas");
7902
- this.recordingBuffer = this.frame?.toBuffer();
7903
- const recordingBufferLength = this.recordingBuffer?.length;
7904
- if (recordingBufferLength) {
7905
- if (this.recordingBuffer) {
7906
- const frameControlBuffer = Buffer.from(JSON.stringify({
7907
- frameNumber: this.framesCount
7908
- }));
7909
- const frameBuffer = Buffer.concat([
7910
- this.recordingBuffer,
7911
- frameControlBuffer
7912
- ]);
7913
- this.mediaTransmissionStarted = true;
7914
- this.writeStream(frameBuffer, {
7915
- frameNumber: this.framesCount,
7916
- onFlushedCallback: this.onFlushed.bind(this)
8227
+ if (command.args) {
8228
+ const debugLine = string_summarize(`Server commanded: ${command.command} with:`, command.args);
8229
+ this.options.logger.debug(debugLine);
8230
+ } else this.options.logger.debug(`Server commanded: ${command.command}`);
8231
+ switch(command.command){
8232
+ case "ready":
8233
+ this.emit("SERVER_READY");
8234
+ if (!this.userMediaTimeout) if (this.options.loadUserMediaOnRecord) this.show();
8235
+ else this.loadUserMedia();
8236
+ break;
8237
+ case "preview":
8238
+ this.preview(command.args);
8239
+ break;
8240
+ case "error":
8241
+ {
8242
+ let explanation = "(No explanation given)";
8243
+ if (command.args?.err?.message) explanation = command.args.err.message;
8244
+ const err = error_createError({
8245
+ message: "Websocket error from Videomail server",
8246
+ explanation,
8247
+ err: deserializeError(command.args?.err),
8248
+ options: this.options
7917
8249
  });
7918
- this.visuals.checkTimer(elapsedTime);
8250
+ this.emit("ERROR", {
8251
+ err
8252
+ });
8253
+ break;
8254
+ }
8255
+ case "confirmFrame":
8256
+ this.updateFrameProgress(command.args);
8257
+ break;
8258
+ case "confirmSample":
8259
+ this.updateSampleProgress(command.args);
8260
+ break;
8261
+ case "beginAudioEncoding":
8262
+ this.emit("BEGIN_AUDIO_ENCODING");
8263
+ break;
8264
+ case "beginVideoEncoding":
8265
+ this.emit("BEGIN_VIDEO_ENCODING");
8266
+ break;
8267
+ default:
8268
+ {
8269
+ const err = error_createError({
8270
+ message: `Unknown server command: ${command.command}`,
8271
+ options: this.options
8272
+ });
8273
+ this.emit("ERROR", {
8274
+ err
8275
+ });
8276
+ break;
7919
8277
  }
7920
- } else throw error_createError({
7921
- message: "Failed to extract webcam data.",
7922
- options: this.options
7923
- });
7924
8278
  }
7925
8279
  } catch (exc) {
7926
8280
  this.emit("ERROR", {
@@ -7928,156 +8282,34 @@ class Recorder extends util_Despot {
7928
8282
  });
7929
8283
  }
7930
8284
  }
7931
- createLoop() {
7932
- const newLoop = animitter({
7933
- fps: this.options.video.fps
7934
- }, this.draw.bind(this));
7935
- this.originalAnimationFrameObject = newLoop.getRequestAnimationFrameObject();
7936
- return newLoop;
7937
- }
7938
- record() {
7939
- if (this.unloaded || this.cameraRecovery) return;
7940
- if (!this.connected) {
7941
- this.options.logger.debug("Recorder: reconnecting before recording …");
7942
- this.initSocket(()=>{
7943
- this.once("SERVER_READY", ()=>{
7944
- if (this.unloaded) return;
7945
- if (this.userMediaLoaded || this.options.loadUserMediaOnRecord) this.record();
7946
- else this.once("USER_MEDIA_READY", ()=>{
7947
- if (!this.unloaded) this.record();
7948
- });
7949
- });
7950
- });
7951
- return;
7952
- }
7953
- if (!this.userMediaLoaded) {
7954
- if (this.options.loadUserMediaOnRecord) this.loadUserMedia({
7955
- recordWhenReady: true
7956
- });
7957
- else {
7958
- const err = error_createError({
7959
- message: "Load and enable your camera first",
7960
- options: this.options
7961
- });
7962
- this.emit("ERROR", {
7963
- err
7964
- });
7965
- }
7966
- return;
7967
- }
7968
- this.recordingDimensions = void 0;
7969
- try {
7970
- if (!this.userMedia) throw new Error("No user media defined, unable to create canvas");
7971
- this.canvas = this.userMedia.createCanvas();
7972
- } catch (exc) {
7973
- const err = error_createError({
7974
- exc,
7975
- options: this.options
7976
- });
7977
- this.emit("ERROR", {
7978
- err
7979
- });
7980
- return;
7981
- }
7982
- this.ctx = this.canvas.getContext("2d");
7983
- if (!this.canvas.width) {
7984
- const err = error_createError({
7985
- message: "Canvas has an invalid width.",
7986
- options: this.options
7987
- });
7988
- this.emit("ERROR", {
7989
- err
7990
- });
7991
- return;
7992
- }
7993
- if (!this.canvas.height) {
7994
- const err = error_createError({
7995
- message: "Canvas has an invalid height.",
7996
- options: this.options
7997
- });
7998
- this.emit("ERROR", {
7999
- err
8000
- });
8001
- return;
8002
- }
8003
- this.recordingDimensions = {
8004
- width: this.canvas.width,
8005
- height: this.canvas.height
8006
- };
8007
- this.frame = new canvas_to_buffer(this.canvas, this.options.image.types, this.options.image.quality);
8008
- this.options.logger.debug("Recorder: record()");
8009
- this.recordingActive = true;
8010
- this.recordingTransportFailed = false;
8285
+ unload(params) {
8286
+ if (this.unloaded || !this.built) return;
8287
+ const e = params?.e;
8288
+ let cause;
8289
+ if (e) cause = e.type;
8290
+ const prettyCause = cause ? `, cause: ${cause}` : "";
8291
+ this.options.logger.debug(`Recorder: unload()${prettyCause}`);
8292
+ this.recordingActive = false;
8011
8293
  this.mediaTransmissionStarted = false;
8012
- this.recordingAudioSampleRate = void 0;
8013
- this.userMedia.record();
8014
- this.emit("RECORDING", {
8015
- framesCount: this.framesCount
8016
- });
8017
- this.loop.on("update", (_deltaTime, elapsedTime)=>{
8018
- let avgFPS;
8019
- avgFPS = 0 !== elapsedTime ? Math.round(this.framesCount / elapsedTime * 1000) : void 0;
8020
- this.options.logger.debug(`Recorder updates avgFps = ${avgFPS} at frame ${this.framesCount}`);
8021
- });
8022
- this.loop.start();
8023
- }
8024
- setAnimationFrameObject(newObj) {
8025
- if (this.loop) {
8026
- const isRecording = this.isRecording();
8027
- this.loop.stop();
8028
- this.loop.setRequestAnimationFrameObject(newObj);
8029
- if (isRecording) this.loop.start();
8030
- }
8031
- }
8032
- restoreAnimationFrameObject() {
8033
- this.options.logger.debug("Recorder: restoreAnimationFrameObject()");
8034
- this.setAnimationFrameObject(this.originalAnimationFrameObject);
8035
- }
8036
- loopWithTimeouts() {
8037
- this.options.logger.debug("Recorder: loopWithTimeouts()");
8038
- const wantedInterval = 1e3 / this.options.video.fps;
8039
- let processingTime = 0;
8040
- let start;
8041
- const raf = (fn)=>setTimeout(()=>{
8042
- start = Date.now();
8043
- fn();
8044
- processingTime = Date.now() - start;
8045
- }, wantedInterval - processingTime);
8046
- const cancel = (id)=>{
8047
- window.clearTimeout(id);
8048
- };
8049
- this.setAnimationFrameObject({
8050
- requestAnimationFrame: raf,
8051
- cancelAnimationFrame: cancel
8052
- });
8294
+ this.reset();
8295
+ this.recordingDimensions = void 0;
8296
+ this.clearUserMediaTimeout();
8297
+ this.clearConnectionTimeout();
8298
+ this.clearReconnectTimeout();
8299
+ this.clearRetryTimeout();
8300
+ this.clearStopTimeout();
8301
+ this.stopPings();
8302
+ this.userMediaLoading = false;
8303
+ this.connecting = false;
8304
+ this.reconnecting = false;
8305
+ this.reconnectAttempts = 0;
8306
+ if (this.userMedia) this.userMedia.unloadRemainingEventListeners();
8307
+ this.unloaded = true;
8308
+ this.discardSocket();
8309
+ this.built = this.connecting = this.connected = false;
8053
8310
  }
8054
8311
  correctDimensions() {
8055
- if (!this.recorderElement) return;
8056
- const widthDimension = dimensions_useFullWidth(this.options.video.mobileBreakPoint);
8057
- if (this.options.video.stretch || widthDimension) {
8058
- this.recorderElement.style.width = "100%";
8059
- this.recorderElement.style.removeProperty("height");
8060
- } else {
8061
- if (this.options.video.width) {
8062
- const recorderWidth = this.getRecorderWidth(true);
8063
- if (recorderWidth?.value) this.recorderElement.width = recorderWidth.value;
8064
- else this.recorderElement.style.removeProperty("width");
8065
- }
8066
- if (this.options.video.height) {
8067
- const recorderHeight = this.getRecorderHeight(true);
8068
- if (recorderHeight?.value) this.recorderElement.height = recorderHeight.value;
8069
- else this.recorderElement.style.removeProperty("height");
8070
- }
8071
- }
8072
- }
8073
- switchFacingMode() {
8074
- if (this.cameraRecovery || !util_getBrowser(this.options).isMobile()) return;
8075
- if ("user" === this.facingMode) this.facingMode = "environment";
8076
- else if ("environment" === this.facingMode) this.facingMode = "user";
8077
- else this.options.logger.warn(`Recorder: unsupported facing mode ${string_pretty(this.facingMode)}`);
8078
- this.loadGenuineUserMedia({
8079
- switchingFacingMode: this.facingMode
8080
- });
8312
+ this.dimensions.correctDimensions();
8081
8313
  }
8082
8314
  initEvents() {
8083
8315
  this.options.logger.debug("Recorder: initEvents()");
@@ -8149,100 +8381,20 @@ class Recorder extends util_Despot {
8149
8381
  }
8150
8382
  this.built = true;
8151
8383
  }
8152
- isPaused() {
8153
- return this.userMedia?.isPaused() && !this.loop.isRunning();
8154
- }
8155
- isRecording() {
8156
- return Boolean(this.loop?.isRunning() && !this.isPaused() && !this.isNotifying() && this.stream && !this.stream.destroyed);
8157
- }
8158
- hide() {
8159
- if (!this.isHidden()) {
8160
- if (this.recorderElement) html_hideElement(this.recorderElement);
8161
- this.clearUserMediaTimeout();
8162
- this.clearRetryTimeout();
8163
- }
8164
- }
8165
- isUnloaded() {
8166
- return this.unloaded;
8167
- }
8168
8384
  getRecorderWidth(responsive) {
8169
- if (this.userMedia?.hasVideoWidth()) return this.userMedia.getRawWidth(responsive);
8170
- else if (responsive && this.options.video.width) return this.limitWidth(this.options.video.width);
8171
- const dimension = {
8172
- unit: "px",
8173
- value: this.options.video.width
8174
- };
8175
- return dimension;
8176
- }
8177
- getRecordingDimensions() {
8178
- return this.recordingDimensions && {
8179
- ...this.recordingDimensions
8180
- };
8385
+ return this.dimensions.getRecorderWidth(responsive);
8181
8386
  }
8182
8387
  getRecorderHeight(responsive, useBoundingClientRect) {
8183
- let recorderHeight;
8184
- if (this.recorderElement && useBoundingClientRect) {
8185
- const height = this.recorderElement.getBoundingClientRect().height;
8186
- recorderHeight = {
8187
- unit: "px",
8188
- value: height
8189
- };
8190
- } else if (this.userMedia) {
8191
- let height = this.userMedia.getRawHeight(responsive);
8192
- if (void 0 !== height && this.options.video.width) {
8193
- const ratio = this.getRatio();
8194
- if (void 0 !== ratio) {
8195
- const idealHeight = this.options.video.width * ratio;
8196
- height = Math.min(idealHeight, height);
8197
- }
8198
- }
8199
- recorderHeight = {
8200
- unit: "px",
8201
- value: height
8202
- };
8203
- } else if (responsive && this.options.video.height) recorderHeight = this.calculateHeight(responsive);
8204
- else if (this.options.video.height) {
8205
- const height = this.options.video.height;
8206
- recorderHeight = {
8207
- unit: "px",
8208
- value: height
8209
- };
8210
- }
8211
- return recorderHeight;
8388
+ return this.dimensions.getRecorderHeight(responsive, useBoundingClientRect);
8212
8389
  }
8213
8390
  getRatio() {
8214
- let ratio;
8215
- if (this.userMedia) {
8216
- const userMediaVideoWidth = this.userMedia.getVideoWidth();
8217
- const userMediaVideoHeight = this.userMedia.getVideoHeight();
8218
- if (!userMediaVideoWidth || userMediaVideoWidth < 1) ratio = this.visuals.getRatio();
8219
- else if (userMediaVideoHeight) ratio = userMediaVideoHeight / userMediaVideoWidth;
8220
- } else ratio = dimensions_getRatio(this.options);
8221
- return ratio;
8391
+ return this.dimensions.getRatio();
8222
8392
  }
8223
8393
  calculateWidth(responsive) {
8224
- let videoHeight;
8225
- if (this.userMedia) videoHeight = this.userMedia.getVideoHeight();
8226
- else if (this.recorderElement) videoHeight = this.recorderElement.videoHeight || this.recorderElement.height;
8227
- const ratio = this.getRatio();
8228
- return dimensions_calculateWidth(responsive, this.options, videoHeight, ratio);
8394
+ return this.dimensions.calculateWidth(responsive);
8229
8395
  }
8230
8396
  calculateHeight(responsive) {
8231
- let videoDimension;
8232
- if (this.userMedia) {
8233
- const videoHeight = this.userMedia.getVideoHeight();
8234
- videoDimension = {
8235
- value: videoHeight,
8236
- unit: "px"
8237
- };
8238
- } else if (this.recorderElement) {
8239
- const videoHeight = this.recorderElement.videoHeight || this.recorderElement.height;
8240
- videoDimension = {
8241
- value: videoHeight,
8242
- unit: "px"
8243
- };
8244
- } else videoDimension = dimensions_calculateHeight(responsive, void 0, this.options, this.getRatio(), this.recorderElement);
8245
- return videoDimension;
8397
+ return this.dimensions.calculateHeight(responsive);
8246
8398
  }
8247
8399
  getRawVisualUserMedia() {
8248
8400
  return this.recorderElement;
@@ -8259,14 +8411,14 @@ class Recorder extends util_Despot {
8259
8411
  limitHeight(height) {
8260
8412
  return this.visuals.limitHeight(height);
8261
8413
  }
8262
- isUserMediaLoaded() {
8263
- return this.userMediaLoaded;
8264
- }
8265
8414
  constructor(visuals, replay, options){
8266
- super("Recorder", options), recorder_define_property(this, "visuals", void 0), recorder_define_property(this, "replay", void 0), recorder_define_property(this, "pendingWrites", new boundedWriteQueue()), recorder_define_property(this, "loop", void 0), recorder_define_property(this, "originalAnimationFrameObject", void 0), recorder_define_property(this, "samplesCount", 0), recorder_define_property(this, "framesCount", 0), recorder_define_property(this, "recordingAudioSampleRate", void 0), recorder_define_property(this, "recordingStats", void 0), recorder_define_property(this, "confirmedFrameNumber", 0), recorder_define_property(this, "confirmedSampleNumber", 0), recorder_define_property(this, "recorderElement", void 0), recorder_define_property(this, "userMedia", void 0), recorder_define_property(this, "userMediaTimeout", void 0), recorder_define_property(this, "retryTimeout", void 0), recorder_define_property(this, "reconnectTimeout", void 0), recorder_define_property(this, "connectionTimeout", void 0), recorder_define_property(this, "backpressureTimeout", void 0), recorder_define_property(this, "stopTimeout", void 0), recorder_define_property(this, "frameProgress", void 0), recorder_define_property(this, "sampleProgress", void 0), recorder_define_property(this, "canvas", void 0), recorder_define_property(this, "recordingDimensions", void 0), recorder_define_property(this, "ctx", void 0), recorder_define_property(this, "userMediaLoaded", void 0), recorder_define_property(this, "userMediaLoading", false), recorder_define_property(this, "mediaRequestId", 0), recorder_define_property(this, "cameraRecovery", void 0), recorder_define_property(this, "resolveCameraRecovery", void 0), recorder_define_property(this, "resumeAfterCameraRecovery", false), recorder_define_property(this, "submitting", false), recorder_define_property(this, "unloaded", void 0), recorder_define_property(this, "stopTime", void 0), recorder_define_property(this, "stream", void 0), recorder_define_property(this, "streamBackpressured", false), recorder_define_property(this, "resumeAfterDrain", false), recorder_define_property(this, "mediaTransmissionStarted", false), recorder_define_property(this, "recordingActive", false), recorder_define_property(this, "recordingTransportFailed", false), recorder_define_property(this, "connecting", false), recorder_define_property(this, "connected", false), recorder_define_property(this, "connectionFailed", false), recorder_define_property(this, "reconnecting", false), recorder_define_property(this, "reconnectAttempts", 0), recorder_define_property(this, "blocking", false), recorder_define_property(this, "built", false), recorder_define_property(this, "key", void 0), recorder_define_property(this, "waitingTime", void 0), recorder_define_property(this, "connectingStartedAt", void 0), recorder_define_property(this, "lastCloseEvent", void 0), recorder_define_property(this, "lastSocketError", void 0), recorder_define_property(this, "pingInterval", void 0), recorder_define_property(this, "frame", void 0), recorder_define_property(this, "recordingBuffer", void 0), recorder_define_property(this, "facingMode", void 0);
8267
- this.visuals = visuals;
8268
- this.replay = replay;
8269
- this.facingMode = options.video.facingMode;
8415
+ super(visuals, replay, options), recorder_define_property(this, "dimensions", void 0);
8416
+ this.dimensions = new recorder_dimensions({
8417
+ options,
8418
+ visuals,
8419
+ getRecorderElement: ()=>this.recorderElement,
8420
+ getUserMedia: ()=>this.userMedia
8421
+ });
8270
8422
  }
8271
8423
  }
8272
8424
  const visuals_recorder = Recorder;
@@ -9158,7 +9310,8 @@ class Container extends util_Despot {
9158
9310
  }
9159
9311
  else runValidation = false;
9160
9312
  if (runValidation) {
9161
- const targetName = event?.target?.name;
9313
+ const target = event?.target;
9314
+ const targetName = target instanceof Element ? target.getAttribute("name") : void 0;
9162
9315
  if (targetName) this.emit("VALIDATING", {
9163
9316
  targetName
9164
9317
  });
@@ -9193,27 +9346,10 @@ class Container extends util_Despot {
9193
9346
  }
9194
9347
  }
9195
9348
  if (valid) {
9196
- const recipients = this.form.getRecipients();
9197
- const toIsConfigured = "to" in recipients;
9198
- const ccIsConfigured = "cc" in recipients;
9199
- const bccIsConfigured = "bcc" in recipients;
9200
- const hasTo = recipients.to && recipients.to.length > 0;
9201
- const hasCc = recipients.cc && recipients.cc.length > 0;
9202
- const hasBcc = recipients.bcc && recipients.bcc.length > 0;
9203
- if (toIsConfigured) {
9204
- if (!hasTo) if (ccIsConfigured && bccIsConfigured) {
9205
- if (!hasCc && !hasBcc) valid = false;
9206
- } else if (ccIsConfigured) {
9207
- if (!hasCc) valid = false;
9208
- } else if (bccIsConfigured) {
9209
- if (!hasBcc) valid = false;
9210
- } else valid = false;
9211
- } else if (ccIsConfigured) {
9212
- if (!hasCc) {
9213
- if (bccIsConfigured && !hasBcc) valid = false;
9214
- }
9349
+ if (!email_isRecipientSelectionValid(this.form.getRecipients())) {
9350
+ valid = false;
9351
+ whyInvalid = "At least one recipient is required";
9215
9352
  }
9216
- if (!valid) whyInvalid = "At least one recipient is required";
9217
9353
  }
9218
9354
  } else {
9219
9355
  valid = visualsValid;