videomail-client 16.1.1 → 16.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.4.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
6367
- });
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
6371
- });
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
- }
6378
- record(cb) {
6379
- this.options.logger.debug("AudioRecorder: record()");
6380
- if (this.scriptProcessor) this.scriptProcessor.onaudioprocess = (e)=>{
6381
- this.onAudioProcess(e, cb);
6382
- };
6383
- }
6384
- async resume() {
6385
- const context = this.getAudioContext();
6386
- if ("running" !== context.state) await context.resume();
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);
6387
6260
  }
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,
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
6332
+ });
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
6355
+ });
6356
+ }
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.",
6397
6365
  options: this.options
6398
- }));
6399
- this.options.logger.error(err);
6366
+ });
6367
+ this.failMediaTransmission(err);
6368
+ }, this.options.timeouts.connection);
6369
+ }
6370
+ shouldStartBackpressureTimeout() {
6371
+ return void 0 === this.backpressureTimeout && this.mediaTransmissionStarted;
6372
+ }
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,345 +6422,687 @@ 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;
6482
- }
6483
- hasInvalidDimensions() {
6484
- if (!this.rawVisualUserMedia) return false;
6485
- const { videoWidth, videoHeight } = this.rawVisualUserMedia;
6486
- return videoWidth > 0 && videoWidth < 3 || videoHeight > 0 && videoHeight < 3;
6425
+ class RecorderConnection extends streamWriter {
6426
+ canReconnectAfterStreamClose() {
6427
+ return Boolean(this.connected && this.userMediaLoaded);
6487
6428
  }
6488
- logEvent(eventType, params) {
6489
- this.options.logger.debug(`UserMedia: ... ${EVENT_ASCII} event ${eventType}: ${string_pretty(params)}`);
6429
+ shouldReportStreamClose() {
6430
+ return !this.connecting && !this.blocking;
6490
6431
  }
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
6728
+ });
6729
+ this.emit("ERROR", {
6730
+ err
6566
6731
  });
6567
- this.clearInitializationListeners?.();
6568
- videoCallback();
6569
- };
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();
6582
- }
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
6593
- });
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
6732
  }
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
6617
6733
  });
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
- };
6625
- 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
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)}`);
6632
6742
  });
6633
- 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}`);
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
6777
+ };
6778
+ this.writeStream(Buffer.from(JSON.stringify(commandObj)), cb ? {
6779
+ onFlushedCallback: ()=>{
6780
+ queueMicrotask(cb);
6781
+ }
6782
+ } : void 0);
6638
6783
  }
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
6784
+ } else {
6785
+ this.options.logger.debug(`Reconnecting for the command ${command} …`);
6786
+ this.initSocket(()=>{
6787
+ this.writeCommand(command, args, cb);
6643
6788
  });
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);
6654
6789
  }
6655
6790
  }
6656
- getVideoTrackLabel() {
6657
- return this.videoTrackLabel;
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);
6658
6793
  }
6659
- isReady() {
6660
- return Boolean(this.rawVisualUserMedia?.srcObject || this.rawVisualUserMedia?.currentSrc);
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);
6661
6831
  }
6662
- stop(visualStream, params) {
6663
- 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();
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)
6672
6896
  });
6673
- this.setVisualStream();
6674
- this.audioRecorder?.stop();
6675
- this.audioRecorder = void 0;
6676
- }
6677
- if (!params?.preserveRecordingState) this.paused = this.recording = false;
6897
+ }, params);
6678
6898
  } catch (exc) {
6679
- this.emit("ERROR", {
6680
- exc
6899
+ localStream.getTracks().forEach((track)=>{
6900
+ track.stop();
6901
+ });
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();
6681
6911
  });
6912
+ this.userMediaLoading = false;
6913
+ this.clearUserMediaTimeout();
6914
+ this.cancelCameraRecovery();
6682
6915
  }
6683
6916
  }
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;
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
+ });
6691
6940
  }
6692
- getVideoHeight() {
6693
- if (!this.rawVisualUserMedia) return;
6694
- return this.rawVisualUserMedia.videoHeight || this.rawVisualUserMedia.height;
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");
6949
+ try {
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
6970
+ });
6971
+ else {
6972
+ this.options.logger.debug("Recorder: no error listeners attached but throwing exception further");
6973
+ throw exc;
6974
+ }
6975
+ }
6695
6976
  }
6696
- getVideoWidth() {
6697
- if (!this.rawVisualUserMedia) return;
6698
- return this.rawVisualUserMedia.videoWidth || this.rawVisualUserMedia.width;
6977
+ isNotifying() {
6978
+ return this.visuals.isNotifying();
6699
6979
  }
6700
- hasVideoWidth() {
6701
- const videoWidth = this.getVideoWidth();
6702
- return videoWidth && videoWidth > 0;
6980
+ isHidden() {
6981
+ return !this.recorderElement || html_isHidden(this.recorderElement);
6703
6982
  }
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;
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;
6713
6988
  }
6714
- widthDimension.value = rawWidth;
6715
- return widthDimension;
6716
6989
  }
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;
6990
+ clearStopTimeout() {
6991
+ if (void 0 === this.stopTimeout) return;
6992
+ window.clearTimeout(this.stopTimeout);
6993
+ this.stopTimeout = void 0;
6730
6994
  }
6731
- getRawVisuals() {
6732
- return this.rawVisualUserMedia;
6995
+ isReady() {
6996
+ return this.userMedia?.isReady();
6733
6997
  }
6734
- pause() {
6735
- this.paused = true;
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();
6736
7007
  }
6737
- isPaused() {
6738
- return this.paused;
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);
6739
7018
  }
6740
- resume() {
6741
- this.paused = false;
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
+ });
6742
7030
  }
6743
- record() {
6744
- this.recording = true;
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);
7050
+ });
7051
+ } else {
7052
+ this.userMediaLoaded = false;
7053
+ this.loadUserMedia({
7054
+ recovering: true
7055
+ });
7056
+ }
7057
+ return recovery;
6745
7058
  }
6746
- isRecording() {
6747
- return this.recording;
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
+ });
6748
7067
  }
6749
- getAudioSampleRate() {
6750
- if (this.audioRecorder) return this.audioRecorder.getSampleRate();
6751
- return -1;
7068
+ hide() {
7069
+ if (!this.isHidden()) {
7070
+ if (this.recorderElement) html_hideElement(this.recorderElement);
7071
+ this.clearUserMediaTimeout();
7072
+ this.clearRetryTimeout();
7073
+ }
6752
7074
  }
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
- };
7075
+ isUnloaded() {
7076
+ return this.unloaded;
6762
7077
  }
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
- });
7078
+ isUserMediaLoaded() {
7079
+ return this.userMediaLoaded;
7080
+ }
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;
6777
7085
  }
6778
7086
  }
