easyproctor-hml 2.6.0 → 2.7.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/index.js CHANGED
@@ -22329,14 +22329,17 @@ module.exports = __toCommonJS(index_exports);
22329
22329
 
22330
22330
  // src/modules/checkPermissions.ts
22331
22331
  async function checkPermissions() {
22332
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
22333
+ return false;
22334
+ }
22332
22335
  try {
22333
22336
  const constraints = {
22334
22337
  audio: true,
22335
22338
  video: true
22336
22339
  };
22337
22340
  const stream4 = await navigator.mediaDevices.getUserMedia(constraints);
22338
- stream4.getTracks().forEach((el) => {
22339
- el.stop();
22341
+ stream4.getTracks().forEach((track) => {
22342
+ track.stop();
22340
22343
  });
22341
22344
  return true;
22342
22345
  } catch (error) {
@@ -26430,7 +26433,6 @@ var BaseDetection = class {
26430
26433
  // objectDetection
26431
26434
  runningMode: this.runningMode
26432
26435
  });
26433
- console.log("BaseDetection initializeDetector", this.detectorType);
26434
26436
  }
26435
26437
  stopDetection() {
26436
26438
  this.animationFrameId && clearTimeout(this.animationFrameId);
@@ -26440,7 +26442,7 @@ var BaseDetection = class {
26440
26442
  this.createdVideo && this.video && document.body.removeChild(this.video);
26441
26443
  this.createdVideo = false;
26442
26444
  }
26443
- enableCam(cameraStream) {
26445
+ enableCam(cameraStream, delay = 1e4) {
26444
26446
  var _a2;
26445
26447
  if (!this.detector) {
26446
26448
  console.log("Wait! Detector not loaded yet.");
@@ -26470,7 +26472,7 @@ var BaseDetection = class {
26470
26472
  (_a2 = this.video) == null ? void 0 : _a2.addEventListener("loadeddata", () => {
26471
26473
  this.animationFrameId = setTimeout(() => {
26472
26474
  that.predictWebcam();
26473
- }, 1e4);
26475
+ }, delay);
26474
26476
  });
26475
26477
  const style = document.createElement("style");
26476
26478
  style.type = "text/css";
@@ -26487,7 +26489,6 @@ var BaseDetection = class {
26487
26489
  }
26488
26490
  `;
26489
26491
  document.getElementsByTagName("head")[0].appendChild(style);
26490
- console.log("BaseDetection enableCam OK");
26491
26492
  }
26492
26493
  async predictWebcam() {
26493
26494
  if (this.detecting == false) return;
@@ -26617,7 +26618,6 @@ var FaceDetection = class extends BaseDetection {
26617
26618
  );
26618
26619
  this.emmitedPositionAlert = false;
26619
26620
  this.emmitedFaceAlert = false;
26620
- console.log("FaceDetection constructor");
26621
26621
  this.numFacesSent = -1;
26622
26622
  }
26623
26623
  stopDetection() {
@@ -30190,13 +30190,28 @@ var {
30190
30190
  var DEV_BASE_URL = "https://proctoring-api-dev.easyproctor.tech/api";
30191
30191
  var HOMOL_BASE_URL = "https://proctoring-api-hml.easyproctor.tech/api";
30192
30192
  var PROD_BASE_URL = "https://proctoring-api.easyproctor.tech/api";
30193
+ var REALTIME_DEV_BASE_URL = "https://easyproctor-realtime-api-dev.easyproctor.tech/api";
30194
+ var REALTIME_HOMOL_BASE_URL = "https://easyproctor-realtime-api-hml.easyproctor.tech/api";
30195
+ var REALTIME_PROD_BASE_URL = "https://easyproctor-realtime-api.easyproctor.tech/api";
30193
30196
  var BackendService = class {
30194
30197
  constructor(options) {
30195
30198
  this.options = options;
30196
- this.baseUrl = this.selectBaseUrl(options.type);
30199
+ this.baseUrl = this.selectBaseUrl(options.type, options.isRealtime);
30197
30200
  this.token = options.token;
30198
30201
  }
30199
- selectBaseUrl(type) {
30202
+ getBaseUrl() {
30203
+ return this.baseUrl;
30204
+ }
30205
+ selectBaseUrl(type, isRealtime) {
30206
+ if (isRealtime) {
30207
+ if (type === "dev") {
30208
+ return REALTIME_DEV_BASE_URL;
30209
+ } else if (type === "homol") {
30210
+ return REALTIME_HOMOL_BASE_URL;
30211
+ } else {
30212
+ return REALTIME_PROD_BASE_URL;
30213
+ }
30214
+ }
30200
30215
  if (type === "dev") {
30201
30216
  return DEV_BASE_URL;
30202
30217
  } else if (type === "homol") {
@@ -30207,6 +30222,9 @@ var BackendService = class {
30207
30222
  return PROD_BASE_URL;
30208
30223
  }
30209
30224
  }
30225
+ setRealtime(isRealtime) {
30226
+ this.baseUrl = this.selectBaseUrl(this.options.type, isRealtime);
30227
+ }
30210
30228
  getSocketUrl() {
30211
30229
  return this.baseUrl.replace("/api", "/hub/sockethub");
30212
30230
  }
@@ -30317,6 +30335,18 @@ var BackendService = class {
30317
30335
  });
30318
30336
  return url2.data;
30319
30337
  }
30338
+ async initiateUpload(token, objectName, contentType) {
30339
+ const url2 = await this.makeRequestAxios({
30340
+ path: `/upload/initiate-upload`,
30341
+ method: "POST",
30342
+ jwt: token,
30343
+ body: {
30344
+ objectName,
30345
+ contentType
30346
+ }
30347
+ });
30348
+ return url2.data;
30349
+ }
30320
30350
  async saveAlerts(proctoringOptions, proctoringSession) {
30321
30351
  await this.makeRequest({
30322
30352
  path: "/proctoring/save-alerts",
@@ -30420,6 +30450,22 @@ var BackendService = class {
30420
30450
  });
30421
30451
  return result.data;
30422
30452
  }
30453
+ async checkUpload(token, objectName, contentType) {
30454
+ try {
30455
+ const result = await this.makeRequestAxios({
30456
+ path: `/Upload/check`,
30457
+ method: "POST",
30458
+ jwt: token,
30459
+ body: {
30460
+ objectName,
30461
+ contentType
30462
+ }
30463
+ });
30464
+ return result.data === true;
30465
+ } catch (e3) {
30466
+ return false;
30467
+ }
30468
+ }
30423
30469
  async getServerHour(token) {
30424
30470
  return await this.makeRequest({
30425
30471
  path: `/Proctoring/server-hour`,
@@ -30620,7 +30666,8 @@ var SpyCam = class {
30620
30666
  this.context = context;
30621
30667
  this.backend = new BackendService({
30622
30668
  type: (context == null ? void 0 : context.type) || "prod",
30623
- token: context.token
30669
+ token: context.token,
30670
+ isRealtime: false
30624
30671
  });
30625
30672
  this.currentIsPlugged = true;
30626
30673
  }
@@ -30772,6 +30819,14 @@ var getDefaultProctoringVideoOptions = {
30772
30819
  minHeight: 480
30773
30820
  };
30774
30821
 
30822
+ // src/utils/verifyVersion.ts
30823
+ function versionVerify() {
30824
+ const agentStr = window.navigator.userAgent.split("SEB/");
30825
+ if (agentStr.length > 1)
30826
+ return agentStr[1];
30827
+ else return "1.0.0.0";
30828
+ }
30829
+
30775
30830
  // src/utils/browserInformations.ts
30776
30831
  function fnBrowserDetect() {
30777
30832
  const userAgent = navigator.userAgent;
@@ -30800,13 +30855,16 @@ function isMobileDevice() {
30800
30855
  }
30801
30856
  return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
30802
30857
  }
30858
+ function isSafeBrowser() {
30859
+ return versionVerify() !== "1.0.0.0";
30860
+ }
30803
30861
 
30804
30862
  // src/plugins/recorder.ts
30805
30863
  var proctoringId;
30806
30864
  function setRecorderProctoringId(id) {
30807
30865
  proctoringId = id;
30808
30866
  }
30809
- function recorder(stream4, buffer, onBufferSizeError = false, onBufferSizeErrorCallback, audio = false) {
30867
+ function recorder(stream4, buffer, onBufferSizeError = false, onBufferSizeErrorCallback, audio = false, recorderOpts) {
30810
30868
  let resolvePromise;
30811
30869
  let onBufferSizeInterval;
30812
30870
  let lastEvent;
@@ -30814,6 +30872,7 @@ function recorder(stream4, buffer, onBufferSizeError = false, onBufferSizeErrorC
30814
30872
  bufferSize = 0;
30815
30873
  let startTime;
30816
30874
  let duration = 0;
30875
+ let chunkIndex = 0;
30817
30876
  let recorderOptions = {
30818
30877
  // eslint-disable-next-line no-useless-escape
30819
30878
  mimeType: "video/webm",
@@ -30848,6 +30907,10 @@ function recorder(stream4, buffer, onBufferSizeError = false, onBufferSizeErrorC
30848
30907
  mediaRecorder2.ondataavailable = (e3) => {
30849
30908
  bufferSize = bufferSize + e3.data.size;
30850
30909
  if (e3.data.size > 0) {
30910
+ if (recorderOpts == null ? void 0 : recorderOpts.onChunkAvailable) {
30911
+ recorderOpts.onChunkAvailable(e3.data, chunkIndex);
30912
+ chunkIndex++;
30913
+ }
30851
30914
  buffer.push(e3.data);
30852
30915
  }
30853
30916
  };
@@ -30879,7 +30942,13 @@ function recorder(stream4, buffer, onBufferSizeError = false, onBufferSizeErrorC
30879
30942
  };
30880
30943
  try {
30881
30944
  console.log("State antes do start:", recorder2.state);
30882
- recorder2.start(1e4);
30945
+ chunkIndex = 0;
30946
+ if ((recorderOpts == null ? void 0 : recorderOpts.timeslice) && (recorderOpts == null ? void 0 : recorderOpts.timeslice) > 0) {
30947
+ recorder2.start(recorderOpts.timeslice);
30948
+ } else {
30949
+ recorder2.start(1e4);
30950
+ }
30951
+ bufferSize = 0;
30883
30952
  startTime = new Date(Date.now());
30884
30953
  } catch (e3) {
30885
30954
  console.error("Recorder erro ao chamar start event:", e3);
@@ -30921,6 +30990,9 @@ function recorder(stream4, buffer, onBufferSizeError = false, onBufferSizeErrorC
30921
30990
  console.log("recorder onstop");
30922
30991
  duration = Date.now() - startTime.getTime() || 0;
30923
30992
  console.log("duration no onstop", duration);
30993
+ stream4.getTracks().forEach((el) => {
30994
+ el.stop();
30995
+ });
30924
30996
  resolvePromise && resolvePromise();
30925
30997
  };
30926
30998
  mediaRecorder.stop();
@@ -30930,9 +31002,6 @@ function recorder(stream4, buffer, onBufferSizeError = false, onBufferSizeErrorC
30930
31002
  console.log("stopRecording Recorder n\xE3o est\xE1 em estado recording");
30931
31003
  resolve();
30932
31004
  }
30933
- stream4.getTracks().forEach((el) => {
30934
- el.stop();
30935
- });
30936
31005
  });
30937
31006
  }
30938
31007
  function pauseRecording() {
@@ -31026,36 +31095,34 @@ var UploadService = class {
31026
31095
  this.proctoringId = proctoringId2;
31027
31096
  }
31028
31097
  async uploadPackage(data, token) {
31029
- const { file, onProgress } = data;
31098
+ const { file } = data;
31030
31099
  try {
31031
- const progressCallback = (e3) => {
31032
- const progress = e3.loadedBytes / file.size * 100;
31033
- onProgress && onProgress(Math.round(progress));
31034
- };
31035
- const uploadUrl = await this.backend.getSignedUrl(token, file, this.proctoringId);
31036
- const uploaded = await axios_default.request({
31100
+ console.log("Upload service: uploadPackage");
31101
+ var uploadUrl = "";
31102
+ await this.backend.getSignedUrl(token, file, this.proctoringId).then((result) => uploadUrl = result).catch((error) => {
31103
+ throw error;
31104
+ });
31105
+ console.log("Upload service: uploadUrl", uploadUrl);
31106
+ await axios_default.request({
31037
31107
  url: uploadUrl,
31038
31108
  method: "PUT",
31039
31109
  headers: {
31040
31110
  "Content-Type": file.type,
31041
31111
  "x-ms-blob-type": "BlockBlob"
31042
31112
  },
31043
- data: file,
31044
- onUploadProgress: (p3) => {
31045
- progressCallback({ loadedBytes: p3.loaded });
31046
- }
31047
- }).then(() => true).catch(() => false);
31048
- return {
31049
- storage: "upload" /* none */,
31050
- url: uploadUrl,
31051
- uploaded
31052
- };
31113
+ data: file
31114
+ }).then(() => {
31115
+ return true;
31116
+ }).catch((error) => {
31117
+ throw error;
31118
+ });
31119
+ return true;
31053
31120
  } catch (err) {
31054
- trackers.registerError(this.proctoringId, `Failed to upload to AWS
31121
+ trackers.registerError(this.proctoringId, `Failed to upload package ${err}
31055
31122
  File name: ${file.name}
31056
31123
  File type: ${file.type}
31057
31124
  File size: ${file.size}`);
31058
- throw new Error("Failed to upload to AWS");
31125
+ throw err;
31059
31126
  }
31060
31127
  }
31061
31128
  async uploadImages(data, token, packSize) {
@@ -31342,10 +31409,617 @@ var VolumeMeter = class {
31342
31409
  }
31343
31410
  };
31344
31411
 
31412
+ // src/new-flow/chunk/ChunkStorageService.ts
31413
+ var _ChunkStorageService = class _ChunkStorageService {
31414
+ constructor() {
31415
+ this.db = null;
31416
+ }
31417
+ /**
31418
+ * Abre a conexão com o IndexedDB, criando o banco e o object store se necessário.
31419
+ */
31420
+ async connect() {
31421
+ if (this.db) return this.db;
31422
+ return new Promise((resolve, reject) => {
31423
+ const request = window.indexedDB.open(
31424
+ _ChunkStorageService.DB_NAME,
31425
+ _ChunkStorageService.DB_VERSION
31426
+ );
31427
+ request.onerror = () => {
31428
+ reject(new Error("N\xE3o foi poss\xEDvel conectar ao IndexedDB para chunks."));
31429
+ };
31430
+ request.onupgradeneeded = () => {
31431
+ const db = request.result;
31432
+ if (db.objectStoreNames.contains(_ChunkStorageService.STORE_NAME)) {
31433
+ db.deleteObjectStore(_ChunkStorageService.STORE_NAME);
31434
+ }
31435
+ const store = db.createObjectStore(_ChunkStorageService.STORE_NAME, {
31436
+ keyPath: "id",
31437
+ autoIncrement: true
31438
+ });
31439
+ store.createIndex("proctoringId", "proctoringId", { unique: false });
31440
+ store.createIndex("uploaded", "uploaded", { unique: false });
31441
+ store.createIndex("proctoringId_uploaded", ["proctoringId", "uploaded"], {
31442
+ unique: false
31443
+ });
31444
+ };
31445
+ request.onsuccess = () => {
31446
+ this.db = request.result;
31447
+ resolve(this.db);
31448
+ };
31449
+ });
31450
+ }
31451
+ /**
31452
+ * Salva um chunk de vídeo no IndexedDB.
31453
+ */
31454
+ async saveChunk(chunk) {
31455
+ const db = await this.connect();
31456
+ return new Promise((resolve, reject) => {
31457
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
31458
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
31459
+ const request = store.add(chunk);
31460
+ request.onsuccess = () => {
31461
+ resolve(request.result);
31462
+ };
31463
+ request.onerror = () => {
31464
+ var _a2;
31465
+ reject(new Error(`Erro ao salvar chunk no IndexedDB: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
31466
+ };
31467
+ });
31468
+ }
31469
+ /**
31470
+ * Retorna todos os chunks pendentes (não enviados) de um proctoringId específico.
31471
+ */
31472
+ async getPendingChunks(proctoringId2) {
31473
+ const db = await this.connect();
31474
+ return new Promise((resolve, reject) => {
31475
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
31476
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
31477
+ const index = store.index("proctoringId_uploaded");
31478
+ const range = IDBKeyRange.only([proctoringId2, 0]);
31479
+ const request = index.getAll(range);
31480
+ request.onsuccess = () => {
31481
+ const chunks = request.result.sort(
31482
+ (a3, b3) => a3.chunkIndex - b3.chunkIndex
31483
+ );
31484
+ resolve(chunks);
31485
+ };
31486
+ request.onerror = () => {
31487
+ var _a2;
31488
+ reject(new Error(`Erro ao buscar chunks pendentes: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
31489
+ };
31490
+ });
31491
+ }
31492
+ /**
31493
+ * Retorna todos os chunks (enviados ou não) de um proctoringId específico.
31494
+ */
31495
+ async getAllChunks(proctoringId2) {
31496
+ const db = await this.connect();
31497
+ return new Promise((resolve, reject) => {
31498
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
31499
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
31500
+ const index = store.index("proctoringId");
31501
+ const range = IDBKeyRange.only(proctoringId2);
31502
+ const request = index.getAll(range);
31503
+ request.onsuccess = () => {
31504
+ const chunks = request.result.sort(
31505
+ (a3, b3) => a3.chunkIndex - b3.chunkIndex
31506
+ );
31507
+ resolve(chunks);
31508
+ };
31509
+ request.onerror = () => {
31510
+ var _a2;
31511
+ reject(new Error(`Erro ao buscar todos os chunks: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
31512
+ };
31513
+ });
31514
+ }
31515
+ /**
31516
+ * Marca um chunk como enviado (uploaded = 1).
31517
+ */
31518
+ async markAsUploaded(chunkId) {
31519
+ const db = await this.connect();
31520
+ return new Promise((resolve, reject) => {
31521
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
31522
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
31523
+ const getRequest = store.get(chunkId);
31524
+ getRequest.onsuccess = () => {
31525
+ const chunk = getRequest.result;
31526
+ if (!chunk) {
31527
+ resolve();
31528
+ return;
31529
+ }
31530
+ chunk.uploaded = 1;
31531
+ const putRequest = store.put(chunk);
31532
+ putRequest.onsuccess = () => resolve();
31533
+ putRequest.onerror = () => {
31534
+ var _a2;
31535
+ return reject(new Error(`Erro ao marcar chunk como enviado: ${(_a2 = putRequest.error) == null ? void 0 : _a2.message}`));
31536
+ };
31537
+ };
31538
+ getRequest.onerror = () => {
31539
+ var _a2;
31540
+ return reject(new Error(`Erro ao buscar chunk para marcar: ${(_a2 = getRequest.error) == null ? void 0 : _a2.message}`));
31541
+ };
31542
+ });
31543
+ }
31544
+ /**
31545
+ * Remove todos os chunks já enviados de um proctoringId para liberar espaço.
31546
+ */
31547
+ async clearUploadedChunks(proctoringId2) {
31548
+ const db = await this.connect();
31549
+ return new Promise((resolve, reject) => {
31550
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
31551
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
31552
+ const index = store.index("proctoringId_uploaded");
31553
+ const range = IDBKeyRange.only([proctoringId2, 1]);
31554
+ const request = index.openCursor(range);
31555
+ request.onsuccess = () => {
31556
+ const cursor = request.result;
31557
+ if (cursor) {
31558
+ cursor.delete();
31559
+ cursor.continue();
31560
+ } else {
31561
+ resolve();
31562
+ }
31563
+ };
31564
+ request.onerror = () => {
31565
+ var _a2;
31566
+ return reject(new Error(`Erro ao limpar chunks enviados: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
31567
+ };
31568
+ });
31569
+ }
31570
+ /**
31571
+ * Remove TODOS os chunks de um proctoringId (limpeza completa pós-finalização).
31572
+ */
31573
+ async clearAllChunks(proctoringId2) {
31574
+ const db = await this.connect();
31575
+ return new Promise((resolve, reject) => {
31576
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
31577
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
31578
+ const index = store.index("proctoringId");
31579
+ const range = IDBKeyRange.only(proctoringId2);
31580
+ const request = index.openCursor(range);
31581
+ request.onsuccess = () => {
31582
+ const cursor = request.result;
31583
+ if (cursor) {
31584
+ cursor.delete();
31585
+ cursor.continue();
31586
+ } else {
31587
+ resolve();
31588
+ }
31589
+ };
31590
+ request.onerror = () => {
31591
+ var _a2;
31592
+ return reject(new Error(`Erro ao limpar todos os chunks: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
31593
+ };
31594
+ });
31595
+ }
31596
+ /**
31597
+ * Verifica se existem chunks pendentes para qualquer proctoringId.
31598
+ * Útil na recuperação pós-crash.
31599
+ */
31600
+ async hasAnyPendingChunks() {
31601
+ const db = await this.connect();
31602
+ return new Promise((resolve, reject) => {
31603
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
31604
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
31605
+ const index = store.index("uploaded");
31606
+ const range = IDBKeyRange.only(0);
31607
+ const request = index.count(range);
31608
+ request.onsuccess = () => {
31609
+ resolve(request.result > 0);
31610
+ };
31611
+ request.onerror = () => {
31612
+ var _a2;
31613
+ return reject(new Error(`Erro ao verificar chunks pendentes: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
31614
+ };
31615
+ });
31616
+ }
31617
+ /**
31618
+ * Retorna todos os proctoringIds que possuem chunks pendentes.
31619
+ * Útil na recuperação pós-crash para saber quais sessões precisam ser finalizadas.
31620
+ */
31621
+ async getPendingProctoringIds() {
31622
+ const db = await this.connect();
31623
+ return new Promise((resolve, reject) => {
31624
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
31625
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
31626
+ const index = store.index("uploaded");
31627
+ const range = IDBKeyRange.only(0);
31628
+ const request = index.getAll(range);
31629
+ request.onsuccess = () => {
31630
+ const chunks = request.result;
31631
+ const ids = [...new Set(chunks.map((c3) => c3.proctoringId))];
31632
+ resolve(ids);
31633
+ };
31634
+ request.onerror = () => {
31635
+ var _a2;
31636
+ return reject(new Error(`Erro ao buscar proctoringIds pendentes: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
31637
+ };
31638
+ });
31639
+ }
31640
+ /**
31641
+ * Fecha a conexão com o banco.
31642
+ */
31643
+ close() {
31644
+ if (this.db) {
31645
+ this.db.close();
31646
+ this.db = null;
31647
+ }
31648
+ }
31649
+ };
31650
+ _ChunkStorageService.DB_NAME = "EasyProctorChunksDb";
31651
+ /** Incrementado para v2 para recriar índices com tipos numéricos em vez de boolean */
31652
+ _ChunkStorageService.DB_VERSION = 2;
31653
+ _ChunkStorageService.STORE_NAME = "chunks";
31654
+ var ChunkStorageService = _ChunkStorageService;
31655
+
31656
+ // src/new-flow/chunk/BackgroundUploadService.ts
31657
+ var DEFAULT_CONFIG = {
31658
+ pollInterval: 5e3,
31659
+ maxRetries: 5,
31660
+ baseRetryDelay: 2e3,
31661
+ cleanAfterUpload: true
31662
+ };
31663
+ var BackgroundUploadService = class _BackgroundUploadService {
31664
+ constructor(proctoringId2, token, backend, chunkStorage, config) {
31665
+ this.pollTimer = null;
31666
+ this.isProcessing = false;
31667
+ this.isRunning = false;
31668
+ /** Mapa de chunkId -> número de tentativas já feitas */
31669
+ this.retryCount = /* @__PURE__ */ new Map();
31670
+ /** GCS Resumable Upload State */
31671
+ this.sessionUrl = null;
31672
+ this.currentOffset = 0;
31673
+ this.totalBytesPurged = 0;
31674
+ this.STORAGE_KEY_PREFIX = "ep_upload_session_";
31675
+ this.GCS_CHUNK_SIZE = 256 * 1024;
31676
+ this.proctoringId = proctoringId2.trim();
31677
+ this.token = token;
31678
+ this.backend = backend;
31679
+ this.chunkStorage = chunkStorage;
31680
+ this.config = { ...DEFAULT_CONFIG, ...config };
31681
+ this.loadSessionState();
31682
+ }
31683
+ loadSessionState() {
31684
+ try {
31685
+ const stored = localStorage.getItem(`${this.STORAGE_KEY_PREFIX}${this.proctoringId}`);
31686
+ if (stored) {
31687
+ const { sessionUrl, currentOffset, totalBytesPurged } = JSON.parse(stored);
31688
+ this.sessionUrl = sessionUrl;
31689
+ this.currentOffset = currentOffset;
31690
+ this.totalBytesPurged = totalBytesPurged || 0;
31691
+ }
31692
+ } catch (e3) {
31693
+ console.warn("[BackgroundUpload] Erro ao carregar estado da sess\xE3o:", e3);
31694
+ }
31695
+ }
31696
+ saveSessionState() {
31697
+ try {
31698
+ localStorage.setItem(
31699
+ `${this.STORAGE_KEY_PREFIX}${this.proctoringId}`,
31700
+ JSON.stringify({
31701
+ sessionUrl: this.sessionUrl,
31702
+ currentOffset: this.currentOffset,
31703
+ totalBytesPurged: this.totalBytesPurged
31704
+ })
31705
+ );
31706
+ } catch (e3) {
31707
+ console.warn("[BackgroundUpload] Erro ao salvar estado da sess\xE3o:", e3);
31708
+ }
31709
+ }
31710
+ clearSessionState() {
31711
+ try {
31712
+ localStorage.removeItem(`${this.STORAGE_KEY_PREFIX}${this.proctoringId}`);
31713
+ this.sessionUrl = null;
31714
+ this.currentOffset = 0;
31715
+ this.totalBytesPurged = 0;
31716
+ } catch (e3) {
31717
+ console.warn("[BackgroundUpload] Erro ao limpar estado da sess\xE3o:", e3);
31718
+ }
31719
+ }
31720
+ /**
31721
+ * Inicia o serviço de upload em background. Faz polling periódico no IndexedDB
31722
+ * para enviar chunks pendentes.
31723
+ */
31724
+ start() {
31725
+ if (this.isRunning) return;
31726
+ this.isRunning = true;
31727
+ console.log(`[BackgroundUpload] Iniciando servi\xE7o para proctoringId: ${this.proctoringId}`);
31728
+ this.processQueue();
31729
+ this.pollTimer = setInterval(() => {
31730
+ this.processQueue(false);
31731
+ }, this.config.pollInterval);
31732
+ }
31733
+ /**
31734
+ * Para o serviço de upload em background.
31735
+ */
31736
+ stop() {
31737
+ this.isRunning = false;
31738
+ if (this.pollTimer) {
31739
+ clearInterval(this.pollTimer);
31740
+ this.pollTimer = null;
31741
+ }
31742
+ console.log(`[BackgroundUpload] Servi\xE7o parado para proctoringId: ${this.proctoringId}`);
31743
+ }
31744
+ /**
31745
+ * Força o processamento de todos os chunks pendentes e encerra a sessão GCS.
31746
+ * Útil quando a gravação é finalizada.
31747
+ */
31748
+ async flush() {
31749
+ console.log(`[BackgroundUpload] Flush: enviando todos os chunks pendentes e finalizando...`);
31750
+ let waitAttempts = 0;
31751
+ while (this.isProcessing && waitAttempts < 10) {
31752
+ await this.sleep(1e3);
31753
+ waitAttempts++;
31754
+ }
31755
+ let flushRetries = 0;
31756
+ const maxFlushRetries = 3;
31757
+ while (flushRetries < maxFlushRetries) {
31758
+ try {
31759
+ await this.processQueue(true);
31760
+ console.log(`[BackgroundUpload] Flush completado com sucesso.`);
31761
+ return;
31762
+ } catch (error) {
31763
+ flushRetries++;
31764
+ console.error(`[BackgroundUpload] Erro no flush (tentativa ${flushRetries}/${maxFlushRetries}):`, error);
31765
+ if (flushRetries < maxFlushRetries) {
31766
+ await this.sleep(2e3);
31767
+ }
31768
+ }
31769
+ }
31770
+ throw new Error(`[BackgroundUpload] Falha ao finalizar upload ap\xF3s ${maxFlushRetries} tentativas.`);
31771
+ }
31772
+ /**
31773
+ * Sincroniza o offset local com o estado real no Google Cloud Storage.
31774
+ */
31775
+ async syncOffset() {
31776
+ if (!this.sessionUrl) return 0;
31777
+ try {
31778
+ console.log(`[BackgroundUpload] Sincronizando offset com GCS...`);
31779
+ const response = await fetch(this.sessionUrl, {
31780
+ method: "PUT",
31781
+ headers: {
31782
+ "Content-Range": "bytes */*"
31783
+ }
31784
+ });
31785
+ console.log(`[BackgroundUpload] Status da sincroniza\xE7\xE3o (syncOffset): ${response.status}`);
31786
+ if (response.status === 308) {
31787
+ const range = response.headers.get("Range");
31788
+ if (range) {
31789
+ const lastByte = parseInt(range.split("-")[1], 10);
31790
+ this.currentOffset = lastByte + 1;
31791
+ this.saveSessionState();
31792
+ console.log(`[BackgroundUpload] Offset sincronizado: ${this.currentOffset}`);
31793
+ } else {
31794
+ this.currentOffset = 0;
31795
+ }
31796
+ } else if (response.ok || response.status === 201) {
31797
+ console.log("[BackgroundUpload] Sincroniza\xE7\xE3o indicou upload JA FINALIZADO.");
31798
+ this.currentOffset = -1;
31799
+ } else {
31800
+ console.warn(`[BackgroundUpload] Status inesperado na sincroniza\xE7\xE3o: ${response.status}`);
31801
+ }
31802
+ } catch (error) {
31803
+ console.warn("[BackgroundUpload] Erro ao sincronizar offset:", error);
31804
+ }
31805
+ return this.currentOffset;
31806
+ }
31807
+ /**
31808
+ * Verifica e envia chunks pendentes para o backend.
31809
+ * @param isFinal Se true, não alinha a 256KB e fecha a sessão com /TOTAL no header.
31810
+ */
31811
+ async processQueue(isFinal = false) {
31812
+ var _a2, _b;
31813
+ if (this.isProcessing) return;
31814
+ this.isProcessing = true;
31815
+ try {
31816
+ if (this.sessionUrl) {
31817
+ await this.syncOffset();
31818
+ if (this.currentOffset === -1) {
31819
+ console.log("[BackgroundUpload] Sess\xE3o j\xE1 finalizada no servidor.");
31820
+ this.clearSessionState();
31821
+ this.isProcessing = false;
31822
+ return;
31823
+ }
31824
+ }
31825
+ const allChunks = await this.chunkStorage.getAllChunks(this.proctoringId);
31826
+ const pendingChunks = allChunks.filter((c3) => c3.uploaded === 0);
31827
+ if (pendingChunks.length === 0 && !isFinal) {
31828
+ this.isProcessing = false;
31829
+ return;
31830
+ }
31831
+ console.log(`[BackgroundUpload] ${pendingChunks.length} chunks pendentes encontrados. Modo final: ${isFinal}`);
31832
+ let virtualStart = this.totalBytesPurged;
31833
+ const chunksWithMeta = allChunks.map((c3) => {
31834
+ const start = virtualStart;
31835
+ const end = start + c3.blob.size - 1;
31836
+ virtualStart += c3.blob.size;
31837
+ return { chunk: c3, start, end };
31838
+ });
31839
+ let combinedBlobParts = [];
31840
+ let lastProcessedChunkId = null;
31841
+ let finalChunkIndex = 0;
31842
+ let mimeType = pendingChunks[0].mimeType;
31843
+ for (const meta of chunksWithMeta) {
31844
+ if (this.currentOffset > meta.end) continue;
31845
+ const sliceStart = Math.max(0, this.currentOffset - meta.start);
31846
+ const chunkSlice = meta.chunk.blob.slice(sliceStart);
31847
+ combinedBlobParts.push(chunkSlice);
31848
+ lastProcessedChunkId = meta.chunk.id;
31849
+ finalChunkIndex = meta.chunk.chunkIndex;
31850
+ }
31851
+ if (combinedBlobParts.length === 0 && !isFinal) {
31852
+ this.isProcessing = false;
31853
+ return;
31854
+ }
31855
+ let fullBlob = new Blob(combinedBlobParts, { type: mimeType });
31856
+ let sendableSize = fullBlob.size;
31857
+ let totalSizeForHeader = void 0;
31858
+ if (!isFinal) {
31859
+ sendableSize = Math.floor(fullBlob.size / this.GCS_CHUNK_SIZE) * this.GCS_CHUNK_SIZE;
31860
+ if (sendableSize === 0) {
31861
+ console.log("[BackgroundUpload] Dados insuficientes para atingir 256KB. Aguardando novo chunk...");
31862
+ this.isProcessing = false;
31863
+ return;
31864
+ }
31865
+ } else {
31866
+ totalSizeForHeader = virtualStart;
31867
+ }
31868
+ const blobToSend = fullBlob.slice(0, sendableSize);
31869
+ try {
31870
+ await this.uploadData(blobToSend, mimeType, finalChunkIndex, totalSizeForHeader);
31871
+ for (const meta of chunksWithMeta) {
31872
+ if (meta.chunk.uploaded === 0 && meta.end < this.currentOffset) {
31873
+ await this.chunkStorage.markAsUploaded(meta.chunk.id);
31874
+ this.retryCount.delete(meta.chunk.id);
31875
+ (_a2 = this.onChunkUploaded) == null ? void 0 : _a2.call(this, meta.chunk.id, meta.chunk.chunkIndex);
31876
+ console.log(`[BackgroundUpload] Chunk ${meta.chunk.chunkIndex} marcado como enviado.`);
31877
+ }
31878
+ }
31879
+ if (this.config.cleanAfterUpload) {
31880
+ const chunksToClear = chunksWithMeta.filter((meta) => meta.chunk.uploaded === 1 || meta.chunk.uploaded === 0 && meta.end < this.currentOffset);
31881
+ const sizePurged = chunksToClear.reduce((acc, meta) => acc + meta.chunk.blob.size, 0);
31882
+ await this.chunkStorage.clearUploadedChunks(this.proctoringId);
31883
+ if (sizePurged > 0) {
31884
+ this.totalBytesPurged += sizePurged;
31885
+ this.saveSessionState();
31886
+ console.log(`[BackgroundUpload] ${sizePurged} bytes limpos do armazenamento local. Total purgado: ${this.totalBytesPurged}`);
31887
+ }
31888
+ }
31889
+ if (isFinal) {
31890
+ this.clearSessionState();
31891
+ }
31892
+ } catch (error) {
31893
+ console.error("[BackgroundUpload] Falha no upload:", error);
31894
+ (_b = this.onUploadError) == null ? void 0 : _b.call(this, lastProcessedChunkId || 0, error);
31895
+ }
31896
+ } catch (error) {
31897
+ console.error("[BackgroundUpload] Erro ao processar fila:", error);
31898
+ } finally {
31899
+ this.isProcessing = false;
31900
+ }
31901
+ }
31902
+ /**
31903
+ * Faz o upload bruto de dados para a sessão GCS.
31904
+ */
31905
+ async uploadData(blob, mimeType, chunkIndex, totalSize) {
31906
+ const fileName = `EP_${this.proctoringId}_camera_0.webm`;
31907
+ if (!this.sessionUrl) {
31908
+ const initiateUrl = await this.backend.initiateUpload(this.token, `${this.proctoringId}/${fileName}`, mimeType);
31909
+ const startResponse = await fetch(initiateUrl, {
31910
+ method: "POST",
31911
+ headers: { "x-goog-resumable": "start", "Content-Type": mimeType }
31912
+ });
31913
+ if (!startResponse.ok) throw new Error(`Falha ao iniciar: ${startResponse.status}`);
31914
+ this.sessionUrl = startResponse.headers.get("Location");
31915
+ if (!this.sessionUrl) throw new Error("Location header ausente");
31916
+ try {
31917
+ const urlObj = new URL(this.sessionUrl);
31918
+ const pathParts = urlObj.pathname.split("/");
31919
+ let bucket = pathParts[1];
31920
+ let object = decodeURIComponent(pathParts.slice(2).join("/"));
31921
+ if (pathParts.includes("b") && pathParts.includes("o")) {
31922
+ const bIdx = pathParts.indexOf("b") + 1;
31923
+ const oIdx = pathParts.indexOf("o") + 1;
31924
+ bucket = pathParts[bIdx];
31925
+ object = decodeURIComponent(pathParts.slice(oIdx).join("/"));
31926
+ }
31927
+ console.log(`[BackgroundUpload] Sess\xE3o Iniciada -> Bucket: ${bucket}, Objeto: ${object}`);
31928
+ } catch (e3) {
31929
+ console.log(`[BackgroundUpload] Sess\xE3o Iniciada. URL: ${this.sessionUrl}`);
31930
+ }
31931
+ this.currentOffset = 0;
31932
+ this.saveSessionState();
31933
+ } else {
31934
+ console.log(`[BackgroundUpload] Usando sess\xE3o GCS existente: ${this.sessionUrl}`);
31935
+ }
31936
+ const start = this.currentOffset;
31937
+ const end = start + blob.size - 1;
31938
+ const totalHeader = totalSize !== void 0 ? totalSize.toString() : "*";
31939
+ const contentRangeHeader = blob.size === 0 && totalSize !== void 0 ? `bytes */${totalHeader}` : `bytes ${start}-${end}/${totalHeader}`;
31940
+ console.log(`[BackgroundUpload] Enviando ${blob.size > 0 ? "dados" : "finaliza\xE7\xE3o"}: ${contentRangeHeader} (Size: ${blob.size})`);
31941
+ const response = await fetch(this.sessionUrl, {
31942
+ method: "PUT",
31943
+ headers: { "Content-Range": contentRangeHeader },
31944
+ body: blob.size > 0 ? blob : null
31945
+ // Usa null para garantir corpo vazio se necessário
31946
+ });
31947
+ console.log(`[BackgroundUpload] Resposta GCS (uploadData): ${response.status}`);
31948
+ if (response.status !== 200 && response.status !== 201 && response.status !== 308) {
31949
+ const errorText = await response.text();
31950
+ console.error(`[BackgroundUpload] Erro GCS: ${errorText}`);
31951
+ throw new Error(`Status HTTP inesperado: ${response.status}`);
31952
+ }
31953
+ const rangeHeader = response.headers.get("Range");
31954
+ if (rangeHeader) {
31955
+ const lastByte = parseInt(rangeHeader.split("-")[1], 10);
31956
+ this.currentOffset = lastByte + 1;
31957
+ } else {
31958
+ this.currentOffset += blob.size;
31959
+ }
31960
+ this.saveSessionState();
31961
+ trackers.registerUploadFile(
31962
+ this.proctoringId,
31963
+ `GCS Stream Upload
31964
+ Size: ${blob.size}
31965
+ Range: ${start}-${end}
31966
+ Last Index: ${chunkIndex}`,
31967
+ "CameraChunk"
31968
+ );
31969
+ }
31970
+ /**
31971
+ * Método estático para recuperação pós-crash.
31972
+ * Verifica o IndexedDB em busca de chunks pendentes de qualquer sessão
31973
+ * e tenta enviar.
31974
+ */
31975
+ static async recoverPendingUploads(backend, token) {
31976
+ const chunkStorage = new ChunkStorageService();
31977
+ const recoveredIds = [];
31978
+ try {
31979
+ const pendingIds = await chunkStorage.getPendingProctoringIds();
31980
+ if (pendingIds.length === 0) {
31981
+ console.log("[BackgroundUpload] Nenhum chunk pendente encontrado para recupera\xE7\xE3o.");
31982
+ return recoveredIds;
31983
+ }
31984
+ console.log(
31985
+ `[BackgroundUpload] Recupera\xE7\xE3o p\xF3s-crash: ${pendingIds.length} sess\xE3o(\xF5es) com chunks pendentes.`
31986
+ );
31987
+ for (const proctoringId2 of pendingIds) {
31988
+ try {
31989
+ const service = new _BackgroundUploadService(
31990
+ proctoringId2,
31991
+ token,
31992
+ backend,
31993
+ chunkStorage,
31994
+ { cleanAfterUpload: true }
31995
+ );
31996
+ await service.flush();
31997
+ recoveredIds.push(proctoringId2);
31998
+ console.log(
31999
+ `[BackgroundUpload] Chunks da sess\xE3o ${proctoringId2} recuperados com sucesso.`
32000
+ );
32001
+ } catch (error) {
32002
+ console.error(
32003
+ `[BackgroundUpload] Erro ao recuperar chunks da sess\xE3o ${proctoringId2}:`,
32004
+ error
32005
+ );
32006
+ }
32007
+ }
32008
+ } catch (error) {
32009
+ console.error("[BackgroundUpload] Erro geral na recupera\xE7\xE3o:", error);
32010
+ }
32011
+ return recoveredIds;
32012
+ }
32013
+ sleep(ms2) {
32014
+ return new Promise((resolve) => setTimeout(resolve, ms2));
32015
+ }
32016
+ };
32017
+
31345
32018
  // src/new-flow/recorders/CameraRecorder.ts
31346
32019
  var import_jszip_min = __toESM(require_jszip_min());
31347
- var import_fix_webm_duration = __toESM(require_fix_webm_duration());
31348
- var CameraRecorder = class {
32020
+ var pkg = require_fix_webm_duration();
32021
+ var fixWebmDuration = pkg.default || pkg;
32022
+ var _CameraRecorder = class _CameraRecorder {
31349
32023
  constructor(options, videoOptions, paramsConfig, backend, backendToken) {
31350
32024
  this.blobs = [];
31351
32025
  this.paramsConfig = {
@@ -31390,6 +32064,7 @@ var CameraRecorder = class {
31390
32064
  this.blobsRTC = [];
31391
32065
  this.imageCount = 0;
31392
32066
  this.filesToUpload = [];
32067
+ this.pendingPackages = [];
31393
32068
  this.animationFrameId = null;
31394
32069
  this.isCanvasLoopActive = false;
31395
32070
  this.hardwareStream = null;
@@ -31397,8 +32072,16 @@ var CameraRecorder = class {
31397
32072
  this.videoElement = null;
31398
32073
  this.duration = 0;
31399
32074
  this.stopped = false;
32075
+ this.backgroundUpload = null;
32076
+ this.chunkIndex = 0;
32077
+ /** Lista de promises de chunks sendo salvos no IndexedDB para evitar race conditions no stop */
32078
+ this.pendingChunkSaves = [];
32079
+ // Handlers bound para poder remover os listeners depois
32080
+ this.boundVisibilityHandler = null;
32081
+ this.boundPageHideHandler = null;
31400
32082
  this.currentRetries = 0;
31401
32083
  this.packageCount = 0;
32084
+ this.failedUploads = 0;
31402
32085
  this.noiseWait = 20;
31403
32086
  this.options = options;
31404
32087
  this.videoOptions = videoOptions;
@@ -31406,10 +32089,122 @@ var CameraRecorder = class {
31406
32089
  this.backendToken = backendToken;
31407
32090
  paramsConfig && (this.paramsConfig = paramsConfig);
31408
32091
  }
32092
+ /**
32093
+ * Determina se o fluxo de chunks e lifecycle deve estar ativo.
32094
+ * Retorna true se:
32095
+ * 1. O proctoringId já foi definido (ou seja, estamos em uma sessão real, NÃO no checkDevices)
32096
+ * 2. E (`useChunkRecording` foi explicitamente setado como true OU o dispositivo é mobile)
32097
+ */
32098
+ get isChunkEnabled() {
32099
+ return !!this.proctoringId && this.options.proctoringType === "REALTIME" && !isSafeBrowser();
32100
+ }
31409
32101
  setProctoringId(proctoringId2) {
31410
32102
  this.proctoringId = proctoringId2;
31411
32103
  this.proctoringId && this.backend && (this.upload = new UploadService(this.proctoringId, this.backend));
31412
32104
  setRecorderProctoringId(proctoringId2);
32105
+ if (this.isChunkEnabled) {
32106
+ this.chunkStorage = new ChunkStorageService();
32107
+ if (this.backend && this.backendToken) {
32108
+ this.backgroundUpload = new BackgroundUploadService(
32109
+ this.proctoringId,
32110
+ this.backendToken,
32111
+ this.backend,
32112
+ this.chunkStorage,
32113
+ { pollInterval: 5e3, maxRetries: 5, cleanAfterUpload: true }
32114
+ );
32115
+ }
32116
+ this.persistSessionState("IN_PROGRESS");
32117
+ console.log(
32118
+ `[CameraRecorder] Chunk recording ATIVO (type: ${this.options.proctoringType}, mobile: ${isMobileDevice()})`
32119
+ );
32120
+ } else {
32121
+ console.log(
32122
+ `[CameraRecorder] Chunk recording INATIVO (type: ${this.options.proctoringType}) \u2014 modo cl\xE1ssico.`
32123
+ );
32124
+ }
32125
+ }
32126
+ // ========================
32127
+ // Session State Persistence (localStorage)
32128
+ // ========================
32129
+ persistSessionState(status) {
32130
+ try {
32131
+ const data = {
32132
+ proctoringId: this.proctoringId,
32133
+ status,
32134
+ timestamp: Date.now()
32135
+ };
32136
+ localStorage.setItem(_CameraRecorder.LS_SESSION_KEY, JSON.stringify(data));
32137
+ } catch (e3) {
32138
+ console.warn("[CameraRecorder] N\xE3o foi poss\xEDvel salvar estado no localStorage:", e3);
32139
+ }
32140
+ }
32141
+ clearSessionState() {
32142
+ try {
32143
+ localStorage.removeItem(_CameraRecorder.LS_SESSION_KEY);
32144
+ } catch (e3) {
32145
+ console.warn("[CameraRecorder] N\xE3o foi poss\xEDvel limpar estado do localStorage:", e3);
32146
+ }
32147
+ }
32148
+ /**
32149
+ * Verifica se existe uma sessão ativa anterior no localStorage.
32150
+ * Retorna os dados da sessão se ela estiver em IN_PROGRESS, null caso contrário.
32151
+ */
32152
+ static checkForActiveSession() {
32153
+ try {
32154
+ const raw = localStorage.getItem(_CameraRecorder.LS_SESSION_KEY);
32155
+ if (!raw) return null;
32156
+ const data = JSON.parse(raw);
32157
+ if (data.status === "IN_PROGRESS") return data;
32158
+ return null;
32159
+ } catch {
32160
+ return null;
32161
+ }
32162
+ }
32163
+ // ========================
32164
+ // Page Lifecycle Management
32165
+ // ========================
32166
+ setupLifecycleListeners() {
32167
+ this.boundVisibilityHandler = () => this.handleVisibilityChange();
32168
+ this.boundPageHideHandler = () => this.handlePageHide();
32169
+ document.addEventListener("visibilitychange", this.boundVisibilityHandler);
32170
+ window.addEventListener("pagehide", this.boundPageHideHandler);
32171
+ }
32172
+ removeLifecycleListeners() {
32173
+ if (this.boundVisibilityHandler) {
32174
+ document.removeEventListener("visibilitychange", this.boundVisibilityHandler);
32175
+ this.boundVisibilityHandler = null;
32176
+ }
32177
+ if (this.boundPageHideHandler) {
32178
+ window.removeEventListener("pagehide", this.boundPageHideHandler);
32179
+ this.boundPageHideHandler = null;
32180
+ }
32181
+ }
32182
+ handleVisibilityChange() {
32183
+ var _a2;
32184
+ if (document.visibilityState === "hidden") {
32185
+ console.log("[CameraRecorder] P\xE1gina ficou invis\xEDvel \u2014 sess\xE3o potencialmente interrompida.");
32186
+ this.persistSessionState("INTERRUPTED");
32187
+ this.proctoringId && trackers.registerError(
32188
+ this.proctoringId,
32189
+ "Visibility API: P\xE1gina ficou oculta (hidden). Poss\xEDvel troca de app ou minimiza\xE7\xE3o."
32190
+ );
32191
+ } else if (document.visibilityState === "visible") {
32192
+ console.log("[CameraRecorder] P\xE1gina vis\xEDvel novamente \u2014 verificando estado da grava\xE7\xE3o.");
32193
+ this.persistSessionState("IN_PROGRESS");
32194
+ this.proctoringId && trackers.registerError(
32195
+ this.proctoringId,
32196
+ "Visibility API: P\xE1gina voltou a ficar vis\xEDvel. Usu\xE1rio retornou."
32197
+ );
32198
+ (_a2 = this.onVisibilityRestored) == null ? void 0 : _a2.call(this);
32199
+ }
32200
+ }
32201
+ handlePageHide() {
32202
+ console.log("[CameraRecorder] pagehide detectado \u2014 persistindo estado.");
32203
+ this.persistSessionState("INTERRUPTED");
32204
+ this.proctoringId && trackers.registerError(
32205
+ this.proctoringId,
32206
+ "Page Lifecycle: pagehide event detectado. P\xE1gina est\xE1 sendo descarregada."
32207
+ );
31413
32208
  }
31414
32209
  async initializeDetectors() {
31415
32210
  var _a2, _b, _c2;
@@ -31562,10 +32357,15 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31562
32357
  await new Promise((r3) => setTimeout(r3, 300));
31563
32358
  }
31564
32359
  async startRecording() {
31565
- var _a2, _b, _c2, _d, _e3, _f, _g;
31566
- console.log("CameraRecorder startRecording");
32360
+ var _a2, _b, _c2, _d, _e3, _f, _g, _h;
31567
32361
  await this.startStream();
31568
32362
  await this.attachAndWarmup(this.cameraStream);
32363
+ const recorderOpts = this.isChunkEnabled ? {
32364
+ timeslice: _CameraRecorder.CHUNK_TIMESLICE_MS,
32365
+ onChunkAvailable: (blob, idx) => {
32366
+ this.handleNewChunk(blob, idx);
32367
+ }
32368
+ } : {};
31569
32369
  const {
31570
32370
  startRecording,
31571
32371
  stopRecording,
@@ -31581,7 +32381,8 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31581
32381
  this.blobs,
31582
32382
  this.options.onBufferSizeError,
31583
32383
  (e3) => this.bufferError(e3),
31584
- false
32384
+ false,
32385
+ recorderOpts
31585
32386
  );
31586
32387
  this.recordingStart = startRecording;
31587
32388
  this.recordingStop = stopRecording;
@@ -31591,13 +32392,18 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31591
32392
  this.getBufferSize = getBufferSize;
31592
32393
  this.getStartTime = getStartTime;
31593
32394
  this.getDuration = getDuration;
32395
+ this.chunkIndex = 0;
32396
+ if (this.isChunkEnabled) {
32397
+ (_a2 = this.backgroundUpload) == null ? void 0 : _a2.start();
32398
+ this.setupLifecycleListeners();
32399
+ }
31594
32400
  try {
31595
32401
  await new Promise((r3) => setTimeout(r3, 500));
31596
32402
  await this.recordingStart();
31597
32403
  } catch (error) {
31598
32404
  console.log("Camera Recorder error", error);
31599
32405
  this.stopRecording();
31600
- const maxRetries = ((_a2 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _a2.maxRetries) || 3;
32406
+ const maxRetries = ((_b = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _b.maxRetries) || 3;
31601
32407
  if (this.currentRetries < maxRetries) {
31602
32408
  console.log("Camera Recorder retry", this.currentRetries);
31603
32409
  this.currentRetries++;
@@ -31607,16 +32413,17 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31607
32413
  }
31608
32414
  }
31609
32415
  this.stopped = false;
31610
- if (((_b = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _b.detectPerson) || ((_c2 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _c2.detectCellPhone) || ((_d = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _d.detectFace)) {
32416
+ if (((_c2 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _c2.detectPerson) || ((_d = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _d.detectCellPhone) || ((_e3 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _e3.detectFace)) {
31611
32417
  await this.initializeDetectors();
31612
32418
  }
31613
- if ((_e3 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _e3.detectFace) {
32419
+ if ((_f = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _f.detectFace) {
31614
32420
  await this.faceDetection.enableCam(this.cameraStream);
31615
32421
  }
31616
- if (((_f = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _f.detectPerson) || ((_g = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _g.detectCellPhone)) {
32422
+ if (((_g = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _g.detectPerson) || ((_h = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _h.detectCellPhone)) {
31617
32423
  await this.objectDetection.enableCam(this.cameraStream);
31618
32424
  }
31619
32425
  this.filesToUpload = [];
32426
+ this.pendingPackages = [];
31620
32427
  if (this.options.proctoringType == "REALTIME") {
31621
32428
  await this.startRealtimeCapture();
31622
32429
  }
@@ -31636,6 +32443,7 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31636
32443
  this.intervalNoiseDetection && clearInterval(this.intervalNoiseDetection);
31637
32444
  this.recordingStop && await this.recordingStop();
31638
32445
  this.duration = this.getDuration ? this.getDuration() : 0;
32446
+ await new Promise((r3) => setTimeout(r3, 200));
31639
32447
  try {
31640
32448
  if (this.animationFrameId) {
31641
32449
  cancelAnimationFrame(this.animationFrameId);
@@ -31666,9 +32474,54 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31666
32474
  console.error("Erro ao parar os streams de m\xEDdia.");
31667
32475
  }
31668
32476
  if (this.options.proctoringType == "REALTIME" && this.upload && this.backendToken) {
31669
- await this.sendPackage(this.filesToUpload);
32477
+ this.pendingPackages.push(this.filesToUpload.slice(0, this.filesToUpload.length));
32478
+ await this.sendPackage();
31670
32479
  await this.filesToUpload.splice(0, this.filesToUpload.length);
31671
32480
  }
32481
+ if (this.isChunkEnabled) {
32482
+ if (this.backgroundUpload) {
32483
+ try {
32484
+ if (this.pendingChunkSaves.length > 0) {
32485
+ console.log(`[CameraRecorder] Aguardando ${this.pendingChunkSaves.length} salvamentos de chunks pendentes...`);
32486
+ await Promise.all(this.pendingChunkSaves);
32487
+ }
32488
+ await this.backgroundUpload.flush();
32489
+ } catch (e3) {
32490
+ console.warn("[CameraRecorder] Erro ao fazer flush dos chunks:", e3);
32491
+ }
32492
+ this.backgroundUpload.stop();
32493
+ }
32494
+ this.removeLifecycleListeners();
32495
+ this.persistSessionState("FINISHED");
32496
+ }
32497
+ }
32498
+ /**
32499
+ * Callback chamado pelo recorder a cada novo chunk de vídeo disponível.
32500
+ * Salva o chunk no IndexedDB para persistência e recuperação.
32501
+ */
32502
+ async handleNewChunk(blob, idx) {
32503
+ if (!this.proctoringId || !this.chunkStorage) return;
32504
+ const savePromise = (async () => {
32505
+ var _a2;
32506
+ try {
32507
+ await this.chunkStorage.saveChunk({
32508
+ proctoringId: this.proctoringId,
32509
+ chunkIndex: this.chunkIndex,
32510
+ blob,
32511
+ timestamp: Date.now(),
32512
+ uploaded: 0,
32513
+ mimeType: ((_a2 = this.recorderOptions) == null ? void 0 : _a2.mimeType) || "video/webm"
32514
+ });
32515
+ this.chunkIndex++;
32516
+ console.log(`[CameraRecorder] Chunk ${this.chunkIndex - 1} salvo no IndexedDB.`);
32517
+ } catch (error) {
32518
+ console.error("[CameraRecorder] Erro ao salvar chunk no IndexedDB:", error);
32519
+ }
32520
+ })();
32521
+ this.pendingChunkSaves.push(savePromise);
32522
+ savePromise.finally(() => {
32523
+ this.pendingChunkSaves = this.pendingChunkSaves.filter((p3) => p3 !== savePromise);
32524
+ });
31672
32525
  }
31673
32526
  async pauseRecording() {
31674
32527
  await this.recordingPause();
@@ -31715,9 +32568,9 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31715
32568
  if (this.proctoringId == void 0) return;
31716
32569
  if (packSize == this.imageCount) {
31717
32570
  this.imageCount = 0;
31718
- const framesToSend = [...this.filesToUpload];
31719
- this.sendPackage(framesToSend);
31720
- await this.filesToUpload.splice(0, this.filesToUpload.length);
32571
+ this.pendingPackages.push(this.filesToUpload.slice(0, packSize));
32572
+ this.sendPackage();
32573
+ await this.filesToUpload.splice(0, packSize);
31721
32574
  }
31722
32575
  let imageName = `${this.proctoringId}_${this.imageCount + 1}.jpg`;
31723
32576
  imageFile = await this.getFile(image_data_url, imageName, "image/jpeg");
@@ -31731,54 +32584,61 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31731
32584
  var _a2;
31732
32585
  this.configImageCapture();
31733
32586
  this.imageCount = 0;
32587
+ this.pendingPackages = [];
31734
32588
  await this.captureFrame();
31735
32589
  this.imageInterval = setInterval(async () => {
31736
32590
  await this.captureFrame();
31737
32591
  }, ((_a2 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _a2.realtimeCaptureInterval) * 1e3);
31738
32592
  }
31739
32593
  // envia pacote de imagens
31740
- async sendPackage(framesToSend) {
32594
+ async sendPackage() {
31741
32595
  var _a2, _b;
31742
- let pending = false;
31743
- let undeliveredPackagesCount = 0;
31744
32596
  const packSize = (_a2 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _a2.realtimePackageSize;
31745
32597
  const packCaptureInterval = (_b = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _b.realtimeCaptureInterval;
31746
- if (this.upload && this.backendToken && !pending && this.filesToUpload.length > 0) {
31747
- undeliveredPackagesCount = 0;
31748
- pending = true;
31749
- const zip = new import_jszip_min.default();
31750
- for (const file of framesToSend) {
31751
- zip.file(file.name, file);
31752
- }
31753
- const blob = await zip.generateAsync({ type: "blob" });
31754
- let packageName = "realtime_package_" + packSize * packCaptureInterval * this.packageCount + ".zip";
31755
- const myPackage = new File(
31756
- [blob],
31757
- packageName,
31758
- { type: "application/zip" }
31759
- );
31760
- const uploadResult = await this.upload.uploadPackage(
31761
- {
31762
- file: myPackage
31763
- },
31764
- this.backendToken
31765
- );
31766
- if (uploadResult.uploaded == true) {
31767
- this.packageCount++;
31768
- } else {
31769
- console.log("erro no upload do pacote");
32598
+ if (this.upload && this.backendToken && this.pendingPackages.length > 0) {
32599
+ let packagesToDelete = [];
32600
+ let packageIndex = 0;
32601
+ for (const packageToSend of this.pendingPackages) {
32602
+ const zip = new import_jszip_min.default();
32603
+ for (const file of packageToSend) {
32604
+ zip.file(file.name, file);
32605
+ }
32606
+ const blob = await zip.generateAsync({ type: "blob" });
32607
+ let packageName = "realtime_package_" + packSize * packCaptureInterval * this.packageCount + ".zip";
32608
+ const myPackage = new File(
32609
+ [blob],
32610
+ packageName,
32611
+ { type: "application/zip" }
32612
+ );
32613
+ try {
32614
+ const uploadResult = await this.upload.uploadPackage(
32615
+ {
32616
+ file: myPackage
32617
+ },
32618
+ this.backendToken
32619
+ );
32620
+ if (uploadResult == true) {
32621
+ this.packageCount++;
32622
+ this.failedUploads = 0;
32623
+ packagesToDelete.push(packageIndex++);
32624
+ }
32625
+ } catch (error) {
32626
+ this.failedUploads++;
32627
+ if (this.failedUploads >= 2) {
32628
+ this.options.onRealtimeAlertsCallback({
32629
+ status: "ALERT",
32630
+ description: "Realtime n\xE3o est\xE1 enviando pacotes",
32631
+ type: "error_upload_package",
32632
+ category: "error_upload_package",
32633
+ begin: 0,
32634
+ end: 0
32635
+ });
32636
+ }
32637
+ break;
32638
+ }
31770
32639
  }
31771
- pending = false;
31772
- } else if (pending) {
31773
- undeliveredPackagesCount++;
31774
- if (undeliveredPackagesCount == 3) {
31775
- undeliveredPackagesCount = 0;
31776
- let newCanvasWidth = this.videoOptions.width / 2;
31777
- let newCanvasHeight = this.videoOptions.height / 2;
31778
- if (newCanvasWidth < 320) newCanvasWidth = 320;
31779
- if (newCanvasHeight < 180) newCanvasHeight = 180;
31780
- this.canvas.width = newCanvasWidth;
31781
- this.canvas.height = newCanvasHeight;
32640
+ for (const packageToDelete of packagesToDelete) {
32641
+ await this.pendingPackages.splice(packageToDelete, 1);
31782
32642
  }
31783
32643
  }
31784
32644
  }
@@ -31797,16 +32657,32 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31797
32657
  if (this.blobs != null)
31798
32658
  trackers.registerSaveOnSession(
31799
32659
  this.proctoringId,
31800
- `Blobs Length: ${this.blobs.length} Buffer Size: ${this.getBufferSize()} `
32660
+ `Blobs Length: ${this.blobs.length} Buffer Size: ${this.getBufferSize()} ChunkEnabled: ${this.isChunkEnabled}`
31801
32661
  );
31802
32662
  const settings = this.cameraStream.getVideoTracks()[0].getSettings();
31803
32663
  const settingsAudio = this.cameraStream.getAudioTracks()[0].getSettings();
31804
32664
  if (this.options.proctoringType == "VIDEO" || this.options.proctoringType == "REALTIME" || this.options.proctoringType == "IMAGE" && ((_a2 = this.paramsConfig.imageBehaviourParameters) == null ? void 0 : _a2.saveVideo)) {
32665
+ let videoFile;
32666
+ if (this.isChunkEnabled) {
32667
+ const isStable = await this.checkInternetStability();
32668
+ if (isStable) {
32669
+ } else {
32670
+ if (this.backend && this.backendToken && this.proctoringId) {
32671
+ const fileName = `EP_${this.proctoringId}_camera_0.webm`;
32672
+ const objectName = `${this.proctoringId}/${fileName}`;
32673
+ const isUploaded = await this.backend.checkUpload(this.backendToken, objectName, "video/webm");
32674
+ if (isUploaded) {
32675
+ this.chunkStorage && await this.chunkStorage.clearAllChunks(session.id);
32676
+ return;
32677
+ }
32678
+ }
32679
+ }
32680
+ }
31805
32681
  const rawBlob = new Blob(this.blobs, {
31806
32682
  type: ((_b = this.recorderOptions) == null ? void 0 : _b.mimeType) || "video/webm"
31807
32683
  });
31808
- const fixedBlob = await (0, import_fix_webm_duration.default)(rawBlob, this.duration);
31809
- const fileWithDuration = new File(
32684
+ const fixedBlob = await fixWebmDuration(rawBlob, this.duration);
32685
+ videoFile = new File(
31810
32686
  [fixedBlob],
31811
32687
  `EP_${session.id}_camera_0.webm`,
31812
32688
  { type: rawBlob.type }
@@ -31818,7 +32694,7 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31818
32694
 
31819
32695
  Video:
31820
32696
  ${JSON.stringify(this.recorderOptions)}`,
31821
- file: fileWithDuration,
32697
+ file: videoFile,
31822
32698
  origin: "Camera" /* Camera */
31823
32699
  });
31824
32700
  }
@@ -31833,6 +32709,28 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31833
32709
  });
31834
32710
  });
31835
32711
  }
32712
+ /**
32713
+ * Verifica se a internet está estável para realizar o upload do vídeo na íntegra.
32714
+ */
32715
+ async checkInternetStability() {
32716
+ var _a2;
32717
+ if (!navigator.onLine) return false;
32718
+ try {
32719
+ const controller = new AbortController();
32720
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
32721
+ const baseUrl = (_a2 = this.backend) == null ? void 0 : _a2.getBaseUrl();
32722
+ if (!baseUrl) return true;
32723
+ const response = await fetch(`${baseUrl}/Client/health`, {
32724
+ method: "GET",
32725
+ signal: controller.signal
32726
+ });
32727
+ clearTimeout(timeoutId);
32728
+ return response.status < 500;
32729
+ } catch (e3) {
32730
+ console.warn("[CameraRecorder] Internet inst\xE1vel ou lenta detectada para upload integral.");
32731
+ return false;
32732
+ }
32733
+ }
31836
32734
  onNoiseDetected() {
31837
32735
  var _a2, _b, _c2;
31838
32736
  if (this.options.proctoringType === "REALTIME") return;
@@ -31845,7 +32743,6 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31845
32743
  }
31846
32744
  const volume = (_b = (_a2 = this.volumeMeter) == null ? void 0 : _a2.getVolume()) != null ? _b : 0;
31847
32745
  if (volume >= (((_c2 = this.paramsConfig.audioBehaviourParameters) == null ? void 0 : _c2.noiseLimit) || 40)) {
31848
- console.log("entrou" + this.noiseWait);
31849
32746
  if (this.noiseWait >= 20) {
31850
32747
  this.options.onRealtimeAlertsCallback({
31851
32748
  status: "ALERT",
@@ -31858,6 +32755,14 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31858
32755
  this.noiseWait++;
31859
32756
  }
31860
32757
  };
32758
+ // ========================
32759
+ // Chunk & Lifecycle
32760
+ // ========================
32761
+ /** Intervalo de cada chunk em ms (padrão: 60 segundos) */
32762
+ _CameraRecorder.CHUNK_TIMESLICE_MS = 6e4;
32763
+ /** Chave do localStorage para persistir estado da sessão */
32764
+ _CameraRecorder.LS_SESSION_KEY = "ep_proctoring_session";
32765
+ var CameraRecorder = _CameraRecorder;
31861
32766
 
31862
32767
  // src/new-flow/checkers/DeviceCheckerUI.ts
31863
32768
  var DeviceCheckerUI = class {
@@ -32930,7 +33835,8 @@ var _DeviceCheckerService = class _DeviceCheckerService {
32930
33835
  this.context = context;
32931
33836
  this.backend = new BackendService({
32932
33837
  type: (context == null ? void 0 : context.type) || "prod",
32933
- token: context.token
33838
+ token: context.token,
33839
+ isRealtime: false
32934
33840
  });
32935
33841
  }
32936
33842
  getDeviceCheckResult() {
@@ -33091,7 +33997,7 @@ var _DeviceCheckerService = class _DeviceCheckerService {
33091
33997
  videoDeviceInterface(stream4) {
33092
33998
  this.DeviceCheckerUI && this.DeviceCheckerUI.videoDeviceInterfaceUI(stream4);
33093
33999
  this.isUnderResolution();
33094
- this.faceDetection.enableCam(stream4);
34000
+ this.faceDetection.enableCam(stream4, 1e3);
33095
34001
  }
33096
34002
  audioDeviceInterface(stream4) {
33097
34003
  this.volumeMeter = new VolumeMeter(this.cameraRecorder.cameraStream);
@@ -36357,6 +37263,7 @@ var NoiseRecorder = class {
36357
37263
  this.MAX_PRE_ROLL_CHUNKS = 4;
36358
37264
  this.lastNoiseTime = 0;
36359
37265
  this.SILENCE_THRESHOLD = 3e3;
37266
+ this.filesToUpload = [];
36360
37267
  this.optionsProctoring = optionsProctoring;
36361
37268
  this.proctoringSession = proctoringSession;
36362
37269
  this.paramsConfig = paramsConfig;
@@ -36448,7 +37355,7 @@ var NoiseRecorder = class {
36448
37355
  }
36449
37356
  }
36450
37357
  async stopSoundRecord() {
36451
- var _a2;
37358
+ var _a2, _b;
36452
37359
  if (!this.recordingInProgress && this.recordingChunks.length === 0) return;
36453
37360
  this.recordingEndTime = Date.now() - (((_a2 = this.cameraRecorder.getStartTime()) == null ? void 0 : _a2.getTime()) || 0);
36454
37361
  if (this.optionsProctoring.proctoringType !== "REALTIME") return;
@@ -36462,7 +37369,33 @@ var NoiseRecorder = class {
36462
37369
  type: "audio/wav"
36463
37370
  }
36464
37371
  );
36465
- this.uploadRecord(file);
37372
+ let filesToSend = [...this.filesToUpload];
37373
+ for (const myFile of filesToSend) {
37374
+ try {
37375
+ await ((_b = this.upload) == null ? void 0 : _b.upload(
37376
+ {
37377
+ file: myFile
37378
+ },
37379
+ this.backendToken
37380
+ ));
37381
+ this.filesToUpload.splice(this.filesToUpload.indexOf(myFile), 1);
37382
+ } catch (error) {
37383
+ break;
37384
+ }
37385
+ }
37386
+ try {
37387
+ if (file && this.upload && this.backendToken) {
37388
+ this.upload.upload(
37389
+ {
37390
+ file
37391
+ },
37392
+ this.backendToken
37393
+ );
37394
+ }
37395
+ } catch (error) {
37396
+ console.log("error Noise recorder adicionando na fila", error);
37397
+ this.filesToUpload.push(file);
37398
+ }
36466
37399
  this.recordingChunks = [];
36467
37400
  this.recordingInProgress = false;
36468
37401
  }
@@ -36476,16 +37409,6 @@ var NoiseRecorder = class {
36476
37409
  a3.click();
36477
37410
  window.URL.revokeObjectURL(url2);
36478
37411
  }
36479
- uploadRecord(file) {
36480
- if (file && this.upload && this.backendToken) {
36481
- this.upload.upload(
36482
- {
36483
- file
36484
- },
36485
- this.backendToken
36486
- );
36487
- }
36488
- }
36489
37412
  // CLASSIFIER -< Media Pipe
36490
37413
  // Verify if has speech in the classifier array
36491
37414
  hasDesiredResult(array) {
@@ -36592,7 +37515,8 @@ registerProcessor("audio-processor", AudioProcessor);
36592
37515
  `;
36593
37516
 
36594
37517
  // src/new-flow/recorders/ScreenRecorder.ts
36595
- var import_fix_webm_duration2 = __toESM(require_fix_webm_duration());
37518
+ var pkg2 = require_fix_webm_duration();
37519
+ var fixWebmDuration2 = pkg2.default || pkg2;
36596
37520
  var ScreenRecorder = class {
36597
37521
  constructor(options) {
36598
37522
  this.blobs = [];
@@ -36688,7 +37612,7 @@ var ScreenRecorder = class {
36688
37612
  const rawBlob = new Blob(this.blobs, {
36689
37613
  type: "video/webm"
36690
37614
  });
36691
- const fixedBlob = await (0, import_fix_webm_duration2.default)(rawBlob, this.duration);
37615
+ const fixedBlob = await fixWebmDuration2(rawBlob, this.duration);
36692
37616
  const file = new File(
36693
37617
  [fixedBlob],
36694
37618
  `EP_${session.id}_screen_0.webm`,
@@ -36793,14 +37717,6 @@ function getGeolocation() {
36793
37717
  });
36794
37718
  }
36795
37719
 
36796
- // src/utils/verifyVersion.ts
36797
- function versionVerify() {
36798
- const agentStr = window.navigator.userAgent.split("SEB/");
36799
- if (agentStr.length > 1)
36800
- return agentStr[1];
36801
- else return "1.0.0.0";
36802
- }
36803
-
36804
37720
  // src/proctoring/Auth.ts
36805
37721
  var Auth = class {
36806
37722
  constructor(cpf, backend) {
@@ -36946,7 +37862,8 @@ var _ExternalCameraChecker = class _ExternalCameraChecker {
36946
37862
  this.onRealtimeAlertsCallback = onRealtimeAlertsCallback;
36947
37863
  this.backend = new BackendService({
36948
37864
  type: (context == null ? void 0 : context.type) || "prod",
36949
- token: context.token
37865
+ token: context.token,
37866
+ isRealtime: false
36950
37867
  });
36951
37868
  this.currentStep = -1 /* WAITING */;
36952
37869
  }
@@ -37679,6 +38596,12 @@ var Proctoring = class {
37679
38596
  this.serviceType = "Upload" /* Upload */;
37680
38597
  this.onStopSharingScreenCallback = () => {
37681
38598
  };
38599
+ /** Callback notificando que o usuário saiu do browser (minimizou/trocou de aba) */
38600
+ this.onVisibilityLostCallback = () => {
38601
+ };
38602
+ /** Callback notificando que o usuário retornou ao browser */
38603
+ this.onVisibilityRestoredCallback = () => {
38604
+ };
37682
38605
  this.onLostFocusCallback = () => {
37683
38606
  };
37684
38607
  this.onLostFocusAlertRecorderCallback = (response) => {
@@ -37694,13 +38617,16 @@ var Proctoring = class {
37694
38617
  await this.internalOnRealtimeAlerts(response);
37695
38618
  return;
37696
38619
  };
38620
+ this.realtimeAlertsToSend = [];
37697
38621
  this.onBufferSizeErrorCallback = (cameraStream) => {
37698
38622
  return;
37699
38623
  };
37700
38624
  var _a2;
37701
38625
  this.backend = new BackendService({
37702
38626
  type: context.type || "prod",
37703
- token: context.token
38627
+ token: context.token,
38628
+ isRealtime: false
38629
+ // Default false, atualizado no start() via backend.setRealtime()
37704
38630
  });
37705
38631
  this.repository = new IndexDbSessionRepository("EasyProctorDb", "exams2");
37706
38632
  this.repositoryDevices = new IndexDbSessionRepository(
@@ -37710,6 +38636,12 @@ var Proctoring = class {
37710
38636
  ((_a2 = this.context.credentials) == null ? void 0 : _a2.cpf) && (this.auth = new Auth(this.context.credentials.cpf, this.backend));
37711
38637
  this.appChecker = new ExternalCameraChecker(this.context, (response) => this.onRealtimeAlertsCallback(response));
37712
38638
  }
38639
+ setOnVisibilityLostCallback(cb) {
38640
+ this.onVisibilityLostCallback = cb;
38641
+ }
38642
+ setOnVisibilityRestoredCallback(cb) {
38643
+ this.onVisibilityRestoredCallback = cb;
38644
+ }
37713
38645
  setOnStopSharingScreenCallback(cb) {
37714
38646
  this.onStopSharingScreenCallback = async () => {
37715
38647
  var _a2, _b, _c2, _d;
@@ -37789,6 +38721,8 @@ var Proctoring = class {
37789
38721
  return 25 /* FocusOff */;
37790
38722
  case "focus":
37791
38723
  return 25 /* FocusOff */;
38724
+ case "error_upload_package":
38725
+ return 44 /* RealtimeOffline */;
37792
38726
  default:
37793
38727
  return null;
37794
38728
  }
@@ -37829,19 +38763,53 @@ var Proctoring = class {
37829
38763
  };
37830
38764
  await verifyFace(1);
37831
38765
  }
38766
+ async sendPendingRealtimeAlerts() {
38767
+ let alertsToSend = [...this.realtimeAlertsToSend];
38768
+ for (const alert of alertsToSend) {
38769
+ try {
38770
+ if (alert.status === "ALERT") {
38771
+ await this.backend.startRealtimeAlert({
38772
+ proctoringId: this.proctoringId,
38773
+ begin: alert.begin,
38774
+ end: alert.end,
38775
+ alert: this.convertRealtimeCategoryToAlertCategory(alert.category)
38776
+ });
38777
+ } else if (alert.status === "OK") {
38778
+ await this.stopRealtimeAlert(alert);
38779
+ }
38780
+ this.realtimeAlertsToSend.splice(this.realtimeAlertsToSend.indexOf(alert), 1);
38781
+ } catch (error) {
38782
+ console.log("error sendPendingRealtimeAlerts", error);
38783
+ this.realtimeAlertsToSend.push(alert);
38784
+ break;
38785
+ }
38786
+ }
38787
+ }
37832
38788
  async internalOnRealtimeAlerts(response) {
37833
- if (this.sessionOptions.proctoringType === "REALTIME" && (response.type === "face_detection_on_stream" || response.type === "person_detection_on_stream" || response.type === "lost_focus" || response.type === "focus")) {
38789
+ if (this.sessionOptions.proctoringType === "REALTIME" && (response.type === "face_detection_on_stream" || response.type === "person_detection_on_stream" || response.type === "lost_focus" || response.type === "focus" || response.type === "error_upload_package")) {
38790
+ await this.sendPendingRealtimeAlerts();
37834
38791
  if (response.status === "ALERT") {
37835
38792
  if (this.allRecorders.cameraRecorder.stopped) return;
37836
- await this.backend.startRealtimeAlert({
37837
- proctoringId: this.proctoringId,
37838
- begin: response.begin,
37839
- end: response.end,
37840
- alert: this.convertRealtimeCategoryToAlertCategory(response.category)
37841
- });
38793
+ try {
38794
+ await this.backend.startRealtimeAlert({
38795
+ proctoringId: this.proctoringId,
38796
+ begin: response.begin,
38797
+ end: response.end,
38798
+ alert: this.convertRealtimeCategoryToAlertCategory(response.category)
38799
+ });
38800
+ } catch (error) {
38801
+ console.log("error startRealtimeAlert " + response.category);
38802
+ console.log("error startRealtimeAlert adicionando na fila", error);
38803
+ this.realtimeAlertsToSend.push(response);
38804
+ }
37842
38805
  } else if (response.status === "OK") {
37843
38806
  if (this.allRecorders.cameraRecorder.stopped && response.description !== "face_stop") return;
37844
- await this.stopRealtimeAlert(response);
38807
+ try {
38808
+ await this.stopRealtimeAlert(response);
38809
+ } catch (error) {
38810
+ console.log("error stopRealtimeAlert adicionando na fila", error);
38811
+ this.realtimeAlertsToSend.push(response);
38812
+ }
37845
38813
  }
37846
38814
  }
37847
38815
  }
@@ -37849,8 +38817,8 @@ var Proctoring = class {
37849
38817
  this.setOnLostFocusAlertRecorderCallback();
37850
38818
  this.setOnFocusAlertRecorderCallback();
37851
38819
  this.onRealtimeAlertsCallback = async (response) => {
37852
- await this.internalOnRealtimeAlerts(response);
37853
38820
  options.data && options.data(response);
38821
+ await this.internalOnRealtimeAlerts(response);
37854
38822
  };
37855
38823
  }
37856
38824
  setOnBufferSizeErrorCallback(cb) {
@@ -37946,8 +38914,10 @@ var Proctoring = class {
37946
38914
  await this.repositoryDevices.save({ ...devices, id: "devices" });
37947
38915
  this.sessionOptions = { ...getDefaultProctoringOptions, ...options };
37948
38916
  this.videoOptions = validatePartialVideoOptions(_videoOptions);
38917
+ if (this.sessionOptions.proctoringType === "REALTIME") {
38918
+ this.backend.setRealtime(true);
38919
+ }
37949
38920
  await this.initConfig(options.useGeolocation);
37950
- await this.verifyBrowser();
37951
38921
  this.sessionOptions.captureScreen && await this.verifyMultipleMonitors(this.sessionOptions);
37952
38922
  try {
37953
38923
  if (options == null ? void 0 : options.useSpyScan) {
@@ -38005,6 +38975,20 @@ var Proctoring = class {
38005
38975
  } catch (error) {
38006
38976
  throw SAFE_BROWSER_API_NOT_FOUND;
38007
38977
  }
38978
+ this.allRecorders.cameraRecorder.onVisibilityRestored = () => {
38979
+ console.log("[Proctoring] Usu\xE1rio retornou ao browser.");
38980
+ this.onVisibilityRestoredCallback();
38981
+ };
38982
+ if (this.sessionOptions.proctoringType === "REALTIME" && !isSafeBrowser()) {
38983
+ try {
38984
+ await BackgroundUploadService.recoverPendingUploads(
38985
+ this.backend,
38986
+ this.context.token
38987
+ );
38988
+ } catch (e3) {
38989
+ console.warn("[Proctoring] Erro ao recuperar chunks de sess\xE3o anterior:", e3);
38990
+ }
38991
+ }
38008
38992
  try {
38009
38993
  console.log("Starting recorders");
38010
38994
  await this.recorder.startAll();
@@ -38084,6 +39068,7 @@ Error: ${error}`
38084
39068
  this.spyCam && this.spyCam.stopCheckSpyCam();
38085
39069
  this.appChecker && await this.appChecker.disconnectWebSocket();
38086
39070
  await this.recorder.saveAllOnSession();
39071
+ await this.sendPendingRealtimeAlerts();
38087
39072
  await this.repository.save(this.proctoringSession);
38088
39073
  let uploader;
38089
39074
  let uploaderServices;
@@ -38368,7 +39353,8 @@ var _SignTerm = class _SignTerm {
38368
39353
  constructor(context) {
38369
39354
  this.backend = new BackendService({
38370
39355
  type: (context == null ? void 0 : context.type) || "prod",
38371
- token: context.token
39356
+ token: context.token,
39357
+ isRealtime: false
38372
39358
  });
38373
39359
  }
38374
39360
  async signInTerms() {
@@ -38598,6 +39584,8 @@ function useProctoring(proctoringOptions, enviromentConfig = "prod") {
38598
39584
  const onBufferSizeError = proctoring.setOnBufferSizeErrorCallback.bind(proctoring);
38599
39585
  const onStopSharingScreen = proctoring.setOnStopSharingScreenCallback.bind(proctoring);
38600
39586
  const onRealtimeAlerts = proctoring.onRealtimeAlerts.bind(proctoring);
39587
+ const onVisibilityLost = proctoring.setOnVisibilityLostCallback.bind(proctoring);
39588
+ const onVisibilityRestored = proctoring.setOnVisibilityRestoredCallback.bind(proctoring);
38601
39589
  const signInTerms = signTerm.signInTerms.bind(signTerm);
38602
39590
  const checkDevices = checker.checkDevices.bind(checker);
38603
39591
  const checkExternalCamera = proctoring.appChecker.checkExternalCamera.bind(proctoring.appChecker);
@@ -38635,7 +39623,9 @@ function useProctoring(proctoringOptions, enviromentConfig = "prod") {
38635
39623
  startExternalCameraSession,
38636
39624
  takeExternalCameraPicture,
38637
39625
  goToExternalCameraPositionStep,
38638
- startExternalCameraTransmission
39626
+ startExternalCameraTransmission,
39627
+ onVisibilityLost,
39628
+ onVisibilityRestored
38639
39629
  };
38640
39630
  }
38641
39631