easyproctor-hml 2.7.13 → 2.7.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -30855,13 +30855,16 @@ function isMobileDevice() {
30855
30855
  }
30856
30856
  return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
30857
30857
  }
30858
+ function isSafeBrowser() {
30859
+ return versionVerify() !== "1.0.0.0";
30860
+ }
30858
30861
 
30859
30862
  // src/plugins/recorder.ts
30860
30863
  var proctoringId;
30861
30864
  function setRecorderProctoringId(id) {
30862
30865
  proctoringId = id;
30863
30866
  }
30864
- function recorder(stream4, buffer, onBufferSizeError = false, onBufferSizeErrorCallback, audio = false) {
30867
+ function recorder(stream4, buffer, onBufferSizeError = false, onBufferSizeErrorCallback, audio = false, recorderOpts) {
30865
30868
  let resolvePromise;
30866
30869
  let onBufferSizeInterval;
30867
30870
  let lastEvent;
@@ -30869,6 +30872,7 @@ function recorder(stream4, buffer, onBufferSizeError = false, onBufferSizeErrorC
30869
30872
  bufferSize = 0;
30870
30873
  let startTime;
30871
30874
  let duration = 0;
30875
+ let chunkIndex = 0;
30872
30876
  let recorderOptions = {
30873
30877
  // eslint-disable-next-line no-useless-escape
30874
30878
  mimeType: "video/webm",
@@ -30903,6 +30907,10 @@ function recorder(stream4, buffer, onBufferSizeError = false, onBufferSizeErrorC
30903
30907
  mediaRecorder2.ondataavailable = (e3) => {
30904
30908
  bufferSize = bufferSize + e3.data.size;
30905
30909
  if (e3.data.size > 0) {
30910
+ if (recorderOpts == null ? void 0 : recorderOpts.onChunkAvailable) {
30911
+ recorderOpts.onChunkAvailable(e3.data, chunkIndex);
30912
+ chunkIndex++;
30913
+ }
30906
30914
  buffer.push(e3.data);
30907
30915
  }
30908
30916
  };
@@ -30934,7 +30942,13 @@ function recorder(stream4, buffer, onBufferSizeError = false, onBufferSizeErrorC
30934
30942
  };
30935
30943
  try {
30936
30944
  console.log("State antes do start:", recorder2.state);
30937
- recorder2.start(1e4);
30945
+ chunkIndex = 0;
30946
+ if ((recorderOpts == null ? void 0 : recorderOpts.timeslice) && (recorderOpts == null ? void 0 : recorderOpts.timeslice) > 0) {
30947
+ recorder2.start(recorderOpts.timeslice);
30948
+ } else {
30949
+ recorder2.start(1e4);
30950
+ }
30951
+ bufferSize = 0;
30938
30952
  startTime = new Date(Date.now());
30939
30953
  } catch (e3) {
30940
30954
  console.error("Recorder erro ao chamar start event:", e3);
@@ -30988,9 +31002,6 @@ function recorder(stream4, buffer, onBufferSizeError = false, onBufferSizeErrorC
30988
31002
  console.log("stopRecording Recorder n\xE3o est\xE1 em estado recording");
30989
31003
  resolve();
30990
31004
  }
30991
- stream4.getTracks().forEach((el) => {
30992
- el.stop();
30993
- });
30994
31005
  });
30995
31006
  }
30996
31007
  function pauseRecording() {
@@ -31398,11 +31409,623 @@ var VolumeMeter = class {
31398
31409
  }
31399
31410
  };
31400
31411
 
31412
+ // src/new-flow/chunk/ChunkStorageService.ts
31413
+ var _ChunkStorageService = class _ChunkStorageService {
31414
+ constructor() {
31415
+ this.db = null;
31416
+ }
31417
+ /**
31418
+ * Abre a conexão com o IndexedDB, criando o banco e o object store se necessário.
31419
+ */
31420
+ async connect() {
31421
+ if (this.db) return this.db;
31422
+ return new Promise((resolve, reject) => {
31423
+ const request = window.indexedDB.open(
31424
+ _ChunkStorageService.DB_NAME,
31425
+ _ChunkStorageService.DB_VERSION
31426
+ );
31427
+ request.onerror = () => {
31428
+ var _a2, _b;
31429
+ console.error("IndexedDB error:", request.error);
31430
+ reject(
31431
+ new Error(
31432
+ `N\xE3o foi poss\xEDvel conectar ao IndexedDB para chunks: ${(_a2 = request.error) == null ? void 0 : _a2.name} - ${(_b = request.error) == null ? void 0 : _b.message}`
31433
+ )
31434
+ );
31435
+ };
31436
+ request.onupgradeneeded = () => {
31437
+ const db = request.result;
31438
+ if (db.objectStoreNames.contains(_ChunkStorageService.STORE_NAME)) {
31439
+ db.deleteObjectStore(_ChunkStorageService.STORE_NAME);
31440
+ }
31441
+ const store = db.createObjectStore(_ChunkStorageService.STORE_NAME, {
31442
+ keyPath: "id",
31443
+ autoIncrement: true
31444
+ });
31445
+ store.createIndex("proctoringId", "proctoringId", { unique: false });
31446
+ store.createIndex("uploaded", "uploaded", { unique: false });
31447
+ store.createIndex("proctoringId_uploaded", ["proctoringId", "uploaded"], {
31448
+ unique: false
31449
+ });
31450
+ };
31451
+ request.onsuccess = () => {
31452
+ this.db = request.result;
31453
+ resolve(this.db);
31454
+ };
31455
+ });
31456
+ }
31457
+ /**
31458
+ * Salva um chunk de vídeo no IndexedDB.
31459
+ */
31460
+ async saveChunk(chunk) {
31461
+ const db = await this.connect();
31462
+ return new Promise((resolve, reject) => {
31463
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
31464
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
31465
+ const request = store.add(chunk);
31466
+ request.onsuccess = () => {
31467
+ resolve(request.result);
31468
+ };
31469
+ request.onerror = () => {
31470
+ var _a2;
31471
+ reject(new Error(`Erro ao salvar chunk no IndexedDB: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
31472
+ };
31473
+ });
31474
+ }
31475
+ /**
31476
+ * Retorna todos os chunks pendentes (não enviados) de um proctoringId específico.
31477
+ */
31478
+ async getPendingChunks(proctoringId2) {
31479
+ const db = await this.connect();
31480
+ return new Promise((resolve, reject) => {
31481
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
31482
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
31483
+ const index = store.index("proctoringId_uploaded");
31484
+ const range = IDBKeyRange.only([proctoringId2, 0]);
31485
+ const request = index.getAll(range);
31486
+ request.onsuccess = () => {
31487
+ const chunks = request.result.sort(
31488
+ (a3, b3) => a3.chunkIndex - b3.chunkIndex
31489
+ );
31490
+ resolve(chunks);
31491
+ };
31492
+ request.onerror = () => {
31493
+ var _a2;
31494
+ reject(new Error(`Erro ao buscar chunks pendentes: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
31495
+ };
31496
+ });
31497
+ }
31498
+ /**
31499
+ * Retorna todos os chunks (enviados ou não) de um proctoringId específico.
31500
+ */
31501
+ async getAllChunks(proctoringId2) {
31502
+ const db = await this.connect();
31503
+ return new Promise((resolve, reject) => {
31504
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
31505
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
31506
+ const index = store.index("proctoringId");
31507
+ const range = IDBKeyRange.only(proctoringId2);
31508
+ const request = index.getAll(range);
31509
+ request.onsuccess = () => {
31510
+ const chunks = request.result.sort(
31511
+ (a3, b3) => a3.chunkIndex - b3.chunkIndex
31512
+ );
31513
+ resolve(chunks);
31514
+ };
31515
+ request.onerror = () => {
31516
+ var _a2;
31517
+ reject(new Error(`Erro ao buscar todos os chunks: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
31518
+ };
31519
+ });
31520
+ }
31521
+ /**
31522
+ * Marca um chunk como enviado (uploaded = 1).
31523
+ */
31524
+ async markAsUploaded(chunkId) {
31525
+ const db = await this.connect();
31526
+ return new Promise((resolve, reject) => {
31527
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
31528
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
31529
+ const getRequest = store.get(chunkId);
31530
+ getRequest.onsuccess = () => {
31531
+ const chunk = getRequest.result;
31532
+ if (!chunk) {
31533
+ resolve();
31534
+ return;
31535
+ }
31536
+ chunk.uploaded = 1;
31537
+ const putRequest = store.put(chunk);
31538
+ putRequest.onsuccess = () => resolve();
31539
+ putRequest.onerror = () => {
31540
+ var _a2;
31541
+ return reject(new Error(`Erro ao marcar chunk como enviado: ${(_a2 = putRequest.error) == null ? void 0 : _a2.message}`));
31542
+ };
31543
+ };
31544
+ getRequest.onerror = () => {
31545
+ var _a2;
31546
+ return reject(new Error(`Erro ao buscar chunk para marcar: ${(_a2 = getRequest.error) == null ? void 0 : _a2.message}`));
31547
+ };
31548
+ });
31549
+ }
31550
+ /**
31551
+ * Remove todos os chunks já enviados de um proctoringId para liberar espaço.
31552
+ */
31553
+ async clearUploadedChunks(proctoringId2) {
31554
+ const db = await this.connect();
31555
+ return new Promise((resolve, reject) => {
31556
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
31557
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
31558
+ const index = store.index("proctoringId_uploaded");
31559
+ const range = IDBKeyRange.only([proctoringId2, 1]);
31560
+ const request = index.openCursor(range);
31561
+ request.onsuccess = () => {
31562
+ const cursor = request.result;
31563
+ if (cursor) {
31564
+ cursor.delete();
31565
+ cursor.continue();
31566
+ } else {
31567
+ resolve();
31568
+ }
31569
+ };
31570
+ request.onerror = () => {
31571
+ var _a2;
31572
+ return reject(new Error(`Erro ao limpar chunks enviados: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
31573
+ };
31574
+ });
31575
+ }
31576
+ /**
31577
+ * Remove TODOS os chunks de um proctoringId (limpeza completa pós-finalização).
31578
+ */
31579
+ async clearAllChunks(proctoringId2) {
31580
+ const db = await this.connect();
31581
+ return new Promise((resolve, reject) => {
31582
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
31583
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
31584
+ const index = store.index("proctoringId");
31585
+ const range = IDBKeyRange.only(proctoringId2);
31586
+ const request = index.openCursor(range);
31587
+ request.onsuccess = () => {
31588
+ const cursor = request.result;
31589
+ if (cursor) {
31590
+ cursor.delete();
31591
+ cursor.continue();
31592
+ } else {
31593
+ resolve();
31594
+ }
31595
+ };
31596
+ request.onerror = () => {
31597
+ var _a2;
31598
+ return reject(new Error(`Erro ao limpar todos os chunks: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
31599
+ };
31600
+ });
31601
+ }
31602
+ /**
31603
+ * Verifica se existem chunks pendentes para qualquer proctoringId.
31604
+ * Útil na recuperação pós-crash.
31605
+ */
31606
+ async hasAnyPendingChunks() {
31607
+ const db = await this.connect();
31608
+ return new Promise((resolve, reject) => {
31609
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
31610
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
31611
+ const index = store.index("uploaded");
31612
+ const range = IDBKeyRange.only(0);
31613
+ const request = index.count(range);
31614
+ request.onsuccess = () => {
31615
+ resolve(request.result > 0);
31616
+ };
31617
+ request.onerror = () => {
31618
+ var _a2;
31619
+ return reject(new Error(`Erro ao verificar chunks pendentes: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
31620
+ };
31621
+ });
31622
+ }
31623
+ /**
31624
+ * Retorna todos os proctoringIds que possuem chunks pendentes.
31625
+ * Útil na recuperação pós-crash para saber quais sessões precisam ser finalizadas.
31626
+ */
31627
+ async getPendingProctoringIds() {
31628
+ const db = await this.connect();
31629
+ return new Promise((resolve, reject) => {
31630
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
31631
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
31632
+ const index = store.index("uploaded");
31633
+ const range = IDBKeyRange.only(0);
31634
+ const request = index.getAll(range);
31635
+ request.onsuccess = () => {
31636
+ const chunks = request.result;
31637
+ const ids = [...new Set(chunks.map((c3) => c3.proctoringId))];
31638
+ resolve(ids);
31639
+ };
31640
+ request.onerror = () => {
31641
+ var _a2;
31642
+ return reject(new Error(`Erro ao buscar proctoringIds pendentes: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
31643
+ };
31644
+ });
31645
+ }
31646
+ /**
31647
+ * Fecha a conexão com o banco.
31648
+ */
31649
+ close() {
31650
+ if (this.db) {
31651
+ this.db.close();
31652
+ this.db = null;
31653
+ }
31654
+ }
31655
+ };
31656
+ _ChunkStorageService.DB_NAME = "EasyProctorChunksDb";
31657
+ /** Incrementado para v2 para recriar índices com tipos numéricos em vez de boolean */
31658
+ _ChunkStorageService.DB_VERSION = 2;
31659
+ _ChunkStorageService.STORE_NAME = "chunks";
31660
+ var ChunkStorageService = _ChunkStorageService;
31661
+
31662
+ // src/new-flow/chunk/BackgroundUploadService.ts
31663
+ var DEFAULT_CONFIG = {
31664
+ pollInterval: 5e3,
31665
+ maxRetries: 5,
31666
+ baseRetryDelay: 2e3,
31667
+ cleanAfterUpload: true
31668
+ };
31669
+ var BackgroundUploadService = class _BackgroundUploadService {
31670
+ constructor(proctoringId2, token, backend, chunkStorage, config) {
31671
+ this.pollTimer = null;
31672
+ this.isProcessing = false;
31673
+ this.isRunning = false;
31674
+ /** Mapa de chunkId -> número de tentativas já feitas */
31675
+ this.retryCount = /* @__PURE__ */ new Map();
31676
+ /** GCS Resumable Upload State */
31677
+ this.sessionUrl = null;
31678
+ this.currentOffset = 0;
31679
+ this.totalBytesPurged = 0;
31680
+ this.STORAGE_KEY_PREFIX = "ep_upload_session_";
31681
+ this.GCS_CHUNK_SIZE = 256 * 1024;
31682
+ this.proctoringId = proctoringId2.trim();
31683
+ this.token = token;
31684
+ this.backend = backend;
31685
+ this.chunkStorage = chunkStorage;
31686
+ this.config = { ...DEFAULT_CONFIG, ...config };
31687
+ this.loadSessionState();
31688
+ }
31689
+ loadSessionState() {
31690
+ try {
31691
+ const stored = localStorage.getItem(`${this.STORAGE_KEY_PREFIX}${this.proctoringId}`);
31692
+ if (stored) {
31693
+ const { sessionUrl, currentOffset, totalBytesPurged } = JSON.parse(stored);
31694
+ this.sessionUrl = sessionUrl;
31695
+ this.currentOffset = currentOffset;
31696
+ this.totalBytesPurged = totalBytesPurged || 0;
31697
+ }
31698
+ } catch (e3) {
31699
+ console.warn("[BackgroundUpload] Erro ao carregar estado da sess\xE3o:", e3);
31700
+ }
31701
+ }
31702
+ saveSessionState() {
31703
+ try {
31704
+ localStorage.setItem(
31705
+ `${this.STORAGE_KEY_PREFIX}${this.proctoringId}`,
31706
+ JSON.stringify({
31707
+ sessionUrl: this.sessionUrl,
31708
+ currentOffset: this.currentOffset,
31709
+ totalBytesPurged: this.totalBytesPurged
31710
+ })
31711
+ );
31712
+ } catch (e3) {
31713
+ console.warn("[BackgroundUpload] Erro ao salvar estado da sess\xE3o:", e3);
31714
+ }
31715
+ }
31716
+ clearSessionState() {
31717
+ try {
31718
+ localStorage.removeItem(`${this.STORAGE_KEY_PREFIX}${this.proctoringId}`);
31719
+ this.sessionUrl = null;
31720
+ this.currentOffset = 0;
31721
+ this.totalBytesPurged = 0;
31722
+ } catch (e3) {
31723
+ console.warn("[BackgroundUpload] Erro ao limpar estado da sess\xE3o:", e3);
31724
+ }
31725
+ }
31726
+ /**
31727
+ * Inicia o serviço de upload em background. Faz polling periódico no IndexedDB
31728
+ * para enviar chunks pendentes.
31729
+ */
31730
+ start() {
31731
+ if (this.isRunning) return;
31732
+ this.isRunning = true;
31733
+ console.log(`[BackgroundUpload] Iniciando servi\xE7o para proctoringId: ${this.proctoringId}`);
31734
+ this.processQueue();
31735
+ this.pollTimer = setInterval(() => {
31736
+ this.processQueue(false);
31737
+ }, this.config.pollInterval);
31738
+ }
31739
+ /**
31740
+ * Para o serviço de upload em background.
31741
+ */
31742
+ stop() {
31743
+ this.isRunning = false;
31744
+ if (this.pollTimer) {
31745
+ clearInterval(this.pollTimer);
31746
+ this.pollTimer = null;
31747
+ }
31748
+ console.log(`[BackgroundUpload] Servi\xE7o parado para proctoringId: ${this.proctoringId}`);
31749
+ }
31750
+ /**
31751
+ * Força o processamento de todos os chunks pendentes e encerra a sessão GCS.
31752
+ * Útil quando a gravação é finalizada.
31753
+ */
31754
+ async flush() {
31755
+ console.log(`[BackgroundUpload] Flush: enviando todos os chunks pendentes e finalizando...`);
31756
+ let waitAttempts = 0;
31757
+ while (this.isProcessing && waitAttempts < 10) {
31758
+ await this.sleep(1e3);
31759
+ waitAttempts++;
31760
+ }
31761
+ let flushRetries = 0;
31762
+ const maxFlushRetries = 3;
31763
+ while (flushRetries < maxFlushRetries) {
31764
+ try {
31765
+ await this.processQueue(true);
31766
+ console.log(`[BackgroundUpload] Flush completado com sucesso.`);
31767
+ return;
31768
+ } catch (error) {
31769
+ flushRetries++;
31770
+ console.error(`[BackgroundUpload] Erro no flush (tentativa ${flushRetries}/${maxFlushRetries}):`, error);
31771
+ if (flushRetries < maxFlushRetries) {
31772
+ await this.sleep(2e3);
31773
+ }
31774
+ }
31775
+ }
31776
+ throw new Error(`[BackgroundUpload] Falha ao finalizar upload ap\xF3s ${maxFlushRetries} tentativas.`);
31777
+ }
31778
+ /**
31779
+ * Sincroniza o offset local com o estado real no Google Cloud Storage.
31780
+ */
31781
+ async syncOffset() {
31782
+ if (!this.sessionUrl) return 0;
31783
+ try {
31784
+ console.log(`[BackgroundUpload] Sincronizando offset com GCS...`);
31785
+ const response = await fetch(this.sessionUrl, {
31786
+ method: "PUT",
31787
+ headers: {
31788
+ "Content-Range": "bytes */*"
31789
+ }
31790
+ });
31791
+ console.log(`[BackgroundUpload] Status da sincroniza\xE7\xE3o (syncOffset): ${response.status}`);
31792
+ if (response.status === 308) {
31793
+ const range = response.headers.get("Range");
31794
+ if (range) {
31795
+ const lastByte = parseInt(range.split("-")[1], 10);
31796
+ this.currentOffset = lastByte + 1;
31797
+ this.saveSessionState();
31798
+ console.log(`[BackgroundUpload] Offset sincronizado: ${this.currentOffset}`);
31799
+ } else {
31800
+ this.currentOffset = 0;
31801
+ }
31802
+ } else if (response.ok || response.status === 201) {
31803
+ console.log("[BackgroundUpload] Sincroniza\xE7\xE3o indicou upload JA FINALIZADO.");
31804
+ this.currentOffset = -1;
31805
+ } else {
31806
+ console.warn(`[BackgroundUpload] Status inesperado na sincroniza\xE7\xE3o: ${response.status}`);
31807
+ }
31808
+ } catch (error) {
31809
+ console.warn("[BackgroundUpload] Erro ao sincronizar offset:", error);
31810
+ }
31811
+ return this.currentOffset;
31812
+ }
31813
+ /**
31814
+ * Verifica e envia chunks pendentes para o backend.
31815
+ * @param isFinal Se true, não alinha a 256KB e fecha a sessão com /TOTAL no header.
31816
+ */
31817
+ async processQueue(isFinal = false) {
31818
+ var _a2, _b;
31819
+ if (this.isProcessing) return;
31820
+ this.isProcessing = true;
31821
+ try {
31822
+ if (this.sessionUrl) {
31823
+ await this.syncOffset();
31824
+ if (this.currentOffset === -1) {
31825
+ console.log("[BackgroundUpload] Sess\xE3o j\xE1 finalizada no servidor.");
31826
+ this.clearSessionState();
31827
+ this.isProcessing = false;
31828
+ return;
31829
+ }
31830
+ }
31831
+ const allChunks = await this.chunkStorage.getAllChunks(this.proctoringId);
31832
+ const pendingChunks = allChunks.filter((c3) => c3.uploaded === 0);
31833
+ if (pendingChunks.length === 0 && !isFinal) {
31834
+ this.isProcessing = false;
31835
+ return;
31836
+ }
31837
+ console.log(`[BackgroundUpload] ${pendingChunks.length} chunks pendentes encontrados. Modo final: ${isFinal}`);
31838
+ let virtualStart = this.totalBytesPurged;
31839
+ const chunksWithMeta = allChunks.map((c3) => {
31840
+ const start = virtualStart;
31841
+ const end = start + c3.blob.size - 1;
31842
+ virtualStart += c3.blob.size;
31843
+ return { chunk: c3, start, end };
31844
+ });
31845
+ let combinedBlobParts = [];
31846
+ let lastProcessedChunkId = null;
31847
+ let finalChunkIndex = 0;
31848
+ let mimeType = pendingChunks[0].mimeType;
31849
+ for (const meta of chunksWithMeta) {
31850
+ if (this.currentOffset > meta.end) continue;
31851
+ const sliceStart = Math.max(0, this.currentOffset - meta.start);
31852
+ const chunkSlice = meta.chunk.blob.slice(sliceStart);
31853
+ combinedBlobParts.push(chunkSlice);
31854
+ lastProcessedChunkId = meta.chunk.id;
31855
+ finalChunkIndex = meta.chunk.chunkIndex;
31856
+ }
31857
+ if (combinedBlobParts.length === 0 && !isFinal) {
31858
+ this.isProcessing = false;
31859
+ return;
31860
+ }
31861
+ let fullBlob = new Blob(combinedBlobParts, { type: mimeType });
31862
+ let sendableSize = fullBlob.size;
31863
+ let totalSizeForHeader = void 0;
31864
+ if (!isFinal) {
31865
+ sendableSize = Math.floor(fullBlob.size / this.GCS_CHUNK_SIZE) * this.GCS_CHUNK_SIZE;
31866
+ if (sendableSize === 0) {
31867
+ console.log("[BackgroundUpload] Dados insuficientes para atingir 256KB. Aguardando novo chunk...");
31868
+ this.isProcessing = false;
31869
+ return;
31870
+ }
31871
+ } else {
31872
+ totalSizeForHeader = virtualStart;
31873
+ }
31874
+ const blobToSend = fullBlob.slice(0, sendableSize);
31875
+ try {
31876
+ await this.uploadData(blobToSend, mimeType, finalChunkIndex, totalSizeForHeader);
31877
+ for (const meta of chunksWithMeta) {
31878
+ if (meta.chunk.uploaded === 0 && meta.end < this.currentOffset) {
31879
+ await this.chunkStorage.markAsUploaded(meta.chunk.id);
31880
+ this.retryCount.delete(meta.chunk.id);
31881
+ (_a2 = this.onChunkUploaded) == null ? void 0 : _a2.call(this, meta.chunk.id, meta.chunk.chunkIndex);
31882
+ console.log(`[BackgroundUpload] Chunk ${meta.chunk.chunkIndex} marcado como enviado.`);
31883
+ }
31884
+ }
31885
+ if (this.config.cleanAfterUpload) {
31886
+ const chunksToClear = chunksWithMeta.filter((meta) => meta.chunk.uploaded === 1 || meta.chunk.uploaded === 0 && meta.end < this.currentOffset);
31887
+ const sizePurged = chunksToClear.reduce((acc, meta) => acc + meta.chunk.blob.size, 0);
31888
+ await this.chunkStorage.clearUploadedChunks(this.proctoringId);
31889
+ if (sizePurged > 0) {
31890
+ this.totalBytesPurged += sizePurged;
31891
+ this.saveSessionState();
31892
+ console.log(`[BackgroundUpload] ${sizePurged} bytes limpos do armazenamento local. Total purgado: ${this.totalBytesPurged}`);
31893
+ }
31894
+ }
31895
+ if (isFinal) {
31896
+ this.clearSessionState();
31897
+ }
31898
+ } catch (error) {
31899
+ console.error("[BackgroundUpload] Falha no upload:", error);
31900
+ (_b = this.onUploadError) == null ? void 0 : _b.call(this, lastProcessedChunkId || 0, error);
31901
+ }
31902
+ } catch (error) {
31903
+ console.error("[BackgroundUpload] Erro ao processar fila:", error);
31904
+ } finally {
31905
+ this.isProcessing = false;
31906
+ }
31907
+ }
31908
+ /**
31909
+ * Faz o upload bruto de dados para a sessão GCS.
31910
+ */
31911
+ async uploadData(blob, mimeType, chunkIndex, totalSize) {
31912
+ const fileName = `EP_${this.proctoringId}_camera_0.webm`;
31913
+ if (!this.sessionUrl) {
31914
+ const initiateUrl = await this.backend.initiateUpload(this.token, `${this.proctoringId}/${fileName}`, mimeType);
31915
+ const startResponse = await fetch(initiateUrl, {
31916
+ method: "POST",
31917
+ headers: { "x-goog-resumable": "start", "Content-Type": mimeType }
31918
+ });
31919
+ if (!startResponse.ok) throw new Error(`Falha ao iniciar: ${startResponse.status}`);
31920
+ this.sessionUrl = startResponse.headers.get("Location");
31921
+ if (!this.sessionUrl) throw new Error("Location header ausente");
31922
+ try {
31923
+ const urlObj = new URL(this.sessionUrl);
31924
+ const pathParts = urlObj.pathname.split("/");
31925
+ let bucket = pathParts[1];
31926
+ let object = decodeURIComponent(pathParts.slice(2).join("/"));
31927
+ if (pathParts.includes("b") && pathParts.includes("o")) {
31928
+ const bIdx = pathParts.indexOf("b") + 1;
31929
+ const oIdx = pathParts.indexOf("o") + 1;
31930
+ bucket = pathParts[bIdx];
31931
+ object = decodeURIComponent(pathParts.slice(oIdx).join("/"));
31932
+ }
31933
+ console.log(`[BackgroundUpload] Sess\xE3o Iniciada -> Bucket: ${bucket}, Objeto: ${object}`);
31934
+ } catch (e3) {
31935
+ console.log(`[BackgroundUpload] Sess\xE3o Iniciada. URL: ${this.sessionUrl}`);
31936
+ }
31937
+ this.currentOffset = 0;
31938
+ this.saveSessionState();
31939
+ } else {
31940
+ console.log(`[BackgroundUpload] Usando sess\xE3o GCS existente: ${this.sessionUrl}`);
31941
+ }
31942
+ const start = this.currentOffset;
31943
+ const end = start + blob.size - 1;
31944
+ const totalHeader = totalSize !== void 0 ? totalSize.toString() : "*";
31945
+ const contentRangeHeader = blob.size === 0 && totalSize !== void 0 ? `bytes */${totalHeader}` : `bytes ${start}-${end}/${totalHeader}`;
31946
+ console.log(`[BackgroundUpload] Enviando ${blob.size > 0 ? "dados" : "finaliza\xE7\xE3o"}: ${contentRangeHeader} (Size: ${blob.size})`);
31947
+ const response = await fetch(this.sessionUrl, {
31948
+ method: "PUT",
31949
+ headers: { "Content-Range": contentRangeHeader },
31950
+ body: blob.size > 0 ? blob : null
31951
+ // Usa null para garantir corpo vazio se necessário
31952
+ });
31953
+ console.log(`[BackgroundUpload] Resposta GCS (uploadData): ${response.status}`);
31954
+ if (response.status !== 200 && response.status !== 201 && response.status !== 308) {
31955
+ const errorText = await response.text();
31956
+ console.error(`[BackgroundUpload] Erro GCS: ${errorText}`);
31957
+ throw new Error(`Status HTTP inesperado: ${response.status}`);
31958
+ }
31959
+ const rangeHeader = response.headers.get("Range");
31960
+ if (rangeHeader) {
31961
+ const lastByte = parseInt(rangeHeader.split("-")[1], 10);
31962
+ this.currentOffset = lastByte + 1;
31963
+ } else {
31964
+ this.currentOffset += blob.size;
31965
+ }
31966
+ this.saveSessionState();
31967
+ trackers.registerUploadFile(
31968
+ this.proctoringId,
31969
+ `GCS Stream Upload
31970
+ Size: ${blob.size}
31971
+ Range: ${start}-${end}
31972
+ Last Index: ${chunkIndex}`,
31973
+ "CameraChunk"
31974
+ );
31975
+ }
31976
+ /**
31977
+ * Método estático para recuperação pós-crash.
31978
+ * Verifica o IndexedDB em busca de chunks pendentes de qualquer sessão
31979
+ * e tenta enviar.
31980
+ */
31981
+ static async recoverPendingUploads(backend, token) {
31982
+ const chunkStorage = new ChunkStorageService();
31983
+ const recoveredIds = [];
31984
+ try {
31985
+ const pendingIds = await chunkStorage.getPendingProctoringIds();
31986
+ if (pendingIds.length === 0) {
31987
+ console.log("[BackgroundUpload] Nenhum chunk pendente encontrado para recupera\xE7\xE3o.");
31988
+ return recoveredIds;
31989
+ }
31990
+ console.log(
31991
+ `[BackgroundUpload] Recupera\xE7\xE3o p\xF3s-crash: ${pendingIds.length} sess\xE3o(\xF5es) com chunks pendentes.`
31992
+ );
31993
+ for (const proctoringId2 of pendingIds) {
31994
+ try {
31995
+ const service = new _BackgroundUploadService(
31996
+ proctoringId2,
31997
+ token,
31998
+ backend,
31999
+ chunkStorage,
32000
+ { cleanAfterUpload: true }
32001
+ );
32002
+ await service.flush();
32003
+ recoveredIds.push(proctoringId2);
32004
+ console.log(
32005
+ `[BackgroundUpload] Chunks da sess\xE3o ${proctoringId2} recuperados com sucesso.`
32006
+ );
32007
+ } catch (error) {
32008
+ console.error(
32009
+ `[BackgroundUpload] Erro ao recuperar chunks da sess\xE3o ${proctoringId2}:`,
32010
+ error
32011
+ );
32012
+ }
32013
+ }
32014
+ } catch (error) {
32015
+ console.error("[BackgroundUpload] Erro geral na recupera\xE7\xE3o:", error);
32016
+ }
32017
+ return recoveredIds;
32018
+ }
32019
+ sleep(ms2) {
32020
+ return new Promise((resolve) => setTimeout(resolve, ms2));
32021
+ }
32022
+ };
32023
+
31401
32024
  // src/new-flow/recorders/CameraRecorder.ts
31402
32025
  var import_jszip_min = __toESM(require_jszip_min());
31403
32026
  var pkg = require_fix_webm_duration();
31404
32027
  var fixWebmDuration = pkg.default || pkg;
31405
- var CameraRecorder = class {
32028
+ var _CameraRecorder = class _CameraRecorder {
31406
32029
  constructor(options, videoOptions, paramsConfig, backend, backendToken) {
31407
32030
  this.blobs = [];
31408
32031
  this.paramsConfig = {
@@ -31455,6 +32078,13 @@ var CameraRecorder = class {
31455
32078
  this.videoElement = null;
31456
32079
  this.duration = 0;
31457
32080
  this.stopped = false;
32081
+ this.backgroundUpload = null;
32082
+ this.chunkIndex = 0;
32083
+ /** Lista de promises de chunks sendo salvos no IndexedDB para evitar race conditions no stop */
32084
+ this.pendingChunkSaves = [];
32085
+ // Handlers bound para poder remover os listeners depois
32086
+ this.boundVisibilityHandler = null;
32087
+ this.boundPageHideHandler = null;
31458
32088
  this.currentRetries = 0;
31459
32089
  this.packageCount = 0;
31460
32090
  this.failedUploads = 0;
@@ -31465,10 +32095,122 @@ var CameraRecorder = class {
31465
32095
  this.backendToken = backendToken;
31466
32096
  paramsConfig && (this.paramsConfig = paramsConfig);
31467
32097
  }
32098
+ /**
32099
+ * Determina se o fluxo de chunks e lifecycle deve estar ativo.
32100
+ * Retorna true se:
32101
+ * 1. O proctoringId já foi definido (ou seja, estamos em uma sessão real, NÃO no checkDevices)
32102
+ * 2. E (`useChunkRecording` foi explicitamente setado como true OU o dispositivo é mobile)
32103
+ */
32104
+ get isChunkEnabled() {
32105
+ return !!this.proctoringId && this.options.proctoringType === "REALTIME" && !isSafeBrowser();
32106
+ }
31468
32107
  setProctoringId(proctoringId2) {
31469
32108
  this.proctoringId = proctoringId2;
31470
32109
  this.proctoringId && this.backend && (this.upload = new UploadService(this.proctoringId, this.backend));
31471
32110
  setRecorderProctoringId(proctoringId2);
32111
+ if (this.isChunkEnabled) {
32112
+ this.chunkStorage = new ChunkStorageService();
32113
+ if (this.backend && this.backendToken) {
32114
+ this.backgroundUpload = new BackgroundUploadService(
32115
+ this.proctoringId,
32116
+ this.backendToken,
32117
+ this.backend,
32118
+ this.chunkStorage,
32119
+ { pollInterval: 5e3, maxRetries: 5, cleanAfterUpload: true }
32120
+ );
32121
+ }
32122
+ this.persistSessionState("IN_PROGRESS");
32123
+ console.log(
32124
+ `[CameraRecorder] Chunk recording ATIVO (type: ${this.options.proctoringType}, mobile: ${isMobileDevice()})`
32125
+ );
32126
+ } else {
32127
+ console.log(
32128
+ `[CameraRecorder] Chunk recording INATIVO (type: ${this.options.proctoringType}) \u2014 modo cl\xE1ssico.`
32129
+ );
32130
+ }
32131
+ }
32132
+ // ========================
32133
+ // Session State Persistence (localStorage)
32134
+ // ========================
32135
+ persistSessionState(status) {
32136
+ try {
32137
+ const data = {
32138
+ proctoringId: this.proctoringId,
32139
+ status,
32140
+ timestamp: Date.now()
32141
+ };
32142
+ localStorage.setItem(_CameraRecorder.LS_SESSION_KEY, JSON.stringify(data));
32143
+ } catch (e3) {
32144
+ console.warn("[CameraRecorder] N\xE3o foi poss\xEDvel salvar estado no localStorage:", e3);
32145
+ }
32146
+ }
32147
+ clearSessionState() {
32148
+ try {
32149
+ localStorage.removeItem(_CameraRecorder.LS_SESSION_KEY);
32150
+ } catch (e3) {
32151
+ console.warn("[CameraRecorder] N\xE3o foi poss\xEDvel limpar estado do localStorage:", e3);
32152
+ }
32153
+ }
32154
+ /**
32155
+ * Verifica se existe uma sessão ativa anterior no localStorage.
32156
+ * Retorna os dados da sessão se ela estiver em IN_PROGRESS, null caso contrário.
32157
+ */
32158
+ static checkForActiveSession() {
32159
+ try {
32160
+ const raw = localStorage.getItem(_CameraRecorder.LS_SESSION_KEY);
32161
+ if (!raw) return null;
32162
+ const data = JSON.parse(raw);
32163
+ if (data.status === "IN_PROGRESS") return data;
32164
+ return null;
32165
+ } catch {
32166
+ return null;
32167
+ }
32168
+ }
32169
+ // ========================
32170
+ // Page Lifecycle Management
32171
+ // ========================
32172
+ setupLifecycleListeners() {
32173
+ this.boundVisibilityHandler = () => this.handleVisibilityChange();
32174
+ this.boundPageHideHandler = () => this.handlePageHide();
32175
+ document.addEventListener("visibilitychange", this.boundVisibilityHandler);
32176
+ window.addEventListener("pagehide", this.boundPageHideHandler);
32177
+ }
32178
+ removeLifecycleListeners() {
32179
+ if (this.boundVisibilityHandler) {
32180
+ document.removeEventListener("visibilitychange", this.boundVisibilityHandler);
32181
+ this.boundVisibilityHandler = null;
32182
+ }
32183
+ if (this.boundPageHideHandler) {
32184
+ window.removeEventListener("pagehide", this.boundPageHideHandler);
32185
+ this.boundPageHideHandler = null;
32186
+ }
32187
+ }
32188
+ handleVisibilityChange() {
32189
+ var _a2;
32190
+ if (document.visibilityState === "hidden") {
32191
+ console.log("[CameraRecorder] P\xE1gina ficou invis\xEDvel \u2014 sess\xE3o potencialmente interrompida.");
32192
+ this.persistSessionState("INTERRUPTED");
32193
+ this.proctoringId && trackers.registerError(
32194
+ this.proctoringId,
32195
+ "Visibility API: P\xE1gina ficou oculta (hidden). Poss\xEDvel troca de app ou minimiza\xE7\xE3o."
32196
+ );
32197
+ } else if (document.visibilityState === "visible") {
32198
+ console.log("[CameraRecorder] P\xE1gina vis\xEDvel novamente \u2014 verificando estado da grava\xE7\xE3o.");
32199
+ this.persistSessionState("IN_PROGRESS");
32200
+ this.proctoringId && trackers.registerError(
32201
+ this.proctoringId,
32202
+ "Visibility API: P\xE1gina voltou a ficar vis\xEDvel. Usu\xE1rio retornou."
32203
+ );
32204
+ (_a2 = this.onVisibilityRestored) == null ? void 0 : _a2.call(this);
32205
+ }
32206
+ }
32207
+ handlePageHide() {
32208
+ console.log("[CameraRecorder] pagehide detectado \u2014 persistindo estado.");
32209
+ this.persistSessionState("INTERRUPTED");
32210
+ this.proctoringId && trackers.registerError(
32211
+ this.proctoringId,
32212
+ "Page Lifecycle: pagehide event detectado. P\xE1gina est\xE1 sendo descarregada."
32213
+ );
31472
32214
  }
31473
32215
  async initializeDetectors() {
31474
32216
  var _a2, _b, _c2;
@@ -31621,9 +32363,15 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31621
32363
  await new Promise((r3) => setTimeout(r3, 300));
31622
32364
  }
31623
32365
  async startRecording() {
31624
- var _a2, _b, _c2, _d, _e3, _f, _g;
32366
+ var _a2, _b, _c2, _d, _e3, _f, _g, _h;
31625
32367
  await this.startStream();
31626
32368
  await this.attachAndWarmup(this.cameraStream);
32369
+ const recorderOpts = this.isChunkEnabled ? {
32370
+ timeslice: _CameraRecorder.CHUNK_TIMESLICE_MS,
32371
+ onChunkAvailable: (blob, idx) => {
32372
+ this.handleNewChunk(blob, idx);
32373
+ }
32374
+ } : {};
31627
32375
  const {
31628
32376
  startRecording,
31629
32377
  stopRecording,
@@ -31639,7 +32387,8 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31639
32387
  this.blobs,
31640
32388
  this.options.onBufferSizeError,
31641
32389
  (e3) => this.bufferError(e3),
31642
- false
32390
+ false,
32391
+ recorderOpts
31643
32392
  );
31644
32393
  this.recordingStart = startRecording;
31645
32394
  this.recordingStop = stopRecording;
@@ -31649,13 +32398,18 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31649
32398
  this.getBufferSize = getBufferSize;
31650
32399
  this.getStartTime = getStartTime;
31651
32400
  this.getDuration = getDuration;
32401
+ this.chunkIndex = 0;
32402
+ if (this.isChunkEnabled) {
32403
+ (_a2 = this.backgroundUpload) == null ? void 0 : _a2.start();
32404
+ this.setupLifecycleListeners();
32405
+ }
31652
32406
  try {
31653
32407
  await new Promise((r3) => setTimeout(r3, 500));
31654
32408
  await this.recordingStart();
31655
32409
  } catch (error) {
31656
32410
  console.log("Camera Recorder error", error);
31657
32411
  this.stopRecording();
31658
- const maxRetries = ((_a2 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _a2.maxRetries) || 3;
32412
+ const maxRetries = ((_b = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _b.maxRetries) || 3;
31659
32413
  if (this.currentRetries < maxRetries) {
31660
32414
  console.log("Camera Recorder retry", this.currentRetries);
31661
32415
  this.currentRetries++;
@@ -31665,13 +32419,13 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31665
32419
  }
31666
32420
  }
31667
32421
  this.stopped = false;
31668
- 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)) {
32422
+ 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)) {
31669
32423
  await this.initializeDetectors();
31670
32424
  }
31671
- if ((_e3 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _e3.detectFace) {
32425
+ if ((_f = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _f.detectFace) {
31672
32426
  await this.faceDetection.enableCam(this.cameraStream);
31673
32427
  }
31674
- if (((_f = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _f.detectPerson) || ((_g = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _g.detectCellPhone)) {
32428
+ if (((_g = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _g.detectPerson) || ((_h = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _h.detectCellPhone)) {
31675
32429
  await this.objectDetection.enableCam(this.cameraStream);
31676
32430
  }
31677
32431
  this.filesToUpload = [];
@@ -31730,6 +32484,50 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31730
32484
  await this.sendPackage();
31731
32485
  await this.filesToUpload.splice(0, this.filesToUpload.length);
31732
32486
  }
32487
+ if (this.isChunkEnabled) {
32488
+ if (this.backgroundUpload) {
32489
+ try {
32490
+ if (this.pendingChunkSaves.length > 0) {
32491
+ console.log(`[CameraRecorder] Aguardando ${this.pendingChunkSaves.length} salvamentos de chunks pendentes...`);
32492
+ await Promise.all(this.pendingChunkSaves);
32493
+ }
32494
+ await this.backgroundUpload.flush();
32495
+ } catch (e3) {
32496
+ console.warn("[CameraRecorder] Erro ao fazer flush dos chunks:", e3);
32497
+ }
32498
+ this.backgroundUpload.stop();
32499
+ }
32500
+ this.removeLifecycleListeners();
32501
+ this.persistSessionState("FINISHED");
32502
+ }
32503
+ }
32504
+ /**
32505
+ * Callback chamado pelo recorder a cada novo chunk de vídeo disponível.
32506
+ * Salva o chunk no IndexedDB para persistência e recuperação.
32507
+ */
32508
+ async handleNewChunk(blob, idx) {
32509
+ if (!this.proctoringId || !this.chunkStorage) return;
32510
+ const savePromise = (async () => {
32511
+ var _a2;
32512
+ try {
32513
+ await this.chunkStorage.saveChunk({
32514
+ proctoringId: this.proctoringId,
32515
+ chunkIndex: this.chunkIndex,
32516
+ blob,
32517
+ timestamp: Date.now(),
32518
+ uploaded: 0,
32519
+ mimeType: ((_a2 = this.recorderOptions) == null ? void 0 : _a2.mimeType) || "video/webm"
32520
+ });
32521
+ this.chunkIndex++;
32522
+ console.log(`[CameraRecorder] Chunk ${this.chunkIndex - 1} salvo no IndexedDB.`);
32523
+ } catch (error) {
32524
+ console.error("[CameraRecorder] Erro ao salvar chunk no IndexedDB:", error);
32525
+ }
32526
+ })();
32527
+ this.pendingChunkSaves.push(savePromise);
32528
+ savePromise.finally(() => {
32529
+ this.pendingChunkSaves = this.pendingChunkSaves.filter((p3) => p3 !== savePromise);
32530
+ });
31733
32531
  }
31734
32532
  async pauseRecording() {
31735
32533
  await this.recordingPause();
@@ -31865,16 +32663,30 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31865
32663
  if (this.blobs != null)
31866
32664
  trackers.registerSaveOnSession(
31867
32665
  this.proctoringId,
31868
- `Blobs Length: ${this.blobs.length} Buffer Size: ${this.getBufferSize()} `
32666
+ `Blobs Length: ${this.blobs.length} Buffer Size: ${this.getBufferSize()} ChunkEnabled: ${this.isChunkEnabled}`
31869
32667
  );
31870
32668
  const settings = this.cameraStream.getVideoTracks()[0].getSettings();
31871
32669
  const settingsAudio = this.cameraStream.getAudioTracks()[0].getSettings();
31872
32670
  if (this.options.proctoringType == "VIDEO" || this.options.proctoringType == "REALTIME" || this.options.proctoringType == "IMAGE" && ((_a2 = this.paramsConfig.imageBehaviourParameters) == null ? void 0 : _a2.saveVideo)) {
32671
+ if (this.isChunkEnabled) {
32672
+ const isStable = await this.checkInternetStability();
32673
+ if (isStable) {
32674
+ } else {
32675
+ if (this.backend && this.backendToken && this.proctoringId) {
32676
+ const fileName = `EP_${this.proctoringId}_camera_0.webm`;
32677
+ const objectName = `${this.proctoringId}/${fileName}`;
32678
+ const isUploaded = await this.backend.checkUpload(this.backendToken, objectName, "video/webm");
32679
+ if (isUploaded) {
32680
+ this.chunkStorage && await this.chunkStorage.clearAllChunks(session.id);
32681
+ return;
32682
+ }
32683
+ }
32684
+ }
32685
+ }
31873
32686
  const rawBlob = new Blob(this.blobs, {
31874
32687
  type: ((_b = this.recorderOptions) == null ? void 0 : _b.mimeType) || "video/webm"
31875
32688
  });
31876
32689
  const fixedBlob = await fixWebmDuration(rawBlob, this.duration);
31877
- const arrayBuffer = await fixedBlob.arrayBuffer();
31878
32690
  session.addRecording({
31879
32691
  device: `Audio
31880
32692
  Sample Rate: ${settingsAudio.sampleRate}
@@ -31882,7 +32694,7 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31882
32694
 
31883
32695
  Video:
31884
32696
  ${JSON.stringify(this.recorderOptions)}`,
31885
- arrayBuffer,
32697
+ arrayBuffer: await fixedBlob.arrayBuffer(),
31886
32698
  origin: "Camera" /* Camera */
31887
32699
  });
31888
32700
  }
@@ -31897,6 +32709,28 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31897
32709
  });
31898
32710
  });
31899
32711
  }
32712
+ /**
32713
+ * Verifica se a internet está estável para realizar o upload do vídeo na íntegra.
32714
+ */
32715
+ async checkInternetStability() {
32716
+ var _a2;
32717
+ if (!navigator.onLine) return false;
32718
+ try {
32719
+ const controller = new AbortController();
32720
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
32721
+ const baseUrl = (_a2 = this.backend) == null ? void 0 : _a2.getBaseUrl();
32722
+ if (!baseUrl) return true;
32723
+ const response = await fetch(`${baseUrl}/Client/health`, {
32724
+ method: "GET",
32725
+ signal: controller.signal
32726
+ });
32727
+ clearTimeout(timeoutId);
32728
+ return response.status < 500;
32729
+ } catch (e3) {
32730
+ console.warn("[CameraRecorder] Internet inst\xE1vel ou lenta detectada para upload integral.");
32731
+ return false;
32732
+ }
32733
+ }
31900
32734
  onNoiseDetected() {
31901
32735
  var _a2, _b, _c2;
31902
32736
  if (this.options.proctoringType === "REALTIME") return;
@@ -31921,6 +32755,14 @@ Setting: ${JSON.stringify(settings, null, 2)}`
31921
32755
  this.noiseWait++;
31922
32756
  }
31923
32757
  };
32758
+ // ========================
32759
+ // Chunk & Lifecycle
32760
+ // ========================
32761
+ /** Intervalo de cada chunk em ms (padrão: 60 segundos) */
32762
+ _CameraRecorder.CHUNK_TIMESLICE_MS = 6e4;
32763
+ /** Chave do localStorage para persistir estado da sessão */
32764
+ _CameraRecorder.LS_SESSION_KEY = "ep_proctoring_session";
32765
+ var CameraRecorder = _CameraRecorder;
31924
32766
 
31925
32767
  // src/new-flow/checkers/DeviceCheckerUI.ts
31926
32768
  var DeviceCheckerUI = class {
@@ -33839,19 +34681,9 @@ var ProctoringUploader = class {
33839
34681
  }
33840
34682
  toFile(rec) {
33841
34683
  const suffix = rec.origin === "Screen" /* Screen */ ? "screen" : rec.origin === "Camera" /* Camera */ ? "camera" : "audio";
33842
- const name = `EP_${this.proctoringId}_${suffix}_0.webm`;
34684
+ const name = `EP_${this.session.id}_${suffix}_0.webm`;
33843
34685
  return new File([rec.arrayBuffer], name, { type: "video/webm" });
33844
34686
  }
33845
- getFileType(origin2) {
33846
- switch (origin2) {
33847
- case "Camera" /* Camera */:
33848
- return "Camera";
33849
- case "Screen" /* Screen */:
33850
- return "Screen";
33851
- case "Mic" /* Mic */:
33852
- return "Audio";
33853
- }
33854
- }
33855
34687
  async uploadFile(rec, token, onProgress) {
33856
34688
  const file = this.toFile(rec);
33857
34689
  for await (const uploadService of this.uploadServices) {
@@ -33863,34 +34695,36 @@ var ProctoringUploader = class {
33863
34695
  }
33864
34696
  },
33865
34697
  token
33866
- ).catch(async (e3) => {
33867
- console.log("Upload File Error", e3), trackers.registerError(
33868
- this.proctoringId,
33869
- `Upload File
34698
+ ).catch(
34699
+ async (e3) => {
34700
+ console.log("Upload File Error", e3), trackers.registerError(this.proctoringId, `Upload File
33870
34701
  Name: ${file.name}
33871
34702
  Error: ${e3.message}
33872
- Size: ${e3.error}`
33873
- );
33874
- await uploadService.upload(
33875
- {
33876
- file,
33877
- onProgress: (progress) => {
33878
- if (onProgress) onProgress(progress);
33879
- }
33880
- },
33881
- token
33882
- );
33883
- });
34703
+ Size: ${e3.error}`);
34704
+ await uploadService.upload(
34705
+ {
34706
+ file,
34707
+ onProgress: (progress) => {
34708
+ if (onProgress) onProgress(progress);
34709
+ }
34710
+ },
34711
+ token
34712
+ );
34713
+ }
34714
+ );
33884
34715
  if (result) {
33885
- const fileType = this.getFileType(rec.origin);
33886
- trackers.registerUploadFile(
33887
- this.proctoringId,
33888
- `Upload File
34716
+ let fileType = "";
34717
+ if (rec.origin === "Camera" /* Camera */) {
34718
+ fileType = "Camera";
34719
+ } else if (rec.origin === "Screen" /* Screen */) {
34720
+ fileType = "Screen";
34721
+ } else if (rec.origin === "Mic" /* Mic */) {
34722
+ fileType = "Audio";
34723
+ }
34724
+ trackers.registerUploadFile(this.proctoringId, `Upload File
33889
34725
  Name: ${file.name}
33890
34726
  Type: ${file.type}
33891
- Size: ${file.size}`,
33892
- fileType
33893
- );
34727
+ Size: ${file.size}`, fileType);
33894
34728
  return result;
33895
34729
  }
33896
34730
  }
@@ -36700,10 +37534,9 @@ var ScreenRecorder = class {
36700
37534
  type: "video/webm"
36701
37535
  });
36702
37536
  const fixedBlob = await fixWebmDuration2(rawBlob, this.duration);
36703
- const arrayBuffer = await fixedBlob.arrayBuffer();
36704
37537
  session.addRecording({
36705
37538
  device: "",
36706
- arrayBuffer,
37539
+ arrayBuffer: await fixedBlob.arrayBuffer(),
36707
37540
  origin: "Screen" /* Screen */
36708
37541
  });
36709
37542
  }
@@ -38055,6 +38888,20 @@ var Proctoring = class {
38055
38888
  } catch (error) {
38056
38889
  throw SAFE_BROWSER_API_NOT_FOUND;
38057
38890
  }
38891
+ this.allRecorders.cameraRecorder.onVisibilityRestored = () => {
38892
+ console.log("[Proctoring] Usu\xE1rio retornou ao browser.");
38893
+ this.onVisibilityRestoredCallback();
38894
+ };
38895
+ if (this.sessionOptions.proctoringType === "REALTIME" && !isSafeBrowser()) {
38896
+ try {
38897
+ await BackgroundUploadService.recoverPendingUploads(
38898
+ this.backend,
38899
+ this.context.token
38900
+ );
38901
+ } catch (e3) {
38902
+ console.warn("[Proctoring] Erro ao recuperar chunks de sess\xE3o anterior:", e3);
38903
+ }
38904
+ }
38058
38905
  try {
38059
38906
  console.log("Starting recorders");
38060
38907
  await this.recorder.startAll();
@@ -38135,9 +38982,7 @@ Error: ${error}`
38135
38982
  this.appChecker && await this.appChecker.disconnectWebSocket();
38136
38983
  await this.recorder.saveAllOnSession();
38137
38984
  await this.sendPendingRealtimeAlerts();
38138
- trackers.registerError(this.proctoringId, `finish this.repository.save starting`);
38139
38985
  await this.repository.save(this.proctoringSession);
38140
- trackers.registerError(this.proctoringId, `finish this.repository.save finished`);
38141
38986
  let uploader;
38142
38987
  let uploaderServices;
38143
38988
  if (versionVerify() !== "1.0.0.0") {