6779
- const visuals_userMedia = UserMedia;
6780
- var Buffer = __webpack_require__("dY").hp;
6781
- function recorder_define_property(obj, key, value) {
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) {
6782
7106
  if (key in obj) Object.defineProperty(obj, key, {
6783
7107
  value: value,
6784
7108
  enumerable: true,
@@ -6788,1139 +7112,1319 @@ function recorder_define_property(obj, key, value) {
6788
7112
  else obj[key] = value;
6789
7113
  return obj;
6790
7114
  }
6791
- const PIPE_SYMBOL = "°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸ ";
6792
- class Recorder extends util_Despot {
6793
- hasExceededWebSocketSendBuffer(buffer, opts) {
6794
- return buffer.length > 0 && !this.pendingWrites.enqueue(buffer, opts);
6795
- }
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
6803
- });
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);
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);
6812
7124
  }
6813
- writeToStream(stream, buffer, opts) {
6814
- const onFlushedCallback = opts?.onFlushedCallback;
7125
+ onUserMediaReady(params) {
6815
7126
  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
6829
- });
6830
- }
6831
- });
6832
- if (!canContinue) {
6833
- this.streamBackpressured = true;
6834
- this.startBackpressureTimeout();
6835
- this.pauseForBackpressure();
6836
- return false;
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."));
6837
7132
  }
6838
- return true;
6839
- } catch (exc) {
6840
- const err = error_createError({
6841
- message: "Failed writing to server",
6842
- explanation: `stream.write() failed because of ${string_pretty(exc)}`,
6843
- options: this.options,
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);
7146
+ } catch (exc) {
7147
+ if (params?.recovering) this.failCameraRecovery(exc);
7148
+ else this.emit("ERROR", {
6844
7149
  exc
6845
7150
  });
6846
- this.handleWriteFailure(err);
6847
- return false;
6848
- }
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();
6858
7151
  }
6859
- this.resumeRecordingAfterDrain();
6860
- }
6861
- isStaleStream(stream) {
6862
- return this.stream !== stream || stream.destroyed;
6863
7152
  }
6864
- canPauseForBackpressure() {
6865
- return Boolean(this.recordingActive && !this.userMedia?.isPaused() && this.loop?.isRunning());
7153
+ calculateFrameProgress() {
7154
+ return calculateProgress(this.confirmedFrameNumber, this.framesCount);
6866
7155
  }
6867
- pauseForBackpressure() {
6868
- if (!this.canPauseForBackpressure()) return;
6869
- this.resumeAfterDrain = true;
6870
- this.loop.stop();
7156
+ calculateSampleProgress() {
7157
+ return calculateProgress(this.confirmedSampleNumber, this.samplesCount);
6871
7158
  }
6872
- resumeRecordingAfterDrain() {
6873
- if (!this.resumeAfterDrain) return;
6874
- this.resumeAfterDrain = false;
6875
- if (this.canResumeAfterDrain()) this.loop?.start();
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
+ });
6876
7166
  }
6877
- canResumeAfterDrain() {
6878
- return this.recordingActive && !this.userMedia?.isPaused();
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();
6879
7175
  }
6880
- handleWriteFailure(err) {
6881
- if (this.mediaTransmissionStarted) return void this.failMediaTransmission(err);
6882
- this.emit("ERROR", {
6883
- err
7176
+ updateSampleProgress(args) {
7177
+ this.confirmedSampleNumber = getConfirmedProgressNumber({
7178
+ args,
7179
+ kind: "sample",
7180
+ options: this.options
6884
7181
  });
7182
+ this.sampleProgress = this.calculateSampleProgress();
7183
+ this.updateOverallProgress();
6885
7184
  }
6886
- failMediaTransmission(err) {
6887
- if (this.recordingTransportFailed) return;
6888
- this.recordingTransportFailed = true;
6889
- this.recordingActive = false;
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;
6890
7191
  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
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
6907
7209
  });
7210
+ if (this.stopTime) this.waitingTime = Date.now() - this.stopTime;
7211
+ this.recordingStats ??= {};
7212
+ this.recordingStats.waitingTime = this.waitingTime;
6908
7213
  }
6909
- startBackpressureTimeout() {
6910
- if (!this.shouldStartBackpressureTimeout()) return;
6911
- this.backpressureTimeout = window.setTimeout(()=>{
6912
- this.backpressureTimeout = void 0;
6913
- if (this.shouldIgnoreBackpressureTimeout()) return;
6914
- 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.",
6917
- options: this.options
6918
- });
6919
- this.failMediaTransmission(err);
6920
- }, this.options.timeouts.connection);
7214
+ cancelAnimationFrame() {
7215
+ this.loop?.dispose();
6921
7216
  }
6922
- shouldStartBackpressureTimeout() {
6923
- return void 0 === this.backpressureTimeout && this.mediaTransmissionStarted;
7217
+ getIntervalSum() {
7218
+ if (!this.loop) return;
7219
+ return this.loop.getElapsedTime();
6924
7220
  }
6925
- shouldIgnoreBackpressureTimeout() {
6926
- return !this.streamBackpressured || this.recordingTransportFailed;
7221
+ getAvgInterval() {
7222
+ const intervalSum = this.getIntervalSum() ?? 0;
7223
+ if (!this.hasValidInterval(intervalSum)) return;
7224
+ return intervalSum / this.framesCount;
6927
7225
  }
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);
7226
+ getAvgFps() {
7227
+ const intervalSum = this.getIntervalSum() ?? 0;
7228
+ if (!this.hasValidInterval(intervalSum)) return;
7229
+ return this.framesCount / intervalSum * 1000;
6941
7230
  }
6942
- handleStreamDrain(stream) {
6943
- this.options.logger.debug(`${PIPE_SYMBOL}Stream *drain* event emitted`);
6944
- this.flushPendingWrites(stream);
7231
+ hasValidInterval(intervalSum) {
7232
+ return intervalSum > 0;
6945
7233
  }
6946
- canReconnectAfterStreamClose() {
6947
- return Boolean(this.connected && this.userMediaLoaded);
7234
+ getRecordingStats() {
7235
+ return this.recordingStats;
6948
7236
  }
6949
- shouldReportStreamClose() {
6950
- return !this.connecting && !this.blocking;
7237
+ getAudioSampleRate() {
7238
+ return this.recordingAudioSampleRate ?? this.userMedia?.getAudioSampleRate();
6951
7239
  }
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
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
6958
7248
  });
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"
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
+ });
6975
7273
  });
6976
- }, 0);
7274
+ this.reset();
7275
+ }, 60);
6977
7276
  }
6978
- shouldIgnoreStreamClose() {
6979
- return this.recordingTransportFailed || Boolean(this.unloaded) || this.validate();
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);
6980
7284
  }
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
- return;
6993
- }
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
7000
- });
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
- })
7009
- });
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
+ }
7010
7304
  }
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);
7305
+ validate() {
7306
+ return Boolean(this.key) && void 0 === this.canvas;
7017
7307
  }
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;
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();
7023
7319
  }
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);
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();
7032
7334
  }
