easyproctor-hml 2.7.13 → 2.8.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
@@ -9340,6 +9340,9 @@ var FaceDetection = class extends BaseDetection {
9340
9340
  if (this.emmitedFaceAlert) {
9341
9341
  this.handleOk("face_stop", "face_detection_on_stream");
9342
9342
  }
9343
+ this.numFacesSent = -1;
9344
+ this.emmitedPositionAlert = false;
9345
+ this.emmitedFaceAlert = false;
9343
9346
  }
9344
9347
  // displayVideoDetections(result: { detections: any; }) {
9345
9348
  // // console.log(result);
@@ -12699,7 +12702,8 @@ var getDefaultProctoringOptions = {
12699
12702
  useSpyScan: false,
12700
12703
  useExternalCamera: false,
12701
12704
  useChallenge: false,
12702
- screenRecorderOptions: { width: 1280, height: 720 }
12705
+ screenRecorderOptions: { width: 1280, height: 720 },
12706
+ auto: false
12703
12707
  };
12704
12708
 
12705
12709
  // src/proctoring/options/ProctoringVideoOptions.ts
@@ -12758,6 +12762,9 @@ function isMobileDevice() {
12758
12762
  }
12759
12763
  return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
12760
12764
  }
12765
+ function isSafeBrowser() {
12766
+ return versionVerify() !== "1.0.0.0";
12767
+ }
12761
12768
 
12762
12769
  // src/plugins/recorder.ts
12763
12770
  var proctoringId;
@@ -13251,61 +13258,623 @@ var ObjectDetection = class extends BaseDetection {
13251
13258
  }
13252
13259
  };
13253
13260
 
