videomail-client 16.0.2 → 16.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/umd/index.js CHANGED
@@ -11121,7 +11121,7 @@
11121
11121
  var client = __webpack_require__("Q");
11122
11122
  var client_default = /*#__PURE__*/ __webpack_require__.n(client);
11123
11123
  var package_namespaceObject = {
11124
- rE: "16.0.2"
11124
+ rE: "16.1.0"
11125
11125
  };
11126
11126
  function isAudioEnabled(options) {
11127
11127
  return Boolean(options.audio.enabled);
@@ -15029,12 +15029,14 @@
15029
15029
  GET: "get"
15030
15030
  };
15031
15031
  class Form extends util_Despot {
15032
+ static nextFormId = 0;
15032
15033
  container;
15033
15034
  formElement;
15034
- validate = (event)=>{
15035
- this.container.validate(event);
15036
- };
15037
15035
  keyInput;
15036
+ submitButton;
15037
+ submitButtonFormAttribute = null;
15038
+ generatedFormId;
15039
+ isSubmitting = false;
15038
15040
  FORM_FIELDS = {};
15039
15041
  constructor(container, formElement, options){
15040
15042
  super("Form", options);
@@ -15052,6 +15054,12 @@
15052
15054
  sendCopy: options.selectors.sendCopyInputName
15053
15055
  };
15054
15056
  }