7033
- show() {
7034
- html_showElement(this.recorderElement);
7335
+ onFlushed(opts) {
7336
+ const frameNumber = opts.frameNumber;
7337
+ if (1 === frameNumber) this.emit("FIRST_FRAME_SENT");
7035
7338
  }
7036
- onUserMediaReady(params) {
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");
7037
7342
  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."));
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
+ });
7043
7371
  }
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
7372
  } catch (exc) {
7058
- if (params?.recovering) this.failCameraRecovery(exc);
7059
- else this.emit("ERROR", {
7373
+ this.emit("ERROR", {
7060
7374
  exc
7061
7375
  });
7062
7376
  }
7063
7377
  }
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;
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;
7069
7384
  }
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;
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();
7395
+ });
7396
+ });
7397
+ });
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
+ });
7412
+ }
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();
7421
+ } catch (exc) {
7422
+ const err = error_createError({
7423
+ exc,
7424
+ options: this.options
7425
+ });
7426
+ this.emit("ERROR", {
7427
+ err
7428
+ });
7429
+ return;
7430
+ }
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;
7441
+ }
7442
+ if (!this.canvas.height) {
7443
+ const err = error_createError({
7444
+ message: "Canvas has an invalid height.",
7445
+ options: this.options
7446
+ });
7447
+ this.emit("ERROR", {
7448
+ err
7449
+ });
7450
+ return;
7451
+ }
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
7465
+ });
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}`);
7470
+ });
7471
+ loop.start();
7075
7472
  }
7076
- clearBackpressureTimeout() {
7077
- if (void 0 === this.backpressureTimeout) return;
7078
- window.clearTimeout(this.backpressureTimeout);
7079
- this.backpressureTimeout = void 0;
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
+ }
7080
7480
  }
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;
7481
+ restoreAnimationFrameObject() {
7482
+ this.options.logger.debug("Recorder: restoreAnimationFrameObject()");
7483
+ if (this.originalAnimationFrameObject) this.setAnimationFrameObject(this.originalAnimationFrameObject);
7086
7484
  }
7087
- handleConnectionFailure(params) {
7088
- if (this.retryConnection(params)) return;
7089
- this.failConnection(params);
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
+ });
7090
7501
  }
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;
7502
+ isPaused() {
7503
+ return this.userMedia?.isPaused() && !this.loop?.isRunning();
7504
+ }
7505
+ isUserMediaPaused() {
7506
+ return this.userMedia?.isPaused() ?? false;
7507
+ }
7508
+ isCaptureLoopRunning() {
7509
+ return this.loop?.isRunning() ?? false;
7510
+ }
7511
+ stopCaptureLoop() {
7512
+ this.loop?.stop();
7105
7513
  }
7106
- canRetryConnection() {
7107
- return Boolean(this.reconnecting && this.userMediaLoaded && this.isOnline() && !this.unloaded && !this.blocking);
7514
+ startCaptureLoop() {
7515
+ this.loop?.start();
7108
7516
  }
7109
- shouldContinueReconnect() {
7110
- return this.reconnecting && !this.unloaded && !this.blocking;
7517
+ isRecording() {
7518
+ return Boolean(this.loop?.isRunning() && !this.isPaused() && !this.isNotifying() && this.stream && !this.stream.destroyed);
7111
7519
  }
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();
7520
+ getRecordingDimensions() {
7521
+ return this.recordingDimensions && {
7522
+ ...this.recordingDimensions
7523
+ };
7121
7524
  }
7122
- isOnline() {
7123
- return navigator.onLine;
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;
7124
7528
  }
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
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;
7562
+ }
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;
7582
+ }
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
+ }
7626
+ }
7627
+ }
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
7155
7635
  };
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.`;
7636
+ return dimension;
7637
+ }
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
+ };
7164
7668
  }
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
- });
7669
+ return recorderHeight;
7179
7670
  }
7180
- shouldIgnoreConnectionFailure() {
7181
- return this.connectionFailed || this.connected || Boolean(this.unloaded) || this.validate();
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;
7182
7681
  }
7183
- calculateFrameProgress() {
7184
- return `${(this.confirmedFrameNumber / (this.framesCount || 1) * 100).toFixed(2)}%`;
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());
7185
7689
  }
7186
- calculateSampleProgress() {
7187
- return `${(this.confirmedSampleNumber / (this.samplesCount || 1) * 100).toFixed(2)}%`;
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;
7188
7708
  }
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
- });
7709
+ constructor(context){
7710
+ dimensions_define_property(this, "context", void 0);
7711
+ this.context = context;
7712
+ }
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
+ const AUDIO_RECORDER_PROCESSOR_NAME = "videomail-pcm-recorder";
7744
+ function registerAudioRecorderProcessor(name, Processor, register) {
7745
+ class PcmRecorderProcessor extends Processor {
7746
+ process(inputs) {
7747
+ if (this.recordingId < 0) return false;
7748
+ const channel = inputs[0]?.[0];
7749
+ if (!channel || 0 === this.recordingId) return true;
7750
+ let inputOffset = 0;
7751
+ while(inputOffset < channel.length){
7752
+ const count = Math.min(this.samples.length - this.offset, channel.length - inputOffset);
7753
+ this.samples.set(channel.subarray(inputOffset, inputOffset + count), this.offset);
7754
+ this.offset += count;
7755
+ inputOffset += count;
7756
+ if (this.offset === this.samples.length) {
7757
+ const samples = this.samples;
7758
+ this.port.postMessage({
7759
+ recordingId: this.recordingId,
7760
+ samples
7761
+ }, [
7762
+ samples.buffer
7763
+ ]);
7764
+ this.samples = new Float32Array(this.offset);
7765
+ this.offset = 0;
7766
+ }
7767
+ }
7768
+ return true;
7769
+ }
7770
+ constructor(options){
7771
+ super();
7772
+ this.samples = new Float32Array(options.processorOptions.bufferSize);
7773
+ this.offset = 0;
7774
+ this.recordingId = 0;
7775
+ this.port.onmessage = (event)=>{
7776
+ const recordingId = event.data;
7777
+ if ("number" != typeof recordingId) return;
7778
+ if (!Number.isSafeInteger(recordingId) || recordingId < -1) return;
7779
+ this.recordingId = recordingId;
7780
+ this.offset = 0;
7781
+ };
7782
+ }
7783
+ }
7784
+ register(name, PcmRecorderProcessor);
7785
+ }
7786
+ function AudioRecorder_define_property(obj, key, value) {
7787
+ if (key in obj) Object.defineProperty(obj, key, {
7788
+ value: value,
7789
+ enumerable: true,
7790
+ configurable: true,
7791
+ writable: true
7792
+ });
7793
+ else obj[key] = value;
7794
+ return obj;
7795
+ }
7796
+ const CHANNELS = 1;
7797
+ const DEFAULT_BUFFER_SIZE = 2048;
7798
+ const MIN_BUFFER_SIZE = 256;
7799
+ const MAX_BUFFER_SIZE = 16384;
7800
+ class AudioRecorder extends util_Despot {
7801
+ getAudioContext() {
7802
+ if (!this.vcAudioContext) this.vcAudioContext = new window.AudioContext();
7803
+ return this.vcAudioContext;
7196
7804
  }
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",
7805
+ getBufferSize() {
7806
+ const configuredSize = this.options.audio.bufferSize;
7807
+ const bufferSize = "auto" === configuredSize ? DEFAULT_BUFFER_SIZE : configuredSize;
7808
+ if (void 0 === bufferSize) throw error_createError({
7809
+ message: "Audio buffer size is required.",
7205
7810
  options: this.options
7206
7811
  });
7207
- this.confirmedFrameNumber = args.frame;
7208
- this.frameProgress = this.calculateFrameProgress();
7209
- this.updateOverallProgress();
7210
- }
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",
7812
+ const validBufferSize = Number.isInteger(bufferSize) && bufferSize >= MIN_BUFFER_SIZE && bufferSize <= MAX_BUFFER_SIZE && is_power_of_two(bufferSize);
7813
+ if (!validBufferSize) throw error_createError({
7814
+ message: `Audio buffer size must be a power of two between ${MIN_BUFFER_SIZE} and ${MAX_BUFFER_SIZE}. The current buffer size is ${bufferSize}.`,
7219
7815
  options: this.options
7220
7816
  });
7221
- this.confirmedSampleNumber = args.sample;
7222
- this.sampleProgress = this.calculateSampleProgress();
7223
- this.updateOverallProgress();
7817
+ return bufferSize;
7224
7818
  }
