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/esm/index.js CHANGED
@@ -5044,14 +5044,17 @@ var require_browser = __commonJS({
5044
5044
 
5045
5045
  // src/modules/checkPermissions.ts
5046
5046
  async function checkPermissions() {
5047
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
5048
+ return false;
5049
+ }
5047
5050
  try {
5048
5051
  const constraints = {
5049
5052
  audio: true,
5050
5053
  video: true
5051
5054
  };
5052
5055
  const stream = await navigator.mediaDevices.getUserMedia(constraints);
5053
- stream.getTracks().forEach((el) => {
5054
- el.stop();
5056
+ stream.getTracks().forEach((track) => {
5057
+ track.stop();
5055
5058
  });
5056
5059
  return true;
5057
5060
  } catch (error) {
@@ -9145,7 +9148,6 @@ var BaseDetection = class {
9145
9148
  // objectDetection
9146
9149
  runningMode: this.runningMode
9147
9150
  });
9148
- console.log("BaseDetection initializeDetector", this.detectorType);
9149
9151
  }
9150
9152
  stopDetection() {
9151
9153
  this.animationFrameId && clearTimeout(this.animationFrameId);
@@ -9155,7 +9157,7 @@ var BaseDetection = class {
9155
9157
  this.createdVideo && this.video && document.body.removeChild(this.video);
9156
9158
  this.createdVideo = false;
9157
9159
  }
9158
- enableCam(cameraStream) {
9160
+ enableCam(cameraStream, delay = 1e4) {
9159
9161
  var _a2;
9160
9162
  if (!this.detector) {
9161
9163
  console.log("Wait! Detector not loaded yet.");
@@ -9185,7 +9187,7 @@ var BaseDetection = class {
9185
9187
  (_a2 = this.video) == null ? void 0 : _a2.addEventListener("loadeddata", () => {
9186
9188
  this.animationFrameId = setTimeout(() => {
9187
9189
  that.predictWebcam();
9188
- }, 1e4);
9190
+ }, delay);
9189
9191
  });
9190
9192
  const style = document.createElement("style");
9191
9193
  style.type = "text/css";
@@ -9202,7 +9204,6 @@ var BaseDetection = class {
9202
9204
  }
9203
9205
  `;
9204
9206
  document.getElementsByTagName("head")[0].appendChild(style);
9205
- console.log("BaseDetection enableCam OK");
9206
9207
  }
9207
9208
  async predictWebcam() {
9208
9209
  if (this.detecting == false) return;
@@ -9332,7 +9333,6 @@ var FaceDetection = class extends BaseDetection {
9332
9333
  );
9333
9334
  this.emmitedPositionAlert = false;
9334
9335
  this.emmitedFaceAlert = false;
9335
- console.log("FaceDetection constructor");
9336
9336
  this.numFacesSent = -1;
9337
9337
  }
9338
9338
  stopDetection() {
@@ -12093,13 +12093,28 @@ var {
12093
12093
  var DEV_BASE_URL = "https://proctoring-api-dev.easyproctor.tech/api";
12094
12094
  var HOMOL_BASE_URL = "https://proctoring-api-hml.easyproctor.tech/api";
12095
12095
  var PROD_BASE_URL = "https://proctoring-api.easyproctor.tech/api";
12096
+ var REALTIME_DEV_BASE_URL = "https://easyproctor-realtime-api-dev.easyproctor.tech/api";
12097
+ var REALTIME_HOMOL_BASE_URL = "https://easyproctor-realtime-api-hml.easyproctor.tech/api";
12098
+ var REALTIME_PROD_BASE_URL = "https://easyproctor-realtime-api.easyproctor.tech/api";
12096
12099
  var BackendService = class {
12097
12100
  constructor(options) {
12098
12101
  this.options = options;
12099
- this.baseUrl = this.selectBaseUrl(options.type);
12102
+ this.baseUrl = this.selectBaseUrl(options.type, options.isRealtime);
12100
12103
  this.token = options.token;
12101
12104
  }
12102
- selectBaseUrl(type) {
12105
+ getBaseUrl() {
12106
+ return this.baseUrl;
12107
+ }
12108
+ selectBaseUrl(type, isRealtime) {
12109
+ if (isRealtime) {
12110
+ if (type === "dev") {
12111
+ return REALTIME_DEV_BASE_URL;
12112
+ } else if (type === "homol") {
12113
+ return REALTIME_HOMOL_BASE_URL;
12114
+ } else {
12115
+ return REALTIME_PROD_BASE_URL;
12116
+ }
12117
+ }
12103
12118
  if (type === "dev") {
12104
12119
  return DEV_BASE_URL;
12105
12120
  } else if (type === "homol") {
@@ -12110,6 +12125,9 @@ var BackendService = class {
12110
12125
  return PROD_BASE_URL;
12111
12126
  }
12112
12127
  }
12128
+ setRealtime(isRealtime) {
12129
+ this.baseUrl = this.selectBaseUrl(this.options.type, isRealtime);
12130
+ }
12113
12131
  getSocketUrl() {
12114
12132
  return this.baseUrl.replace("/api", "/hub/sockethub");
12115
12133
  }
@@ -12220,6 +12238,18 @@ var BackendService = class {
12220
12238
  });
12221
12239
  return url.data;
12222
12240
  }
12241
+ async initiateUpload(token, objectName, contentType) {
12242
+ const url = await this.makeRequestAxios({
12243
+ path: `/upload/initiate-upload`,
12244
+ method: "POST",
12245
+ jwt: token,
12246
+ body: {
12247
+ objectName,
12248
+ contentType
12249
+ }
12250
+ });
12251
+ return url.data;
12252
+ }
12223
12253
  async saveAlerts(proctoringOptions, proctoringSession) {
12224
12254
  await this.makeRequest({
12225
12255
  path: "/proctoring/save-alerts",
@@ -12323,6 +12353,22 @@ var BackendService = class {
12323
12353
  });
12324
12354
  return result.data;
12325
12355
  }
12356
+ async checkUpload(token, objectName, contentType) {
12357
+ try {
12358
+ const result = await this.makeRequestAxios({
12359
+ path: `/Upload/check`,
12360
+ method: "POST",
12361
+ jwt: token,
12362
+ body: {
12363
+ objectName,
12364
+ contentType
12365
+ }
12366
+ });
12367
+ return result.data === true;
12368
+ } catch (e3) {
12369
+ return false;
12370
+ }
12371
+ }
12326
12372
  async getServerHour(token) {
12327
12373
  return await this.makeRequest({
12328
12374
  path: `/Proctoring/server-hour`,
@@ -12523,7 +12569,8 @@ var SpyCam = class {
12523
12569
  this.context = context;
12524
12570
  this.backend = new BackendService({
12525
12571
  type: (context == null ? void 0 : context.type) || "prod",
12526
- token: context.token
12572
+ token: context.token,
12573
+ isRealtime: false
12527
12574
  });
12528
12575
  this.currentIsPlugged = true;
12529
12576
  }
@@ -12675,6 +12722,14 @@ var getDefaultProctoringVideoOptions = {
12675
12722
  minHeight: 480
12676
12723
  };
12677
12724
 
12725
+ // src/utils/verifyVersion.ts
12726
+ function versionVerify() {
12727
+ const agentStr = window.navigator.userAgent.split("SEB/");
12728
+ if (agentStr.length > 1)
12729
+ return agentStr[1];
12730
+ else return "1.0.0.0";
12731
+ }
12732
+
12678
12733
  // src/utils/browserInformations.ts
12679
12734
  function fnBrowserDetect() {
12680
12735
  const userAgent = navigator.userAgent;
@@ -12703,13 +12758,16 @@ function isMobileDevice() {
12703
12758
  }
12704
12759
  return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
12705
12760
  }
12761
+ function isSafeBrowser() {
12762
+ return versionVerify() !== "1.0.0.0";
12763
+ }
12706
12764
 
12707
12765
  // src/plugins/recorder.ts
12708
12766
  var proctoringId;
12709
12767
  function setRecorderProctoringId(id) {
12710
12768
  proctoringId = id;
12711
12769
  }
12712
- function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCallback, audio = false) {
12770
+ function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCallback, audio = false, recorderOpts) {
12713
12771
  let resolvePromise;
12714
12772
  let onBufferSizeInterval;
12715
12773
  let lastEvent;
@@ -12717,6 +12775,7 @@ function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCa
12717
12775
  bufferSize = 0;
12718
12776
  let startTime;
12719
12777
  let duration = 0;
12778
+ let chunkIndex = 0;
12720
12779
  let recorderOptions = {
12721
12780
  // eslint-disable-next-line no-useless-escape
12722
12781
  mimeType: "video/webm",
@@ -12751,6 +12810,10 @@ function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCa
12751
12810
  mediaRecorder2.ondataavailable = (e3) => {
12752
12811
  bufferSize = bufferSize + e3.data.size;
12753
12812
  if (e3.data.size > 0) {
12813
+ if (recorderOpts == null ? void 0 : recorderOpts.onChunkAvailable) {
12814
+ recorderOpts.onChunkAvailable(e3.data, chunkIndex);
12815
+ chunkIndex++;
12816
+ }
12754
12817
  buffer.push(e3.data);
12755
12818
  }
12756
12819
  };
@@ -12782,7 +12845,13 @@ function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCa
12782
12845
  };
12783
12846
  try {
12784
12847
  console.log("State antes do start:", recorder2.state);
12785
- recorder2.start(1e4);
12848
+ chunkIndex = 0;
12849
+ if ((recorderOpts == null ? void 0 : recorderOpts.timeslice) && (recorderOpts == null ? void 0 : recorderOpts.timeslice) > 0) {
12850
+ recorder2.start(recorderOpts.timeslice);
12851
+ } else {
12852
+ recorder2.start(1e4);
12853
+ }
12854
+ bufferSize = 0;
12786
12855
  startTime = new Date(Date.now());
12787
12856
  } catch (e3) {
12788
12857
  console.error("Recorder erro ao chamar start event:", e3);
@@ -12824,6 +12893,9 @@ function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCa
12824
12893
  console.log("recorder onstop");
12825
12894
  duration = Date.now() - startTime.getTime() || 0;
12826
12895
  console.log("duration no onstop", duration);
12896
+ stream.getTracks().forEach((el) => {
12897
+ el.stop();
12898
+ });
12827
12899
  resolvePromise && resolvePromise();
12828
12900
  };
12829
12901
  mediaRecorder.stop();
@@ -12833,9 +12905,6 @@ function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCa
12833
12905
  console.log("stopRecording Recorder n\xE3o est\xE1 em estado recording");
12834
12906
  resolve();
12835
12907
  }
12836
- stream.getTracks().forEach((el) => {
12837
- el.stop();
12838
- });
12839
12908
  });
12840
12909
  }
12841
12910
  function pauseRecording() {
@@ -12929,36 +12998,34 @@ var UploadService = class {
12929
12998
  this.proctoringId = proctoringId2;
12930
12999
  }
12931
13000
  async uploadPackage(data, token) {
12932
- const { file, onProgress } = data;
13001
+ const { file } = data;
12933
13002
  try {
12934
- const progressCallback = (e3) => {
12935
- const progress = e3.loadedBytes / file.size * 100;
12936
- onProgress && onProgress(Math.round(progress));
12937
- };
12938
- const uploadUrl = await this.backend.getSignedUrl(token, file, this.proctoringId);
12939
- const uploaded = await axios_default.request({
13003
+ console.log("Upload service: uploadPackage");
13004
+ var uploadUrl = "";
13005
+ await this.backend.getSignedUrl(token, file, this.proctoringId).then((result) => uploadUrl = result).catch((error) => {
13006
+ throw error;
13007
+ });
13008
+ console.log("Upload service: uploadUrl", uploadUrl);
13009
+ await axios_default.request({
12940
13010
  url: uploadUrl,
12941
13011
  method: "PUT",
12942
13012
  headers: {
12943
13013
  "Content-Type": file.type,
12944
13014
  "x-ms-blob-type": "BlockBlob"
12945
13015
  },
12946
- data: file,
12947
- onUploadProgress: (p3) => {
12948
- progressCallback({ loadedBytes: p3.loaded });
12949
- }
12950
- }).then(() => true).catch(() => false);
12951
- return {
12952
- storage: "upload" /* none */,
12953
- url: uploadUrl,
12954
- uploaded
12955
- };
13016
+ data: file
13017
+ }).then(() => {
13018
+ return true;
13019
+ }).catch((error) => {
13020
+ throw error;
13021
+ });
13022
+ return true;
12956
13023
  } catch (err) {
12957
- trackers.registerError(this.proctoringId, `Failed to upload to AWS
13024
+ trackers.registerError(this.proctoringId, `Failed to upload package ${err}
12958
13025
  File name: ${file.name}
12959
13026
  File type: ${file.type}
12960
13027
  File size: ${file.size}`);
12961
- throw new Error("Failed to upload to AWS");
13028
+ throw err;
12962
13029
  }
12963
13030
  }
12964
13031
  async uploadImages(data, token, packSize) {
@@ -13245,10 +13312,617 @@ var VolumeMeter = class {
13245
13312
  }
13246
13313
  };
13247
13314
 
13315
+ // src/new-flow/chunk/ChunkStorageService.ts
13316
+ var _ChunkStorageService = class _ChunkStorageService {
13317
+ constructor() {
13318
+ this.db = null;
13319
+ }
13320
+ /**
13321
+ * Abre a conexão com o IndexedDB, criando o banco e o object store se necessário.
13322
+ */
13323
+ async connect() {
13324
+ if (this.db) return this.db;
13325
+ return new Promise((resolve, reject) => {
13326
+ const request = window.indexedDB.open(
13327
+ _ChunkStorageService.DB_NAME,
13328
+ _ChunkStorageService.DB_VERSION
13329
+ );
13330
+ request.onerror = () => {
13331
+ reject(new Error("N\xE3o foi poss\xEDvel conectar ao IndexedDB para chunks."));
13332
+ };
13333
+ request.onupgradeneeded = () => {
13334
+ const db = request.result;
13335
+ if (db.objectStoreNames.contains(_ChunkStorageService.STORE_NAME)) {
13336
+ db.deleteObjectStore(_ChunkStorageService.STORE_NAME);
13337
+ }
13338
+ const store = db.createObjectStore(_ChunkStorageService.STORE_NAME, {
13339
+ keyPath: "id",
13340
+ autoIncrement: true
13341
+ });
13342
+ store.createIndex("proctoringId", "proctoringId", { unique: false });
13343
+ store.createIndex("uploaded", "uploaded", { unique: false });
13344
+ store.createIndex("proctoringId_uploaded", ["proctoringId", "uploaded"], {
13345
+ unique: false
13346
+ });
13347
+ };
13348
+ request.onsuccess = () => {
13349
+ this.db = request.result;
13350
+ resolve(this.db);
13351
+ };
13352
+ });
13353
+ }
13354
+ /**
13355
+ * Salva um chunk de vídeo no IndexedDB.
13356
+ */
13357
+ async saveChunk(chunk) {
13358
+ const db = await this.connect();
13359
+ return new Promise((resolve, reject) => {
13360
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
13361
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13362
+ const request = store.add(chunk);
13363
+ request.onsuccess = () => {
13364
+ resolve(request.result);
13365
+ };
13366
+ request.onerror = () => {
13367
+ var _a2;
13368
+ reject(new Error(`Erro ao salvar chunk no IndexedDB: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13369
+ };
13370
+ });
13371
+ }
13372
+ /**
13373
+ * Retorna todos os chunks pendentes (não enviados) de um proctoringId específico.
13374
+ */
13375
+ async getPendingChunks(proctoringId2) {
13376
+ const db = await this.connect();
13377
+ return new Promise((resolve, reject) => {
13378
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
13379
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13380
+ const index = store.index("proctoringId_uploaded");
13381
+ const range = IDBKeyRange.only([proctoringId2, 0]);
13382
+ const request = index.getAll(range);
13383
+ request.onsuccess = () => {
13384
+ const chunks = request.result.sort(
13385
+ (a3, b3) => a3.chunkIndex - b3.chunkIndex
13386
+ );
13387
+ resolve(chunks);
13388
+ };
13389
+ request.onerror = () => {
13390
+ var _a2;
13391
+ reject(new Error(`Erro ao buscar chunks pendentes: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13392
+ };
13393
+ });
13394
+ }
13395
+ /**
13396
+ * Retorna todos os chunks (enviados ou não) de um proctoringId específico.
13397
+ */
13398
+ async getAllChunks(proctoringId2) {
13399
+ const db = await this.connect();
13400
+ return new Promise((resolve, reject) => {
13401
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
13402
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13403
+ const index = store.index("proctoringId");
13404
+ const range = IDBKeyRange.only(proctoringId2);
13405
+ const request = index.getAll(range);
13406
+ request.onsuccess = () => {
13407
+ const chunks = request.result.sort(
13408
+ (a3, b3) => a3.chunkIndex - b3.chunkIndex
13409
+ );
13410
+ resolve(chunks);
13411
+ };
13412
+ request.onerror = () => {
13413
+ var _a2;
13414
+ reject(new Error(`Erro ao buscar todos os chunks: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13415
+ };
13416
+ });
13417
+ }
13418
+ /**
13419
+ * Marca um chunk como enviado (uploaded = 1).
13420
+ */
13421
+ async markAsUploaded(chunkId) {
13422
+ const db = await this.connect();
13423
+ return new Promise((resolve, reject) => {
13424
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
13425
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13426
+ const getRequest = store.get(chunkId);
13427
+ getRequest.onsuccess = () => {
13428
+ const chunk = getRequest.result;
13429
+ if (!chunk) {
13430
+ resolve();
13431
+ return;
13432
+ }
13433
+ chunk.uploaded = 1;
13434
+ const putRequest = store.put(chunk);
13435
+ putRequest.onsuccess = () => resolve();
13436
+ putRequest.onerror = () => {
13437
+ var _a2;
13438
+ return reject(new Error(`Erro ao marcar chunk como enviado: ${(_a2 = putRequest.error) == null ? void 0 : _a2.message}`));
13439
+ };
13440
+ };
13441
+ getRequest.onerror = () => {
13442
+ var _a2;
13443
+ return reject(new Error(`Erro ao buscar chunk para marcar: ${(_a2 = getRequest.error) == null ? void 0 : _a2.message}`));
13444
+ };
13445
+ });
13446
+ }
13447
+ /**
13448
+ * Remove todos os chunks já enviados de um proctoringId para liberar espaço.
13449
+ */
13450
+ async clearUploadedChunks(proctoringId2) {
13451
+ const db = await this.connect();
13452
+ return new Promise((resolve, reject) => {
13453
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
13454
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13455
+ const index = store.index("proctoringId_uploaded");
13456
+ const range = IDBKeyRange.only([proctoringId2, 1]);
13457
+ const request = index.openCursor(range);
13458
+ request.onsuccess = () => {
13459
+ const cursor = request.result;
13460
+ if (cursor) {
13461
+ cursor.delete();
13462
+ cursor.continue();
13463
+ } else {
13464
+ resolve();
13465
+ }
13466
+ };
13467
+ request.onerror = () => {
13468
+ var _a2;
13469
+ return reject(new Error(`Erro ao limpar chunks enviados: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13470
+ };
13471
+ });
13472
+ }
13473
+ /**
13474
+ * Remove TODOS os chunks de um proctoringId (limpeza completa pós-finalização).
13475
+ */
13476
+ async clearAllChunks(proctoringId2) {
13477
+ const db = await this.connect();
13478
+ return new Promise((resolve, reject) => {
13479
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
13480
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13481
+ const index = store.index("proctoringId");
13482
+ const range = IDBKeyRange.only(proctoringId2);
13483
+ const request = index.openCursor(range);
13484
+ request.onsuccess = () => {
13485
+ const cursor = request.result;
13486
+ if (cursor) {
13487
+ cursor.delete();
13488
+ cursor.continue();
13489
+ } else {
13490
+ resolve();
13491
+ }
13492
+ };
13493
+ request.onerror = () => {
13494
+ var _a2;
13495
+ return reject(new Error(`Erro ao limpar todos os chunks: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13496
+ };
13497
+ });
13498
+ }
13499
+ /**
13500
+ * Verifica se existem chunks pendentes para qualquer proctoringId.
13501
+ * Útil na recuperação pós-crash.
13502
+ */
13503
+ async hasAnyPendingChunks() {
13504
+ const db = await this.connect();
13505
+ return new Promise((resolve, reject) => {
13506
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
13507
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13508
+ const index = store.index("uploaded");
13509
+ const range = IDBKeyRange.only(0);
13510
+ const request = index.count(range);
13511
+ request.onsuccess = () => {
13512
+ resolve(request.result > 0);
13513
+ };
13514
+ request.onerror = () => {
13515
+ var _a2;
13516
+ return reject(new Error(`Erro ao verificar chunks pendentes: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13517
+ };
13518
+ });
13519
+ }
13520
+ /**
13521
+ * Retorna todos os proctoringIds que possuem chunks pendentes.
13522
+ * Útil na recuperação pós-crash para saber quais sessões precisam ser finalizadas.
13523
+ */
13524
+ async getPendingProctoringIds() {
13525
+ const db = await this.connect();
13526
+ return new Promise((resolve, reject) => {
13527
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
13528
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13529
+ const index = store.index("uploaded");
13530
+ const range = IDBKeyRange.only(0);
13531
+ const request = index.getAll(range);
13532
+ request.onsuccess = () => {
13533
+ const chunks = request.result;
13534
+ const ids = [...new Set(chunks.map((c3) => c3.proctoringId))];
13535
+ resolve(ids);
13536
+ };
13537
+ request.onerror = () => {
13538
+ var _a2;
13539
+ return reject(new Error(`Erro ao buscar proctoringIds pendentes: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13540
+ };
13541
+ });
13542
+ }
13543
+ /**
13544
+ * Fecha a conexão com o banco.
13545
+ */
13546
+ close() {
13547
+ if (this.db) {
13548
+ this.db.close();
13549
+ this.db = null;
13550
+ }
13551
+ }
13552
+ };
13553
+ _ChunkStorageService.DB_NAME = "EasyProctorChunksDb";
13554
+ /** Incrementado para v2 para recriar índices com tipos numéricos em vez de boolean */
13555
+ _ChunkStorageService.DB_VERSION = 2;
13556
+ _ChunkStorageService.STORE_NAME = "chunks";
13557
+ var ChunkStorageService = _ChunkStorageService;
13558
+
13559
+ // src/new-flow/chunk/BackgroundUploadService.ts
13560
+ var DEFAULT_CONFIG = {
13561
+ pollInterval: 5e3,
13562
+ maxRetries: 5,
13563
+ baseRetryDelay: 2e3,
13564
+ cleanAfterUpload: true
13565
+ };
13566
+ var BackgroundUploadService = class _BackgroundUploadService {
13567
+ constructor(proctoringId2, token, backend, chunkStorage, config) {
13568
+ this.pollTimer = null;
13569
+ this.isProcessing = false;
13570
+ this.isRunning = false;
13571
+ /** Mapa de chunkId -> número de tentativas já feitas */
13572
+ this.retryCount = /* @__PURE__ */ new Map();
13573
+ /** GCS Resumable Upload State */
13574
+ this.sessionUrl = null;
13575
+ this.currentOffset = 0;
13576
+ this.totalBytesPurged = 0;
13577
+ this.STORAGE_KEY_PREFIX = "ep_upload_session_";
13578
+ this.GCS_CHUNK_SIZE = 256 * 1024;
13579
+ this.proctoringId = proctoringId2.trim();
13580
+ this.token = token;
13581
+ this.backend = backend;
13582
+ this.chunkStorage = chunkStorage;
13583
+ this.config = { ...DEFAULT_CONFIG, ...config };
13584
+ this.loadSessionState();
13585
+ }
13586
+ loadSessionState() {
13587
+ try {
13588
+ const stored = localStorage.getItem(`${this.STORAGE_KEY_PREFIX}${this.proctoringId}`);
13589
+ if (stored) {
13590
+ const { sessionUrl, currentOffset, totalBytesPurged } = JSON.parse(stored);
13591
+ this.sessionUrl = sessionUrl;
13592
+ this.currentOffset = currentOffset;
13593
+ this.totalBytesPurged = totalBytesPurged || 0;
13594
+ }
13595
+ } catch (e3) {
13596
+ console.warn("[BackgroundUpload] Erro ao carregar estado da sess\xE3o:", e3);
13597
+ }
13598
+ }
13599
+ saveSessionState() {
13600
+ try {
13601
+ localStorage.setItem(
13602
+ `${this.STORAGE_KEY_PREFIX}${this.proctoringId}`,
13603
+ JSON.stringify({
13604
+ sessionUrl: this.sessionUrl,
13605
+ currentOffset: this.currentOffset,
13606
+ totalBytesPurged: this.totalBytesPurged
13607
+ })
13608
+ );
13609
+ } catch (e3) {
13610
+ console.warn("[BackgroundUpload] Erro ao salvar estado da sess\xE3o:", e3);
13611
+ }
13612
+ }
13613
+ clearSessionState() {
13614
+ try {
13615
+ localStorage.removeItem(`${this.STORAGE_KEY_PREFIX}${this.proctoringId}`);
13616
+ this.sessionUrl = null;
13617
+ this.currentOffset = 0;
13618
+ this.totalBytesPurged = 0;
13619
+ } catch (e3) {
13620
+ console.warn("[BackgroundUpload] Erro ao limpar estado da sess\xE3o:", e3);
13621
+ }
13622
+ }
13623
+ /**
13624
+ * Inicia o serviço de upload em background. Faz polling periódico no IndexedDB
13625
+ * para enviar chunks pendentes.
13626
+ */
13627
+ start() {
13628
+ if (this.isRunning) return;
13629
+ this.isRunning = true;
13630
+ console.log(`[BackgroundUpload] Iniciando servi\xE7o para proctoringId: ${this.proctoringId}`);
13631
+ this.processQueue();
13632
+ this.pollTimer = setInterval(() => {
13633
+ this.processQueue(false);
13634
+ }, this.config.pollInterval);
13635
+ }
13636
+ /**
13637
+ * Para o serviço de upload em background.
13638
+ */
13639
+ stop() {
13640
+ this.isRunning = false;
13641
+ if (this.pollTimer) {
13642
+ clearInterval(this.pollTimer);
13643
+ this.pollTimer = null;
13644
+ }
13645
+ console.log(`[BackgroundUpload] Servi\xE7o parado para proctoringId: ${this.proctoringId}`);
13646
+ }
13647
+ /**
13648
+ * Força o processamento de todos os chunks pendentes e encerra a sessão GCS.
13649
+ * Útil quando a gravação é finalizada.
13650
+ */
13651
+ async flush() {
13652
+ console.log(`[BackgroundUpload] Flush: enviando todos os chunks pendentes e finalizando...`);
13653
+ let waitAttempts = 0;
13654
+ while (this.isProcessing && waitAttempts < 10) {
13655
+ await this.sleep(1e3);
13656
+ waitAttempts++;
13657
+ }
13658
+ let flushRetries = 0;
13659
+ const maxFlushRetries = 3;
13660
+ while (flushRetries < maxFlushRetries) {
13661
+ try {
13662
+ await this.processQueue(true);
13663
+ console.log(`[BackgroundUpload] Flush completado com sucesso.`);
13664
+ return;
13665
+ } catch (error) {
13666
+ flushRetries++;
13667
+ console.error(`[BackgroundUpload] Erro no flush (tentativa ${flushRetries}/${maxFlushRetries}):`, error);
13668
+ if (flushRetries < maxFlushRetries) {
13669
+ await this.sleep(2e3);
13670
+ }
13671
+ }
13672
+ }
13673
+ throw new Error(`[BackgroundUpload] Falha ao finalizar upload ap\xF3s ${maxFlushRetries} tentativas.`);
13674
+ }
13675
+ /**
13676
+ * Sincroniza o offset local com o estado real no Google Cloud Storage.
13677
+ */
13678
+ async syncOffset() {
13679
+ if (!this.sessionUrl) return 0;
13680
+ try {
13681
+ console.log(`[BackgroundUpload] Sincronizando offset com GCS...`);
13682
+ const response = await fetch(this.sessionUrl, {
13683
+ method: "PUT",
13684
+ headers: {
13685
+ "Content-Range": "bytes */*"
13686
+ }
13687
+ });
13688
+ console.log(`[BackgroundUpload] Status da sincroniza\xE7\xE3o (syncOffset): ${response.status}`);
13689
+ if (response.status === 308) {
13690
+ const range = response.headers.get("Range");
13691
+ if (range) {
13692
+ const lastByte = parseInt(range.split("-")[1], 10);
13693
+ this.currentOffset = lastByte + 1;
13694
+ this.saveSessionState();
13695
+ console.log(`[BackgroundUpload] Offset sincronizado: ${this.currentOffset}`);
13696
+ } else {
13697
+ this.currentOffset = 0;
13698
+ }
13699
+ } else if (response.ok || response.status === 201) {
13700
+ console.log("[BackgroundUpload] Sincroniza\xE7\xE3o indicou upload JA FINALIZADO.");
13701
+ this.currentOffset = -1;
13702
+ } else {
13703
+ console.warn(`[BackgroundUpload] Status inesperado na sincroniza\xE7\xE3o: ${response.status}`);
13704
+ }
13705
+ } catch (error) {
13706
+ console.warn("[BackgroundUpload] Erro ao sincronizar offset:", error);
13707
+ }
13708
+ return this.currentOffset;
13709
+ }
13710
+ /**
13711
+ * Verifica e envia chunks pendentes para o backend.
13712
+ * @param isFinal Se true, não alinha a 256KB e fecha a sessão com /TOTAL no header.
13713
+ */
13714
+ async processQueue(isFinal = false) {
13715
+ var _a2, _b;
13716
+ if (this.isProcessing) return;
13717
+ this.isProcessing = true;
13718
+ try {
13719
+ if (this.sessionUrl) {
13720
+ await this.syncOffset();
13721
+ if (this.currentOffset === -1) {
13722
+ console.log("[BackgroundUpload] Sess\xE3o j\xE1 finalizada no servidor.");
13723
+ this.clearSessionState();
13724
+ this.isProcessing = false;
13725
+ return;
13726
+ }
13727
+ }
13728
+ const allChunks = await this.chunkStorage.getAllChunks(this.proctoringId);
13729
+ const pendingChunks = allChunks.filter((c3) => c3.uploaded === 0);
13730
+ if (pendingChunks.length === 0 && !isFinal) {
13731
+ this.isProcessing = false;
13732
+ return;
13733
+ }
13734
+ console.log(`[BackgroundUpload] ${pendingChunks.length} chunks pendentes encontrados. Modo final: ${isFinal}`);
13735
+ let virtualStart = this.totalBytesPurged;
13736
+ const chunksWithMeta = allChunks.map((c3) => {
13737
+ const start = virtualStart;
13738
+ const end = start + c3.blob.size - 1;
13739
+ virtualStart += c3.blob.size;
13740
+ return { chunk: c3, start, end };
13741
+ });
13742
+ let combinedBlobParts = [];
13743
+ let lastProcessedChunkId = null;
13744
+ let finalChunkIndex = 0;
13745
+ let mimeType = pendingChunks[0].mimeType;
13746
+ for (const meta of chunksWithMeta) {
13747
+ if (this.currentOffset > meta.end) continue;
13748
+ const sliceStart = Math.max(0, this.currentOffset - meta.start);
13749
+ const chunkSlice = meta.chunk.blob.slice(sliceStart);
13750
+ combinedBlobParts.push(chunkSlice);
13751
+ lastProcessedChunkId = meta.chunk.id;
13752
+ finalChunkIndex = meta.chunk.chunkIndex;
13753
+ }
13754
+ if (combinedBlobParts.length === 0 && !isFinal) {
13755
+ this.isProcessing = false;
13756
+ return;
13757
+ }
13758
+ let fullBlob = new Blob(combinedBlobParts, { type: mimeType });
13759
+ let sendableSize = fullBlob.size;
13760
+ let totalSizeForHeader = void 0;
13761
+ if (!isFinal) {
13762
+ sendableSize = Math.floor(fullBlob.size / this.GCS_CHUNK_SIZE) * this.GCS_CHUNK_SIZE;
13763
+ if (sendableSize === 0) {
13764
+ console.log("[BackgroundUpload] Dados insuficientes para atingir 256KB. Aguardando novo chunk...");
13765
+ this.isProcessing = false;
13766
+ return;
13767
+ }
13768
+ } else {
13769
+ totalSizeForHeader = virtualStart;
13770
+ }
13771
+ const blobToSend = fullBlob.slice(0, sendableSize);
13772
+ try {
13773
+ await this.uploadData(blobToSend, mimeType, finalChunkIndex, totalSizeForHeader);
13774
+ for (const meta of chunksWithMeta) {
13775
+ if (meta.chunk.uploaded === 0 && meta.end < this.currentOffset) {
13776
+ await this.chunkStorage.markAsUploaded(meta.chunk.id);
13777
+ this.retryCount.delete(meta.chunk.id);
13778
+ (_a2 = this.onChunkUploaded) == null ? void 0 : _a2.call(this, meta.chunk.id, meta.chunk.chunkIndex);
13779
+ console.log(`[BackgroundUpload] Chunk ${meta.chunk.chunkIndex} marcado como enviado.`);
13780
+ }
13781
+ }
13782
+ if (this.config.cleanAfterUpload) {
13783
+ const chunksToClear = chunksWithMeta.filter((meta) => meta.chunk.uploaded === 1 || meta.chunk.uploaded === 0 && meta.end < this.currentOffset);
13784
+ const sizePurged = chunksToClear.reduce((acc, meta) => acc + meta.chunk.blob.size, 0);
13785
+ await this.chunkStorage.clearUploadedChunks(this.proctoringId);
13786
+ if (sizePurged > 0) {
13787
+ this.totalBytesPurged += sizePurged;
13788
+ this.saveSessionState();
13789
+ console.log(`[BackgroundUpload] ${sizePurged} bytes limpos do armazenamento local. Total purgado: ${this.totalBytesPurged}`);
13790
+ }
13791
+ }
13792
+ if (isFinal) {
13793
+ this.clearSessionState();
13794
+ }
13795
+ } catch (error) {
13796
+ console.error("[BackgroundUpload] Falha no upload:", error);
13797
+ (_b = this.onUploadError) == null ? void 0 : _b.call(this, lastProcessedChunkId || 0, error);
13798
+ }
13799
+ } catch (error) {
13800
+ console.error("[BackgroundUpload] Erro ao processar fila:", error);
13801
+ } finally {
13802
+ this.isProcessing = false;
13803
+ }
13804
+ }
13805
+ /**
13806
+ * Faz o upload bruto de dados para a sessão GCS.
13807
+ */
13808
+ async uploadData(blob, mimeType, chunkIndex, totalSize) {
13809
+ const fileName = `EP_${this.proctoringId}_camera_0.webm`;
13810
+ if (!this.sessionUrl) {
13811
+ const initiateUrl = await this.backend.initiateUpload(this.token, `${this.proctoringId}/${fileName}`, mimeType);
13812
+ const startResponse = await fetch(initiateUrl, {
13813
+ method: "POST",
13814
+ headers: { "x-goog-resumable": "start", "Content-Type": mimeType }
13815
+ });
13816
+ if (!startResponse.ok) throw new Error(`Falha ao iniciar: ${startResponse.status}`);
13817
+ this.sessionUrl = startResponse.headers.get("Location");
13818
+ if (!this.sessionUrl) throw new Error("Location header ausente");
13819
+ try {
13820
+ const urlObj = new URL(this.sessionUrl);
13821
+ const pathParts = urlObj.pathname.split("/");
13822
+ let bucket = pathParts[1];
13823
+ let object = decodeURIComponent(pathParts.slice(2).join("/"));
13824
+ if (pathParts.includes("b") && pathParts.includes("o")) {
13825
+ const bIdx = pathParts.indexOf("b") + 1;
13826
+ const oIdx = pathParts.indexOf("o") + 1;
13827
+ bucket = pathParts[bIdx];
13828
+ object = decodeURIComponent(pathParts.slice(oIdx).join("/"));
13829
+ }
13830
+ console.log(`[BackgroundUpload] Sess\xE3o Iniciada -> Bucket: ${bucket}, Objeto: ${object}`);
13831
+ } catch (e3) {
13832
+ console.log(`[BackgroundUpload] Sess\xE3o Iniciada. URL: ${this.sessionUrl}`);
13833
+ }
13834
+ this.currentOffset = 0;
13835
+ this.saveSessionState();
13836
+ } else {
13837
+ console.log(`[BackgroundUpload] Usando sess\xE3o GCS existente: ${this.sessionUrl}`);
13838
+ }
13839
+ const start = this.currentOffset;
13840
+ const end = start + blob.size - 1;
13841
+ const totalHeader = totalSize !== void 0 ? totalSize.toString() : "*";
13842
+ const contentRangeHeader = blob.size === 0 && totalSize !== void 0 ? `bytes */${totalHeader}` : `bytes ${start}-${end}/${totalHeader}`;
13843
+ console.log(`[BackgroundUpload] Enviando ${blob.size > 0 ? "dados" : "finaliza\xE7\xE3o"}: ${contentRangeHeader} (Size: ${blob.size})`);
13844
+ const response = await fetch(this.sessionUrl, {
13845
+ method: "PUT",
13846
+ headers: { "Content-Range": contentRangeHeader },
13847
+ body: blob.size > 0 ? blob : null
13848
+ // Usa null para garantir corpo vazio se necessário
13849
+ });
13850
+ console.log(`[BackgroundUpload] Resposta GCS (uploadData): ${response.status}`);
13851
+ if (response.status !== 200 && response.status !== 201 && response.status !== 308) {
13852
+ const errorText = await response.text();
13853
+ console.error(`[BackgroundUpload] Erro GCS: ${errorText}`);
13854
+ throw new Error(`Status HTTP inesperado: ${response.status}`);
13855
+ }
13856
+ const rangeHeader = response.headers.get("Range");
13857
+ if (rangeHeader) {
13858
+ const lastByte = parseInt(rangeHeader.split("-")[1], 10);
13859
+ this.currentOffset = lastByte + 1;
13860
+ } else {
13861
+ this.currentOffset += blob.size;
13862
+ }
13863
+ this.saveSessionState();
13864
+ trackers.registerUploadFile(
13865
+ this.proctoringId,
13866
+ `GCS Stream Upload
13867
+ Size: ${blob.size}
13868
+ Range: ${start}-${end}
13869
+ Last Index: ${chunkIndex}`,
13870
+ "CameraChunk"
13871
+ );
13872
+ }
13873
+ /**
13874
+ * Método estático para recuperação pós-crash.
13875
+ * Verifica o IndexedDB em busca de chunks pendentes de qualquer sessão
13876
+ * e tenta enviar.
13877
+ */
13878
+ static async recoverPendingUploads(backend, token) {
13879
+ const chunkStorage = new ChunkStorageService();
13880
+ const recoveredIds = [];
13881
+ try {
13882
+ const pendingIds = await chunkStorage.getPendingProctoringIds();
13883
+ if (pendingIds.length === 0) {
13884
+ console.log("[BackgroundUpload] Nenhum chunk pendente encontrado para recupera\xE7\xE3o.");
13885
+ return recoveredIds;
13886
+ }
13887
+ console.log(
13888
+ `[BackgroundUpload] Recupera\xE7\xE3o p\xF3s-crash: ${pendingIds.length} sess\xE3o(\xF5es) com chunks pendentes.`
13889
+ );
13890
+ for (const proctoringId2 of pendingIds) {
13891
+ try {
13892
+ const service = new _BackgroundUploadService(
13893
+ proctoringId2,
13894
+ token,
13895
+ backend,
13896
+ chunkStorage,
13897
+ { cleanAfterUpload: true }
13898
+ );
13899
+ await service.flush();
13900
+ recoveredIds.push(proctoringId2);
13901
+ console.log(
13902
+ `[BackgroundUpload] Chunks da sess\xE3o ${proctoringId2} recuperados com sucesso.`
13903
+ );
13904
+ } catch (error) {
13905
+ console.error(
13906
+ `[BackgroundUpload] Erro ao recuperar chunks da sess\xE3o ${proctoringId2}:`,
13907
+ error
13908
+ );
13909
+ }
13910
+ }
13911
+ } catch (error) {
13912
+ console.error("[BackgroundUpload] Erro geral na recupera\xE7\xE3o:", error);
13913
+ }
13914
+ return recoveredIds;
13915
+ }
13916
+ sleep(ms2) {
13917
+ return new Promise((resolve) => setTimeout(resolve, ms2));
13918
+ }
13919
+ };
13920
+
13248
13921
  // src/new-flow/recorders/CameraRecorder.ts
13249
13922
  var import_jszip_min = __toESM(require_jszip_min());
13250
- var import_fix_webm_duration = __toESM(require_fix_webm_duration());
13251
- var CameraRecorder = class {
13923
+ var pkg = require_fix_webm_duration();
13924
+ var fixWebmDuration = pkg.default || pkg;
13925
+ var _CameraRecorder = class _CameraRecorder {
13252
13926
  constructor(options, videoOptions, paramsConfig, backend, backendToken) {
13253
13927
  this.blobs = [];
13254
13928
  this.paramsConfig = {
@@ -13293,6 +13967,7 @@ var CameraRecorder = class {
13293
13967
  this.blobsRTC = [];
13294
13968
  this.imageCount = 0;
13295
13969
  this.filesToUpload = [];
13970
+ this.pendingPackages = [];
13296
13971
  this.animationFrameId = null;
13297
13972
  this.isCanvasLoopActive = false;
13298
13973
  this.hardwareStream = null;
@@ -13300,8 +13975,16 @@ var CameraRecorder = class {
13300
13975
  this.videoElement = null;
13301
13976
  this.duration = 0;
13302
13977
  this.stopped = false;
13978
+ this.backgroundUpload = null;
13979
+ this.chunkIndex = 0;
13980
+ /** Lista de promises de chunks sendo salvos no IndexedDB para evitar race conditions no stop */
13981
+ this.pendingChunkSaves = [];
13982
+ // Handlers bound para poder remover os listeners depois
13983
+ this.boundVisibilityHandler = null;
13984
+ this.boundPageHideHandler = null;
13303
13985
  this.currentRetries = 0;
13304
13986
  this.packageCount = 0;
13987
+ this.failedUploads = 0;
13305
13988
  this.noiseWait = 20;
13306
13989
  this.options = options;
13307
13990
  this.videoOptions = videoOptions;
@@ -13309,10 +13992,122 @@ var CameraRecorder = class {
13309
13992
  this.backendToken = backendToken;
13310
13993
  paramsConfig && (this.paramsConfig = paramsConfig);
13311
13994
  }
13995
+ /**
13996
+ * Determina se o fluxo de chunks e lifecycle deve estar ativo.
13997
+ * Retorna true se:
13998
+ * 1. O proctoringId já foi definido (ou seja, estamos em uma sessão real, NÃO no checkDevices)
13999
+ * 2. E (`useChunkRecording` foi explicitamente setado como true OU o dispositivo é mobile)
14000
+ */
14001
+ get isChunkEnabled() {
14002
+ return !!this.proctoringId && this.options.proctoringType === "REALTIME" && !isSafeBrowser();
14003
+ }
13312
14004
  setProctoringId(proctoringId2) {
13313
14005
  this.proctoringId = proctoringId2;
13314
14006
  this.proctoringId && this.backend && (this.upload = new UploadService(this.proctoringId, this.backend));
13315
14007
  setRecorderProctoringId(proctoringId2);
14008
+ if (this.isChunkEnabled) {
14009
+ this.chunkStorage = new ChunkStorageService();
14010
+ if (this.backend && this.backendToken) {
14011
+ this.backgroundUpload = new BackgroundUploadService(
14012
+ this.proctoringId,
14013
+ this.backendToken,
14014
+ this.backend,
14015
+ this.chunkStorage,
14016
+ { pollInterval: 5e3, maxRetries: 5, cleanAfterUpload: true }
14017
+ );
14018
+ }
14019
+ this.persistSessionState("IN_PROGRESS");
14020
+ console.log(
14021
+ `[CameraRecorder] Chunk recording ATIVO (type: ${this.options.proctoringType}, mobile: ${isMobileDevice()})`
14022
+ );
14023
+ } else {
14024
+ console.log(
14025
+ `[CameraRecorder] Chunk recording INATIVO (type: ${this.options.proctoringType}) \u2014 modo cl\xE1ssico.`
14026
+ );
14027
+ }
14028
+ }
14029
+ // ========================
14030
+ // Session State Persistence (localStorage)
14031
+ // ========================
14032
+ persistSessionState(status) {
14033
+ try {
14034
+ const data = {
14035
+ proctoringId: this.proctoringId,
14036
+ status,
14037
+ timestamp: Date.now()
14038
+ };
14039
+ localStorage.setItem(_CameraRecorder.LS_SESSION_KEY, JSON.stringify(data));
14040
+ } catch (e3) {
14041
+ console.warn("[CameraRecorder] N\xE3o foi poss\xEDvel salvar estado no localStorage:", e3);
14042
+ }
14043
+ }
14044
+ clearSessionState() {
14045
+ try {
14046
+ localStorage.removeItem(_CameraRecorder.LS_SESSION_KEY);
14047
+ } catch (e3) {
14048
+ console.warn("[CameraRecorder] N\xE3o foi poss\xEDvel limpar estado do localStorage:", e3);
14049
+ }
14050
+ }
14051
+ /**
14052
+ * Verifica se existe uma sessão ativa anterior no localStorage.
14053
+ * Retorna os dados da sessão se ela estiver em IN_PROGRESS, null caso contrário.
14054
+ */
14055
+ static checkForActiveSession() {
14056
+ try {
14057
+ const raw = localStorage.getItem(_CameraRecorder.LS_SESSION_KEY);
14058
+ if (!raw) return null;
14059
+ const data = JSON.parse(raw);
14060
+ if (data.status === "IN_PROGRESS") return data;
14061
+ return null;
14062
+ } catch (e3) {
14063
+ return null;
14064
+ }
14065
+ }
14066
+ // ========================
14067
+ // Page Lifecycle Management
14068
+ // ========================
14069
+ setupLifecycleListeners() {
14070
+ this.boundVisibilityHandler = () => this.handleVisibilityChange();
14071
+ this.boundPageHideHandler = () => this.handlePageHide();
14072
+ document.addEventListener("visibilitychange", this.boundVisibilityHandler);
14073
+ window.addEventListener("pagehide", this.boundPageHideHandler);
14074
+ }
14075
+ removeLifecycleListeners() {
14076
+ if (this.boundVisibilityHandler) {
14077
+ document.removeEventListener("visibilitychange", this.boundVisibilityHandler);
14078
+ this.boundVisibilityHandler = null;
14079
+ }
14080
+ if (this.boundPageHideHandler) {
14081
+ window.removeEventListener("pagehide", this.boundPageHideHandler);
14082
+ this.boundPageHideHandler = null;
14083
+ }
14084
+ }
14085
+ handleVisibilityChange() {
14086
+ var _a2;
14087
+ if (document.visibilityState === "hidden") {
14088
+ console.log("[CameraRecorder] P\xE1gina ficou invis\xEDvel \u2014 sess\xE3o potencialmente interrompida.");
14089
+ this.persistSessionState("INTERRUPTED");
14090
+ this.proctoringId && trackers.registerError(
14091
+ this.proctoringId,
14092
+ "Visibility API: P\xE1gina ficou oculta (hidden). Poss\xEDvel troca de app ou minimiza\xE7\xE3o."
14093
+ );
14094
+ } else if (document.visibilityState === "visible") {
14095
+ console.log("[CameraRecorder] P\xE1gina vis\xEDvel novamente \u2014 verificando estado da grava\xE7\xE3o.");
14096
+ this.persistSessionState("IN_PROGRESS");
14097
+ this.proctoringId && trackers.registerError(
14098
+ this.proctoringId,
14099
+ "Visibility API: P\xE1gina voltou a ficar vis\xEDvel. Usu\xE1rio retornou."
14100
+ );
14101
+ (_a2 = this.onVisibilityRestored) == null ? void 0 : _a2.call(this);
14102
+ }
14103
+ }
14104
+ handlePageHide() {
14105
+ console.log("[CameraRecorder] pagehide detectado \u2014 persistindo estado.");
14106
+ this.persistSessionState("INTERRUPTED");
14107
+ this.proctoringId && trackers.registerError(
14108
+ this.proctoringId,
14109
+ "Page Lifecycle: pagehide event detectado. P\xE1gina est\xE1 sendo descarregada."
14110
+ );
13316
14111
  }
13317
14112
  async initializeDetectors() {
13318
14113
  var _a2, _b, _c2;
@@ -13465,10 +14260,15 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13465
14260
  await new Promise((r3) => setTimeout(r3, 300));
13466
14261
  }
13467
14262
  async startRecording() {
13468
- var _a2, _b, _c2, _d, _e3, _f, _g;
13469
- console.log("CameraRecorder startRecording");
14263
+ var _a2, _b, _c2, _d, _e3, _f, _g, _h;
13470
14264
  await this.startStream();
13471
14265
  await this.attachAndWarmup(this.cameraStream);
14266
+ const recorderOpts = this.isChunkEnabled ? {
14267
+ timeslice: _CameraRecorder.CHUNK_TIMESLICE_MS,
14268
+ onChunkAvailable: (blob, idx) => {
14269
+ this.handleNewChunk(blob, idx);
14270
+ }
14271
+ } : {};
13472
14272
  const {
13473
14273
  startRecording,
13474
14274
  stopRecording,
@@ -13484,7 +14284,8 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13484
14284
  this.blobs,
13485
14285
  this.options.onBufferSizeError,
13486
14286
  (e3) => this.bufferError(e3),
13487
- false
14287
+ false,
14288
+ recorderOpts
13488
14289
  );
13489
14290
  this.recordingStart = startRecording;
13490
14291
  this.recordingStop = stopRecording;
@@ -13494,13 +14295,18 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13494
14295
  this.getBufferSize = getBufferSize;
13495
14296
  this.getStartTime = getStartTime;
13496
14297
  this.getDuration = getDuration;
14298
+ this.chunkIndex = 0;
14299
+ if (this.isChunkEnabled) {
14300
+ (_a2 = this.backgroundUpload) == null ? void 0 : _a2.start();
14301
+ this.setupLifecycleListeners();
14302
+ }
13497
14303
  try {
13498
14304
  await new Promise((r3) => setTimeout(r3, 500));
13499
14305
  await this.recordingStart();
13500
14306
  } catch (error) {
13501
14307
  console.log("Camera Recorder error", error);
13502
14308
  this.stopRecording();
13503
- const maxRetries = ((_a2 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _a2.maxRetries) || 3;
14309
+ const maxRetries = ((_b = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _b.maxRetries) || 3;
13504
14310
  if (this.currentRetries < maxRetries) {
13505
14311
  console.log("Camera Recorder retry", this.currentRetries);
13506
14312
  this.currentRetries++;
@@ -13510,16 +14316,17 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13510
14316
  }
13511
14317
  }
13512
14318
  this.stopped = false;
13513
- 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)) {
14319
+ 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)) {
13514
14320
  await this.initializeDetectors();
13515
14321
  }
13516
- if ((_e3 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _e3.detectFace) {
14322
+ if ((_f = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _f.detectFace) {
13517
14323
  await this.faceDetection.enableCam(this.cameraStream);
13518
14324
  }
13519
- if (((_f = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _f.detectPerson) || ((_g = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _g.detectCellPhone)) {
14325
+ if (((_g = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _g.detectPerson) || ((_h = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _h.detectCellPhone)) {
13520
14326
  await this.objectDetection.enableCam(this.cameraStream);
13521
14327
  }
13522
14328
  this.filesToUpload = [];
14329
+ this.pendingPackages = [];
13523
14330
  if (this.options.proctoringType == "REALTIME") {
13524
14331
  await this.startRealtimeCapture();
13525
14332
  }
@@ -13539,6 +14346,7 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13539
14346
  this.intervalNoiseDetection && clearInterval(this.intervalNoiseDetection);
13540
14347
  this.recordingStop && await this.recordingStop();
13541
14348
  this.duration = this.getDuration ? this.getDuration() : 0;
14349
+ await new Promise((r3) => setTimeout(r3, 200));
13542
14350
  try {
13543
14351
  if (this.animationFrameId) {
13544
14352
  cancelAnimationFrame(this.animationFrameId);
@@ -13569,9 +14377,54 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13569
14377
  console.error("Erro ao parar os streams de m\xEDdia.");
13570
14378
  }
13571
14379
  if (this.options.proctoringType == "REALTIME" && this.upload && this.backendToken) {
13572
- await this.sendPackage(this.filesToUpload);
14380
+ this.pendingPackages.push(this.filesToUpload.slice(0, this.filesToUpload.length));
14381
+ await this.sendPackage();
13573
14382
  await this.filesToUpload.splice(0, this.filesToUpload.length);
13574
14383
  }
14384
+ if (this.isChunkEnabled) {
14385
+ if (this.backgroundUpload) {
14386
+ try {
14387
+ if (this.pendingChunkSaves.length > 0) {
14388
+ console.log(`[CameraRecorder] Aguardando ${this.pendingChunkSaves.length} salvamentos de chunks pendentes...`);
14389
+ await Promise.all(this.pendingChunkSaves);
14390
+ }
14391
+ await this.backgroundUpload.flush();
14392
+ } catch (e3) {
14393
+ console.warn("[CameraRecorder] Erro ao fazer flush dos chunks:", e3);
14394
+ }
14395
+ this.backgroundUpload.stop();
14396
+ }
14397
+ this.removeLifecycleListeners();
14398
+ this.persistSessionState("FINISHED");
14399
+ }
14400
+ }
14401
+ /**
14402
+ * Callback chamado pelo recorder a cada novo chunk de vídeo disponível.
14403
+ * Salva o chunk no IndexedDB para persistência e recuperação.
14404
+ */
14405
+ async handleNewChunk(blob, idx) {
14406
+ if (!this.proctoringId || !this.chunkStorage) return;
14407
+ const savePromise = (async () => {
14408
+ var _a2;
14409
+ try {
14410
+ await this.chunkStorage.saveChunk({
14411
+ proctoringId: this.proctoringId,
14412
+ chunkIndex: this.chunkIndex,
14413
+ blob,
14414
+ timestamp: Date.now(),
14415
+ uploaded: 0,
14416
+ mimeType: ((_a2 = this.recorderOptions) == null ? void 0 : _a2.mimeType) || "video/webm"
14417
+ });
14418
+ this.chunkIndex++;
14419
+ console.log(`[CameraRecorder] Chunk ${this.chunkIndex - 1} salvo no IndexedDB.`);
14420
+ } catch (error) {
14421
+ console.error("[CameraRecorder] Erro ao salvar chunk no IndexedDB:", error);
14422
+ }
14423
+ })();
14424
+ this.pendingChunkSaves.push(savePromise);
14425
+ savePromise.finally(() => {
14426
+ this.pendingChunkSaves = this.pendingChunkSaves.filter((p3) => p3 !== savePromise);
14427
+ });
13575
14428
  }
13576
14429
  async pauseRecording() {
13577
14430
  await this.recordingPause();
@@ -13618,9 +14471,9 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13618
14471
  if (this.proctoringId == void 0) return;
13619
14472
  if (packSize == this.imageCount) {
13620
14473
  this.imageCount = 0;
13621
- const framesToSend = [...this.filesToUpload];
13622
- this.sendPackage(framesToSend);
13623
- await this.filesToUpload.splice(0, this.filesToUpload.length);
14474
+ this.pendingPackages.push(this.filesToUpload.slice(0, packSize));
14475
+ this.sendPackage();
14476
+ await this.filesToUpload.splice(0, packSize);
13624
14477
  }
13625
14478
  let imageName = `${this.proctoringId}_${this.imageCount + 1}.jpg`;
13626
14479
  imageFile = await this.getFile(image_data_url, imageName, "image/jpeg");
@@ -13634,54 +14487,61 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13634
14487
  var _a2;
13635
14488
  this.configImageCapture();
13636
14489
  this.imageCount = 0;
14490
+ this.pendingPackages = [];
13637
14491
  await this.captureFrame();
13638
14492
  this.imageInterval = setInterval(async () => {
13639
14493
  await this.captureFrame();
13640
14494
  }, ((_a2 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _a2.realtimeCaptureInterval) * 1e3);
13641
14495
  }
13642
14496
  // envia pacote de imagens
13643
- async sendPackage(framesToSend) {
14497
+ async sendPackage() {
13644
14498
  var _a2, _b;
13645
- let pending = false;
13646
- let undeliveredPackagesCount = 0;
13647
14499
  const packSize = (_a2 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _a2.realtimePackageSize;
13648
14500
  const packCaptureInterval = (_b = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _b.realtimeCaptureInterval;
13649
- if (this.upload && this.backendToken && !pending && this.filesToUpload.length > 0) {
13650
- undeliveredPackagesCount = 0;
13651
- pending = true;
13652
- const zip = new import_jszip_min.default();
13653
- for (const file of framesToSend) {
13654
- zip.file(file.name, file);
13655
- }
13656
- const blob = await zip.generateAsync({ type: "blob" });
13657
- let packageName = "realtime_package_" + packSize * packCaptureInterval * this.packageCount + ".zip";
13658
- const myPackage = new File(
13659
- [blob],
13660
- packageName,
13661
- { type: "application/zip" }
13662
- );
13663
- const uploadResult = await this.upload.uploadPackage(
13664
- {
13665
- file: myPackage
13666
- },
13667
- this.backendToken
13668
- );
13669
- if (uploadResult.uploaded == true) {
13670
- this.packageCount++;
13671
- } else {
13672
- console.log("erro no upload do pacote");
14501
+ if (this.upload && this.backendToken && this.pendingPackages.length > 0) {
14502
+ let packagesToDelete = [];
14503
+ let packageIndex = 0;
14504
+ for (const packageToSend of this.pendingPackages) {
14505
+ const zip = new import_jszip_min.default();
14506
+ for (const file of packageToSend) {
14507
+ zip.file(file.name, file);
14508
+ }
14509
+ const blob = await zip.generateAsync({ type: "blob" });
14510
+ let packageName = "realtime_package_" + packSize * packCaptureInterval * this.packageCount + ".zip";
14511
+ const myPackage = new File(
14512
+ [blob],
14513
+ packageName,
14514
+ { type: "application/zip" }
14515
+ );
14516
+ try {
14517
+ const uploadResult = await this.upload.uploadPackage(
14518
+ {
14519
+ file: myPackage
14520
+ },
14521
+ this.backendToken
14522
+ );
14523
+ if (uploadResult == true) {
14524
+ this.packageCount++;
14525
+ this.failedUploads = 0;
14526
+ packagesToDelete.push(packageIndex++);
14527
+ }
14528
+ } catch (error) {
14529
+ this.failedUploads++;
14530
+ if (this.failedUploads >= 2) {
14531
+ this.options.onRealtimeAlertsCallback({
14532
+ status: "ALERT",
14533
+ description: "Realtime n\xE3o est\xE1 enviando pacotes",
14534
+ type: "error_upload_package",
14535
+ category: "error_upload_package",
14536
+ begin: 0,
14537
+ end: 0
14538
+ });
14539
+ }
14540
+ break;
14541
+ }
13673
14542
  }
13674
- pending = false;
13675
- } else if (pending) {
13676
- undeliveredPackagesCount++;
13677
- if (undeliveredPackagesCount == 3) {
13678
- undeliveredPackagesCount = 0;
13679
- let newCanvasWidth = this.videoOptions.width / 2;
13680
- let newCanvasHeight = this.videoOptions.height / 2;
13681
- if (newCanvasWidth < 320) newCanvasWidth = 320;
13682
- if (newCanvasHeight < 180) newCanvasHeight = 180;
13683
- this.canvas.width = newCanvasWidth;
13684
- this.canvas.height = newCanvasHeight;
14543
+ for (const packageToDelete of packagesToDelete) {
14544
+ await this.pendingPackages.splice(packageToDelete, 1);
13685
14545
  }
13686
14546
  }
13687
14547
  }
@@ -13700,16 +14560,32 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13700
14560
  if (this.blobs != null)
13701
14561
  trackers.registerSaveOnSession(
13702
14562
  this.proctoringId,
13703
- `Blobs Length: ${this.blobs.length} Buffer Size: ${this.getBufferSize()} `
14563
+ `Blobs Length: ${this.blobs.length} Buffer Size: ${this.getBufferSize()} ChunkEnabled: ${this.isChunkEnabled}`
13704
14564
  );
13705
14565
  const settings = this.cameraStream.getVideoTracks()[0].getSettings();
13706
14566
  const settingsAudio = this.cameraStream.getAudioTracks()[0].getSettings();
13707
14567
  if (this.options.proctoringType == "VIDEO" || this.options.proctoringType == "REALTIME" || this.options.proctoringType == "IMAGE" && ((_a2 = this.paramsConfig.imageBehaviourParameters) == null ? void 0 : _a2.saveVideo)) {
14568
+ let videoFile;
14569
+ if (this.isChunkEnabled) {
14570
+ const isStable = await this.checkInternetStability();
14571
+ if (isStable) {
14572
+ } else {
14573
+ if (this.backend && this.backendToken && this.proctoringId) {
14574
+ const fileName = `EP_${this.proctoringId}_camera_0.webm`;
14575
+ const objectName = `${this.proctoringId}/${fileName}`;
14576
+ const isUploaded = await this.backend.checkUpload(this.backendToken, objectName, "video/webm");
14577
+ if (isUploaded) {
14578
+ this.chunkStorage && await this.chunkStorage.clearAllChunks(session.id);
14579
+ return;
14580
+ }
14581
+ }
14582
+ }
14583
+ }
13708
14584
  const rawBlob = new Blob(this.blobs, {
13709
14585
  type: ((_b = this.recorderOptions) == null ? void 0 : _b.mimeType) || "video/webm"
13710
14586
  });
13711
- const fixedBlob = await (0, import_fix_webm_duration.default)(rawBlob, this.duration);
13712
- const fileWithDuration = new File(
14587
+ const fixedBlob = await fixWebmDuration(rawBlob, this.duration);
14588
+ videoFile = new File(
13713
14589
  [fixedBlob],
13714
14590
  `EP_${session.id}_camera_0.webm`,
13715
14591
  { type: rawBlob.type }
@@ -13721,7 +14597,7 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13721
14597
 
13722
14598
  Video:
13723
14599
  ${JSON.stringify(this.recorderOptions)}`,
13724
- file: fileWithDuration,
14600
+ file: videoFile,
13725
14601
  origin: "Camera" /* Camera */
13726
14602
  });
13727
14603
  }
@@ -13736,6 +14612,28 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13736
14612
  });
13737
14613
  });
13738
14614
  }
14615
+ /**
14616
+ * Verifica se a internet está estável para realizar o upload do vídeo na íntegra.
14617
+ */
14618
+ async checkInternetStability() {
14619
+ var _a2;
14620
+ if (!navigator.onLine) return false;
14621
+ try {
14622
+ const controller = new AbortController();
14623
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
14624
+ const baseUrl = (_a2 = this.backend) == null ? void 0 : _a2.getBaseUrl();
14625
+ if (!baseUrl) return true;
14626
+ const response = await fetch(`${baseUrl}/Client/health`, {
14627
+ method: "GET",
14628
+ signal: controller.signal
14629
+ });
14630
+ clearTimeout(timeoutId);
14631
+ return response.status < 500;
14632
+ } catch (e3) {
14633
+ console.warn("[CameraRecorder] Internet inst\xE1vel ou lenta detectada para upload integral.");
14634
+ return false;
14635
+ }
14636
+ }
13739
14637
  onNoiseDetected() {
13740
14638
  var _a2, _b, _c2;
13741
14639
  if (this.options.proctoringType === "REALTIME") return;
@@ -13748,7 +14646,6 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13748
14646
  }
13749
14647
  const volume = (_b = (_a2 = this.volumeMeter) == null ? void 0 : _a2.getVolume()) != null ? _b : 0;
13750
14648
  if (volume >= (((_c2 = this.paramsConfig.audioBehaviourParameters) == null ? void 0 : _c2.noiseLimit) || 40)) {
13751
- console.log("entrou" + this.noiseWait);
13752
14649
  if (this.noiseWait >= 20) {
13753
14650
  this.options.onRealtimeAlertsCallback({
13754
14651
  status: "ALERT",
@@ -13761,6 +14658,14 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13761
14658
  this.noiseWait++;
13762
14659
  }
13763
14660
  };
14661
+ // ========================
14662
+ // Chunk & Lifecycle
14663
+ // ========================
14664
+ /** Intervalo de cada chunk em ms (padrão: 60 segundos) */
14665
+ _CameraRecorder.CHUNK_TIMESLICE_MS = 6e4;
14666
+ /** Chave do localStorage para persistir estado da sessão */
14667
+ _CameraRecorder.LS_SESSION_KEY = "ep_proctoring_session";
14668
+ var CameraRecorder = _CameraRecorder;
13764
14669
 
13765
14670
  // src/new-flow/checkers/DeviceCheckerUI.ts
13766
14671
  var DeviceCheckerUI = class {
@@ -14833,7 +15738,8 @@ var _DeviceCheckerService = class _DeviceCheckerService {
14833
15738
  this.context = context;
14834
15739
  this.backend = new BackendService({
14835
15740
  type: (context == null ? void 0 : context.type) || "prod",
14836
- token: context.token
15741
+ token: context.token,
15742
+ isRealtime: false
14837
15743
  });
14838
15744
  }
14839
15745
  getDeviceCheckResult() {
@@ -14994,7 +15900,7 @@ var _DeviceCheckerService = class _DeviceCheckerService {
14994
15900
  videoDeviceInterface(stream) {
14995
15901
  this.DeviceCheckerUI && this.DeviceCheckerUI.videoDeviceInterfaceUI(stream);
14996
15902
  this.isUnderResolution();
14997
- this.faceDetection.enableCam(stream);
15903
+ this.faceDetection.enableCam(stream, 1e3);
14998
15904
  }
14999
15905
  audioDeviceInterface(stream) {
15000
15906
  this.volumeMeter = new VolumeMeter(this.cameraRecorder.cameraStream);
@@ -18260,6 +19166,7 @@ var NoiseRecorder = class {
18260
19166
  this.MAX_PRE_ROLL_CHUNKS = 4;
18261
19167
  this.lastNoiseTime = 0;
18262
19168
  this.SILENCE_THRESHOLD = 3e3;
19169
+ this.filesToUpload = [];
18263
19170
  this.optionsProctoring = optionsProctoring;
18264
19171
  this.proctoringSession = proctoringSession;
18265
19172
  this.paramsConfig = paramsConfig;
@@ -18351,7 +19258,7 @@ var NoiseRecorder = class {
18351
19258
  }
18352
19259
  }
18353
19260
  async stopSoundRecord() {
18354
- var _a2;
19261
+ var _a2, _b;
18355
19262
  if (!this.recordingInProgress && this.recordingChunks.length === 0) return;
18356
19263
  this.recordingEndTime = Date.now() - (((_a2 = this.cameraRecorder.getStartTime()) == null ? void 0 : _a2.getTime()) || 0);
18357
19264
  if (this.optionsProctoring.proctoringType !== "REALTIME") return;
@@ -18365,7 +19272,33 @@ var NoiseRecorder = class {
18365
19272
  type: "audio/wav"
18366
19273
  }
18367
19274
  );
18368
- this.uploadRecord(file);
19275
+ let filesToSend = [...this.filesToUpload];
19276
+ for (const myFile of filesToSend) {
19277
+ try {
19278
+ await ((_b = this.upload) == null ? void 0 : _b.upload(
19279
+ {
19280
+ file: myFile
19281
+ },
19282
+ this.backendToken
19283
+ ));
19284
+ this.filesToUpload.splice(this.filesToUpload.indexOf(myFile), 1);
19285
+ } catch (error) {
19286
+ break;
19287
+ }
19288
+ }
19289
+ try {
19290
+ if (file && this.upload && this.backendToken) {
19291
+ this.upload.upload(
19292
+ {
19293
+ file
19294
+ },
19295
+ this.backendToken
19296
+ );
19297
+ }
19298
+ } catch (error) {
19299
+ console.log("error Noise recorder adicionando na fila", error);
19300
+ this.filesToUpload.push(file);
19301
+ }
18369
19302
  this.recordingChunks = [];
18370
19303
  this.recordingInProgress = false;
18371
19304
  }
@@ -18379,16 +19312,6 @@ var NoiseRecorder = class {
18379
19312
  a3.click();
18380
19313
  window.URL.revokeObjectURL(url);
18381
19314
  }
18382
- uploadRecord(file) {
18383
- if (file && this.upload && this.backendToken) {
18384
- this.upload.upload(
18385
- {
18386
- file
18387
- },
18388
- this.backendToken
18389
- );
18390
- }
18391
- }
18392
19315
  // CLASSIFIER -< Media Pipe
18393
19316
  // Verify if has speech in the classifier array
18394
19317
  hasDesiredResult(array) {
@@ -18495,7 +19418,8 @@ registerProcessor("audio-processor", AudioProcessor);
18495
19418
  `;
18496
19419
 
18497
19420
  // src/new-flow/recorders/ScreenRecorder.ts
18498
- var import_fix_webm_duration2 = __toESM(require_fix_webm_duration());
19421
+ var pkg2 = require_fix_webm_duration();
19422
+ var fixWebmDuration2 = pkg2.default || pkg2;
18499
19423
  var ScreenRecorder = class {
18500
19424
  constructor(options) {
18501
19425
  this.blobs = [];
@@ -18591,7 +19515,7 @@ var ScreenRecorder = class {
18591
19515
  const rawBlob = new Blob(this.blobs, {
18592
19516
  type: "video/webm"
18593
19517
  });
18594
- const fixedBlob = await (0, import_fix_webm_duration2.default)(rawBlob, this.duration);
19518
+ const fixedBlob = await fixWebmDuration2(rawBlob, this.duration);
18595
19519
  const file = new File(
18596
19520
  [fixedBlob],
18597
19521
  `EP_${session.id}_screen_0.webm`,
@@ -18696,14 +19620,6 @@ function getGeolocation() {
18696
19620
  });
18697
19621
  }
18698
19622
 
18699
- // src/utils/verifyVersion.ts
18700
- function versionVerify() {
18701
- const agentStr = window.navigator.userAgent.split("SEB/");
18702
- if (agentStr.length > 1)
18703
- return agentStr[1];
18704
- else return "1.0.0.0";
18705
- }
18706
-
18707
19623
  // src/proctoring/Auth.ts
18708
19624
  var Auth = class {
18709
19625
  constructor(cpf, backend) {
@@ -21697,7 +22613,8 @@ var _ExternalCameraChecker = class _ExternalCameraChecker {
21697
22613
  this.onRealtimeAlertsCallback = onRealtimeAlertsCallback;
21698
22614
  this.backend = new BackendService({
21699
22615
  type: (context == null ? void 0 : context.type) || "prod",
21700
- token: context.token
22616
+ token: context.token,
22617
+ isRealtime: false
21701
22618
  });
21702
22619
  this.currentStep = -1 /* WAITING */;
21703
22620
  }
@@ -22430,6 +23347,12 @@ var Proctoring = class {
22430
23347
  this.serviceType = "Upload" /* Upload */;
22431
23348
  this.onStopSharingScreenCallback = () => {
22432
23349
  };
23350
+ /** Callback notificando que o usuário saiu do browser (minimizou/trocou de aba) */
23351
+ this.onVisibilityLostCallback = () => {
23352
+ };
23353
+ /** Callback notificando que o usuário retornou ao browser */
23354
+ this.onVisibilityRestoredCallback = () => {
23355
+ };
22433
23356
  this.onLostFocusCallback = () => {
22434
23357
  };
22435
23358
  this.onLostFocusAlertRecorderCallback = (response) => {
@@ -22445,13 +23368,16 @@ var Proctoring = class {
22445
23368
  await this.internalOnRealtimeAlerts(response);
22446
23369
  return;
22447
23370
  };
23371
+ this.realtimeAlertsToSend = [];
22448
23372
  this.onBufferSizeErrorCallback = (cameraStream) => {
22449
23373
  return;
22450
23374
  };
22451
23375
  var _a2;
22452
23376
  this.backend = new BackendService({
22453
23377
  type: context.type || "prod",
22454
- token: context.token
23378
+ token: context.token,
23379
+ isRealtime: false
23380
+ // Default false, atualizado no start() via backend.setRealtime()
22455
23381
  });
22456
23382
  this.repository = new IndexDbSessionRepository("EasyProctorDb", "exams2");
22457
23383
  this.repositoryDevices = new IndexDbSessionRepository(
@@ -22461,6 +23387,12 @@ var Proctoring = class {
22461
23387
  ((_a2 = this.context.credentials) == null ? void 0 : _a2.cpf) && (this.auth = new Auth(this.context.credentials.cpf, this.backend));
22462
23388
  this.appChecker = new ExternalCameraChecker(this.context, (response) => this.onRealtimeAlertsCallback(response));
22463
23389
  }
23390
+ setOnVisibilityLostCallback(cb) {
23391
+ this.onVisibilityLostCallback = cb;
23392
+ }
23393
+ setOnVisibilityRestoredCallback(cb) {
23394
+ this.onVisibilityRestoredCallback = cb;
23395
+ }
22464
23396
  setOnStopSharingScreenCallback(cb) {
22465
23397
  this.onStopSharingScreenCallback = async () => {
22466
23398
  var _a2, _b, _c2, _d;
@@ -22540,6 +23472,8 @@ var Proctoring = class {
22540
23472
  return 25 /* FocusOff */;
22541
23473
  case "focus":
22542
23474
  return 25 /* FocusOff */;
23475
+ case "error_upload_package":
23476
+ return 44 /* RealtimeOffline */;
22543
23477
  default:
22544
23478
  return null;
22545
23479
  }
@@ -22580,19 +23514,53 @@ var Proctoring = class {
22580
23514
  };
22581
23515
  await verifyFace(1);
22582
23516
  }
23517
+ async sendPendingRealtimeAlerts() {
23518
+ let alertsToSend = [...this.realtimeAlertsToSend];
23519
+ for (const alert of alertsToSend) {
23520
+ try {
23521
+ if (alert.status === "ALERT") {
23522
+ await this.backend.startRealtimeAlert({
23523
+ proctoringId: this.proctoringId,
23524
+ begin: alert.begin,
23525
+ end: alert.end,
23526
+ alert: this.convertRealtimeCategoryToAlertCategory(alert.category)
23527
+ });
23528
+ } else if (alert.status === "OK") {
23529
+ await this.stopRealtimeAlert(alert);
23530
+ }
23531
+ this.realtimeAlertsToSend.splice(this.realtimeAlertsToSend.indexOf(alert), 1);
23532
+ } catch (error) {
23533
+ console.log("error sendPendingRealtimeAlerts", error);
23534
+ this.realtimeAlertsToSend.push(alert);
23535
+ break;
23536
+ }
23537
+ }
23538
+ }
22583
23539
  async internalOnRealtimeAlerts(response) {
22584
- if (this.sessionOptions.proctoringType === "REALTIME" && (response.type === "face_detection_on_stream" || response.type === "person_detection_on_stream" || response.type === "lost_focus" || response.type === "focus")) {
23540
+ 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")) {
23541
+ await this.sendPendingRealtimeAlerts();
22585
23542
  if (response.status === "ALERT") {
22586
23543
  if (this.allRecorders.cameraRecorder.stopped) return;
22587
- await this.backend.startRealtimeAlert({
22588
- proctoringId: this.proctoringId,
22589
- begin: response.begin,
22590
- end: response.end,
22591
- alert: this.convertRealtimeCategoryToAlertCategory(response.category)
22592
- });
23544
+ try {
23545
+ await this.backend.startRealtimeAlert({
23546
+ proctoringId: this.proctoringId,
23547
+ begin: response.begin,
23548
+ end: response.end,
23549
+ alert: this.convertRealtimeCategoryToAlertCategory(response.category)
23550
+ });
23551
+ } catch (error) {
23552
+ console.log("error startRealtimeAlert " + response.category);
23553
+ console.log("error startRealtimeAlert adicionando na fila", error);
23554
+ this.realtimeAlertsToSend.push(response);
23555
+ }
22593
23556
  } else if (response.status === "OK") {
22594
23557
  if (this.allRecorders.cameraRecorder.stopped && response.description !== "face_stop") return;
22595
- await this.stopRealtimeAlert(response);
23558
+ try {
23559
+ await this.stopRealtimeAlert(response);
23560
+ } catch (error) {
23561
+ console.log("error stopRealtimeAlert adicionando na fila", error);
23562
+ this.realtimeAlertsToSend.push(response);
23563
+ }
22596
23564
  }
22597
23565
  }
22598
23566
  }
@@ -22600,8 +23568,8 @@ var Proctoring = class {
22600
23568
  this.setOnLostFocusAlertRecorderCallback();
22601
23569
  this.setOnFocusAlertRecorderCallback();
22602
23570
  this.onRealtimeAlertsCallback = async (response) => {
22603
- await this.internalOnRealtimeAlerts(response);
22604
23571
  options.data && options.data(response);
23572
+ await this.internalOnRealtimeAlerts(response);
22605
23573
  };
22606
23574
  }
22607
23575
  setOnBufferSizeErrorCallback(cb) {
@@ -22697,8 +23665,10 @@ var Proctoring = class {
22697
23665
  await this.repositoryDevices.save({ ...devices, id: "devices" });
22698
23666
  this.sessionOptions = { ...getDefaultProctoringOptions, ...options };
22699
23667
  this.videoOptions = validatePartialVideoOptions(_videoOptions);
23668
+ if (this.sessionOptions.proctoringType === "REALTIME") {
23669
+ this.backend.setRealtime(true);
23670
+ }
22700
23671
  await this.initConfig(options.useGeolocation);
22701
- await this.verifyBrowser();
22702
23672
  this.sessionOptions.captureScreen && await this.verifyMultipleMonitors(this.sessionOptions);
22703
23673
  try {
22704
23674
  if (options == null ? void 0 : options.useSpyScan) {
@@ -22756,6 +23726,20 @@ var Proctoring = class {
22756
23726
  } catch (error) {
22757
23727
  throw SAFE_BROWSER_API_NOT_FOUND;
22758
23728
  }
23729
+ this.allRecorders.cameraRecorder.onVisibilityRestored = () => {
23730
+ console.log("[Proctoring] Usu\xE1rio retornou ao browser.");
23731
+ this.onVisibilityRestoredCallback();
23732
+ };
23733
+ if (this.sessionOptions.proctoringType === "REALTIME" && !isSafeBrowser()) {
23734
+ try {
23735
+ await BackgroundUploadService.recoverPendingUploads(
23736
+ this.backend,
23737
+ this.context.token
23738
+ );
23739
+ } catch (e3) {
23740
+ console.warn("[Proctoring] Erro ao recuperar chunks de sess\xE3o anterior:", e3);
23741
+ }
23742
+ }
22759
23743
  try {
22760
23744
  console.log("Starting recorders");
22761
23745
  await this.recorder.startAll();
@@ -22835,6 +23819,7 @@ Error: ${error}`
22835
23819
  this.spyCam && this.spyCam.stopCheckSpyCam();
22836
23820
  this.appChecker && await this.appChecker.disconnectWebSocket();
22837
23821
  await this.recorder.saveAllOnSession();
23822
+ await this.sendPendingRealtimeAlerts();
22838
23823
  await this.repository.save(this.proctoringSession);
22839
23824
  let uploader;
22840
23825
  let uploaderServices;
@@ -23119,7 +24104,8 @@ var _SignTerm = class _SignTerm {
23119
24104
  constructor(context) {
23120
24105
  this.backend = new BackendService({
23121
24106
  type: (context == null ? void 0 : context.type) || "prod",
23122
- token: context.token
24107
+ token: context.token,
24108
+ isRealtime: false
23123
24109
  });
23124
24110
  }
23125
24111
  async signInTerms() {
@@ -23349,6 +24335,8 @@ function useProctoring(proctoringOptions, enviromentConfig = "prod") {
23349
24335
  const onBufferSizeError = proctoring.setOnBufferSizeErrorCallback.bind(proctoring);
23350
24336
  const onStopSharingScreen = proctoring.setOnStopSharingScreenCallback.bind(proctoring);
23351
24337
  const onRealtimeAlerts = proctoring.onRealtimeAlerts.bind(proctoring);
24338
+ const onVisibilityLost = proctoring.setOnVisibilityLostCallback.bind(proctoring);
24339
+ const onVisibilityRestored = proctoring.setOnVisibilityRestoredCallback.bind(proctoring);
23352
24340
  const signInTerms = signTerm.signInTerms.bind(signTerm);
23353
24341
  const checkDevices = checker.checkDevices.bind(checker);
23354
24342
  const checkExternalCamera = proctoring.appChecker.checkExternalCamera.bind(proctoring.appChecker);
@@ -23386,7 +24374,9 @@ function useProctoring(proctoringOptions, enviromentConfig = "prod") {
23386
24374
  startExternalCameraSession,
23387
24375
  takeExternalCameraPicture,
23388
24376
  goToExternalCameraPositionStep,
23389
- startExternalCameraTransmission
24377
+ startExternalCameraTransmission,
24378
+ onVisibilityLost,
24379
+ onVisibilityRestored
23390
24380
  };
23391
24381
  }
23392
24382