15057
+ validate = (event)=>{
15058
+ this.container.validate(event);
15059
+ };
15060
+ handleSubmit = (event)=>{
15061
+ if (!event.defaultPrevented) this.doTheSubmit(event);
15062
+ };
15055
15063
  getData() {
15056
15064
  return getFormData(this.formElement, {
15057
15065
  includeDisabled: true
@@ -15235,19 +15243,42 @@
15235
15243
  if (type?.toLowerCase() === "hidden") inputElement.value = "";
15236
15244
  }
15237
15245
  }
15238
- startListeningToSubmitEvents() {
15246
+ connectSubmitButton() {
15239
15247
  const submitButton = this.container.getSubmitButton();
15240
- if (submitButton) submitButton.onclick = this.doTheSubmit.bind(this);
15248
+ if (!submitButton) return;
15249
+ if (submitButton.form === this.formElement) return;
15250
+ if (!this.formElement.id) {
15251
+ let formId;
15252
+ do formId = `videomail-form-${Form.nextFormId++}`;
15253
+ while (document.getElementById(formId))
15254
+ this.formElement.id = formId;
15255
+ this.generatedFormId = formId;
15256
+ }
15257
+ this.submitButton = submitButton;
15258
+ this.submitButtonFormAttribute = submitButton.getAttribute("form");
15259
+ submitButton.setAttribute("form", this.formElement.id);
15260
+ }
15261
+ startListeningToSubmitEvents() {
15262
+ this.connectSubmitButton();
15263
+ this.formElement.addEventListener("submit", this.handleSubmit);
15241
15264
  }
15242
15265
  stopListeningToSubmitEvents() {
15243
- const submitButton = this.container.getSubmitButton();
15244
- if (submitButton) submitButton.onclick = null;
15266
+ this.formElement.removeEventListener("submit", this.handleSubmit);
15267
+ if (this.submitButton) {
15268
+ if (null === this.submitButtonFormAttribute) this.submitButton.removeAttribute("form");
15269
+ else this.submitButton.setAttribute("form", this.submitButtonFormAttribute);
15270
+ this.submitButton = void 0;
15271
+ this.submitButtonFormAttribute = null;
15272
+ }
15273
+ if (this.generatedFormId === this.formElement.id) this.formElement.removeAttribute("id");
15274
+ this.generatedFormId = void 0;
15245
15275
  }
15246
15276
  async doTheSubmit(e) {
15247
15277
  if (e) {
15248
15278
  this.options.logger.debug(`Form: doTheSubmit(${string_pretty(e)})`);
15249
15279
  e.preventDefault();
15250
15280
  } else this.options.logger.debug("Form: doTheSubmit()");
15281
+ if (this.isSubmitting) return false;
15251
15282
  const hasHostAction = this.formElement.hasAttribute("action");
15252
15283
  const url = this.formElement.getAttribute("action") ?? this.options.apiUrl;
15253
15284
  const method = this.formElement.getAttribute("method");
@@ -15262,7 +15293,14 @@
15262
15293
  default:
15263
15294
  chosenMethod = FormMethod.POST;
15264
15295
  }
15265
- if (this.container.hasElement()) await this.container.submitAll(this.getData(), chosenMethod, url, hasHostAction);
15296
+ if (this.container.hasElement()) {
15297
+ this.isSubmitting = true;
15298
+ try {
15299
+ await this.container.submitAll(this.getData(), chosenMethod, url, hasHostAction);
15300
+ } finally{
15301
+ this.isSubmitting = false;
15302
+ }
15303
+ }
15266
15304
  return false;
15267
15305
  }
15268
15306
  getInvalidElement() {
@@ -17888,7 +17926,7 @@
17888
17926
  html_disableElement(radioButtonElement);
17889
17927
  return radioButtonElement;
17890
17928
  }
17891
- makeButton(buttonClass, text, clickHandler, show, id, type, selector, disabled = true) {
17929
+ makeButton(buttonClass, text, clickHandler, show, id, type = "button", selector, disabled = true) {
17892
17930
  let buttonElement;
17893
17931
  buttonElement = id ? document.querySelector(`#${id}`) : selector ? document.querySelector(selector) : this.buttonsElement?.querySelector(`.${buttonClass}`);
17894
17932
  if (buttonElement) buttonElement = html_adjustButton(buttonElement, show, type, disabled);
@@ -18598,10 +18636,14 @@
18598
18636
  this.on("USER_MEDIA_READY", ()=>{
18599
18637
  this.facingMode?.show();
18600
18638
  });
18639
+ this.on("CAMERA_RECOVERING", ()=>{
18640
+ this.facingMode?.hide();
18641
+ });
18601
18642
  this.on("RECORDING", ()=>{
18602
18643
  this.startRecording();
18603
18644
  });
18604
18645
  this.on("RESUMING", ()=>{
18646
+ this.hidePause();
18605
18647
  this.resumeRecording();
18606
18648
  });
18607
18649
  this.on("STOPPING", ()=>{
@@ -19216,6 +19258,7 @@
19216
19258
  class AudioRecorder {
19217
19259
  scriptProcessor;
19218
19260
  audioInput;
19261
+ volume;
19219
19262
  vcAudioContext;
19220
19263
  userMedia;
19221
19264
  options;
@@ -19238,9 +19281,19 @@
19238
19281
  const float32Array = e.inputBuffer.getChannelData(0);
19239
19282
  cb(new esm(float32Array));
19240
19283
  }
19284
+ disconnect() {
19285
+ if (this.scriptProcessor) {
19286
+ this.scriptProcessor.onaudioprocess = null;
19287
+ this.scriptProcessor.disconnect();
19288
+ }
19289
+ this.audioInput?.disconnect();
19290
+ this.volume?.disconnect();
19291
+ }
19241
19292
  init(localMediaStream) {
19242
19293
  this.options.logger.debug("AudioRecorder: init()");
19294
+ this.disconnect();
19243
19295
  const volume = this.getAudioContext().createGain();
19296
+ this.volume = volume;
19244
19297
  try {
19245
19298
  this.audioInput = this.getAudioContext().createMediaStreamSource(localMediaStream);
19246
19299
  } catch (exc) {
@@ -19272,10 +19325,13 @@
19272
19325
  this.onAudioProcess(e, cb);
19273
19326
  };
19274
19327
  }
19328
+ async resume() {
19329
+ const context = this.getAudioContext();
19330
+ if ("running" !== context.state) await context.resume();
19331
+ }
19275
19332
  stop() {
19276
19333
  this.options.logger.debug("AudioRecorder: stop()");
19277
- if (this.scriptProcessor) this.scriptProcessor.onaudioprocess = null;
19278
- if (this.audioInput) this.audioInput.disconnect();
19334
+ this.disconnect();
19279
19335
  if (this.hasAudioContext()) this.getAudioContext().close().then(()=>{
19280
19336
  this.options.logger.debug("AudioRecorder: audio context is closed");
19281
19337
  this.vcAudioContext = void 0;
@@ -19320,6 +19376,8 @@
19320
19376
  onLoadedMetaDataReached = false;
19321
19377
  playingPromiseReached = false;
19322
19378
  videoTrackLabel;
19379
+ clearInitializationListeners;
19380
+ initializationId = 0;
19323
19381
  outputEvent = (e)=>{
19324
19382
  this.logEvent(e.type, {
19325
19383
  readyState: this.rawVisualUserMedia?.readyState
@@ -19361,6 +19419,16 @@
19361
19419
  this.currentVisualStream = void 0;
19362
19420
  }
19363
19421
  }
19422
+ async restorePlayback() {
19423
+ if (!this.rawVisualUserMedia) throw new Error("No camera preview is available.");
19424
+ await this.rawVisualUserMedia.play();
19425
+ await this.audioRecorder?.resume();
19426
+ }
19427
+ hasLiveStream() {
19428
+ const videoReady = this.currentVisualStream?.getVideoTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
19429
+ const audioReady = !isAudioEnabled(this.options) || this.currentVisualStream?.getAudioTracks().some((track)=>"live" === track.readyState && track.enabled && !track.muted);
19430
+ return Boolean(this.currentVisualStream?.active && videoReady && audioReady);
19431
+ }
19364
19432
  hasEnded() {
19365
19433
  if (this.rawVisualUserMedia?.ended) return this.rawVisualUserMedia.ended;
19366
19434
  return !this.currentVisualStream?.active;
@@ -19375,6 +19443,7 @@
19375
19443
  }
19376
19444
  unloadRemainingEventListeners() {
19377
19445
  this.options.logger.debug("UserMedia: unloadRemainingEventListeners()");
19446
+ this.clearInitializationListeners?.();
19378
19447
  mediaEvents.forEach((eventName)=>{
19379
19448
  this.rawVisualUserMedia?.removeEventListener(eventName, this.outputEvent);
19380
19449
  });
@@ -19384,11 +19453,13 @@
19384
19453
  this.removeListener("SENDING_FIRST_FRAME");
19385
19454
  this.audioRecorder?.record(audioCallback);
19386
19455
  }
19387
- init(localMediaStream, videoCallback, audioCallback, endedEarlyCallback, switchingFacingMode) {
19456
+ init(localMediaStream, videoCallback, audioCallback, endedEarlyCallback, params) {
19457
+ this.clearInitializationListeners?.();
19388
19458
  this.stop(localMediaStream, {
19389
19459
  aboutToInitialize: true,
19390
- switchingFacingMode
19460
+ preserveRecordingState: Boolean(params?.switchingFacingMode || params?.recovering)
19391
19461
  });
19462
+ const initializationId = ++this.initializationId;
19392
19463
  this.onPlayReached = false;
19393
19464
  this.onLoadedMetaDataReached = false;
19394
19465
  this.playingPromiseReached = false;
@@ -19401,18 +19472,25 @@
19401
19472
  this.rawVisualUserMedia?.removeEventListener("play", onPlay);
19402
19473
  this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
19403
19474
  };
19475
+ this.clearInitializationListeners = ()=>{
19476
+ this.rawVisualUserMedia?.removeEventListener("play", onPlay);
19477
+ this.rawVisualUserMedia?.removeEventListener("loadedmetadata", onLoadedMetaData);
19478
+ this.clearInitializationListeners = void 0;
19479
+ };
19404
19480
  const play = ()=>{
19405
19481
  try {
19406
19482
  this.rawVisualUserMedia?.load();
19407
19483
  if (this.rawVisualUserMedia?.paused) {
19408
19484
  this.options.logger.debug(`UserMedia: play(): media.readyState=${this.rawVisualUserMedia.readyState}, media.paused=${this.rawVisualUserMedia.paused}, media.ended=${this.rawVisualUserMedia.ended}, media.played=${string_pretty(this.rawVisualUserMedia.played)}`);
19409
19485
  this.rawVisualUserMedia.play().then(()=>{
19486
+ if (initializationId !== this.initializationId) return;
19410
19487
  if (!this.playingPromiseReached) {
19411
19488
  this.options.logger.debug("UserMedia: play promise successful. Playing now.");
19412
19489
  this.playingPromiseReached = true;
19413
19490
  }
19414
19491
  }).catch((exc)=>{
19415
- if (exc instanceof Error) this.options.logger.warn(`Caught pending user media promise exception: ${exc.toString()}`);
19492
+ if (initializationId !== this.initializationId) return;
19493
+ if (exc instanceof Error && !params?.recovering) this.options.logger.warn(`Caught pending user media promise exception: ${exc.toString()}`);
19416
19494
  else {
19417
19495
  unloadAllEventListeners();
19418
19496
  endedEarlyCallback(error_createError({
@@ -19433,16 +19511,28 @@
19433
19511
  this.options.logger.debug(`UserMedia: fireCallbacks(readyState=${readyState}, onPlayReached=${this.onPlayReached}, onLoadedMetaDataReached=${this.onLoadedMetaDataReached})`);
19434
19512
  if (this.onPlayReached && this.onLoadedMetaDataReached) if (this.audioRecorder) try {
19435
19513
  this.audioRecorder.init(localMediaStream);
19436
- this.on("SENDING_FIRST_FRAME", ()=>{
19437
- this.audioRecord(audioCallback);
19438
- });
19439
- videoCallback();
19514
+ const onAudioReady = ()=>{
19515
+ if (initializationId !== this.initializationId || this.currentVisualStream !== localMediaStream) return;
19516
+ if (this.recording) this.audioRecord(audioCallback);
19517
+ else this.on("SENDING_FIRST_FRAME", ()=>{
19518
+ this.audioRecord(audioCallback);
19519
+ });
19520
+ this.clearInitializationListeners?.();
19521
+ videoCallback();
19522
+ };
19523
+ if (params?.recovering) this.audioRecorder.resume().then(onAudioReady).catch(endedEarlyCallback);
19524
+ else onAudioReady();
19440
19525
  } catch (exc) {
19441
19526
  unloadAllEventListeners();
19442
- this.stop();
19527
+ this.stop(void 0, {
19528
+ preserveRecordingState: Boolean(params?.recovering)
19529
+ });
19443
19530
  endedEarlyCallback(exc);
19444
19531
  }
19445
- else videoCallback();
19532
+ else {
19533
+ this.clearInitializationListeners?.();
19534
+ videoCallback();
19535
+ }
19446
19536
  };
19447
19537
  const onPlay = ()=>{
19448
19538
  try {
@@ -19512,9 +19602,8 @@
19512
19602
  this.setVisualStream(localMediaStream);
19513
19603
  play();
19514
19604
  } catch (exc) {
19515
- this.emit("ERROR", {
19516
- exc
19517
- });
19605
+ unloadAllEventListeners();
19606
+ endedEarlyCallback(exc);
19518
19607
  }
19519
19608
  }
19520
19609
  getVideoTrackLabel() {
@@ -19527,6 +19616,8 @@
19527
19616
  try {
19528
19617
  let chosenStream = visualStream;
19529
19618
  if (!params?.aboutToInitialize) {
19619
+ this.initializationId++;
19620
+ this.clearInitializationListeners?.();
19530
19621
  chosenStream ??= this.currentVisualStream;
19531
19622
  const tracks = chosenStream?.getTracks();
19532
19623
  if (tracks) tracks.forEach((track)=>{
@@ -19536,7 +19627,7 @@
19536
19627
  this.audioRecorder?.stop();
19537
19628
  this.audioRecorder = void 0;
19538
19629
  }
19539
- if (!params?.switchingFacingMode) this.paused = this.recording = false;
19630
+ if (!params?.preserveRecordingState) this.paused = this.recording = false;
19540
19631
  } catch (exc) {
19541
19632
  this.emit("ERROR", {
19542
19633
  exc
@@ -19634,6 +19725,7 @@
19634
19725
  originalAnimationFrameObject;
19635
19726
  samplesCount = 0;
19636
19727
  framesCount = 0;
19728
+ recordingAudioSampleRate;
19637
19729
  recordingStats;
19638
19730
  confirmedFrameNumber = 0;
19639
19731
  confirmedSampleNumber = 0;
@@ -19652,6 +19744,10 @@
19652
19744
  ctx;
19653
19745
  userMediaLoaded;
19654
19746
  userMediaLoading = false;
19747
+ mediaRequestId = 0;
19748
+ cameraRecovery;
19749
+ resolveCameraRecovery;
19750
+ resumeAfterCameraRecovery = false;
19655
19751
  submitting = false;
19656
19752
  unloaded;
19657
19753
  stopTime;
@@ -19915,7 +20011,9 @@
19915
20011
  this.pingInterval = void 0;
19916
20012
  }
19917
20013
  onAudioSample(audioSample) {
19918
- if (this.recordingTransportFailed) return;
20014
+ if (this.recordingTransportFailed || this.cameraRecovery) return;
20015
+ const sampleRate = this.userMedia?.getAudioSampleRate();
20016
+ if (sampleRate && sampleRate > 0) this.recordingAudioSampleRate ??= sampleRate;
19919
20017
  this.samplesCount++;
19920
20018
  this.mediaTransmissionStarted = true;
19921
20019
  const audioBuffer = audioSample.toBuffer();
@@ -19928,19 +20026,26 @@
19928
20026
  try {
19929
20027
  this.options.logger.debug(`Recorder: onUserMediaReady(${params ? string_pretty(params) : ""})`);
19930
20028
  const switchingFacingMode = params?.switchingFacingMode;
20029
+ if (params?.recovering) {
20030
+ if (!this.userMedia?.hasLiveStream()) return void this.failCameraRecovery(new Error("The restored camera has no live tracks."));
20031
+ if (void 0 !== this.recordingAudioSampleRate && this.userMedia.getAudioSampleRate() !== this.recordingAudioSampleRate) return void this.failCameraRecovery(new Error("The audio sample rate changed during recovery."));
20032
+ }
19931
20033
  this.userMediaLoading = this.blocking = this.unloaded = this.submitting = false;
19932
20034
  this.userMediaLoaded = true;
19933
20035
  this.clearUserMediaTimeout();
19934
- if (!switchingFacingMode) this.loop = this.createLoop();
20036
+ if (!switchingFacingMode && !params?.recovering) this.loop = this.createLoop();
19935
20037
  this.show();
19936
20038
  if (params?.recordWhenReady) this.record();
19937
20039
  this.emit("USER_MEDIA_READY", {
19938
20040
  switchingFacingMode: params?.switchingFacingMode,
20041
+ recovering: params?.recovering,
19939
20042
  paused: this.isPaused(),
19940
20043
  recordWhenReady: params?.recordWhenReady
19941
20044
  });
20045
+ if (params?.recovering) this.finishCameraRecovery(true);
19942
20046
  } catch (exc) {
19943
- this.emit("ERROR", {
20047
+ if (params?.recovering) this.failCameraRecovery(exc);
20048
+ else this.emit("ERROR", {
19944
20049
  exc
19945
20050
  });
19946
20051
  }
@@ -20311,6 +20416,7 @@
20311
20416
  return this.blocking;
20312
20417
  }
20313
20418
  userMediaErrorCallback(err, usedConstraints) {
20419
+ if (this.cameraRecovery) return void this.failCameraRecovery(err);
20314
20420
  if (this.unloaded) return void this.options.logger.debug(`Recorder: already unloaded. Not going to throw error ${string_pretty(err)}`);
20315
20421
  this.userMediaLoading = false;
20316
20422
  this.clearUserMediaTimeout();
@@ -20342,7 +20448,7 @@
20342
20448
  });
20343
20449
  }
20344
20450
  }
20345
- getUserMediaCallback(localStream, params) {
20451
+ getUserMediaCallback(localStream, params, requestId = this.mediaRequestId) {
20346
20452
  if (!this.userMedia) {
20347
20453
  localStream.getTracks().forEach((track)=>{
20348
20454
  track.stop();
@@ -20355,20 +20461,20 @@
20355
20461
  this.options.logger.debug(`Recorder: getUserMediaCallback(${params ? string_pretty(params) : ""})`);
20356
20462
  if (this.showUserMedia()) try {
20357
20463
  this.userMedia.init(localStream, ()=>{
20358
- if (!this.unloaded) this.onUserMediaReady(params);
20464
+ if (!this.unloaded && requestId === this.mediaRequestId) this.onUserMediaReady(params);
20359
20465
  }, this.onAudioSample.bind(this), (err)=>{
20360
- if (!this.unloaded) this.userMediaErrorCallback(err, {
20466
+ if (!this.unloaded && requestId === this.mediaRequestId) this.userMediaErrorCallback(err, {
20361
20467
  audio: isAudioEnabled(this.options)
20362
20468
  });
20363
- }, params?.switchingFacingMode);
20469
+ }, params);
20364
20470
  } catch (exc) {
20365
20471
  localStream.getTracks().forEach((track)=>{
20366
20472
  track.stop();
20367
20473
  });
20368
20474
  this.userMediaLoading = false;
20369
20475
  this.clearUserMediaTimeout();
20370
- this.emit("ERROR", {
20371
- exc
20476
+ this.userMediaErrorCallback(exc, {
20477
+ audio: isAudioEnabled(this.options)
20372
20478
  });
20373
20479
  }
20374
20480
  else {
@@ -20377,12 +20483,14 @@
20377
20483
  });
20378
20484
  this.userMediaLoading = false;
20379
20485
  this.clearUserMediaTimeout();
20486
+ this.cancelCameraRecovery();
20380
20487
  }
20381
20488
  }
20382
20489
  loadGenuineUserMedia(params) {
20383
20490
  this.options.logger.debug(`Recorder: loadGenuineUserMedia(${params ? string_pretty(params) : ""})`);
20384
20491
  this.emit("ASKING_WEBCAM_PERMISSION");
20385
20492
  this.userMediaLoading = true;
20493
+ const requestId = ++this.mediaRequestId;
20386
20494
  const constraints = {
20387
20495
  video: {
20388
20496
  frameRate: {
@@ -20396,18 +20504,18 @@
20396
20504
  this.options.logger.debug(string_summarize("Recorder: available webcam constraints are:", navigator.mediaDevices.getSupportedConstraints()));
20397
20505
  const streamPromise = navigator.mediaDevices.getUserMedia(constraints);
20398
20506
  streamPromise.then((localStream)=>{
20399
- if (this.unloaded) return void localStream.getTracks().forEach((track)=>{
20507
+ if (this.unloaded || requestId !== this.mediaRequestId) return void localStream.getTracks().forEach((track)=>{
20400
20508
  track.stop();
20401
20509
  });
20402
- this.getUserMediaCallback(localStream, params);
20510
+ this.getUserMediaCallback(localStream, params, requestId);
20403
20511
  }).catch((reason)=>{
20404
- if (!this.unloaded) this.userMediaErrorCallback(reason, constraints);
20512
+ if (!this.unloaded && requestId === this.mediaRequestId) this.userMediaErrorCallback(reason, constraints);
20405
20513
  });
20406
20514
  }
20407
20515
  configureVideoConstraints(constraints, params) {
20408
20516
  const videoConstraints = "object" == typeof constraints.video ? constraints.video : void 0;
20409
20517
  if (!videoConstraints) return;
20410
- if (params?.switchingFacingMode) videoConstraints.facingMode = params.switchingFacingMode;
20518
+ if (params?.switchingFacingMode || params?.recovering) videoConstraints.facingMode = params.switchingFacingMode || this.facingMode;
20411
20519
  if (this.options.video.width) videoConstraints.width = {
20412
20520
  ideal: this.options.video.width
20413
20521
  };
@@ -20435,8 +20543,8 @@
20435
20543
  if (!this.userMediaLoaded) {
20436
20544
  this.userMediaLoading = false;
20437
20545
  const err = util_getBrowser(this.options).getNoAccessIssue();
20438
- this.emit("ERROR", {
20439
- err
20546
+ this.userMediaErrorCallback(err, {
20547
+ audio: isAudioEnabled(this.options)
20440
20548
  });
20441
20549
  }
20442
20550
  }, this.options.timeouts.userMedia);
@@ -20446,6 +20554,7 @@
20446
20554
  this.clearUserMediaTimeout();
20447
20555
  this.options.logger.debug("Recorder: failed to load genuine user media");
20448
20556
  this.userMediaLoading = false;
20557
+ if (this.cameraRecovery) return void this.failCameraRecovery(exc);
20449
20558
  const errorListeners = this.getListeners("ERROR");
20450
20559
  if (errorListeners?.length) this.emit("ERROR", {
20451
20560
  exc
@@ -20569,9 +20678,10 @@
20569
20678
  return this.recordingStats;
20570
20679
  }
20571
20680
  getAudioSampleRate() {
20572
- return this.userMedia?.getAudioSampleRate();
20681
+ return this.recordingAudioSampleRate ?? this.userMedia?.getAudioSampleRate();
20573
20682
  }
20574
20683
  stop(params) {
20684
+ this.cancelCameraRecovery();
20575
20685
  if (!this.userMedia) throw new Error("No user media defined, unable to stop");
20576
20686
  this.options.logger.debug(`Recorder: stop(${params ? string_pretty(params) : ""})`);
20577
20687
  this.recordingActive = false;
@@ -20597,7 +20707,7 @@
20597
20707
  if (this.userMedia) this.recordingStats.videoTrackLabel = this.userMedia.getVideoTrackLabel();
20598
20708
  if (isAudioEnabled(this.options) && this.userMedia) {
20599
20709
  this.recordingStats.samplesCount = this.samplesCount;
20600
- this.recordingStats.sampleRate = this.userMedia.getAudioSampleRate();
20710
+ this.recordingStats.sampleRate = this.recordingAudioSampleRate ?? this.userMedia.getAudioSampleRate();
20601
20711
  }
20602
20712
  this.writeCommand("stop", this.recordingStats, ()=>{
20603
20713
  this.emit("STOPPED", {
@@ -20652,6 +20762,7 @@
20652
20762
  this.built = this.connecting = this.connected = false;
20653
20763
  }
20654
20764
  reset() {
20765
+ this.cancelCameraRecovery();
20655
20766
  if (!this.unloaded) {
20656
20767
  this.options.logger.debug("Recorder: reset()");
20657
20768
  this.emit("RESETTING");
@@ -20679,7 +20790,70 @@
20679
20790
  isReady() {
20680
20791
  return this.userMedia?.isReady();
20681
20792
  }
20793
+ finishCameraRecovery(restored) {
20794
+ this.clearUserMediaTimeout();
20795
+ const resolve = this.resolveCameraRecovery;
20796
+ const resumeRequested = restored && this.resumeAfterCameraRecovery;
20797
+ this.cameraRecovery = void 0;
20798
+ this.resolveCameraRecovery = void 0;
20799
+ this.resumeAfterCameraRecovery = false;
20800
+ resolve?.(restored);
20801
+ if (resumeRequested && this.isPaused()) this.resume();
20802
+ }
20803
+ cancelCameraRecovery() {
20804
+ if (!this.cameraRecovery) return;
20805
+ this.mediaRequestId++;
20806
+ this.userMediaLoading = false;
20807
+ this.userMediaLoaded = false;
20808
+ this.clearUserMediaTimeout();
20809
+ this.userMedia?.stop(void 0, {
20810
+ preserveRecordingState: true
20811
+ });
20812
+ this.finishCameraRecovery(false);
20813
+ }
20814
+ failCameraRecovery(exc) {
20815
+ this.cancelCameraRecovery();
20816
+ const err = error_createError({
20817
+ message: "Camera recovery failed",
20818
+ explanation: "Your recording is still paused. Check camera permissions, then try Resume again or stop to keep the recorded part.",
20819
+ exc,
20820
+ options: this.options
20821
+ });
20822
+ this.emit("CAMERA_RECOVERY_FAILED", {
20823
+ err
20824
+ });
20825
+ }
20826
+ recoverCamera() {
20827
+ if (this.cameraRecovery) return this.cameraRecovery;
20828
+ if (!this.built || this.unloaded || !this.userMedia || this.userMediaLoading || this.submitting || this.key || void 0 !== this.stopTimeout || this.recordingTransportFailed) return Promise.resolve(false);
20829
+ const liveStream = this.userMedia.hasLiveStream();
20830
+ if (liveStream && this.isRecording()) return Promise.resolve(true);
20831
+ if (!liveStream && this.recordingActive && !this.isPaused()) this.pause();
20832
+ const recovery = new Promise((resolve)=>{
20833
+ this.resolveCameraRecovery = resolve;
20834
+ });
20835
+ this.cameraRecovery = recovery;
20836
+ this.emit("CAMERA_RECOVERING");
20837
+ if (liveStream) {
20838
+ this.userMediaTimeout = window.setTimeout(()=>{
20839
+ this.failCameraRecovery(new Error("Camera playback recovery timed out."));
20840
+ }, this.options.timeouts.userMedia);
20841
+ this.userMedia.restorePlayback().then(()=>{
20842
+ if (this.cameraRecovery === recovery) this.finishCameraRecovery(true);
20843
+ }).catch((exc)=>{
20844
+ if (this.cameraRecovery === recovery) this.failCameraRecovery(exc);
20845
+ });
20846
+ } else {
20847
+ this.userMediaLoaded = false;
20848
+ this.loadUserMedia({
20849
+ recovering: true
20850
+ });
20851
+ }
20852
+ return recovery;
20853
+ }
20682
20854
  pause(params) {
20855
+ this.cancelCameraRecovery();
20856
+ this.resumeAfterDrain = false;
20683
20857
  if (params) this.options.logger.debug(`pause() at frame ${this.framesCount} with ${string_pretty(params)}`);
20684
20858
  else this.options.logger.debug(`pause() at frame ${this.framesCount}`);
20685
20859
  this.userMedia?.pause();
@@ -20688,10 +20862,15 @@
20688
20862
  this.sendPings();
20689
20863
  }
20690
20864
  resume() {
20865
+ if (this.cameraRecovery || !this.userMedia?.hasLiveStream()) {
20866
+ this.recoverCamera();
20867
+ this.resumeAfterCameraRecovery = Boolean(this.cameraRecovery);
20868
+ return;
20869
+ }
20691
20870
  this.options.logger.debug(`Recorder: resume() with frame ${this.framesCount}`);
20692
20871
  this.stopPings();
20693
20872
  this.emit("RESUMING");
20694
- this.userMedia?.resume();
20873
+ this.userMedia.resume();
20695
20874
  if (this.streamBackpressured) this.resumeAfterDrain = true;
20696
20875
  else this.loop.start();
20697
20876
  }
@@ -20746,7 +20925,7 @@
20746
20925
  return newLoop;
20747
20926
  }
20748
20927
  record() {
20749
- if (this.unloaded) return;
20928
+ if (this.unloaded || this.cameraRecovery) return;
20750
20929
  if (!this.connected) {
20751
20930
  this.options.logger.debug("Recorder: reconnecting before recording …");
20752
20931
  this.initSocket(()=>{
@@ -20819,6 +20998,7 @@
20819
20998
  this.recordingActive = true;
20820
20999
  this.recordingTransportFailed = false;
20821
21000
  this.mediaTransmissionStarted = false;
21001
+ this.recordingAudioSampleRate = void 0;
20822
21002
  this.userMedia.record();
20823
21003
  this.emit("RECORDING", {
20824
21004
  framesCount: this.framesCount
@@ -20880,7 +21060,7 @@
20880
21060
  }
20881
21061
  }
20882
21062
  switchFacingMode() {
20883
- if (!util_getBrowser(this.options).isMobile()) return;
21063
+ if (this.cameraRecovery || !util_getBrowser(this.options).isMobile()) return;
20884
21064
  if ("user" === this.facingMode) this.facingMode = "environment";
20885
21065
  else if ("environment" === this.facingMode) this.facingMode = "user";
20886
21066
  else this.options.logger.warn(`Recorder: unsupported facing mode ${string_pretty(this.facingMode)}`);
@@ -20891,12 +21071,14 @@
20891
21071
  initEvents() {
20892
21072
  this.options.logger.debug("Recorder: initEvents()");
20893
21073
  this.on("SUBMITTING", ()=>{
21074
+ this.cancelCameraRecovery();
20894
21075
  this.submitting = true;
20895
21076
  });
20896
21077
  this.on("SUBMITTED", ()=>{
20897
21078
  this.submitting = false;
20898
21079
  });
20899
21080
  this.on("BLOCKING", ()=>{
21081
+ this.cancelCameraRecovery();
20900
21082
  this.blocking = true;
20901
21083
  this.clearUserMediaTimeout();
20902
21084
  });
@@ -21324,6 +21506,7 @@
21324
21506
  notifier;
21325
21507
  visualsElement;
21326
21508
  built = false;
21509
+ cameraRecoveryFailed = false;
21327
21510
  constructor(container, options){
21328
21511
  super("Visuals", options);
21329
21512
  this.container = container;
@@ -21358,6 +21541,17 @@
21358
21541
  this.endWaiting();
21359
21542
  this.container.enableForm(false);
21360
21543
  });
21544
+ this.on("CAMERA_RECOVERING", ()=>{
21545
+ this.clearCameraRecoveryNotice();
21546
+ });
21547
+ this.on("CAMERA_RECOVERY_FAILED", ({ err })=>{
21548
+ this.cameraRecoveryFailed = true;
21549
+ this.options.logger.warn(err);
21550
+ if (this.options.displayErrors) this.notifier.notify(err.message, err.explanation, {
21551
+ problem: true
21552
+ });
21553
+ this.endWaiting();
21554
+ });
21361
21555
  this.on("PREVIEW", ()=>{
21362
21556
  this.endWaiting();
21363
21557
  });
@@ -21436,6 +21630,7 @@
21436
21630
  child.remove();
21437
21631
  }
21438
21632
  reset() {
21633
+ this.clearCameraRecoveryNotice();
21439
21634
  this.endWaiting();
21440
21635
  this.recorder.reset();
21441
21636
  }
@@ -21446,11 +21641,13 @@
21446
21641
  this.container.endWaiting();
21447
21642
  }
21448
21643
  stop(params) {
21644
+ this.clearCameraRecoveryNotice();
21449
21645
  this.recorder.stop(params);
21450
21646
  this.recorderInsides.hidePause();
21451
21647
  }
21452
21648
  back(keepHidden = false, cb) {
21453
21649
  this.options.logger.debug(`Visuals: back(keepHidden = ${keepHidden})`);
21650
+ this.cameraRecoveryFailed = false;
21454
21651
  this.replay.hide();
21455
21652
  this.notifier.hide();
21456
21653
  if (keepHidden) {
@@ -21472,6 +21669,7 @@
21472
21669
  if (!this.built) return;
21473
21670
  const e = params?.e;
21474
21671
  this.options.logger.debug(`Visuals: unload(${e ? string_pretty(e) : ""})`);
21672
+ this.cameraRecoveryFailed = false;
21475
21673
  this.recorder.unload(params);
21476
21674
  this.recorderInsides.unload();
21477
21675
  this.replay.unload(params);
@@ -21485,13 +21683,31 @@
21485
21683
  this.recorder.pause(params);
21486
21684
  this.recorderInsides.showPause();
21487
21685
  }
21686
+ clearCameraRecoveryNotice() {
21687
+ if (this.cameraRecoveryFailed) {
21688
+ this.notifier.hide();
21689
+ this.recorder.show();
21690
+ this.cameraRecoveryFailed = false;
21691
+ }
21692
+ }
21693
+ recoverCamera() {
21694
+ if (this.replay.isShown() || this.isNotifying() && !this.cameraRecoveryFailed) return Promise.resolve(false);
21695
+ this.clearCameraRecoveryNotice();
21696
+ return this.recorder.recoverCamera();
21697
+ }
21488
21698
  resume() {
21489
- if (this.recorderInsides.isCountingDown()) this.recorderInsides.resumeCountdown();
21490
- else this.recorder.resume();
21491
- this.recorderInsides.hidePause();
21699
+ this.clearCameraRecoveryNotice();
21700
+ if (this.recorderInsides.isCountingDown()) {
21701
+ this.recorderInsides.resumeCountdown();
21702
+ this.recorderInsides.hidePause();
21703
+ } else {
21704
+ this.recorder.resume();
21705
+ if (!this.recorder.isPaused()) this.recorderInsides.hidePause();
21706
+ }
21492
21707
  }
21493
21708
  pauseOrResume() {
21494
- if (this.isRecordable()) {
21709
+ if (this.cameraRecoveryFailed && this.recorder.isPaused()) this.resume();
21710
+ else if (this.isRecordable()) {
21495
21711
  if (this.isRecording()) this.pause();
21496
21712
  else if (this.recorder.isPaused()) this.resume();
21497
21713
  else if (this.recorder.isReady()) this.recorder.record();
@@ -21734,10 +21950,10 @@
21734
21950
  });
21735
21951
  if (!playerOnly) this.stopListeningToVisibility = this.visibility.onChange((visible)=>{
21736
21952
  if (this.built) if (visible) {
21737
- if (isAutoPauseEnabled(this.options) && this.isCountingDown()) this.resume();
21953
+ if (isAutoPauseEnabled(this.options)) this.recoverCamera();
21738
21954
  this.emit("VISIBLE");
21739
21955
  } else {
21740
- if (isAutoPauseEnabled(this.options) && (this.isCountingDown() || this.isRecording())) this.pause();
21956
+ if (isAutoPauseEnabled(this.options)) this.suspend();
21741
21957
  this.emit("INVISIBLE");
21742
21958
  }
21743
21959
  });
@@ -21925,6 +22141,13 @@
21925
22141
  pause(params) {
21926
22142
  this.visuals.pause(params);
21927
22143
  }
22144
+ suspend() {
22145
+ if (this.built && (this.isCountingDown() || this.isRecording() || this.isPaused())) this.pause();
22146
+ }
22147
+ recoverCamera() {
22148
+ if (!this.built || this.hasError || this.submitted) return Promise.resolve(false);
22149
+ return this.visuals.recoverCamera();
22150
+ }
21928
22151
  validate(event, force = false) {
21929
22152
  let runValidation = true;
21930
22153
  let valid = true;
@@ -22243,6 +22466,12 @@
22243
22466
  record() {
22244
22467
  this.container.record();
22245
22468
  }
22469
+ pause() {
22470
+ this.container.suspend();
22471
+ }
22472
+ recoverCamera() {
22473
+ return this.container.recoverCamera();
22474
+ }
22246
22475
  getLogLines() {
22247
22476
  if (this.options.logger.getLines) return this.options.logger.getLines();
22248
22477
  }