7225
- preview(args) {
7226
- if (!args) throw error_createError({
7227
- message: "Preview arguments are missing.",
7819
+ validateVolume() {
7820
+ const { volume } = this.options.audio;
7821
+ const validVolume = Number.isFinite(volume) && volume >= 0 && volume <= 1;
7822
+ if (!validVolume) throw error_createError({
7823
+ message: `Audio volume must be between zero and one. The current volume is ${volume}.`,
7228
7824
  options: this.options
7229
7825
  });
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;
7253
7826
  }
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;
7290
- }
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;
7827
+ revokeModuleUrl() {
7828
+ if (this.moduleUrl) {
7829
+ URL.revokeObjectURL(this.moduleUrl);
7830
+ this.moduleUrl = void 0;
7320
7831
  }
7832
+ }
7833
+ async loadWorklet(context) {
7834
+ const source = `(${registerAudioRecorderProcessor.toString()})(${JSON.stringify(AUDIO_RECORDER_PROCESSOR_NAME)}, AudioWorkletProcessor, registerProcessor);`;
7835
+ const moduleUrl = URL.createObjectURL(new Blob([
7836
+ source
7837
+ ], {
7838
+ type: "text/javascript"
7839
+ }));
7840
+ this.moduleUrl = moduleUrl;
7321
7841
  try {
7322
- this.stream = websocket_stream(nativeSocket);
7323
- } 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)}`);
7418
- });
7842
+ await context.audioWorklet.addModule(moduleUrl);
7843
+ } finally{
7844
+ if (this.moduleUrl === moduleUrl) this.revokeModuleUrl();
7419
7845
  }
7420
7846
  }
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;
7847
+ onAudioProcess(event) {
7848
+ const callback = this.audioCallback;
7849
+ if (!callback) return;
7850
+ if (!this.userMedia.isRecording() || this.userMedia.isPaused()) return;
7851
+ const message = event.data;
7852
+ if ("object" != typeof message || null === message) return;
7853
+ if (!("recordingId" in message) || message.recordingId !== this.recordingId) return;
7854
+ if (!("samples" in message) || !(message.samples instanceof Float32Array)) return;
7855
+ if (0 === message.samples.length) return;
7856
+ callback(new audio_sample(message.samples));
7428
7857
  }
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);
7858
+ disconnect() {
7859
+ if (this.worklet) {
7860
+ this.worklet.port.onmessage = null;
7861
+ this.worklet.onprocessorerror = null;
7862
+ this.worklet.port.postMessage(-1);
7863
+ this.worklet.port.close();
7864
+ this.worklet.disconnect();
7865
+ this.worklet = void 0;
7452
7866
  }
7453
- else {
7454
- this.options.logger.debug(`Recorder: no error listeners attached but throwing error ${string_pretty(err)}`);
7867
+ this.audioInput?.disconnect();
7868
+ this.volume?.disconnect();
7869
+ this.audioInput = void 0;
7870
+ this.volume = void 0;
7871
+ this.audioCallback = void 0;
7872
+ this.recordingId = 0;
7873
+ }
7874
+ async init(localMediaStream) {
7875
+ this.options.logger.debug("AudioRecorder: init()");
7876
+ const initializationId = ++this.initializationId;
7877
+ this.disconnect();
7878
+ const bufferSize = this.getBufferSize();
7879
+ this.validateVolume();
7880
+ if ("function" != typeof window.AudioContext) throw error_createError({
7881
+ message: "Audio recording requires Web Audio support.",
7882
+ options: this.options
7883
+ });
7884
+ const context = this.getAudioContext();
7885
+ if (void 0 === context.audioWorklet || "function" != typeof window.AudioWorkletNode) throw error_createError({
7886
+ message: "Audio recording requires AudioWorklet support in a secure context.",
7887
+ options: this.options
7888
+ });
7889
+ const workletReady = this.workletReady ??= this.loadWorklet(context);
7890
+ try {
7891
+ await workletReady;
7892
+ } catch (exc) {
7893
+ if (this.workletReady === workletReady) this.workletReady = void 0;
7894
+ if (initializationId !== this.initializationId) return;
7455
7895
  throw error_createError({
7456
- err,
7457
- message: "Unable to process this error since there are no error listeners anymore.",
7896
+ message: "Unable to load the audio recorder worklet. Check AudioWorklet and Content Security Policy support for blob: scripts.",
7897
+ cause: exc,
7458
7898
  options: this.options
7459
7899
  });
7460
7900
  }
7461
- }
7462
- getUserMediaCallback(localStream, params, requestId = this.mediaRequestId) {
7463
- if (!this.userMedia) {
7464
- localStream.getTracks().forEach((track)=>{
7465
- track.stop();
7466
- });
7901
+ if (initializationId !== this.initializationId) return;
7902
+ try {
7903
+ this.audioInput = context.createMediaStreamSource(localMediaStream);
7904
+ } catch (exc) {
7467
7905
  throw error_createError({
7468
- message: "No user media is defined",
7906
+ message: "Webcam has no audio",
7907
+ exc,
7469
7908
  options: this.options
7470
7909
  });
7471
7910
  }
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)
7911
+ try {
7912
+ this.volume = context.createGain();
7913
+ this.volume.gain.value = this.options.audio.volume;
7914
+ this.worklet = new window.AudioWorkletNode(context, AUDIO_RECORDER_PROCESSOR_NAME, {
7915
+ channelCount: CHANNELS,
7916
+ channelCountMode: "explicit",
7917
+ outputChannelCount: [
7918
+ CHANNELS
7919
+ ],
7920
+ processorOptions: {
7921
+ bufferSize
7922
+ }
7923
+ });
7924
+ this.worklet.port.onmessage = (event)=>{
7925
+ this.onAudioProcess(event);
7926
+ };
7927
+ this.worklet.onprocessorerror = ()=>{
7928
+ this.stop();
7929
+ this.emit("ERROR", {
7930
+ err: error_createError({
7931
+ message: "Audio recording processor failed.",
7932
+ options: this.options
7933
+ })
7479
7934
  });
7480
- }, params);
7935
+ };
7936
+ this.audioInput.connect(this.volume);
7937
+ this.volume.connect(this.worklet);
7938
+ this.worklet.connect(context.destination);
7481
7939
  } catch (exc) {
7482
- localStream.getTracks().forEach((track)=>{
7483
- track.stop();
7484
- });
7485
- this.userMediaLoading = false;
7486
- this.clearUserMediaTimeout();
7487
- this.userMediaErrorCallback(exc, {
7488
- audio: isAudioEnabled(this.options)
7940
+ this.disconnect();
7941
+ throw error_createError({
7942
+ message: "Failed to connect the audio recorder.",
7943
+ exc,
7944
+ options: this.options
7489
7945
  });
7490
7946
  }
7491
- else {
7492
- localStream.getTracks().forEach((track)=>{
7947
+ }
7948
+ record(cb) {
7949
+ this.options.logger.debug("AudioRecorder: record()");
7950
+ this.audioCallback = cb;
7951
+ this.updateRecordingState();
7952
+ }
7953
+ updateRecordingState() {
7954
+ if (!this.worklet) return;
7955
+ const shouldRecord = Boolean(this.audioCallback) && this.userMedia.isRecording() && !this.userMedia.isPaused();
7956
+ this.recordingId = shouldRecord ? ++this.recordingSequence : 0;
7957
+ this.worklet.port.postMessage(this.recordingId);
7958
+ }
7959
+ async resume() {
7960
+ const context = this.vcAudioContext;
7961
+ if (!context) return;
7962
+ if ("running" !== context.state) await context.resume();
7963
+ }
7964
+ stop() {
7965
+ this.options.logger.debug("AudioRecorder: stop()");
7966
+ this.initializationId++;
7967
+ this.disconnect();
7968
+ this.revokeModuleUrl();
7969
+ const context = this.vcAudioContext;
7970
+ this.vcAudioContext = void 0;
7971
+ this.workletReady = void 0;
7972
+ if (context && "closed" !== context.state) context.close().then(()=>{
7973
+ this.options.logger.debug("AudioRecorder: audio context is closed");
7974
+ }).catch((err)=>{
7975
+ if (err instanceof Error) return void this.options.logger.error(error_createError({
7976
+ err,
7977
+ options: this.options
7978
+ }));
7979
+ this.options.logger.error(err);
7980
+ });
7981
+ }
7982
+ getSampleRate() {
7983
+ if ("function" == typeof window.AudioContext) return this.getAudioContext().sampleRate;
7984
+ return -1;
7985
+ }
7986
+ constructor(userMedia, options){
7987
+ super("AudioRecorder", options), AudioRecorder_define_property(this, "userMedia", void 0), AudioRecorder_define_property(this, "worklet", void 0), AudioRecorder_define_property(this, "audioInput", void 0), AudioRecorder_define_property(this, "volume", void 0), AudioRecorder_define_property(this, "vcAudioContext", void 0), AudioRecorder_define_property(this, "workletReady", void 0), AudioRecorder_define_property(this, "moduleUrl", void 0), AudioRecorder_define_property(this, "audioCallback", void 0), AudioRecorder_define_property(this, "initializationId", 0), AudioRecorder_define_property(this, "recordingSequence", 0), AudioRecorder_define_property(this, "recordingId", 0);
7988
+ this.userMedia = userMedia;
7989
+ }
7990
+ }
7991
+ const media_AudioRecorder = AudioRecorder;
7992
+ const VIRTUAL_KEYWORDS = [
7993
+ "obs",
7994
+ "virtual",
7995
+ "manycam",
7996
+ "vcam",
7997
+ "splitcam",
7998
+ "droidcam"
7999
+ ];
8000
+ function isVirtualCamera(videoTrack) {
8001
+ const capabilities = videoTrack.getCapabilities();
8002
+ if (!capabilities.frameRate) return true;
8003
+ const isVirtual = VIRTUAL_KEYWORDS.some((keyword)=>videoTrack.label.toLowerCase().includes(keyword));
8004
+ return isVirtual;
8005
+ }
8006
+ const util_isVirtualCamera = isVirtualCamera;
8007
+ function userMedia_define_property(obj, key, value) {
8008
+ if (key in obj) Object.defineProperty(obj, key, {
8009
+ value: value,
8010
+ enumerable: true,
8011
+ configurable: true,
8012
+ writable: true
8013
+ });
8014
+ else obj[key] = value;
8015
+ return obj;
8016
+ }
8017
+ const EVENT_ASCII = "|—O—|";
8018
+ class UserMedia extends util_Despot {
8019
+ attachMediaStream(stream) {
8020
+ if (this.rawVisualUserMedia) {
8021
+ this.rawVisualUserMedia.srcObject = stream;
8022
+ const previousStream = this.currentVisualStream;
8023
+ this.currentVisualStream = stream;
8024
+ if (previousStream && previousStream !== stream) previousStream.getTracks().forEach((track)=>{
7493
8025
  track.stop();
7494
8026
  });
7495
- this.userMediaLoading = false;
7496
- this.clearUserMediaTimeout();
7497
- this.cancelCameraRecovery();
8027
+ } else throw error_createError({
8028
+ message: "Error attaching stream to element.",
8029
+ explanation: "Contact the developer about this",
8030
+ options: this.options
8031
+ });
8032
+ }
8033
+ setVisualStream(localMediaStream) {
8034
+ if (localMediaStream) this.attachMediaStream(localMediaStream);
8035
+ else {
8036
+ if (this.rawVisualUserMedia) {
8037
+ this.rawVisualUserMedia.srcObject = null;
8038
+ this.rawVisualUserMedia.removeAttribute("src");
8039
+ }
8040
+ this.currentVisualStream = void 0;
7498
8041
  }
7499
8042
  }
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);
8043
+ async restorePlayback() {
8044
+ if (!this.rawVisualUserMedia) throw new Error("No camera preview is available.");
8045
+ await this.rawVisualUserMedia.play();
8046
+ await this.audioRecorder?.resume();
8047
+ }
8048
+ hasLiveStream() {
8049
+ const videoReady = this.currentVisualStream?.getVideoTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
8050
+ const audioReady = !isAudioEnabled(this.options) || this.currentVisualStream?.getAudioTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
8051
+ return Boolean(this.currentVisualStream?.active && videoReady && audioReady);
8052
+ }
8053
+ hasEnded() {
8054
+ if (this.rawVisualUserMedia?.ended) return this.rawVisualUserMedia.ended;
8055
+ return !this.currentVisualStream?.active;
8056
+ }
8057
+ hasInvalidDimensions() {
8058
+ if (!this.rawVisualUserMedia) return false;
8059
+ const { videoWidth, videoHeight } = this.rawVisualUserMedia;
8060
+ return videoWidth > 0 && videoWidth < 3 || videoHeight > 0 && videoHeight < 3;
8061
+ }
8062
+ logEvent(eventType, params) {
8063
+ this.options.logger.debug(`UserMedia: ... ${EVENT_ASCII} event ${eventType}: ${string_pretty(params)}`);
8064
+ }
8065
+ unloadRemainingEventListeners() {
8066
+ this.options.logger.debug("UserMedia: unloadRemainingEventListeners()");
8067
+ this.clearInitializationListeners?.();
8068
+ mediaEvents.forEach((eventName)=>{
8069
+ this.rawVisualUserMedia?.removeEventListener(eventName, this.outputEvent);
7524
8070
  });
8071
+ this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
8072
+ }
8073
+ audioRecord(audioCallback) {
8074
+ this.removeListener("SENDING_FIRST_FRAME");
8075
+ this.audioRecorder?.record(audioCallback);
7525
8076
  }
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
8077
+ init(localMediaStream, videoCallback, audioCallback, endedEarlyCallback, params) {
8078
+ this.clearInitializationListeners?.();
8079
+ this.stop(localMediaStream, {
8080
+ aboutToInitialize: true,
8081
+ preserveRecordingState: Boolean(params?.switchingFacingMode || params?.recovering)
8082
+ });
8083
+ const initializationId = ++this.initializationId;
8084
+ this.onPlayReached = false;
8085
+ this.onLoadedMetaDataReached = false;
8086
+ this.playingPromiseReached = false;
8087
+ this.videoTrackLabel = void 0;
8088
+ if (isAudioEnabled(this.options)) this.audioRecorder ??= new media_AudioRecorder(this, this.options);
8089
+ const unloadAllEventListeners = ()=>{
8090
+ this.options.logger.debug("UserMedia: unloadAllEventListeners()");
8091
+ this.unloadRemainingEventListeners();
8092
+ this.removeListener("SENDING_FIRST_FRAME");
8093
+ this.rawVisualUserMedia?.removeEventListener("play", onPlay);
8094
+ this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
8095
+ };
8096
+ this.clearInitializationListeners = ()=>{
8097
+ this.rawVisualUserMedia?.removeEventListener("play", onPlay);
8098
+ this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
8099
+ this.clearInitializationListeners = void 0;
8100
+ };
8101
+ const play = ()=>{
8102
+ try {
8103
+ this.rawVisualUserMedia?.load();
8104
+ if (this.rawVisualUserMedia?.paused) {
8105
+ 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)}`);
8106
+ this.rawVisualUserMedia.play().then(()=>{
8107
+ if (initializationId !== this.initializationId) return;
8108
+ if (!this.playingPromiseReached) {
8109
+ this.options.logger.debug("UserMedia: play promise successful. Playing now.");
8110
+ this.playingPromiseReached = true;
8111
+ }
8112
+ }).catch((exc)=>{
8113
+ if (initializationId !== this.initializationId) return;
8114
+ if (exc instanceof Error && !params?.recovering) this.options.logger.warn(`Caught pending user media promise exception: ${exc.toString()}`);
8115
+ else {
8116
+ unloadAllEventListeners();
8117
+ endedEarlyCallback(error_createError({
8118
+ message: "Failed to play user media upon play event.",
8119
+ exc,
8120
+ options: this.options
8121
+ }));
8122
+ }
8123
+ });
8124
+ }
8125
+ } catch (exc) {
8126
+ unloadAllEventListeners();
8127
+ endedEarlyCallback(exc);
8128
+ }
8129
+ };
8130
+ const fireCallbacks = ()=>{
8131
+ const readyState = this.rawVisualUserMedia?.readyState;
8132
+ this.options.logger.debug(`UserMedia: fireCallbacks(readyState=${readyState}, onPlayReached=${this.onPlayReached}, onLoadedMetaDataReached=${this.onLoadedMetaDataReached})`);
8133
+ if (this.onPlayReached && this.onLoadedMetaDataReached) if (this.audioRecorder) {
8134
+ const initializeAudio = async (audioRecorder)=>{
8135
+ try {
8136
+ await audioRecorder.init(localMediaStream);
8137
+ if (initializationId !== this.initializationId || this.currentVisualStream !== localMediaStream) return;
8138
+ const onAudioReady = ()=>{
8139
+ if (initializationId !== this.initializationId || this.currentVisualStream !== localMediaStream) return;
8140
+ if (this.recording) this.audioRecord(audioCallback);
8141
+ else this.on("SENDING_FIRST_FRAME", ()=>{
8142
+ this.audioRecord(audioCallback);
8143
+ });
8144
+ this.clearInitializationListeners?.();
8145
+ videoCallback();
8146
+ };
8147
+ if (params?.recovering) await audioRecorder.resume();
8148
+ onAudioReady();
8149
+ } catch (exc) {
8150
+ if (initializationId !== this.initializationId) return;
8151
+ unloadAllEventListeners();
8152
+ this.stop(void 0, {
8153
+ preserveRecordingState: Boolean(params?.recovering)
8154
+ });
8155
+ endedEarlyCallback(exc);
8156
+ }
8157
+ };
8158
+ initializeAudio(this.audioRecorder);
8159
+ } else {
8160
+ this.clearInitializationListeners?.();
8161
+ videoCallback();
8162
+ }
8163
+ };
8164
+ const onPlay = ()=>{
8165
+ try {
8166
+ this.logEvent("play", {
8167
+ readyState: this.rawVisualUserMedia?.readyState,
8168
+ audio: isAudioEnabled(this.options),
8169
+ width: this.rawVisualUserMedia?.width,
8170
+ height: this.rawVisualUserMedia?.height,
8171
+ videoWidth: this.rawVisualUserMedia?.videoWidth,
8172
+ videoHeight: this.rawVisualUserMedia?.videoHeight
8173
+ });
8174
+ this.rawVisualUserMedia?.removeEventListener("play", onPlay);
8175
+ if (this.hasEnded() || this.hasInvalidDimensions()) endedEarlyCallback(error_createError({
8176
+ message: "Already busy",
8177
+ explanation: "Probably another browser window is using your webcam?",
8178
+ options: this.options
8179
+ }));
8180
+ else {
8181
+ this.onPlayReached = true;
8182
+ fireCallbacks();
8183
+ }
8184
+ } catch (exc) {
8185
+ unloadAllEventListeners();
8186
+ endedEarlyCallback(exc);
8187
+ }
7532
8188
  };
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
8189
+ const onLoadedMetaData = ()=>{
8190
+ this.logEvent("loadedmetadata", {
8191
+ readyState: this.rawVisualUserMedia?.readyState,
8192
+ paused: this.rawVisualUserMedia?.paused,
8193
+ width: this.rawVisualUserMedia?.width,
8194
+ height: this.rawVisualUserMedia?.height,
8195
+ videoWidth: this.rawVisualUserMedia?.videoWidth,
8196
+ videoHeight: this.rawVisualUserMedia?.videoHeight
8197
+ });
8198
+ this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
8199
+ if (!this.hasEnded() && !this.hasInvalidDimensions()) {
8200
+ this.emit("LOADED_META_DATA");
8201
+ this.onLoadedMetaDataReached = true;
8202
+ fireCallbacks();
8203
+ }
7541
8204
  };
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
8205
  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