13254
- // src/new-flow/recorders/VolumeMeter.ts
13255
- var VolumeMeter = class {
13256
- constructor(stream) {
13257
- this.volume = null;
13258
- this.animationFrameId = null;
13259
- this.stream = stream;
13261
+ // src/new-flow/recorders/CameraRecorder.ts
13262
+ var import_jszip_min = __toESM(require_jszip_min());
13263
+
13264
+ // src/new-flow/chunk/ChunkStorageService.ts
13265
+ var _ChunkStorageService = class _ChunkStorageService {
13266
+ constructor() {
13267
+ this.db = null;
13260
13268
  }
13261
- async start(options = {}) {
13269
+ /**
13270
+ * Abre a conexão com o IndexedDB, criando o banco e o object store se necessário.
13271
+ */
13272
+ async connect() {
13273
+ if (this.db) return this.db;
13262
13274
  return new Promise((resolve, reject) => {
13263
- try {
13264
- this.audioContext = new AudioContext();
13265
- this.analyser = this.audioContext.createAnalyser();
13266
- this.microphone = this.audioContext.createMediaStreamSource(this.stream);
13267
- this.analyser.smoothingTimeConstant = 0.8;
13268
- this.analyser.fftSize = 1024;
13269
- this.microphone.connect(this.analyser);
13270
- const processAudio = () => {
13271
- const array = new Uint8Array(this.analyser.frequencyBinCount);
13272
- this.analyser.getByteFrequencyData(array);
13273
- const arraySum = array.reduce((a3, value) => a3 + value, 0);
13274
- const average = arraySum / array.length;
13275
- this.setVolume(average);
13276
- options.setVolume && options.setVolume(average);
13277
- this.animationFrameId = requestAnimationFrame(processAudio);
13275
+ const request = window.indexedDB.open(
13276
+ _ChunkStorageService.DB_NAME,
13277
+ _ChunkStorageService.DB_VERSION
13278
+ );
13279
+ request.onerror = () => {
13280
+ reject(new Error("N\xE3o foi poss\xEDvel conectar ao IndexedDB para chunks."));
13281
+ };
13282
+ request.onupgradeneeded = () => {
13283
+ const db = request.result;
13284
+ if (db.objectStoreNames.contains(_ChunkStorageService.STORE_NAME)) {
13285
+ db.deleteObjectStore(_ChunkStorageService.STORE_NAME);
13286
+ }
13287
+ const store = db.createObjectStore(_ChunkStorageService.STORE_NAME, {
13288
+ keyPath: "id",
13289
+ autoIncrement: true
13290
+ });
13291
+ store.createIndex("proctoringId", "proctoringId", { unique: false });
13292
+ store.createIndex("uploaded", "uploaded", { unique: false });
13293
+ store.createIndex("proctoringId_uploaded", ["proctoringId", "uploaded"], {
13294
+ unique: false
13295
+ });
13296
+ };
13297
+ request.onsuccess = () => {
13298
+ this.db = request.result;
13299
+ resolve(this.db);
13300
+ };
13301
+ });
13302
+ }
13303
+ /**
13304
+ * Salva um chunk de vídeo no IndexedDB.
13305
+ */
13306
+ async saveChunk(chunk) {
13307
+ const db = await this.connect();
13308
+ return new Promise((resolve, reject) => {
13309
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
13310
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13311
+ const request = store.add(chunk);
13312
+ request.onsuccess = () => {
13313
+ resolve(request.result);
13314
+ };
13315
+ request.onerror = () => {
13316
+ var _a2;
13317
+ reject(new Error(`Erro ao salvar chunk no IndexedDB: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13318
+ };
13319
+ });
13320
+ }
13321
+ /**
13322
+ * Retorna todos os chunks pendentes (não enviados) de um proctoringId específico.
13323
+ */
13324
+ async getPendingChunks(proctoringId2) {
13325
+ const db = await this.connect();
13326
+ return new Promise((resolve, reject) => {
13327
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
13328
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13329
+ const index = store.index("proctoringId_uploaded");
13330
+ const range = IDBKeyRange.only([proctoringId2, 0]);
13331
+ const request = index.getAll(range);
13332
+ request.onsuccess = () => {
13333
+ const chunks = request.result.sort(
13334
+ (a3, b3) => a3.chunkIndex - b3.chunkIndex
13335
+ );
13336
+ resolve(chunks);
13337
+ };
13338
+ request.onerror = () => {
13339
+ var _a2;
13340
+ reject(new Error(`Erro ao buscar chunks pendentes: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13341
+ };
13342
+ });
13343
+ }
13344
+ /**
13345
+ * Retorna todos os chunks (enviados ou não) de um proctoringId específico.
13346
+ */
13347
+ async getAllChunks(proctoringId2) {
13348
+ const db = await this.connect();
13349
+ return new Promise((resolve, reject) => {
13350
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
13351
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13352
+ const index = store.index("proctoringId");
13353
+ const range = IDBKeyRange.only(proctoringId2);
13354
+ const request = index.getAll(range);
13355
+ request.onsuccess = () => {
13356
+ const chunks = request.result.sort(
13357
+ (a3, b3) => a3.chunkIndex - b3.chunkIndex
13358
+ );
13359
+ resolve(chunks);
13360
+ };
13361
+ request.onerror = () => {
13362
+ var _a2;
13363
+ reject(new Error(`Erro ao buscar todos os chunks: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13364
+ };
13365
+ });
13366
+ }
13367
+ /**
13368
+ * Marca um chunk como enviado (uploaded = 1).
13369
+ */
13370
+ async markAsUploaded(chunkId) {
13371
+ const db = await this.connect();
13372
+ return new Promise((resolve, reject) => {
13373
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
13374
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13375
+ const getRequest = store.get(chunkId);
13376
+ getRequest.onsuccess = () => {
13377
+ const chunk = getRequest.result;
13378
+ if (!chunk) {
13379
+ resolve();
13380
+ return;
13381
+ }
13382
+ chunk.uploaded = 1;
13383
+ const putRequest = store.put(chunk);
13384
+ putRequest.onsuccess = () => resolve();
13385
+ putRequest.onerror = () => {
13386
+ var _a2;
13387
+ return reject(new Error(`Erro ao marcar chunk como enviado: ${(_a2 = putRequest.error) == null ? void 0 : _a2.message}`));
13278
13388
  };
13279
- this.animationFrameId = requestAnimationFrame(processAudio);
13280
- resolve(true);
13281
- } catch (error) {
13282
- this.stop();
13283
- reject(`Error: ${error}`);
13284
- }
13389
+ };
13390
+ getRequest.onerror = () => {
13391
+ var _a2;
13392
+ return reject(new Error(`Erro ao buscar chunk para marcar: ${(_a2 = getRequest.error) == null ? void 0 : _a2.message}`));
13393
+ };
13285
13394
  });
13286
13395
  }
13396
+ /**
13397
+ * Remove todos os chunks já enviados de um proctoringId para liberar espaço.
13398
+ */
13399
+ async clearUploadedChunks(proctoringId2) {
13400
+ const db = await this.connect();
13401
+ return new Promise((resolve, reject) => {
13402
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
13403
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13404
+ const index = store.index("proctoringId_uploaded");
13405
+ const range = IDBKeyRange.only([proctoringId2, 1]);
13406
+ const request = index.openCursor(range);
13407
+ request.onsuccess = () => {
13408
+ const cursor = request.result;
13409
+ if (cursor) {
13410
+ cursor.delete();
13411
+ cursor.continue();
13412
+ } else {
13413
+ resolve();
13414
+ }
13415
+ };
13416
+ request.onerror = () => {
13417
+ var _a2;
13418
+ return reject(new Error(`Erro ao limpar chunks enviados: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13419
+ };
13420
+ });
13421
+ }
13422
+ /**
13423
+ * Remove TODOS os chunks de um proctoringId (limpeza completa pós-finalização).
13424
+ */
13425
+ async clearAllChunks(proctoringId2) {
13426
+ const db = await this.connect();
13427
+ return new Promise((resolve, reject) => {
13428
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
13429
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13430
+ const index = store.index("proctoringId");
13431
+ const range = IDBKeyRange.only(proctoringId2);
13432
+ const request = index.openCursor(range);
13433
+ request.onsuccess = () => {
13434
+ const cursor = request.result;
13435
+ if (cursor) {
13436
+ cursor.delete();
13437
+ cursor.continue();
13438
+ } else {
13439
+ resolve();
13440
+ }
13441
+ };
13442
+ request.onerror = () => {
13443
+ var _a2;
13444
+ return reject(new Error(`Erro ao limpar todos os chunks: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13445
+ };
13446
+ });
13447
+ }
13448
+ /**
13449
+ * Verifica se existem chunks pendentes para qualquer proctoringId.
13450
+ * Útil na recuperação pós-crash.
13451
+ */
13452
+ async hasAnyPendingChunks() {
13453
+ const db = await this.connect();
13454
+ return new Promise((resolve, reject) => {
13455
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
13456
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13457
+ const index = store.index("uploaded");
13458
+ const range = IDBKeyRange.only(0);
13459
+ const request = index.count(range);
13460
+ request.onsuccess = () => {
13461
+ resolve(request.result > 0);
13462
+ };
13463
+ request.onerror = () => {
13464
+ var _a2;
13465
+ return reject(new Error(`Erro ao verificar chunks pendentes: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13466
+ };
13467
+ });
13468
+ }
13469
+ /**
13470
+ * Retorna todos os proctoringIds que possuem chunks pendentes.
13471
+ * Útil na recuperação pós-crash para saber quais sessões precisam ser finalizadas.
13472
+ */
13473
+ async getPendingProctoringIds() {
13474
+ const db = await this.connect();
13475
+ return new Promise((resolve, reject) => {
13476
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
13477
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13478
+ const index = store.index("uploaded");
13479
+ const range = IDBKeyRange.only(0);
13480
+ const request = index.getAll(range);
13481
+ request.onsuccess = () => {
13482
+ const chunks = request.result;
13483
+ const ids = [...new Set(chunks.map((c3) => c3.proctoringId))];
13484
+ resolve(ids);
13485
+ };
13486
+ request.onerror = () => {
13487
+ var _a2;
13488
+ return reject(new Error(`Erro ao buscar proctoringIds pendentes: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13489
+ };
13490
+ });
13491
+ }
13492
+ /**
13493
+ * Fecha a conexão com o banco.
13494
+ */
13495
+ close() {
13496
+ if (this.db) {
13497
+ this.db.close();
13498
+ this.db = null;
13499
+ }
13500
+ }
13501
+ };
13502
+ _ChunkStorageService.DB_NAME = "EasyProctorChunksDb";
13503
+ /** v2: índices uploaded numéricos; v3: campo arrayBuffer em vez de blob */
13504
+ _ChunkStorageService.DB_VERSION = 3;
13505
+ _ChunkStorageService.STORE_NAME = "chunks";
13506
+ var ChunkStorageService = _ChunkStorageService;
13507
+
13508
+ // src/new-flow/chunk/BackgroundUploadService.ts
13509
+ var DEFAULT_CONFIG = {
13510
+ pollInterval: 5e3,
13511
+ maxRetries: 5,
13512
+ baseRetryDelay: 2e3,
13513
+ cleanAfterUpload: true
13514
+ };
13515
+ var BackgroundUploadService = class _BackgroundUploadService {
13516
+ constructor(proctoringId2, token, backend, chunkStorage, config) {
13517
+ this.pollTimer = null;
13518
+ this.isProcessing = false;
13519
+ this.isRunning = false;
13520
+ /** Mapa de chunkId -> número de tentativas já feitas */
13521
+ this.retryCount = /* @__PURE__ */ new Map();
13522
+ /** GCS Resumable Upload State */
13523
+ this.sessionUrl = null;
13524
+ this.currentOffset = 0;
13525
+ this.totalBytesPurged = 0;
13526
+ this.STORAGE_KEY_PREFIX = "ep_upload_session_";
13527
+ this.GCS_CHUNK_SIZE = 256 * 1024;
13528
+ this.proctoringId = proctoringId2.trim();
13529
+ this.token = token;
13530
+ this.backend = backend;
13531
+ this.chunkStorage = chunkStorage;
13532
+ this.config = { ...DEFAULT_CONFIG, ...config };
13533
+ this.loadSessionState();
13534
+ }
13535
+ loadSessionState() {
13536
+ try {
13537
+ const stored = localStorage.getItem(`${this.STORAGE_KEY_PREFIX}${this.proctoringId}`);
13538
+ if (stored) {
13539
+ const { sessionUrl, currentOffset, totalBytesPurged } = JSON.parse(stored);
13540
+ this.sessionUrl = sessionUrl;
13541
+ this.currentOffset = currentOffset;
13542
+ this.totalBytesPurged = totalBytesPurged || 0;
13543
+ }
13544
+ } catch (e3) {
13545
+ console.warn("[BackgroundUpload] Erro ao carregar estado da sess\xE3o:", e3);
13546
+ }
13547
+ }
13548
+ saveSessionState() {
13549
+ try {
13550
+ localStorage.setItem(
13551
+ `${this.STORAGE_KEY_PREFIX}${this.proctoringId}`,
13552
+ JSON.stringify({
13553
+ sessionUrl: this.sessionUrl,
13554
+ currentOffset: this.currentOffset,
13555
+ totalBytesPurged: this.totalBytesPurged
13556
+ })
13557
+ );
13558
+ } catch (e3) {
13559
+ console.warn("[BackgroundUpload] Erro ao salvar estado da sess\xE3o:", e3);
13560
+ }
13561
+ }
13562
+ clearSessionState() {
13563
+ try {
13564
+ localStorage.removeItem(`${this.STORAGE_KEY_PREFIX}${this.proctoringId}`);
13565
+ this.sessionUrl = null;
13566
+ this.currentOffset = 0;
13567
+ this.totalBytesPurged = 0;
13568
+ } catch (e3) {
13569
+ console.warn("[BackgroundUpload] Erro ao limpar estado da sess\xE3o:", e3);
13570
+ }
13571
+ }
13572
+ /**
13573
+ * Inicia o serviço de upload em background. Faz polling periódico no IndexedDB
13574
+ * para enviar chunks pendentes.
13575
+ */
13576
+ start() {
13577
+ if (this.isRunning) return;
13578
+ this.isRunning = true;
13579
+ console.log(`[BackgroundUpload] Iniciando servi\xE7o para proctoringId: ${this.proctoringId}`);
13580
+ this.processQueue();
13581
+ this.pollTimer = setInterval(() => {
13582
+ this.processQueue(false);
13583
+ }, this.config.pollInterval);
13584
+ }
13585
+ /**
13586
+ * Para o serviço de upload em background.
13587
+ */
13287
13588
  stop() {
13288
- var _a2, _b, _c2;
13289
- if (this.animationFrameId !== null) {
13290
- cancelAnimationFrame(this.animationFrameId);
13589
+ this.isRunning = false;
13590
+ if (this.pollTimer) {
13591
+ clearInterval(this.pollTimer);
13592
+ this.pollTimer = null;
13291
13593
  }
13292
- (_a2 = this.audioContext) == null ? void 0 : _a2.close();
13293
- (_b = this.microphone) == null ? void 0 : _b.disconnect();
13294
- (_c2 = this.analyser) == null ? void 0 : _c2.disconnect();
13594
+ console.log(`[BackgroundUpload] Servi\xE7o parado para proctoringId: ${this.proctoringId}`);
13295
13595
  }
13296
- getVolume() {
13297
- return this.volume;
13596
+ /**
13597
+ * Força o processamento de todos os chunks pendentes e encerra a sessão GCS.
13598
+ * Útil quando a gravação é finalizada.
13599
+ */
13600
+ async flush() {
13601
+ console.log(`[BackgroundUpload] Flush: enviando todos os chunks pendentes e finalizando...`);
13602
+ let waitAttempts = 0;
13603
+ while (this.isProcessing && waitAttempts < 10) {
13604
+ await this.sleep(1e3);
13605
+ waitAttempts++;
13606
+ }
13607
+ let flushRetries = 0;
13608
+ const maxFlushRetries = 3;
13609
+ while (flushRetries < maxFlushRetries) {
13610
+ try {
13611
+ await this.processQueue(true);
13612
+ console.log(`[BackgroundUpload] Flush completado com sucesso.`);
13613
+ return;
13614
+ } catch (error) {
13615
+ flushRetries++;
13616
+ console.error(`[BackgroundUpload] Erro no flush (tentativa ${flushRetries}/${maxFlushRetries}):`, error);
13617
+ if (flushRetries < maxFlushRetries) {
13618
+ await this.sleep(2e3);
13619
+ }
13620
+ }
13621
+ }
13622
+ throw new Error(`[BackgroundUpload] Falha ao finalizar upload ap\xF3s ${maxFlushRetries} tentativas.`);
13298
13623
  }
13299
- setVolume(value) {
13300
- this.volume = value;
13624
+ /**
13625
+ * Sincroniza o offset local com o estado real no Google Cloud Storage.
13626
+ */
13627
+ async syncOffset() {
13628
+ if (!this.sessionUrl) return 0;
13629
+ try {
13630
+ console.log(`[BackgroundUpload] Sincronizando offset com GCS...`);
13631
+ const response = await fetch(this.sessionUrl, {
13632
+ method: "PUT",
13633
+ headers: {
13634
+ "Content-Range": "bytes */*"
13635
+ }
13636
+ });
13637
+ console.log(`[BackgroundUpload] Status da sincroniza\xE7\xE3o (syncOffset): ${response.status}`);
13638
+ if (response.status === 308) {
13639
+ const range = response.headers.get("Range");
13640
+ if (range) {
13641
+ const lastByte = parseInt(range.split("-")[1], 10);
13642
+ this.currentOffset = lastByte + 1;
13643
+ this.saveSessionState();
13644
+ console.log(`[BackgroundUpload] Offset sincronizado: ${this.currentOffset}`);
13645
+ } else {
13646
+ this.currentOffset = 0;
13647
+ }
13648
+ } else if (response.ok || response.status === 201) {
13649
+ console.log("[BackgroundUpload] Sincroniza\xE7\xE3o indicou upload JA FINALIZADO.");
13650
+ this.currentOffset = -1;
13651
+ } else {
13652
+ console.warn(`[BackgroundUpload] Status inesperado na sincroniza\xE7\xE3o: ${response.status}`);
13653
+ }
13654
+ } catch (error) {
13655
+ console.warn("[BackgroundUpload] Erro ao sincronizar offset:", error);
13656
+ }
13657
+ return this.currentOffset;
13658
+ }
13659
+ /**
13660
+ * Verifica e envia chunks pendentes para o backend.
13661
+ * @param isFinal Se true, não alinha a 256KB e fecha a sessão com /TOTAL no header.
13662
+ */
13663
+ async processQueue(isFinal = false) {
13664
+ var _a2, _b;
13665
+ if (this.isProcessing) return;
13666
+ this.isProcessing = true;
13667
+ try {
13668
+ if (this.sessionUrl) {
13669
+ await this.syncOffset();
13670
+ if (this.currentOffset === -1) {
13671
+ console.log("[BackgroundUpload] Sess\xE3o j\xE1 finalizada no servidor.");
13672
+ this.clearSessionState();
13673
+ this.isProcessing = false;
13674
+ return;
13675
+ }
13676
+ }
13677
+ const allChunks = await this.chunkStorage.getAllChunks(this.proctoringId);
13678
+ const pendingChunks = allChunks.filter((c3) => c3.uploaded === 0);
13679
+ if (pendingChunks.length === 0 && !isFinal) {
13680
+ this.isProcessing = false;
13681
+ return;
13682
+ }
13683
+ console.log(`[BackgroundUpload] ${pendingChunks.length} chunks pendentes encontrados. Modo final: ${isFinal}`);
13684
+ let virtualStart = this.totalBytesPurged;
13685
+ const chunksWithMeta = allChunks.map((c3) => {
13686
+ const start = virtualStart;
13687
+ const byteLength = c3.arrayBuffer.byteLength;
13688
+ const end = start + byteLength - 1;
13689
+ virtualStart += byteLength;
13690
+ return { chunk: c3, start, end };
13691
+ });
13692
+ let combinedBlobParts = [];
13693
+ let lastProcessedChunkId = null;
13694
+ let finalChunkIndex = 0;
13695
+ let mimeType = pendingChunks[0].mimeType;
13696
+ for (const meta of chunksWithMeta) {
13697
+ if (this.currentOffset > meta.end) continue;
13698
+ const sliceStart = Math.max(0, this.currentOffset - meta.start);
13699
+ const sliceBuf = meta.chunk.arrayBuffer.slice(sliceStart);
13700
+ combinedBlobParts.push(new Blob([sliceBuf]));
13701
+ lastProcessedChunkId = meta.chunk.id;
13702
+ finalChunkIndex = meta.chunk.chunkIndex;
13703
+ }
13704
+ if (combinedBlobParts.length === 0 && !isFinal) {
13705
+ this.isProcessing = false;
13706
+ return;
13707
+ }
13708
+ let fullBlob = new Blob(combinedBlobParts, { type: mimeType });
13709
+ let sendableSize = fullBlob.size;
13710
+ let totalSizeForHeader = void 0;
13711
+ if (!isFinal) {
13712
+ sendableSize = Math.floor(fullBlob.size / this.GCS_CHUNK_SIZE) * this.GCS_CHUNK_SIZE;
13713
+ if (sendableSize === 0) {
13714
+ console.log("[BackgroundUpload] Dados insuficientes para atingir 256KB. Aguardando novo chunk...");
13715
+ this.isProcessing = false;
13716
+ return;
13717
+ }
13718
+ } else {
13719
+ totalSizeForHeader = virtualStart;
13720
+ }
13721
+ const blobToSend = fullBlob.slice(0, sendableSize);
13722
+ try {
13723
+ await this.uploadData(blobToSend, mimeType, finalChunkIndex, totalSizeForHeader);
13724
+ for (const meta of chunksWithMeta) {
13725
+ if (meta.chunk.uploaded === 0 && meta.end < this.currentOffset) {
13726
+ await this.chunkStorage.markAsUploaded(meta.chunk.id);
13727
+ this.retryCount.delete(meta.chunk.id);
13728
+ (_a2 = this.onChunkUploaded) == null ? void 0 : _a2.call(this, meta.chunk.id, meta.chunk.chunkIndex);
13729
+ console.log(`[BackgroundUpload] Chunk ${meta.chunk.chunkIndex} marcado como enviado.`);
13730
+ }
13731
+ }
13732
+ if (this.config.cleanAfterUpload) {
13733
+ const chunksToClear = chunksWithMeta.filter((meta) => meta.chunk.uploaded === 1 || meta.chunk.uploaded === 0 && meta.end < this.currentOffset);
13734
+ const sizePurged = chunksToClear.reduce(
13735
+ (acc, meta) => acc + meta.chunk.arrayBuffer.byteLength,
13736
+ 0
13737
+ );
13738
+ await this.chunkStorage.clearUploadedChunks(this.proctoringId);
13739
+ if (sizePurged > 0) {
13740
+ this.totalBytesPurged += sizePurged;
13741
+ this.saveSessionState();
13742
+ console.log(`[BackgroundUpload] ${sizePurged} bytes limpos do armazenamento local. Total purgado: ${this.totalBytesPurged}`);
13743
+ }
13744
+ }
13745
+ if (isFinal) {
13746
+ this.clearSessionState();
13747
+ }
13748
+ } catch (error) {
13749
+ console.error("[BackgroundUpload] Falha no upload:", error);
13750
+ (_b = this.onUploadError) == null ? void 0 : _b.call(this, lastProcessedChunkId || 0, error);
13751
+ }
13752
+ } catch (error) {
13753
+ console.error("[BackgroundUpload] Erro ao processar fila:", error);
13754
+ } finally {
13755
+ this.isProcessing = false;
13756
+ }
13757
+ }
13758
+ /**
13759
+ * Faz o upload bruto de dados para a sessão GCS.
13760
+ */
13761
+ async uploadData(blob, mimeType, chunkIndex, totalSize) {
13762
+ const fileName = `EP_${this.proctoringId}_camera_0.webm`;
13763
+ if (!this.sessionUrl) {
13764
+ const initiateUrl = await this.backend.initiateUpload(this.token, `${this.proctoringId}/${fileName}`, mimeType);
13765
+ const startResponse = await fetch(initiateUrl, {
13766
+ method: "POST",
13767
+ headers: { "x-goog-resumable": "start", "Content-Type": mimeType }
13768
+ });
13769
+ if (!startResponse.ok) throw new Error(`Falha ao iniciar: ${startResponse.status}`);
13770
+ this.sessionUrl = startResponse.headers.get("Location");
13771
+ if (!this.sessionUrl) throw new Error("Location header ausente");
13772
+ try {
13773
+ const urlObj = new URL(this.sessionUrl);
13774
+ const pathParts = urlObj.pathname.split("/");
13775
+ let bucket = pathParts[1];
13776
+ let object = decodeURIComponent(pathParts.slice(2).join("/"));
13777
+ if (pathParts.includes("b") && pathParts.includes("o")) {
13778
+ const bIdx = pathParts.indexOf("b") + 1;
13779
+ const oIdx = pathParts.indexOf("o") + 1;
13780
+ bucket = pathParts[bIdx];
13781
+ object = decodeURIComponent(pathParts.slice(oIdx).join("/"));
13782
+ }
13783
+ console.log(`[BackgroundUpload] Sess\xE3o Iniciada -> Bucket: ${bucket}, Objeto: ${object}`);
13784
+ } catch (e3) {
13785
+ console.log(`[BackgroundUpload] Sess\xE3o Iniciada. URL: ${this.sessionUrl}`);
13786
+ }
13787
+ this.currentOffset = 0;
13788
+ this.saveSessionState();
13789
+ } else {
13790
+ console.log(`[BackgroundUpload] Usando sess\xE3o GCS existente: ${this.sessionUrl}`);
13791
+ }
13792
+ const start = this.currentOffset;
13793
+ const end = start + blob.size - 1;
13794
+ const totalHeader = totalSize !== void 0 ? totalSize.toString() : "*";
13795
+ const contentRangeHeader = blob.size === 0 && totalSize !== void 0 ? `bytes */${totalHeader}` : `bytes ${start}-${end}/${totalHeader}`;
13796
+ console.log(`[BackgroundUpload] Enviando ${blob.size > 0 ? "dados" : "finaliza\xE7\xE3o"}: ${contentRangeHeader} (Size: ${blob.size})`);
13797
+ const response = await fetch(this.sessionUrl, {
13798
+ method: "PUT",
13799
+ headers: { "Content-Range": contentRangeHeader },
13800
+ body: blob.size > 0 ? blob : null
13801
+ // Usa null para garantir corpo vazio se necessário
13802
+ });
13803
+ console.log(`[BackgroundUpload] Resposta GCS (uploadData): ${response.status}`);
13804
+ if (response.status !== 200 && response.status !== 201 && response.status !== 308) {
13805
+ const errorText = await response.text();
13806
+ console.error(`[BackgroundUpload] Erro GCS: ${errorText}`);
13807
+ throw new Error(`Status HTTP inesperado: ${response.status}`);
13808
+ }
13809
+ const rangeHeader = response.headers.get("Range");
13810
+ if (rangeHeader) {
13811
+ const lastByte = parseInt(rangeHeader.split("-")[1], 10);
13812
+ this.currentOffset = lastByte + 1;
13813
+ } else {
13814
+ this.currentOffset += blob.size;
13815
+ }
13816
+ this.saveSessionState();
13817
+ trackers.registerUploadFile(
13818
+ this.proctoringId,
13819
+ `GCS Stream Upload
13820
+ Size: ${blob.size}
13821
+ Range: ${start}-${end}
13822
+ Last Index: ${chunkIndex}`,
13823
+ "CameraChunk"
13824
+ );
13825
+ }
13826
+ /**
13827
+ * Método estático para recuperação pós-crash.
13828
+ * Verifica o IndexedDB em busca de chunks pendentes de qualquer sessão
13829
+ * e tenta enviar.
13830
+ */
13831
+ static async recoverPendingUploads(backend, token) {
13832
+ const chunkStorage = new ChunkStorageService();
13833
+ const recoveredIds = [];
13834
+ try {
13835
+ const pendingIds = await chunkStorage.getPendingProctoringIds();
13836
+ if (pendingIds.length === 0) {
13837
+ console.log("[BackgroundUpload] Nenhum chunk pendente encontrado para recupera\xE7\xE3o.");
13838
+ return recoveredIds;
13839
+ }
13840
+ console.log(
13841
+ `[BackgroundUpload] Recupera\xE7\xE3o p\xF3s-crash: ${pendingIds.length} sess\xE3o(\xF5es) com chunks pendentes.`
13842
+ );
13843
+ for (const proctoringId2 of pendingIds) {
13844
+ try {
13845
+ const service = new _BackgroundUploadService(
13846
+ proctoringId2,
13847
+ token,
13848
+ backend,
13849
+ chunkStorage,
13850
+ { cleanAfterUpload: true }
13851
+ );
13852
+ await service.flush();
13853
+ recoveredIds.push(proctoringId2);
13854
+ console.log(
13855
+ `[BackgroundUpload] Chunks da sess\xE3o ${proctoringId2} recuperados com sucesso.`
13856
+ );
13857
+ } catch (error) {
13858
+ console.error(
13859
+ `[BackgroundUpload] Erro ao recuperar chunks da sess\xE3o ${proctoringId2}:`,
13860
+ error
13861
+ );
13862
+ }
13863
+ }
13864
+ } catch (error) {
13865
+ console.error("[BackgroundUpload] Erro geral na recupera\xE7\xE3o:", error);
13866
+ }
13867
+ return recoveredIds;
13868
+ }
13869
+ sleep(ms2) {
13870
+ return new Promise((resolve) => setTimeout(resolve, ms2));
13301
13871
  }
13302
13872
  };
13303
13873
 
13304
13874
  // src/new-flow/recorders/CameraRecorder.ts
13305
- var import_jszip_min = __toESM(require_jszip_min());
13306
13875
  var pkg = require_fix_webm_duration();
13307
13876
  var fixWebmDuration = pkg.default || pkg;
13308
- var CameraRecorder = class {
13877
+ var _CameraRecorder = class _CameraRecorder {
13309
13878
  constructor(options, videoOptions, paramsConfig, backend, backendToken) {
13310
13879
  this.blobs = [];
13311
13880
  this.paramsConfig = {
@@ -13358,20 +13927,138 @@ var CameraRecorder = class {
13358
13927
  this.videoElement = null;
13359
13928
  this.duration = 0;
13360
13929
  this.stopped = false;
13930
+ this.backgroundUpload = null;
13931
+ this.chunkIndex = 0;
13932
+ /** Lista de promises de chunks sendo salvos no IndexedDB para evitar race conditions no stop */
13933
+ this.pendingChunkSaves = [];
13934
+ // Handlers bound para poder remover os listeners depois
13935
+ this.boundVisibilityHandler = null;
13936
+ this.boundPageHideHandler = null;
13361
13937
  this.currentRetries = 0;
13362
13938
  this.packageCount = 0;
13363
13939
  this.failedUploads = 0;
13364
- this.noiseWait = 20;
13365
13940
  this.options = options;
13366
13941
  this.videoOptions = videoOptions;
13367
13942
  this.backend = backend;
13368
13943
  this.backendToken = backendToken;
13369
13944
  paramsConfig && (this.paramsConfig = paramsConfig);
13370
13945
  }
13946
+ /**
13947
+ * Determina se o fluxo de chunks e lifecycle deve estar ativo.
13948
+ * Retorna true se:
13949
+ * 1. O proctoringId já foi definido (ou seja, estamos em uma sessão real, NÃO no checkDevices)
13950
+ * 2. E (`useChunkRecording` foi explicitamente setado como true OU o dispositivo é mobile)
13951
+ */
13952
+ get isChunkEnabled() {
13953
+ return !!this.proctoringId && this.options.proctoringType === "REALTIME" && !isSafeBrowser();
13954
+ }
13371
13955
  setProctoringId(proctoringId2) {
13372
13956
  this.proctoringId = proctoringId2;
13373
13957
  this.proctoringId && this.backend && (this.upload = new UploadService(this.proctoringId, this.backend));
13374
13958
  setRecorderProctoringId(proctoringId2);
13959
+ if (this.isChunkEnabled) {
13960
+ this.chunkStorage = new ChunkStorageService();
13961
+ if (this.backend && this.backendToken) {
13962
+ this.backgroundUpload = new BackgroundUploadService(
13963
+ this.proctoringId,
13964
+ this.backendToken,
13965
+ this.backend,
13966
+ this.chunkStorage,
13967
+ { pollInterval: 5e3, maxRetries: 5, cleanAfterUpload: true }
13968
+ );
13969
+ }
13970
+ this.persistSessionState("IN_PROGRESS");
13971
+ console.log(
13972
+ `[CameraRecorder] Chunk recording ATIVO (type: ${this.options.proctoringType}, mobile: ${isMobileDevice()})`
13973
+ );
13974
+ } else {
13975
+ console.log(
13976
+ `[CameraRecorder] Chunk recording INATIVO (type: ${this.options.proctoringType}) \u2014 modo cl\xE1ssico.`
13977
+ );
13978
+ }
13979
+ }
13980
+ // ========================
13981
+ // Session State Persistence (localStorage)
13982
+ // ========================
13983
+ persistSessionState(status) {
13984
+ try {
13985
+ const data = {
13986
+ proctoringId: this.proctoringId,
13987
+ status,
13988
+ timestamp: Date.now()
13989
+ };
13990
+ localStorage.setItem(_CameraRecorder.LS_SESSION_KEY, JSON.stringify(data));
13991
+ } catch (e3) {
13992
+ console.warn("[CameraRecorder] N\xE3o foi poss\xEDvel salvar estado no localStorage:", e3);
13993
+ }
13994
+ }
13995
+ clearSessionState() {
13996
+ try {
13997
+ localStorage.removeItem(_CameraRecorder.LS_SESSION_KEY);
13998
+ } catch (e3) {
13999
+ console.warn("[CameraRecorder] N\xE3o foi poss\xEDvel limpar estado do localStorage:", e3);
14000
+ }
14001
+ }
14002
+ /**
14003
+ * Verifica se existe uma sessão ativa anterior no localStorage.
14004
+ * Retorna os dados da sessão se ela estiver em IN_PROGRESS, null caso contrário.
14005
+ */
14006
+ static checkForActiveSession() {
14007
+ try {
14008
+ const raw = localStorage.getItem(_CameraRecorder.LS_SESSION_KEY);
14009
+ if (!raw) return null;
14010
+ const data = JSON.parse(raw);
14011
+ if (data.status === "IN_PROGRESS") return data;
14012
+ return null;
14013
+ } catch (e3) {
14014
+ return null;
14015
+ }
14016
+ }
14017
+ // ========================
14018
+ // Page Lifecycle Management
14019
+ // ========================
14020
+ setupLifecycleListeners() {
14021
+ this.boundVisibilityHandler = () => this.handleVisibilityChange();
14022
+ this.boundPageHideHandler = () => this.handlePageHide();
14023
+ document.addEventListener("visibilitychange", this.boundVisibilityHandler);
14024
+ window.addEventListener("pagehide", this.boundPageHideHandler);
14025
+ }
14026
+ removeLifecycleListeners() {
14027
+ if (this.boundVisibilityHandler) {
14028
+ document.removeEventListener("visibilitychange", this.boundVisibilityHandler);
14029
+ this.boundVisibilityHandler = null;
14030
+ }
14031
+ if (this.boundPageHideHandler) {
14032
+ window.removeEventListener("pagehide", this.boundPageHideHandler);
14033
+ this.boundPageHideHandler = null;
14034
+ }
14035
+ }
14036
+ handleVisibilityChange() {
14037
+ var _a2;
14038
+ if (document.visibilityState === "hidden") {
14039
+ console.log("[CameraRecorder] P\xE1gina ficou invis\xEDvel \u2014 sess\xE3o potencialmente interrompida.");
14040
+ this.persistSessionState("INTERRUPTED");
14041
+ this.proctoringId && trackers.registerError(
14042
+ this.proctoringId,
14043
+ "Visibility API: P\xE1gina ficou oculta (hidden). Poss\xEDvel troca de app ou minimiza\xE7\xE3o."
14044
+ );
14045
+ } else if (document.visibilityState === "visible") {
14046
+ console.log("[CameraRecorder] P\xE1gina vis\xEDvel novamente \u2014 verificando estado da grava\xE7\xE3o.");
14047
+ this.persistSessionState("IN_PROGRESS");
14048
+ this.proctoringId && trackers.registerError(
14049
+ this.proctoringId,
14050
+ "Visibility API: P\xE1gina voltou a ficar vis\xEDvel. Usu\xE1rio retornou."
14051
+ );
14052
+ (_a2 = this.onVisibilityRestored) == null ? void 0 : _a2.call(this);
14053
+ }
14054
+ }
14055
+ handlePageHide() {
14056
+ console.log("[CameraRecorder] pagehide detectado \u2014 persistindo estado.");
14057
+ this.persistSessionState("INTERRUPTED");
14058
+ this.proctoringId && trackers.registerError(
14059
+ this.proctoringId,
14060
+ "Page Lifecycle: pagehide event detectado. P\xE1gina est\xE1 sendo descarregada."
14061
+ );
13375
14062
  }
13376
14063
  async initializeDetectors() {
13377
14064
  var _a2, _b, _c2;
@@ -13633,6 +14320,51 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13633
14320
  await this.sendPackage();
13634
14321
  await this.filesToUpload.splice(0, this.filesToUpload.length);
13635
14322
  }
14323
+ if (this.isChunkEnabled) {
14324
+ if (this.backgroundUpload) {
14325
+ try {
14326
+ if (this.pendingChunkSaves.length > 0) {
14327
+ console.log(`[CameraRecorder] Aguardando ${this.pendingChunkSaves.length} salvamentos de chunks pendentes...`);
14328
+ await Promise.all(this.pendingChunkSaves);
14329
+ }
14330
+ await this.backgroundUpload.flush();
14331
+ } catch (e3) {
14332
+ console.warn("[CameraRecorder] Erro ao fazer flush dos chunks:", e3);
14333
+ }
14334
+ this.backgroundUpload.stop();
14335
+ }
14336
+ this.removeLifecycleListeners();
14337
+ this.persistSessionState("FINISHED");
14338
+ }
14339
+ }
14340
+ /**
14341
+ * Callback chamado pelo recorder a cada novo chunk de vídeo disponível.
14342
+ * Salva o chunk no IndexedDB para persistência e recuperação.
14343
+ */
14344
+ async handleNewChunk(blob, idx) {
14345
+ if (!this.proctoringId || !this.chunkStorage) return;
14346
+ const savePromise = (async () => {
14347
+ var _a2;
14348
+ try {
14349
+ const arrayBuffer = await blob.arrayBuffer();
14350
+ await this.chunkStorage.saveChunk({
14351
+ proctoringId: this.proctoringId,
14352
+ chunkIndex: this.chunkIndex,
14353
+ arrayBuffer,
14354
+ timestamp: Date.now(),
14355
+ uploaded: 0,
14356
+ mimeType: ((_a2 = this.recorderOptions) == null ? void 0 : _a2.mimeType) || "video/webm"
14357
+ });
14358
+ this.chunkIndex++;
14359
+ console.log(`[CameraRecorder] Chunk ${this.chunkIndex - 1} salvo no IndexedDB.`);
14360
+ } catch (error) {
14361
+ console.error("[CameraRecorder] Erro ao salvar chunk no IndexedDB:", error);
14362
+ }
14363
+ })();
14364
+ this.pendingChunkSaves.push(savePromise);
14365
+ savePromise.finally(() => {
14366
+ this.pendingChunkSaves = this.pendingChunkSaves.filter((p3) => p3 !== savePromise);
14367
+ });
13636
14368
  }
13637
14369
  async pauseRecording() {
13638
14370
  await this.recordingPause();
@@ -13773,21 +14505,36 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13773
14505
  const settings = this.cameraStream.getVideoTracks()[0].getSettings();
13774
14506
  const settingsAudio = this.cameraStream.getAudioTracks()[0].getSettings();
13775
14507
  if (this.options.proctoringType == "VIDEO" || this.options.proctoringType == "REALTIME" || this.options.proctoringType == "IMAGE" && ((_a2 = this.paramsConfig.imageBehaviourParameters) == null ? void 0 : _a2.saveVideo)) {
13776
- const rawBlob = new Blob(this.blobs, {
13777
- type: ((_b = this.recorderOptions) == null ? void 0 : _b.mimeType) || "video/webm"
13778
- });
13779
- const fixedBlob = await fixWebmDuration(rawBlob, this.duration);
13780
- const arrayBuffer = await fixedBlob.arrayBuffer();
13781
- session.addRecording({
13782
- device: `Audio
14508
+ let isUploaded = false;
14509
+ if (this.isChunkEnabled) {
14510
+ if (this.backend && this.backendToken && this.proctoringId) {
14511
+ const fileName = `EP_${this.proctoringId}_camera_0.webm`;
14512
+ const objectName = `${this.proctoringId}/${fileName}`;
14513
+ isUploaded = await this.backend.checkUpload(this.backendToken, objectName, "video/webm");
14514
+ this.chunkStorage && await this.chunkStorage.clearAllChunks(session.id);
14515
+ }
14516
+ }
14517
+ if (!isUploaded) {
14518
+ const rawBlob = new Blob(this.blobs, {
14519
+ type: ((_b = this.recorderOptions) == null ? void 0 : _b.mimeType) || "video/webm"
14520
+ });
14521
+ let finalBlob = rawBlob;
14522
+ if (typeof fixWebmDuration === "function") {
14523
+ finalBlob = await fixWebmDuration(rawBlob, this.duration);
14524
+ } else {
14525
+ console.warn("fixWebmDuration n\xE3o dispon\xEDvel");
14526
+ }
14527
+ session.addRecording({
14528
+ device: `Audio
13783
14529
  Sample Rate: ${settingsAudio.sampleRate}
13784
14530
  Sample Size: ${settingsAudio.sampleSize}
13785
14531
 
13786
14532
  Video:
13787
14533
  ${JSON.stringify(this.recorderOptions)}`,
13788
- arrayBuffer,
13789
- origin: "Camera" /* Camera */
13790
- });
14534
+ arrayBuffer: await finalBlob.arrayBuffer(),
14535
+ origin: "Camera" /* Camera */
14536
+ });
14537
+ }
13791
14538
  }
13792
14539
  }
13793
14540
  async getFile(file, name, type) {
@@ -13800,28 +14547,63 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13800
14547
  });
13801
14548
  });
13802
14549
  }
13803
- onNoiseDetected() {
13804
- var _a2, _b, _c2;
13805
- if (this.options.proctoringType === "REALTIME") return;
13806
- if (!this.volumeMeter && this.cameraStream) {
13807
- this.volumeMeter = new VolumeMeter(this.cameraStream);
13808
- this.volumeMeter.start().catch((e3) => {
13809
- console.log(e3);
13810
- this.volumeMeter = void 0;
13811
- });
13812
- }
13813
- const volume = (_b = (_a2 = this.volumeMeter) == null ? void 0 : _a2.getVolume()) != null ? _b : 0;
13814
- if (volume >= (((_c2 = this.paramsConfig.audioBehaviourParameters) == null ? void 0 : _c2.noiseLimit) || 40)) {
13815
- if (this.noiseWait >= 20) {
13816
- this.options.onRealtimeAlertsCallback({
13817
- status: "ALERT",
13818
- description: "Barulho detectado",
13819
- type: "audio_detection_on_stream"
13820
- });
13821
- this.noiseWait = 0;
14550
+ };
14551
+ // ========================
14552
+ // Chunk & Lifecycle
14553
+ // ========================
14554
+ /** Intervalo de cada chunk em ms (padrão: 60 segundos) */
14555
+ _CameraRecorder.CHUNK_TIMESLICE_MS = 6e4;
14556
+ /** Chave do localStorage para persistir estado da sessão */
14557
+ _CameraRecorder.LS_SESSION_KEY = "ep_proctoring_session";
14558
+ var CameraRecorder = _CameraRecorder;
14559
+
14560
+ // src/new-flow/recorders/VolumeMeter.ts
14561
+ var VolumeMeter = class {
14562
+ constructor(stream) {
14563
+ this.volume = null;
14564
+ this.animationFrameId = null;
14565
+ this.stream = stream;
14566
+ }
14567
+ async start(options = {}) {
14568
+ return new Promise((resolve, reject) => {
14569
+ try {
14570
+ this.audioContext = new AudioContext();
14571
+ this.analyser = this.audioContext.createAnalyser();
14572
+ this.microphone = this.audioContext.createMediaStreamSource(this.stream);
14573
+ this.analyser.smoothingTimeConstant = 0.8;
14574
+ this.analyser.fftSize = 1024;
14575
+ this.microphone.connect(this.analyser);
14576
+ const processAudio = () => {
14577
+ const array = new Uint8Array(this.analyser.frequencyBinCount);
14578
+ this.analyser.getByteFrequencyData(array);
14579
+ const arraySum = array.reduce((a3, value) => a3 + value, 0);
14580
+ const average = arraySum / array.length;
14581
+ this.setVolume(average);
14582
+ options.setVolume && options.setVolume(average);
14583
+ this.animationFrameId = requestAnimationFrame(processAudio);
14584
+ };
14585
+ this.animationFrameId = requestAnimationFrame(processAudio);
14586
+ resolve(true);
14587
+ } catch (error) {
14588
+ this.stop();
14589
+ reject(`Error: ${error}`);
13822
14590
  }
14591
+ });
14592
+ }
14593
+ stop() {
14594
+ var _a2, _b, _c2;
14595
+ if (this.animationFrameId !== null) {
14596
+ cancelAnimationFrame(this.animationFrameId);
13823
14597
  }
13824
- this.noiseWait++;
14598
+ (_a2 = this.audioContext) == null ? void 0 : _a2.close();
14599
+ (_b = this.microphone) == null ? void 0 : _b.disconnect();
14600
+ (_c2 = this.analyser) == null ? void 0 : _c2.disconnect();
14601
+ }
14602
+ getVolume() {
14603
+ return this.volume;
14604
+ }
14605
+ setVolume(value) {
14606
+ this.volume = value;
13825
14607
  }
13826
14608
  };
13827
14609
 
@@ -13829,6 +14611,8 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13829
14611
  var DeviceCheckerUI = class {
13830
14612
  constructor(options = getDefaultProctoringOptions, _videoOptions) {
13831
14613
  this.videoOptions = { width: 1080, height: 720, minWidth: 0, minHeight: 0 };
14614
+ this.autoConfirmTimer = null;
14615
+ this.autoConfirmSeconds = 5;
13832
14616
  this.options = {
13833
14617
  ...getDefaultProctoringOptions,
13834
14618
  ...options,
@@ -14594,9 +15378,53 @@ var DeviceCheckerUI = class {
14594
15378
  }));
14595
15379
  }
14596
15380
  closeModal() {
15381
+ this.stopAutoConfirm();
14597
15382
  const checkDevices = document.querySelector("#checkDevices");
14598
15383
  checkDevices == null ? void 0 : checkDevices.remove();
14599
15384
  }
15385
+ verifyAutoConfirm(status) {
15386
+ var _a2;
15387
+ if (!((_a2 = this.options) == null ? void 0 : _a2.auto)) return;
15388
+ let isAllGreen = status.allowedResolution && status.allowedPositionFace && status.allowedAmbient && status.allowedMicrophone;
15389
+ if (this.options.useSpyScan) {
15390
+ isAllGreen = isAllGreen && status.allowedSpyScan === true;
15391
+ }
15392
+ if (isAllGreen) {
15393
+ if (!this.autoConfirmTimer) {
15394
+ this.startAutoConfirm();
15395
+ }
15396
+ } else {
15397
+ if (this.autoConfirmTimer) {
15398
+ this.stopAutoConfirm();
15399
+ }
15400
+ }
15401
+ }
15402
+ startAutoConfirm() {
15403
+ this.autoConfirmSeconds = 5;
15404
+ const btn = document.getElementById("confirmBtn");
15405
+ if (btn && !btn.disabled) {
15406
+ btn.innerText = `Continuando em ${this.autoConfirmSeconds}s...`;
15407
+ this.autoConfirmTimer = setInterval(() => {
15408
+ this.autoConfirmSeconds--;
15409
+ if (this.autoConfirmSeconds <= 0) {
15410
+ this.stopAutoConfirm();
15411
+ btn.click();
15412
+ } else {
15413
+ btn.innerText = `Continuando em ${this.autoConfirmSeconds}s...`;
15414
+ }
15415
+ }, 1e3);
15416
+ }
15417
+ }
15418
+ stopAutoConfirm() {
15419
+ if (this.autoConfirmTimer) {
15420
+ clearInterval(this.autoConfirmTimer);
15421
+ this.autoConfirmTimer = null;
15422
+ }
15423
+ const btn = document.getElementById("confirmBtn");
15424
+ if (btn) {
15425
+ btn.innerText = "Continuar";
15426
+ }
15427
+ }
14600
15428
  modalActions(closeCheckDevices) {
14601
15429
  const cancelBtn = document.getElementById("cancelBtn");
14602
15430
  const confirmBtn = document.getElementById("confirmBtn");
@@ -14963,6 +15791,7 @@ var _DeviceCheckerService = class _DeviceCheckerService {
14963
15791
  }
14964
15792
  }
14965
15793
  onUpdateCallback() {
15794
+ var _a2;
14966
15795
  if (typeof this.onUpdateCb === "function") {
14967
15796
  this.onUpdateCb({
14968
15797
  allowedResolution: this.allowedResolution,
@@ -14973,6 +15802,15 @@ var _DeviceCheckerService = class _DeviceCheckerService {
14973
15802
  faceDetectionAlerts: this.faceDetectionAlerts
14974
15803
  });
14975
15804
  }
15805
+ if (((_a2 = this.options) == null ? void 0 : _a2.auto) && this.DeviceCheckerUI) {
15806
+ this.DeviceCheckerUI.verifyAutoConfirm({
15807
+ allowedResolution: this.allowedResolution,
15808
+ allowedPositionFace: this.allowedPositionFace,
15809
+ allowedAmbient: this.allowedAmbient,
15810
+ allowedMicrophone: this.allowedMicrophone,
15811
+ allowedSpyScan: this.allowedSpyScan
15812
+ });
15813
+ }
14976
15814
  }
14977
15815
  async checkDevices(options = getDefaultProctoringOptions, _videoOptions = getDefaultProctoringVideoOptions) {
14978
15816
  var _a2;
@@ -15179,16 +16017,19 @@ var _DeviceCheckerService = class _DeviceCheckerService {
15179
16017
  }).finally(() => {
15180
16018
  this.DeviceCheckerUI && this.DeviceCheckerUI.waitingSpyDevices(false);
15181
16019
  this.DeviceCheckerUI && this.allowedSpyScan != null && this.DeviceCheckerUI.isSpyDevicesUI(this.allowedSpyScan);
16020
+ this.onUpdateCallback();
15182
16021
  });
15183
16022
  } else {
15184
16023
  this.allowedSpyScan = false;
15185
16024
  this.DeviceCheckerUI && this.DeviceCheckerUI.waitingSpyDevices(false);
15186
16025
  this.DeviceCheckerUI && this.DeviceCheckerUI.isSpyDevicesUI(this.allowedSpyScan);
16026
+ this.onUpdateCallback();
15187
16027
  }
15188
16028
  } catch (error) {
15189
16029
  this.allowedSpyScan = false;
15190
16030
  this.DeviceCheckerUI && this.DeviceCheckerUI.waitingSpyDevices(false);
15191
16031
  this.DeviceCheckerUI && this.DeviceCheckerUI.isSpyDevicesUI(this.allowedSpyScan);
16032
+ this.onUpdateCallback();
15192
16033
  console.log(error);
15193
16034
  }
15194
16035
  }
@@ -15508,8 +16349,8 @@ var CapturePhoto = class {
15508
16349
  }
15509
16350
  };
15510
16351
 
15511
- // src/extension/extension.ts
15512
- var Extension = class {
16352
+ // src/extension/extensionEasyProctor.ts
16353
+ var ExtensionEasyProctor = class {
15513
16354
  constructor() {
15514
16355
  this.hasExtension = false;
15515
16356
  this.tryes = 0;
@@ -15554,6 +16395,89 @@ var Extension = class {
15554
16395
  }
15555
16396
  };
15556
16397
 
16398
+ // src/extension/extensionEasyCatcher.ts
16399
+ var ExtensionEasyCatcher = class {
16400
+ constructor(options) {
16401
+ this.hasExtension = false;
16402
+ this.tryes = 0;
16403
+ this.responseStart = false;
16404
+ this.options = options || {};
16405
+ }
16406
+ /**
16407
+ * Verifica se a extensão está instalada e ativa.
16408
+ * Retorna o número da versão se encontrada, ou lança erro após timeout.
16409
+ */
16410
+ checkExtensionInstalled(timeoutMs = 2e3) {
16411
+ return new Promise((resolve, reject) => {
16412
+ let handled = false;
16413
+ const handler = (event) => {
16414
+ if (event.source === window && event.data.sender === "easyproctor-extension" && event.data.message_name === "version") {
16415
+ handled = true;
16416
+ window.removeEventListener("message", handler);
16417
+ resolve(event.data.message);
16418
+ }
16419
+ };
16420
+ window.addEventListener("message", handler);
16421
+ window.postMessage({
16422
+ type: "easycatcher",
16423
+ func: "verifyExtensionEasycatcher"
16424
+ }, "*");
16425
+ setTimeout(() => {
16426
+ if (!handled) {
16427
+ window.removeEventListener("message", handler);
16428
+ reject(new Error("Extens\xE3o n\xE3o detectada ou n\xE3o respondeu."));
16429
+ }
16430
+ }, timeoutMs);
16431
+ });
16432
+ }
16433
+ /**
16434
+ * Solicita o JSON da sessão atual capturado pela extensão.
16435
+ */
16436
+ getSessionData(timeoutMs = 5e3) {
16437
+ return new Promise((resolve, reject) => {
16438
+ let handled = false;
16439
+ const handler = (event) => {
16440
+ if (event.source === window && event.data.sender === "easyproctor-extension" && event.data.message_name === "data_response") {
16441
+ handled = true;
16442
+ window.removeEventListener("message", handler);
16443
+ resolve(event.data.payload);
16444
+ }
16445
+ };
16446
+ window.addEventListener("message", handler);
16447
+ window.postMessage({
16448
+ type: "easycatcher",
16449
+ func: "getDataExtensionEasycatcher"
16450
+ }, "*");
16451
+ setTimeout(() => {
16452
+ if (!handled) {
16453
+ window.removeEventListener("message", handler);
16454
+ reject(new Error("Timeout ao aguardar dados da extens\xE3o."));
16455
+ }
16456
+ }, timeoutMs);
16457
+ });
16458
+ }
16459
+ start() {
16460
+ return new Promise((resolve, reject) => {
16461
+ let handled = false;
16462
+ const handler = (event) => {
16463
+ if (event.source === window && event.data.sender === "easyproctor-extension" && event.data.message_name === "started_confirmed") {
16464
+ handled = true;
16465
+ window.removeEventListener("message", handler);
16466
+ resolve(true);
16467
+ }
16468
+ };
16469
+ window.addEventListener("message", handler);
16470
+ window.postMessage({ type: "easycatcher", func: "startExtensionEasycatcher" }, "*");
16471
+ setTimeout(() => {
16472
+ if (!handled) {
16473
+ window.removeEventListener("message", handler);
16474
+ reject(new Error("Timeout: Extens\xE3o n\xE3o confirmou o in\xEDcio."));
16475
+ }
16476
+ }, 3e3);
16477
+ });
16478
+ }
16479
+ };
16480
+
15557
16481
  // src/modules/onChangeDevices.ts
15558
16482
  var onChangeDevices = class {
15559
16483
  constructor(repositoryDevices, proctoringId2, sessionOptions, allRecorders) {
@@ -15785,15 +16709,18 @@ Error: ${e3.message}
15785
16709
  );
15786
16710
  });
15787
16711
  if (result) {
15788
- const fileType = this.getFileType(rec.origin);
15789
- trackers.registerUploadFile(
15790
- this.proctoringId,
15791
- `Upload File
16712
+ let fileType = "";
16713
+ if (rec.origin === "Camera" /* Camera */) {
16714
+ fileType = "Camera";
16715
+ } else if (rec.origin === "Screen" /* Screen */) {
16716
+ fileType = "Screen";
16717
+ } else if (rec.origin === "Mic" /* Mic */) {
16718
+ fileType = "Audio";
16719
+ }
16720
+ trackers.registerUploadFile(this.proctoringId, `Upload File
15792
16721
  Name: ${file.name}
15793
16722
  Type: ${file.type}
15794
- Size: ${file.size}`,
15795
- fileType
15796
- );
16723
+ Size: ${file.size}`, fileType);
15797
16724
  return result;
15798
16725
  }
15799
16726
  }
@@ -18602,11 +19529,15 @@ var ScreenRecorder = class {
18602
19529
  const rawBlob = new Blob(this.blobs, {
18603
19530
  type: "video/webm"
18604
19531
  });
18605
- const fixedBlob = await fixWebmDuration2(rawBlob, this.duration);
18606
- const arrayBuffer = await fixedBlob.arrayBuffer();
19532
+ let finalBlob = rawBlob;
19533
+ if (typeof fixWebmDuration2 === "function") {
19534
+ finalBlob = await fixWebmDuration2(rawBlob, this.duration);
19535
+ } else {
19536
+ console.warn("fixWebmDuration n\xE3o dispon\xEDvel");
19537
+ }
18607
19538
  session.addRecording({
18608
19539
  device: "",
18609
- arrayBuffer,
19540
+ arrayBuffer: await finalBlob.arrayBuffer(),
18610
19541
  origin: "Screen" /* Screen */
18611
19542
  });
18612
19543
  }
@@ -22738,7 +23669,10 @@ var Proctoring = class {
22738
23669
  if (this.context.token === void 0) {
22739
23670
  throw TOKEN_MISSING;
22740
23671
  }
22741
- this.extension = new Extension();
23672
+ if (options.useChallenge) {
23673
+ this.extensionEasycatcher = new ExtensionEasyCatcher();
23674
+ }
23675
+ this.extension = new ExtensionEasyProctor();
22742
23676
  this.extension.addEventListener();
22743
23677
  const baseURL = this.backend.selectBaseUrl(this.context.type);
22744
23678
  const devices = await enumarateDevices();
@@ -23110,6 +24044,61 @@ Error: ` + error
23110
24044
  _screenStream: (_a2 = this.allRecorders.screenRecorder) == null ? void 0 : _a2.screenStream
23111
24045
  };
23112
24046
  }
24047
+ async startChallenge(templateId) {
24048
+ var _a2;
24049
+ if (!this.sessionOptions.useChallenge) {
24050
+ throw new Error("useChallenge is set as false on start method");
24051
+ }
24052
+ await this.extensionEasycatcher.checkExtensionInstalled().catch((err) => {
24053
+ throw new Error("EasyCatcher Extension is not installed");
24054
+ });
24055
+ this.extensionEasycatcher.start();
24056
+ const start = Date.now() - ((_a2 = this.allRecorders.cameraRecorder.getStartTime()) == null ? void 0 : _a2.getTime()) || 0;
24057
+ await this.backend.startChallenge({
24058
+ proctoringId: this.proctoringId,
24059
+ templateId,
24060
+ start
24061
+ }).then((resp) => {
24062
+ console.log(resp);
24063
+ this.challengeId = resp.id;
24064
+ }).catch((reason) => {
24065
+ trackers.registerError(
24066
+ this.proctoringId,
24067
+ "N\xE3o foi poss\xEDvel iniciar desafio!"
24068
+ );
24069
+ throw reason;
24070
+ });
24071
+ this.isChallengeRunning = true;
24072
+ }
24073
+ async stopChallenge() {
24074
+ var _a2;
24075
+ if (!this.isChallengeRunning) {
24076
+ throw new Error("Challenge not started");
24077
+ }
24078
+ try {
24079
+ const sessionData = await this.extensionEasycatcher.getSessionData();
24080
+ const end = Date.now() - ((_a2 = this.allRecorders.cameraRecorder.getStartTime()) == null ? void 0 : _a2.getTime()) || 0;
24081
+ await this.backend.stopChallenge(
24082
+ this.challengeId,
24083
+ {
24084
+ end,
24085
+ data: sessionData
24086
+ }
24087
+ ).catch((reason) => {
24088
+ trackers.registerError(
24089
+ this.proctoringId,
24090
+ "N\xE3o foi poss\xEDvel finalizar o desafio no backend!"
24091
+ );
24092
+ return void 0;
24093
+ });
24094
+ this.isChallengeRunning = false;
24095
+ } catch (error) {
24096
+ trackers.registerError(
24097
+ this.proctoringId,
24098
+ "Erro ao recuperar dados da extens\xE3o: " + error.message
24099
+ );
24100
+ }
24101
+ }
23113
24102
  };
23114
24103
 
23115
24104
  // src/proctoring/SignTerm.ts
@@ -23338,6 +24327,8 @@ function useProctoring(proctoringOptions, enviromentConfig = "prod") {
23338
24327
  return originalStart(parameters2, videoOptions);
23339
24328
  };
23340
24329
  const finish = proctoring.finish.bind(proctoring);
24330
+ const startChallenge = proctoring.startChallenge.bind(proctoring);
24331
+ const stopChallenge = proctoring.stopChallenge.bind(proctoring);
23341
24332
  const pause = proctoring.pause.bind(proctoring);
23342
24333
  const resume = proctoring.resume.bind(proctoring);
23343
24334
  const onFocus = proctoring.setOnFocusCallback.bind(proctoring);
@@ -23362,6 +24353,8 @@ function useProctoring(proctoringOptions, enviromentConfig = "prod") {
23362
24353
  login,
23363
24354
  start,
23364
24355
  finish,
24356
+ startChallenge,
24357
+ stopChallenge,
23365
24358
  onFocus,
23366
24359
  onLostFocus,
23367
24360
  onChangeDevices: onChangeDevices2,