8206
+ const videoTrack = media_getFirstVideoTrack(localMediaStream);
8207
+ this.videoTrackLabel = videoTrack?.label;
8208
+ if (videoTrack) if (videoTrack.enabled) if (util_isVirtualCamera(videoTrack)) throw error_createError({
8209
+ message: "Virtual cameras are not allowed.",
8210
+ explanation: `Please use a real camera, not "${videoTrack.label}".`,
8211
+ options: this.options
7572
8212
  });
7573
8213
  else {
7574
- this.options.logger.debug("Recorder: no error listeners attached but throwing exception further");
7575
- throw exc;
8214
+ let description = "";
8215
+ if (videoTrack.label.length > 0) description = description.concat(videoTrack.label);
8216
+ description = description.concat(` with enabled=${videoTrack.enabled}, muted=${videoTrack.muted}, readyState=${videoTrack.readyState}`);
8217
+ this.options.logger.debug(`UserMedia: ${videoTrack.kind} detected. ${description}`);
7576
8218
  }
8219
+ else throw error_createError({
8220
+ message: "Webcam is disabled",
8221
+ explanation: "The video track seems to be disabled. Enable it in your system.",
8222
+ options: this.options
8223
+ });
8224
+ else this.options.logger.debug("UserMedia: detected (but no video tracks exist");
8225
+ this.rawVisualUserMedia?.addEventListener("loadedmetadata", onLoadedMetaData);
8226
+ this.rawVisualUserMedia?.addEventListener("play", onPlay);
8227
+ this.rawVisualUserMedia?.removeEventListener("error", this.handleMediaError);
8228
+ this.rawVisualUserMedia?.addEventListener("error", this.handleMediaError);
8229
+ this.setVisualStream(localMediaStream);
8230
+ play();
8231
+ } catch (exc) {
8232
+ unloadAllEventListeners();
8233
+ endedEarlyCallback(exc);
7577
8234
  }
7578
8235
  }
7579
- executeCommand(command) {
7580
- if (this.unloaded) return;
8236
+ getVideoTrackLabel() {
8237
+ return this.videoTrackLabel;
8238
+ }
8239
+ isReady() {
8240
+ return Boolean(this.rawVisualUserMedia?.srcObject || this.rawVisualUserMedia?.currentSrc);
8241
+ }
8242
+ stop(visualStream, params) {
7581
8243
  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
- }
8244
+ let chosenStream = visualStream;
8245
+ if (!params?.aboutToInitialize) {
8246
+ this.initializationId++;
8247
+ this.clearInitializationListeners?.();
8248
+ chosenStream ??= this.currentVisualStream;
8249
+ const tracks = chosenStream?.getTracks();
8250
+ if (tracks) tracks.forEach((track)=>{
8251
+ track.stop();
8252
+ });
8253
+ this.setVisualStream();
8254
+ this.audioRecorder?.stop();
8255
+ this.audioRecorder = void 0;
7633
8256
  }
8257
+ if (!params?.preserveRecordingState) this.paused = this.recording = false;
7634
8258
  } catch (exc) {
7635
8259
  this.emit("ERROR", {
7636
8260
  exc
7637
8261
  });
7638
8262
  }
7639
8263
  }
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);
8264
+ createCanvas() {
8265
+ const canvas = document.createElement("canvas");
8266
+ const rawWidthDimension = this.getRawWidth(true);
8267
+ if (rawWidthDimension.value) canvas.width = rawWidthDimension.value;
8268
+ const rawHeight = this.getRawHeight(true);
8269
+ if (rawHeight) canvas.height = rawHeight;
8270
+ return canvas;
7730
8271
  }
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);
8272
+ getVideoHeight() {
8273
+ if (!this.rawVisualUserMedia) return;
8274
+ return this.rawVisualUserMedia.videoHeight || this.rawVisualUserMedia.height;
7738
8275
  }
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();
8276
+ getVideoWidth() {
8277
+ if (!this.rawVisualUserMedia) return;
8278
+ return this.rawVisualUserMedia.videoWidth || this.rawVisualUserMedia.width;
7747
8279
  }
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;
8280
+ hasVideoWidth() {
8281
+ const videoWidth = this.getVideoWidth();
8282
+ return videoWidth && videoWidth > 0;
7774
8283
  }
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;
8284
+ getRawWidth(responsive) {
8285
+ let rawWidth;
8286
+ const widthDimension = {
8287
+ unit: "px"
8288
+ };
8289
+ rawWidth = responsive ? this.getVideoWidth() : this.options.video.width ? this.options.video.width : this.getVideoWidth();
8290
+ if (responsive) {
8291
+ const widthDimension = this.recorder.limitWidth(rawWidth);
8292
+ rawWidth = widthDimension?.value;
7784
8293
  }
8294
+ widthDimension.value = rawWidth;
8295
+ return widthDimension;
7785
8296
  }
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;
8297
+ getRawHeight(responsive) {
8298
+ let rawHeight;
8299
+ if (responsive) {
8300
+ rawHeight = this.options.video.height || this.getVideoHeight();
8301
+ const ratio = this.recorder.getRatio();
8302
+ const rawWidth = this.getRawWidth(responsive);
8303
+ if (void 0 !== ratio && rawWidth.value) rawHeight = rawWidth.value * ratio;
8304
+ } else rawHeight = this.options.video.height ? this.options.video.height : this.getVideoHeight();
8305
+ if (responsive) {
8306
+ const heightDimension = this.recorder.limitHeight(rawHeight);
8307
+ rawHeight = heightDimension.value;
7791
8308
  }
8309
+ return rawHeight;
7792
8310
  }
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;
8311
+ getRawVisuals() {
8312
+ return this.rawVisualUserMedia;
7800
8313
  }
7801
- isReady() {
7802
- return this.userMedia?.isReady();
8314
+ pause() {
8315
+ this.paused = true;
8316
+ this.audioRecorder?.updateRecordingState();
7803
8317
  }
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();
8318
+ isPaused() {
8319
+ return this.paused;
7813
8320
  }
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);
8321
+ resume() {
8322
+ this.paused = false;
8323
+ this.audioRecorder?.updateRecordingState();
7824
8324
  }
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
- });
8325
+ record() {
8326
+ this.recording = true;
8327
+ this.audioRecorder?.updateRecordingState();
7836
8328
  }
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;
8329
+ isRecording() {
8330
+ return this.recording;
7864
8331
  }
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();
8332
+ getAudioSampleRate() {
8333
+ if (this.audioRecorder) return this.audioRecorder.getSampleRate();
8334
+ return -1;
7874
8335
  }
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();
8336
+ getCharacteristics() {
8337
+ return {
8338
+ audioSampleRate: this.getAudioSampleRate(),
8339
+ muted: this.rawVisualUserMedia?.muted,
8340
+ width: this.rawVisualUserMedia?.width,
8341
+ height: this.rawVisualUserMedia?.height,
8342
+ videoWidth: this.rawVisualUserMedia?.videoWidth,
8343
+ videoHeight: this.rawVisualUserMedia?.videoHeight
8344
+ };
7887
8345
  }
7888
- onFlushed(opts) {
7889
- const frameNumber = opts.frameNumber;
7890
- if (1 === frameNumber) this.emit("FIRST_FRAME_SENT");
8346
+ constructor(recorder, options){
8347
+ 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)=>{
8348
+ this.logEvent(e.type, {
8349
+ readyState: this.rawVisualUserMedia?.readyState
8350
+ });
8351
+ this.rawVisualUserMedia?.removeEventListener(e.type, this.outputEvent);
8352
+ }), userMedia_define_property(this, "handleMediaError", (event)=>{
8353
+ this.options.logger.warn(`Caught video element error event: ${string_pretty(event)}`);
8354
+ });
8355
+ this.recorder = recorder;
8356
+ this.rawVisualUserMedia = recorder.getRawVisualUserMedia();
8357
+ mediaEvents.forEach((eventName)=>{
8358
+ this.rawVisualUserMedia?.addEventListener(eventName, this.outputEvent, false);
8359
+ });
7891
8360
  }
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");
8361
+ }
8362
+ const visuals_userMedia = UserMedia;
8363
+ function recorder_define_property(obj, key, value) {
8364
+ if (key in obj) Object.defineProperty(obj, key, {
8365
+ value: value,
8366
+ enumerable: true,
8367
+ configurable: true,
8368
+ writable: true
8369
+ });
8370
+ else obj[key] = value;
8371
+ return obj;
8372
+ }
8373
+ class Recorder extends capture {
8374
+ executeCommand(command) {
8375
+ if (this.unloaded) return;
7895
8376
  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)
8377
+ if (command.args) {
8378
+ const debugLine = string_summarize(`Server commanded: ${command.command} with:`, command.args);
8379
+ this.options.logger.debug(debugLine);
8380
+ } else this.options.logger.debug(`Server commanded: ${command.command}`);
8381
+ switch(command.command){
8382
+ case "ready":
8383
+ this.emit("SERVER_READY");
8384
+ if (!this.userMediaTimeout) if (this.options.loadUserMediaOnRecord) this.show();
8385
+ else this.loadUserMedia();
8386
+ break;
8387
+ case "preview":
8388
+ this.preview(command.args);
8389
+ break;
8390
+ case "error":
8391
+ {
8392
+ let explanation = "(No explanation given)";
8393
+ if (command.args?.err?.message) explanation = command.args.err.message;
8394
+ const err = error_createError({
8395
+ message: "Websocket error from Videomail server",
8396
+ explanation,
8397
+ err: deserializeError(command.args?.err),
8398
+ options: this.options
7917
8399
  });
7918
- this.visuals.checkTimer(elapsedTime);
8400
+ this.emit("ERROR", {
8401
+ err
8402
+ });
8403
+ break;
8404
+ }
8405
+ case "confirmFrame":
8406
+ this.updateFrameProgress(command.args);
8407
+ break;
8408
+ case "confirmSample":
8409
+ this.updateSampleProgress(command.args);
8410
+ break;
8411
+ case "beginAudioEncoding":
8412
+ this.emit("BEGIN_AUDIO_ENCODING");
8413
+ break;
8414
+ case "beginVideoEncoding":
8415
+ this.emit("BEGIN_VIDEO_ENCODING");
8416
+ break;
8417
+ default:
8418
+ {
8419
+ const err = error_createError({
8420
+ message: `Unknown server command: ${command.command}`,
8421
+ options: this.options
8422
+ });
8423
+ this.emit("ERROR", {
8424
+ err
8425
+ });
8426
+ break;
7919
8427
  }
7920
- } else throw error_createError({
7921
- message: "Failed to extract webcam data.",
7922
- options: this.options
7923
- });
7924
8428
  }
7925
8429
  } catch (exc) {
7926
8430
  this.emit("ERROR", {
@@ -7928,156 +8432,34 @@ class Recorder extends util_Despot {
7928
8432
  });
7929
8433
  }
7930
8434
  }
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;
8435
+ unload(params) {
8436
+ if (this.unloaded || !this.built) return;
8437
+ const e = params?.e;
8438
+ let cause;
8439
+ if (e) cause = e.type;
8440
+ const prettyCause = cause ? `, cause: ${cause}` : "";
8441
+ this.options.logger.debug(`Recorder: unload()${prettyCause}`);
8442
+ this.recordingActive = false;
8011
8443
  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
- });
8444
+ this.reset();
8445
+ this.recordingDimensions = void 0;
8446
+ this.clearUserMediaTimeout();
8447
+ this.clearConnectionTimeout();
8448
+ this.clearReconnectTimeout();
8449
+ this.clearRetryTimeout();
8450
+ this.clearStopTimeout();
8451
+ this.stopPings();
8452
+ this.userMediaLoading = false;
8453
+ this.connecting = false;
8454
+ this.reconnecting = false;
8455
+ this.reconnectAttempts = 0;
8456
+ if (this.userMedia) this.userMedia.unloadRemainingEventListeners();
8457
+ this.unloaded = true;
8458
+ this.discardSocket();
8459
+ this.built = this.connecting = this.connected = false;
8053
8460
  }
8054
8461
  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
- });
8462
+ this.dimensions.correctDimensions();
8081
8463
  }
8082
8464
  initEvents() {
8083
8465
  this.options.logger.debug("Recorder: initEvents()");
@@ -8149,100 +8531,20 @@ class Recorder extends util_Despot {
8149
8531
  }
8150
8532
  this.built = true;
8151
8533
  }
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
8534
  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
- };
8535
+ return this.dimensions.getRecorderWidth(responsive);
8181
8536
  }
8182
8537
  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;
8538
+ return this.dimensions.getRecorderHeight(responsive, useBoundingClientRect);
8212
8539
  }
8213
8540
  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;
8541
+ return this.dimensions.getRatio();
8222
8542
  }
8223
8543
  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);
8544
+ return this.dimensions.calculateWidth(responsive);
8229
8545
  }
8230
8546
  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;
8547
+ return this.dimensions.calculateHeight(responsive);
8246
8548
  }
8247
8549
  getRawVisualUserMedia() {
8248
8550
  return this.recorderElement;
@@ -8259,14 +8561,14 @@ class Recorder extends util_Despot {
8259
8561
  limitHeight(height) {
8260
8562
  return this.visuals.limitHeight(height);
8261
8563
  }
8262
- isUserMediaLoaded() {
8263
- return this.userMediaLoaded;
8264
- }
8265
8564
  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;
8565
+ super(visuals, replay, options), recorder_define_property(this, "dimensions", void 0);
8566
+ this.dimensions = new recorder_dimensions({
8567
+ options,
8568
+ visuals,
8569
+ getRecorderElement: ()=>this.recorderElement,
8570
+ getUserMedia: ()=>this.userMedia
8571
+ });
8270
8572
  }
8271
8573
  }
8272
8574
  const visuals_recorder = Recorder;
@@ -9158,7 +9460,8 @@ class Container extends util_Despot {
9158
9460
  }
9159
9461
  else runValidation = false;
9160
9462
  if (runValidation) {
9161
- const targetName = event?.target?.name;
9463
+ const target = event?.target;
9464
+ const targetName = target instanceof Element ? target.getAttribute("name") : void 0;
9162
9465
  if (targetName) this.emit("VALIDATING", {
9163
9466
  targetName
9164
9467
  });
@@ -9193,27 +9496,10 @@ class Container extends util_Despot {
9193
9496
  }
9194
9497
  }
9195
9498
  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
- }
9499
+ if (!email_isRecipientSelectionValid(this.form.getRecipients())) {
9500
+ valid = false;
9501
+ whyInvalid = "At least one recipient is required";
9215
9502
  }
9216
- if (!valid) whyInvalid = "At least one recipient is required";
9217
9503
  }
9218
9504
  } else {
9219
9505
  valid = visualsValid;