easyproctor-hml 2.7.12 → 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/esm/index.js CHANGED
@@ -12758,13 +12758,16 @@ function isMobileDevice() {
12758
12758
  }
12759
12759
  return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
12760
12760
  }
12761
+ function isSafeBrowser() {
12762
+ return versionVerify() !== "1.0.0.0";
12763
+ }
12761
12764
 
12762
12765
  // src/plugins/recorder.ts
12763
12766
  var proctoringId;
12764
12767
  function setRecorderProctoringId(id) {
12765
12768
  proctoringId = id;
12766
12769
  }
12767
- function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCallback, audio = false) {
12770
+ function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCallback, audio = false, recorderOpts) {
12768
12771
  let resolvePromise;
12769
12772
  let onBufferSizeInterval;
12770
12773
  let lastEvent;
@@ -12772,6 +12775,7 @@ function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCa
12772
12775
  bufferSize = 0;
12773
12776
  let startTime;
12774
12777
  let duration = 0;
12778
+ let chunkIndex = 0;
12775
12779
  let recorderOptions = {
12776
12780
  // eslint-disable-next-line no-useless-escape
12777
12781
  mimeType: "video/webm",
@@ -12806,6 +12810,10 @@ function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCa
12806
12810
  mediaRecorder2.ondataavailable = (e3) => {
12807
12811
  bufferSize = bufferSize + e3.data.size;
12808
12812
  if (e3.data.size > 0) {
12813
+ if (recorderOpts == null ? void 0 : recorderOpts.onChunkAvailable) {
12814
+ recorderOpts.onChunkAvailable(e3.data, chunkIndex);
12815
+ chunkIndex++;
12816
+ }
12809
12817
  buffer.push(e3.data);
12810
12818
  }
12811
12819
  };
@@ -12837,7 +12845,13 @@ function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCa
12837
12845
  };
12838
12846
  try {
12839
12847
  console.log("State antes do start:", recorder2.state);
12840
- recorder2.start(1e4);
12848
+ chunkIndex = 0;
12849
+ if ((recorderOpts == null ? void 0 : recorderOpts.timeslice) && (recorderOpts == null ? void 0 : recorderOpts.timeslice) > 0) {
12850
+ recorder2.start(recorderOpts.timeslice);
12851
+ } else {
12852
+ recorder2.start(1e4);
12853
+ }
12854
+ bufferSize = 0;
12841
12855
  startTime = new Date(Date.now());
12842
12856
  } catch (e3) {
12843
12857
  console.error("Recorder erro ao chamar start event:", e3);
@@ -12891,9 +12905,6 @@ function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCa
12891
12905
  console.log("stopRecording Recorder n\xE3o est\xE1 em estado recording");
12892
12906
  resolve();
12893
12907
  }
12894
- stream.getTracks().forEach((el) => {
12895
- el.stop();
12896
- });
12897
12908
  });
12898
12909
  }
12899
12910
  function pauseRecording() {
@@ -13301,11 +13312,623 @@ var VolumeMeter = class {
13301
13312
  }
13302
13313
  };
13303
13314
 
13315
+ // src/new-flow/chunk/ChunkStorageService.ts
13316
+ var _ChunkStorageService = class _ChunkStorageService {
13317
+ constructor() {
13318
+ this.db = null;
13319
+ }
13320
+ /**
13321
+ * Abre a conexão com o IndexedDB, criando o banco e o object store se necessário.
13322
+ */
13323
+ async connect() {
13324
+ if (this.db) return this.db;
13325
+ return new Promise((resolve, reject) => {
13326
+ const request = window.indexedDB.open(
13327
+ _ChunkStorageService.DB_NAME,
13328
+ _ChunkStorageService.DB_VERSION
13329
+ );
13330
+ request.onerror = () => {
13331
+ var _a2, _b;
13332
+ console.error("IndexedDB error:", request.error);
13333
+ reject(
13334
+ new Error(
13335
+ `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}`
13336
+ )
13337
+ );
13338
+ };
13339
+ request.onupgradeneeded = () => {
13340
+ const db = request.result;
13341
+ if (db.objectStoreNames.contains(_ChunkStorageService.STORE_NAME)) {
13342
+ db.deleteObjectStore(_ChunkStorageService.STORE_NAME);
13343
+ }
13344
+ const store = db.createObjectStore(_ChunkStorageService.STORE_NAME, {
13345
+ keyPath: "id",
13346
+ autoIncrement: true
13347
+ });
13348
+ store.createIndex("proctoringId", "proctoringId", { unique: false });
13349
+ store.createIndex("uploaded", "uploaded", { unique: false });
13350
+ store.createIndex("proctoringId_uploaded", ["proctoringId", "uploaded"], {
13351
+ unique: false
13352
+ });
13353
+ };
13354
+ request.onsuccess = () => {
13355
+ this.db = request.result;
13356
+ resolve(this.db);
13357
+ };
13358
+ });
13359
+ }
13360
+ /**
13361
+ * Salva um chunk de vídeo no IndexedDB.
13362
+ */
13363
+ async saveChunk(chunk) {
13364
+ const db = await this.connect();
13365
+ return new Promise((resolve, reject) => {
13366
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
13367
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13368
+ const request = store.add(chunk);
13369
+ request.onsuccess = () => {
13370
+ resolve(request.result);
13371
+ };
13372
+ request.onerror = () => {
13373
+ var _a2;
13374
+ reject(new Error(`Erro ao salvar chunk no IndexedDB: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13375
+ };
13376
+ });
13377
+ }
13378
+ /**
13379
+ * Retorna todos os chunks pendentes (não enviados) de um proctoringId específico.
13380
+ */
13381
+ async getPendingChunks(proctoringId2) {
13382
+ const db = await this.connect();
13383
+ return new Promise((resolve, reject) => {
13384
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
13385
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13386
+ const index = store.index("proctoringId_uploaded");
13387
+ const range = IDBKeyRange.only([proctoringId2, 0]);
13388
+ const request = index.getAll(range);
13389
+ request.onsuccess = () => {
13390
+ const chunks = request.result.sort(
13391
+ (a3, b3) => a3.chunkIndex - b3.chunkIndex
13392
+ );
13393
+ resolve(chunks);
13394
+ };
13395
+ request.onerror = () => {
13396
+ var _a2;
13397
+ reject(new Error(`Erro ao buscar chunks pendentes: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13398
+ };
13399
+ });
13400
+ }
13401
+ /**
13402
+ * Retorna todos os chunks (enviados ou não) de um proctoringId específico.
13403
+ */
13404
+ async getAllChunks(proctoringId2) {
13405
+ const db = await this.connect();
13406
+ return new Promise((resolve, reject) => {
13407
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
13408
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13409
+ const index = store.index("proctoringId");
13410
+ const range = IDBKeyRange.only(proctoringId2);
13411
+ const request = index.getAll(range);
13412
+ request.onsuccess = () => {
13413
+ const chunks = request.result.sort(
13414
+ (a3, b3) => a3.chunkIndex - b3.chunkIndex
13415
+ );
13416
+ resolve(chunks);
13417
+ };
13418
+ request.onerror = () => {
13419
+ var _a2;
13420
+ reject(new Error(`Erro ao buscar todos os chunks: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13421
+ };
13422
+ });
13423
+ }
13424
+ /**
13425
+ * Marca um chunk como enviado (uploaded = 1).
13426
+ */
13427
+ async markAsUploaded(chunkId) {
13428
+ const db = await this.connect();
13429
+ return new Promise((resolve, reject) => {
13430
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
13431
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13432
+ const getRequest = store.get(chunkId);
13433
+ getRequest.onsuccess = () => {
13434
+ const chunk = getRequest.result;
13435
+ if (!chunk) {
13436
+ resolve();
13437
+ return;
13438
+ }
13439
+ chunk.uploaded = 1;
13440
+ const putRequest = store.put(chunk);
13441
+ putRequest.onsuccess = () => resolve();
13442
+ putRequest.onerror = () => {
13443
+ var _a2;
13444
+ return reject(new Error(`Erro ao marcar chunk como enviado: ${(_a2 = putRequest.error) == null ? void 0 : _a2.message}`));
13445
+ };
13446
+ };
13447
+ getRequest.onerror = () => {
13448
+ var _a2;
13449
+ return reject(new Error(`Erro ao buscar chunk para marcar: ${(_a2 = getRequest.error) == null ? void 0 : _a2.message}`));
13450
+ };
13451
+ });
13452
+ }
13453
+ /**
13454
+ * Remove todos os chunks já enviados de um proctoringId para liberar espaço.
13455
+ */
13456
+ async clearUploadedChunks(proctoringId2) {
13457
+ const db = await this.connect();
13458
+ return new Promise((resolve, reject) => {
13459
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
13460
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13461
+ const index = store.index("proctoringId_uploaded");
13462
+ const range = IDBKeyRange.only([proctoringId2, 1]);
13463
+ const request = index.openCursor(range);
13464
+ request.onsuccess = () => {
13465
+ const cursor = request.result;
13466
+ if (cursor) {
13467
+ cursor.delete();
13468
+ cursor.continue();
13469
+ } else {
13470
+ resolve();
13471
+ }
13472
+ };
13473
+ request.onerror = () => {
13474
+ var _a2;
13475
+ return reject(new Error(`Erro ao limpar chunks enviados: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13476
+ };
13477
+ });
13478
+ }
13479
+ /**
13480
+ * Remove TODOS os chunks de um proctoringId (limpeza completa pós-finalização).
13481
+ */
13482
+ async clearAllChunks(proctoringId2) {
13483
+ const db = await this.connect();
13484
+ return new Promise((resolve, reject) => {
13485
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
13486
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13487
+ const index = store.index("proctoringId");
13488
+ const range = IDBKeyRange.only(proctoringId2);
13489
+ const request = index.openCursor(range);
13490
+ request.onsuccess = () => {
13491
+ const cursor = request.result;
13492
+ if (cursor) {
13493
+ cursor.delete();
13494
+ cursor.continue();
13495
+ } else {
13496
+ resolve();
13497
+ }
13498
+ };
13499
+ request.onerror = () => {
13500
+ var _a2;
13501
+ return reject(new Error(`Erro ao limpar todos os chunks: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13502
+ };
13503
+ });
13504
+ }
13505
+ /**
13506
+ * Verifica se existem chunks pendentes para qualquer proctoringId.
13507
+ * Útil na recuperação pós-crash.
13508
+ */
13509
+ async hasAnyPendingChunks() {
13510
+ const db = await this.connect();
13511
+ return new Promise((resolve, reject) => {
13512
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
13513
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13514
+ const index = store.index("uploaded");
13515
+ const range = IDBKeyRange.only(0);
13516
+ const request = index.count(range);
13517
+ request.onsuccess = () => {
13518
+ resolve(request.result > 0);
13519
+ };
13520
+ request.onerror = () => {
13521
+ var _a2;
13522
+ return reject(new Error(`Erro ao verificar chunks pendentes: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13523
+ };
13524
+ });
13525
+ }
13526
+ /**
13527
+ * Retorna todos os proctoringIds que possuem chunks pendentes.
13528
+ * Útil na recuperação pós-crash para saber quais sessões precisam ser finalizadas.
13529
+ */
13530
+ async getPendingProctoringIds() {
13531
+ const db = await this.connect();
13532
+ return new Promise((resolve, reject) => {
13533
+ const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
13534
+ const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
13535
+ const index = store.index("uploaded");
13536
+ const range = IDBKeyRange.only(0);
13537
+ const request = index.getAll(range);
13538
+ request.onsuccess = () => {
13539
+ const chunks = request.result;
13540
+ const ids = [...new Set(chunks.map((c3) => c3.proctoringId))];
13541
+ resolve(ids);
13542
+ };
13543
+ request.onerror = () => {
13544
+ var _a2;
13545
+ return reject(new Error(`Erro ao buscar proctoringIds pendentes: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
13546
+ };
13547
+ });
13548
+ }
13549
+ /**
13550
+ * Fecha a conexão com o banco.
13551
+ */
13552
+ close() {
13553
+ if (this.db) {
13554
+ this.db.close();
13555
+ this.db = null;
13556
+ }
13557
+ }
13558
+ };
13559
+ _ChunkStorageService.DB_NAME = "EasyProctorChunksDb";
13560
+ /** Incrementado para v2 para recriar índices com tipos numéricos em vez de boolean */
13561
+ _ChunkStorageService.DB_VERSION = 2;
13562
+ _ChunkStorageService.STORE_NAME = "chunks";
13563
+ var ChunkStorageService = _ChunkStorageService;
13564
+
13565
+ // src/new-flow/chunk/BackgroundUploadService.ts
13566
+ var DEFAULT_CONFIG = {
13567
+ pollInterval: 5e3,
13568
+ maxRetries: 5,
13569
+ baseRetryDelay: 2e3,
13570
+ cleanAfterUpload: true
13571
+ };
13572
+ var BackgroundUploadService = class _BackgroundUploadService {
13573
+ constructor(proctoringId2, token, backend, chunkStorage, config) {
13574
+ this.pollTimer = null;
13575
+ this.isProcessing = false;
13576
+ this.isRunning = false;
13577
+ /** Mapa de chunkId -> número de tentativas já feitas */
13578
+ this.retryCount = /* @__PURE__ */ new Map();
13579
+ /** GCS Resumable Upload State */
13580
+ this.sessionUrl = null;
13581
+ this.currentOffset = 0;
13582
+ this.totalBytesPurged = 0;
13583
+ this.STORAGE_KEY_PREFIX = "ep_upload_session_";
13584
+ this.GCS_CHUNK_SIZE = 256 * 1024;
13585
+ this.proctoringId = proctoringId2.trim();
13586
+ this.token = token;
13587
+ this.backend = backend;
13588
+ this.chunkStorage = chunkStorage;
13589
+ this.config = { ...DEFAULT_CONFIG, ...config };
13590
+ this.loadSessionState();
13591
+ }
13592
+ loadSessionState() {
13593
+ try {
13594
+ const stored = localStorage.getItem(`${this.STORAGE_KEY_PREFIX}${this.proctoringId}`);
13595
+ if (stored) {
13596
+ const { sessionUrl, currentOffset, totalBytesPurged } = JSON.parse(stored);
13597
+ this.sessionUrl = sessionUrl;
13598
+ this.currentOffset = currentOffset;
13599
+ this.totalBytesPurged = totalBytesPurged || 0;
13600
+ }
13601
+ } catch (e3) {
13602
+ console.warn("[BackgroundUpload] Erro ao carregar estado da sess\xE3o:", e3);
13603
+ }
13604
+ }
13605
+ saveSessionState() {
13606
+ try {
13607
+ localStorage.setItem(
13608
+ `${this.STORAGE_KEY_PREFIX}${this.proctoringId}`,
13609
+ JSON.stringify({
13610
+ sessionUrl: this.sessionUrl,
13611
+ currentOffset: this.currentOffset,
13612
+ totalBytesPurged: this.totalBytesPurged
13613
+ })
13614
+ );
13615
+ } catch (e3) {
13616
+ console.warn("[BackgroundUpload] Erro ao salvar estado da sess\xE3o:", e3);
13617
+ }
13618
+ }
13619
+ clearSessionState() {
13620
+ try {
13621
+ localStorage.removeItem(`${this.STORAGE_KEY_PREFIX}${this.proctoringId}`);
13622
+ this.sessionUrl = null;
13623
+ this.currentOffset = 0;
13624
+ this.totalBytesPurged = 0;
13625
+ } catch (e3) {
13626
+ console.warn("[BackgroundUpload] Erro ao limpar estado da sess\xE3o:", e3);
13627
+ }
13628
+ }
13629
+ /**
13630
+ * Inicia o serviço de upload em background. Faz polling periódico no IndexedDB
13631
+ * para enviar chunks pendentes.
13632
+ */
13633
+ start() {
13634
+ if (this.isRunning) return;
13635
+ this.isRunning = true;
13636
+ console.log(`[BackgroundUpload] Iniciando servi\xE7o para proctoringId: ${this.proctoringId}`);
13637
+ this.processQueue();
13638
+ this.pollTimer = setInterval(() => {
13639
+ this.processQueue(false);
13640
+ }, this.config.pollInterval);
13641
+ }
13642
+ /**
13643
+ * Para o serviço de upload em background.
13644
+ */
13645
+ stop() {
13646
+ this.isRunning = false;
13647
+ if (this.pollTimer) {
13648
+ clearInterval(this.pollTimer);
13649
+ this.pollTimer = null;
13650
+ }
13651
+ console.log(`[BackgroundUpload] Servi\xE7o parado para proctoringId: ${this.proctoringId}`);
13652
+ }
13653
+ /**
13654
+ * Força o processamento de todos os chunks pendentes e encerra a sessão GCS.
13655
+ * Útil quando a gravação é finalizada.
13656
+ */
13657
+ async flush() {
13658
+ console.log(`[BackgroundUpload] Flush: enviando todos os chunks pendentes e finalizando...`);
13659
+ let waitAttempts = 0;
13660
+ while (this.isProcessing && waitAttempts < 10) {
13661
+ await this.sleep(1e3);
13662
+ waitAttempts++;
13663
+ }
13664
+ let flushRetries = 0;
13665
+ const maxFlushRetries = 3;
13666
+ while (flushRetries < maxFlushRetries) {
13667
+ try {
13668
+ await this.processQueue(true);
13669
+ console.log(`[BackgroundUpload] Flush completado com sucesso.`);
13670
+ return;
13671
+ } catch (error) {
13672
+ flushRetries++;
13673
+ console.error(`[BackgroundUpload] Erro no flush (tentativa ${flushRetries}/${maxFlushRetries}):`, error);
13674
+ if (flushRetries < maxFlushRetries) {
13675
+ await this.sleep(2e3);
13676
+ }
13677
+ }
13678
+ }
13679
+ throw new Error(`[BackgroundUpload] Falha ao finalizar upload ap\xF3s ${maxFlushRetries} tentativas.`);
13680
+ }
13681
+ /**
13682
+ * Sincroniza o offset local com o estado real no Google Cloud Storage.
13683
+ */
13684
+ async syncOffset() {
13685
+ if (!this.sessionUrl) return 0;
13686
+ try {
13687
+ console.log(`[BackgroundUpload] Sincronizando offset com GCS...`);
13688
+ const response = await fetch(this.sessionUrl, {
13689
+ method: "PUT",
13690
+ headers: {
13691
+ "Content-Range": "bytes */*"
13692
+ }
13693
+ });
13694
+ console.log(`[BackgroundUpload] Status da sincroniza\xE7\xE3o (syncOffset): ${response.status}`);
13695
+ if (response.status === 308) {
13696
+ const range = response.headers.get("Range");
13697
+ if (range) {
13698
+ const lastByte = parseInt(range.split("-")[1], 10);
13699
+ this.currentOffset = lastByte + 1;
13700
+ this.saveSessionState();
13701
+ console.log(`[BackgroundUpload] Offset sincronizado: ${this.currentOffset}`);
13702
+ } else {
13703
+ this.currentOffset = 0;
13704
+ }
13705
+ } else if (response.ok || response.status === 201) {
13706
+ console.log("[BackgroundUpload] Sincroniza\xE7\xE3o indicou upload JA FINALIZADO.");
13707
+ this.currentOffset = -1;
13708
+ } else {
13709
+ console.warn(`[BackgroundUpload] Status inesperado na sincroniza\xE7\xE3o: ${response.status}`);
13710
+ }
13711
+ } catch (error) {
13712
+ console.warn("[BackgroundUpload] Erro ao sincronizar offset:", error);
13713
+ }
13714
+ return this.currentOffset;
13715
+ }
13716
+ /**
13717
+ * Verifica e envia chunks pendentes para o backend.
13718
+ * @param isFinal Se true, não alinha a 256KB e fecha a sessão com /TOTAL no header.
13719
+ */
13720
+ async processQueue(isFinal = false) {
13721
+ var _a2, _b;
13722
+ if (this.isProcessing) return;
13723
+ this.isProcessing = true;
13724
+ try {
13725
+ if (this.sessionUrl) {
13726
+ await this.syncOffset();
13727
+ if (this.currentOffset === -1) {
13728
+ console.log("[BackgroundUpload] Sess\xE3o j\xE1 finalizada no servidor.");
13729
+ this.clearSessionState();
13730
+ this.isProcessing = false;
13731
+ return;
13732
+ }
13733
+ }
13734
+ const allChunks = await this.chunkStorage.getAllChunks(this.proctoringId);
13735
+ const pendingChunks = allChunks.filter((c3) => c3.uploaded === 0);
13736
+ if (pendingChunks.length === 0 && !isFinal) {
13737
+ this.isProcessing = false;
13738
+ return;
13739
+ }
13740
+ console.log(`[BackgroundUpload] ${pendingChunks.length} chunks pendentes encontrados. Modo final: ${isFinal}`);
13741
+ let virtualStart = this.totalBytesPurged;
13742
+ const chunksWithMeta = allChunks.map((c3) => {
13743
+ const start = virtualStart;
13744
+ const end = start + c3.blob.size - 1;
13745
+ virtualStart += c3.blob.size;
13746
+ return { chunk: c3, start, end };
13747
+ });
13748
+ let combinedBlobParts = [];
13749
+ let lastProcessedChunkId = null;
13750
+ let finalChunkIndex = 0;
13751
+ let mimeType = pendingChunks[0].mimeType;
13752
+ for (const meta of chunksWithMeta) {
13753
+ if (this.currentOffset > meta.end) continue;
13754
+ const sliceStart = Math.max(0, this.currentOffset - meta.start);
13755
+ const chunkSlice = meta.chunk.blob.slice(sliceStart);
13756
+ combinedBlobParts.push(chunkSlice);
13757
+ lastProcessedChunkId = meta.chunk.id;
13758
+ finalChunkIndex = meta.chunk.chunkIndex;
13759
+ }
13760
+ if (combinedBlobParts.length === 0 && !isFinal) {
13761
+ this.isProcessing = false;
13762
+ return;
13763
+ }
13764
+ let fullBlob = new Blob(combinedBlobParts, { type: mimeType });
13765
+ let sendableSize = fullBlob.size;
13766
+ let totalSizeForHeader = void 0;
13767
+ if (!isFinal) {
13768
+ sendableSize = Math.floor(fullBlob.size / this.GCS_CHUNK_SIZE) * this.GCS_CHUNK_SIZE;
13769
+ if (sendableSize === 0) {
13770
+ console.log("[BackgroundUpload] Dados insuficientes para atingir 256KB. Aguardando novo chunk...");
13771
+ this.isProcessing = false;
13772
+ return;
13773
+ }
13774
+ } else {
13775
+ totalSizeForHeader = virtualStart;
13776
+ }
13777
+ const blobToSend = fullBlob.slice(0, sendableSize);
13778
+ try {
13779
+ await this.uploadData(blobToSend, mimeType, finalChunkIndex, totalSizeForHeader);
13780
+ for (const meta of chunksWithMeta) {
13781
+ if (meta.chunk.uploaded === 0 && meta.end < this.currentOffset) {
13782
+ await this.chunkStorage.markAsUploaded(meta.chunk.id);
13783
+ this.retryCount.delete(meta.chunk.id);
13784
+ (_a2 = this.onChunkUploaded) == null ? void 0 : _a2.call(this, meta.chunk.id, meta.chunk.chunkIndex);
13785
+ console.log(`[BackgroundUpload] Chunk ${meta.chunk.chunkIndex} marcado como enviado.`);
13786
+ }
13787
+ }
13788
+ if (this.config.cleanAfterUpload) {
13789
+ const chunksToClear = chunksWithMeta.filter((meta) => meta.chunk.uploaded === 1 || meta.chunk.uploaded === 0 && meta.end < this.currentOffset);
13790
+ const sizePurged = chunksToClear.reduce((acc, meta) => acc + meta.chunk.blob.size, 0);
13791
+ await this.chunkStorage.clearUploadedChunks(this.proctoringId);
13792
+ if (sizePurged > 0) {
13793
+ this.totalBytesPurged += sizePurged;
13794
+ this.saveSessionState();
13795
+ console.log(`[BackgroundUpload] ${sizePurged} bytes limpos do armazenamento local. Total purgado: ${this.totalBytesPurged}`);
13796
+ }
13797
+ }
13798
+ if (isFinal) {
13799
+ this.clearSessionState();
13800
+ }
13801
+ } catch (error) {
13802
+ console.error("[BackgroundUpload] Falha no upload:", error);
13803
+ (_b = this.onUploadError) == null ? void 0 : _b.call(this, lastProcessedChunkId || 0, error);
13804
+ }
13805
+ } catch (error) {
13806
+ console.error("[BackgroundUpload] Erro ao processar fila:", error);
13807
+ } finally {
13808
+ this.isProcessing = false;
13809
+ }
13810
+ }
13811
+ /**
13812
+ * Faz o upload bruto de dados para a sessão GCS.
13813
+ */
13814
+ async uploadData(blob, mimeType, chunkIndex, totalSize) {
13815
+ const fileName = `EP_${this.proctoringId}_camera_0.webm`;
13816
+ if (!this.sessionUrl) {
13817
+ const initiateUrl = await this.backend.initiateUpload(this.token, `${this.proctoringId}/${fileName}`, mimeType);
13818
+ const startResponse = await fetch(initiateUrl, {
13819
+ method: "POST",
13820
+ headers: { "x-goog-resumable": "start", "Content-Type": mimeType }
13821
+ });
13822
+ if (!startResponse.ok) throw new Error(`Falha ao iniciar: ${startResponse.status}`);
13823
+ this.sessionUrl = startResponse.headers.get("Location");
13824
+ if (!this.sessionUrl) throw new Error("Location header ausente");
13825
+ try {
13826
+ const urlObj = new URL(this.sessionUrl);
13827
+ const pathParts = urlObj.pathname.split("/");
13828
+ let bucket = pathParts[1];
13829
+ let object = decodeURIComponent(pathParts.slice(2).join("/"));
13830
+ if (pathParts.includes("b") && pathParts.includes("o")) {
13831
+ const bIdx = pathParts.indexOf("b") + 1;
13832
+ const oIdx = pathParts.indexOf("o") + 1;
13833
+ bucket = pathParts[bIdx];
13834
+ object = decodeURIComponent(pathParts.slice(oIdx).join("/"));
13835
+ }
13836
+ console.log(`[BackgroundUpload] Sess\xE3o Iniciada -> Bucket: ${bucket}, Objeto: ${object}`);
13837
+ } catch (e3) {
13838
+ console.log(`[BackgroundUpload] Sess\xE3o Iniciada. URL: ${this.sessionUrl}`);
13839
+ }
13840
+ this.currentOffset = 0;
13841
+ this.saveSessionState();
13842
+ } else {
13843
+ console.log(`[BackgroundUpload] Usando sess\xE3o GCS existente: ${this.sessionUrl}`);
13844
+ }
13845
+ const start = this.currentOffset;
13846
+ const end = start + blob.size - 1;
13847
+ const totalHeader = totalSize !== void 0 ? totalSize.toString() : "*";
13848
+ const contentRangeHeader = blob.size === 0 && totalSize !== void 0 ? `bytes */${totalHeader}` : `bytes ${start}-${end}/${totalHeader}`;
13849
+ console.log(`[BackgroundUpload] Enviando ${blob.size > 0 ? "dados" : "finaliza\xE7\xE3o"}: ${contentRangeHeader} (Size: ${blob.size})`);
13850
+ const response = await fetch(this.sessionUrl, {
13851
+ method: "PUT",
13852
+ headers: { "Content-Range": contentRangeHeader },
13853
+ body: blob.size > 0 ? blob : null
13854
+ // Usa null para garantir corpo vazio se necessário
13855
+ });
13856
+ console.log(`[BackgroundUpload] Resposta GCS (uploadData): ${response.status}`);
13857
+ if (response.status !== 200 && response.status !== 201 && response.status !== 308) {
13858
+ const errorText = await response.text();
13859
+ console.error(`[BackgroundUpload] Erro GCS: ${errorText}`);
13860
+ throw new Error(`Status HTTP inesperado: ${response.status}`);
13861
+ }
13862
+ const rangeHeader = response.headers.get("Range");
13863
+ if (rangeHeader) {
13864
+ const lastByte = parseInt(rangeHeader.split("-")[1], 10);
13865
+ this.currentOffset = lastByte + 1;
13866
+ } else {
13867
+ this.currentOffset += blob.size;
13868
+ }
13869
+ this.saveSessionState();
13870
+ trackers.registerUploadFile(
13871
+ this.proctoringId,
13872
+ `GCS Stream Upload
13873
+ Size: ${blob.size}
13874
+ Range: ${start}-${end}
13875
+ Last Index: ${chunkIndex}`,
13876
+ "CameraChunk"
13877
+ );
13878
+ }
13879
+ /**
13880
+ * Método estático para recuperação pós-crash.
13881
+ * Verifica o IndexedDB em busca de chunks pendentes de qualquer sessão
13882
+ * e tenta enviar.
13883
+ */
13884
+ static async recoverPendingUploads(backend, token) {
13885
+ const chunkStorage = new ChunkStorageService();
13886
+ const recoveredIds = [];
13887
+ try {
13888
+ const pendingIds = await chunkStorage.getPendingProctoringIds();
13889
+ if (pendingIds.length === 0) {
13890
+ console.log("[BackgroundUpload] Nenhum chunk pendente encontrado para recupera\xE7\xE3o.");
13891
+ return recoveredIds;
13892
+ }
13893
+ console.log(
13894
+ `[BackgroundUpload] Recupera\xE7\xE3o p\xF3s-crash: ${pendingIds.length} sess\xE3o(\xF5es) com chunks pendentes.`
13895
+ );
13896
+ for (const proctoringId2 of pendingIds) {
13897
+ try {
13898
+ const service = new _BackgroundUploadService(
13899
+ proctoringId2,
13900
+ token,
13901
+ backend,
13902
+ chunkStorage,
13903
+ { cleanAfterUpload: true }
13904
+ );
13905
+ await service.flush();
13906
+ recoveredIds.push(proctoringId2);
13907
+ console.log(
13908
+ `[BackgroundUpload] Chunks da sess\xE3o ${proctoringId2} recuperados com sucesso.`
13909
+ );
13910
+ } catch (error) {
13911
+ console.error(
13912
+ `[BackgroundUpload] Erro ao recuperar chunks da sess\xE3o ${proctoringId2}:`,
13913
+ error
13914
+ );
13915
+ }
13916
+ }
13917
+ } catch (error) {
13918
+ console.error("[BackgroundUpload] Erro geral na recupera\xE7\xE3o:", error);
13919
+ }
13920
+ return recoveredIds;
13921
+ }
13922
+ sleep(ms2) {
13923
+ return new Promise((resolve) => setTimeout(resolve, ms2));
13924
+ }
13925
+ };
13926
+
13304
13927
  // src/new-flow/recorders/CameraRecorder.ts
13305
13928
  var import_jszip_min = __toESM(require_jszip_min());
13306
13929
  var pkg = require_fix_webm_duration();
13307
13930
  var fixWebmDuration = pkg.default || pkg;
13308
- var CameraRecorder = class {
13931
+ var _CameraRecorder = class _CameraRecorder {
13309
13932
  constructor(options, videoOptions, paramsConfig, backend, backendToken) {
13310
13933
  this.blobs = [];
13311
13934
  this.paramsConfig = {
@@ -13358,6 +13981,13 @@ var CameraRecorder = class {
13358
13981
  this.videoElement = null;
13359
13982
  this.duration = 0;
13360
13983
  this.stopped = false;
13984
+ this.backgroundUpload = null;
13985
+ this.chunkIndex = 0;
13986
+ /** Lista de promises de chunks sendo salvos no IndexedDB para evitar race conditions no stop */
13987
+ this.pendingChunkSaves = [];
13988
+ // Handlers bound para poder remover os listeners depois
13989
+ this.boundVisibilityHandler = null;
13990
+ this.boundPageHideHandler = null;
13361
13991
  this.currentRetries = 0;
13362
13992
  this.packageCount = 0;
13363
13993
  this.failedUploads = 0;
@@ -13368,10 +13998,122 @@ var CameraRecorder = class {
13368
13998
  this.backendToken = backendToken;
13369
13999
  paramsConfig && (this.paramsConfig = paramsConfig);
13370
14000
  }
14001
+ /**
14002
+ * Determina se o fluxo de chunks e lifecycle deve estar ativo.
14003
+ * Retorna true se:
14004
+ * 1. O proctoringId já foi definido (ou seja, estamos em uma sessão real, NÃO no checkDevices)
14005
+ * 2. E (`useChunkRecording` foi explicitamente setado como true OU o dispositivo é mobile)
14006
+ */
14007
+ get isChunkEnabled() {
14008
+ return !!this.proctoringId && this.options.proctoringType === "REALTIME" && !isSafeBrowser();
14009
+ }
13371
14010
  setProctoringId(proctoringId2) {
13372
14011
  this.proctoringId = proctoringId2;
13373
14012
  this.proctoringId && this.backend && (this.upload = new UploadService(this.proctoringId, this.backend));
13374
14013
  setRecorderProctoringId(proctoringId2);
14014
+ if (this.isChunkEnabled) {
14015
+ this.chunkStorage = new ChunkStorageService();
14016
+ if (this.backend && this.backendToken) {
14017
+ this.backgroundUpload = new BackgroundUploadService(
14018
+ this.proctoringId,
14019
+ this.backendToken,
14020
+ this.backend,
14021
+ this.chunkStorage,
14022
+ { pollInterval: 5e3, maxRetries: 5, cleanAfterUpload: true }
14023
+ );
14024
+ }
14025
+ this.persistSessionState("IN_PROGRESS");
14026
+ console.log(
14027
+ `[CameraRecorder] Chunk recording ATIVO (type: ${this.options.proctoringType}, mobile: ${isMobileDevice()})`
14028
+ );
14029
+ } else {
14030
+ console.log(
14031
+ `[CameraRecorder] Chunk recording INATIVO (type: ${this.options.proctoringType}) \u2014 modo cl\xE1ssico.`
14032
+ );
14033
+ }
14034
+ }
14035
+ // ========================
14036
+ // Session State Persistence (localStorage)
14037
+ // ========================
14038
+ persistSessionState(status) {
14039
+ try {
14040
+ const data = {
14041
+ proctoringId: this.proctoringId,
14042
+ status,
14043
+ timestamp: Date.now()
14044
+ };
14045
+ localStorage.setItem(_CameraRecorder.LS_SESSION_KEY, JSON.stringify(data));
14046
+ } catch (e3) {
14047
+ console.warn("[CameraRecorder] N\xE3o foi poss\xEDvel salvar estado no localStorage:", e3);
14048
+ }
14049
+ }
14050
+ clearSessionState() {
14051
+ try {
14052
+ localStorage.removeItem(_CameraRecorder.LS_SESSION_KEY);
14053
+ } catch (e3) {
14054
+ console.warn("[CameraRecorder] N\xE3o foi poss\xEDvel limpar estado do localStorage:", e3);
14055
+ }
14056
+ }
14057
+ /**
14058
+ * Verifica se existe uma sessão ativa anterior no localStorage.
14059
+ * Retorna os dados da sessão se ela estiver em IN_PROGRESS, null caso contrário.
14060
+ */
14061
+ static checkForActiveSession() {
14062
+ try {
14063
+ const raw = localStorage.getItem(_CameraRecorder.LS_SESSION_KEY);
14064
+ if (!raw) return null;
14065
+ const data = JSON.parse(raw);
14066
+ if (data.status === "IN_PROGRESS") return data;
14067
+ return null;
14068
+ } catch (e3) {
14069
+ return null;
14070
+ }
14071
+ }
14072
+ // ========================
14073
+ // Page Lifecycle Management
14074
+ // ========================
14075
+ setupLifecycleListeners() {
14076
+ this.boundVisibilityHandler = () => this.handleVisibilityChange();
14077
+ this.boundPageHideHandler = () => this.handlePageHide();
14078
+ document.addEventListener("visibilitychange", this.boundVisibilityHandler);
14079
+ window.addEventListener("pagehide", this.boundPageHideHandler);
14080
+ }
14081
+ removeLifecycleListeners() {
14082
+ if (this.boundVisibilityHandler) {
14083
+ document.removeEventListener("visibilitychange", this.boundVisibilityHandler);
14084
+ this.boundVisibilityHandler = null;
14085
+ }
14086
+ if (this.boundPageHideHandler) {
14087
+ window.removeEventListener("pagehide", this.boundPageHideHandler);
14088
+ this.boundPageHideHandler = null;
14089
+ }
14090
+ }
14091
+ handleVisibilityChange() {
14092
+ var _a2;
14093
+ if (document.visibilityState === "hidden") {
14094
+ console.log("[CameraRecorder] P\xE1gina ficou invis\xEDvel \u2014 sess\xE3o potencialmente interrompida.");
14095
+ this.persistSessionState("INTERRUPTED");
14096
+ this.proctoringId && trackers.registerError(
14097
+ this.proctoringId,
14098
+ "Visibility API: P\xE1gina ficou oculta (hidden). Poss\xEDvel troca de app ou minimiza\xE7\xE3o."
14099
+ );
14100
+ } else if (document.visibilityState === "visible") {
14101
+ console.log("[CameraRecorder] P\xE1gina vis\xEDvel novamente \u2014 verificando estado da grava\xE7\xE3o.");
14102
+ this.persistSessionState("IN_PROGRESS");
14103
+ this.proctoringId && trackers.registerError(
14104
+ this.proctoringId,
14105
+ "Visibility API: P\xE1gina voltou a ficar vis\xEDvel. Usu\xE1rio retornou."
14106
+ );
14107
+ (_a2 = this.onVisibilityRestored) == null ? void 0 : _a2.call(this);
14108
+ }
14109
+ }
14110
+ handlePageHide() {
14111
+ console.log("[CameraRecorder] pagehide detectado \u2014 persistindo estado.");
14112
+ this.persistSessionState("INTERRUPTED");
14113
+ this.proctoringId && trackers.registerError(
14114
+ this.proctoringId,
14115
+ "Page Lifecycle: pagehide event detectado. P\xE1gina est\xE1 sendo descarregada."
14116
+ );
13375
14117
  }
13376
14118
  async initializeDetectors() {
13377
14119
  var _a2, _b, _c2;
@@ -13524,9 +14266,15 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13524
14266
  await new Promise((r3) => setTimeout(r3, 300));
13525
14267
  }
13526
14268
  async startRecording() {
13527
- var _a2, _b, _c2, _d, _e3, _f, _g;
14269
+ var _a2, _b, _c2, _d, _e3, _f, _g, _h;
13528
14270
  await this.startStream();
13529
14271
  await this.attachAndWarmup(this.cameraStream);
14272
+ const recorderOpts = this.isChunkEnabled ? {
14273
+ timeslice: _CameraRecorder.CHUNK_TIMESLICE_MS,
14274
+ onChunkAvailable: (blob, idx) => {
14275
+ this.handleNewChunk(blob, idx);
14276
+ }
14277
+ } : {};
13530
14278
  const {
13531
14279
  startRecording,
13532
14280
  stopRecording,
@@ -13542,7 +14290,8 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13542
14290
  this.blobs,
13543
14291
  this.options.onBufferSizeError,
13544
14292
  (e3) => this.bufferError(e3),
13545
- false
14293
+ false,
14294
+ recorderOpts
13546
14295
  );
13547
14296
  this.recordingStart = startRecording;
13548
14297
  this.recordingStop = stopRecording;
@@ -13552,13 +14301,18 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13552
14301
  this.getBufferSize = getBufferSize;
13553
14302
  this.getStartTime = getStartTime;
13554
14303
  this.getDuration = getDuration;
14304
+ this.chunkIndex = 0;
14305
+ if (this.isChunkEnabled) {
14306
+ (_a2 = this.backgroundUpload) == null ? void 0 : _a2.start();
14307
+ this.setupLifecycleListeners();
14308
+ }
13555
14309
  try {
13556
14310
  await new Promise((r3) => setTimeout(r3, 500));
13557
14311
  await this.recordingStart();
13558
14312
  } catch (error) {
13559
14313
  console.log("Camera Recorder error", error);
13560
14314
  this.stopRecording();
13561
- const maxRetries = ((_a2 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _a2.maxRetries) || 3;
14315
+ const maxRetries = ((_b = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _b.maxRetries) || 3;
13562
14316
  if (this.currentRetries < maxRetries) {
13563
14317
  console.log("Camera Recorder retry", this.currentRetries);
13564
14318
  this.currentRetries++;
@@ -13568,13 +14322,13 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13568
14322
  }
13569
14323
  }
13570
14324
  this.stopped = false;
13571
- 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)) {
14325
+ 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)) {
13572
14326
  await this.initializeDetectors();
13573
14327
  }
13574
- if ((_e3 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _e3.detectFace) {
14328
+ if ((_f = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _f.detectFace) {
13575
14329
  await this.faceDetection.enableCam(this.cameraStream);
13576
14330
  }
13577
- if (((_f = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _f.detectPerson) || ((_g = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _g.detectCellPhone)) {
14331
+ if (((_g = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _g.detectPerson) || ((_h = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _h.detectCellPhone)) {
13578
14332
  await this.objectDetection.enableCam(this.cameraStream);
13579
14333
  }
13580
14334
  this.filesToUpload = [];
@@ -13633,6 +14387,50 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13633
14387
  await this.sendPackage();
13634
14388
  await this.filesToUpload.splice(0, this.filesToUpload.length);
13635
14389
  }
14390
+ if (this.isChunkEnabled) {
14391
+ if (this.backgroundUpload) {
14392
+ try {
14393
+ if (this.pendingChunkSaves.length > 0) {
14394
+ console.log(`[CameraRecorder] Aguardando ${this.pendingChunkSaves.length} salvamentos de chunks pendentes...`);
14395
+ await Promise.all(this.pendingChunkSaves);
14396
+ }
14397
+ await this.backgroundUpload.flush();
14398
+ } catch (e3) {
14399
+ console.warn("[CameraRecorder] Erro ao fazer flush dos chunks:", e3);
14400
+ }
14401
+ this.backgroundUpload.stop();
14402
+ }
14403
+ this.removeLifecycleListeners();
14404
+ this.persistSessionState("FINISHED");
14405
+ }
14406
+ }
14407
+ /**
14408
+ * Callback chamado pelo recorder a cada novo chunk de vídeo disponível.
14409
+ * Salva o chunk no IndexedDB para persistência e recuperação.
14410
+ */
14411
+ async handleNewChunk(blob, idx) {
14412
+ if (!this.proctoringId || !this.chunkStorage) return;
14413
+ const savePromise = (async () => {
14414
+ var _a2;
14415
+ try {
14416
+ await this.chunkStorage.saveChunk({
14417
+ proctoringId: this.proctoringId,
14418
+ chunkIndex: this.chunkIndex,
14419
+ blob,
14420
+ timestamp: Date.now(),
14421
+ uploaded: 0,
14422
+ mimeType: ((_a2 = this.recorderOptions) == null ? void 0 : _a2.mimeType) || "video/webm"
14423
+ });
14424
+ this.chunkIndex++;
14425
+ console.log(`[CameraRecorder] Chunk ${this.chunkIndex - 1} salvo no IndexedDB.`);
14426
+ } catch (error) {
14427
+ console.error("[CameraRecorder] Erro ao salvar chunk no IndexedDB:", error);
14428
+ }
14429
+ })();
14430
+ this.pendingChunkSaves.push(savePromise);
14431
+ savePromise.finally(() => {
14432
+ this.pendingChunkSaves = this.pendingChunkSaves.filter((p3) => p3 !== savePromise);
14433
+ });
13636
14434
  }
13637
14435
  async pauseRecording() {
13638
14436
  await this.recordingPause();
@@ -13768,20 +14566,30 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13768
14566
  if (this.blobs != null)
13769
14567
  trackers.registerSaveOnSession(
13770
14568
  this.proctoringId,
13771
- `Blobs Length: ${this.blobs.length} Buffer Size: ${this.getBufferSize()} `
14569
+ `Blobs Length: ${this.blobs.length} Buffer Size: ${this.getBufferSize()} ChunkEnabled: ${this.isChunkEnabled}`
13772
14570
  );
13773
14571
  const settings = this.cameraStream.getVideoTracks()[0].getSettings();
13774
14572
  const settingsAudio = this.cameraStream.getAudioTracks()[0].getSettings();
13775
14573
  if (this.options.proctoringType == "VIDEO" || this.options.proctoringType == "REALTIME" || this.options.proctoringType == "IMAGE" && ((_a2 = this.paramsConfig.imageBehaviourParameters) == null ? void 0 : _a2.saveVideo)) {
14574
+ if (this.isChunkEnabled) {
14575
+ const isStable = await this.checkInternetStability();
14576
+ if (isStable) {
14577
+ } else {
14578
+ if (this.backend && this.backendToken && this.proctoringId) {
14579
+ const fileName = `EP_${this.proctoringId}_camera_0.webm`;
14580
+ const objectName = `${this.proctoringId}/${fileName}`;
14581
+ const isUploaded = await this.backend.checkUpload(this.backendToken, objectName, "video/webm");
14582
+ if (isUploaded) {
14583
+ this.chunkStorage && await this.chunkStorage.clearAllChunks(session.id);
14584
+ return;
14585
+ }
14586
+ }
14587
+ }
14588
+ }
13776
14589
  const rawBlob = new Blob(this.blobs, {
13777
14590
  type: ((_b = this.recorderOptions) == null ? void 0 : _b.mimeType) || "video/webm"
13778
14591
  });
13779
14592
  const fixedBlob = await fixWebmDuration(rawBlob, this.duration);
13780
- const fileWithDuration = new File(
13781
- [fixedBlob],
13782
- `EP_${session.id}_camera_0.webm`,
13783
- { type: rawBlob.type }
13784
- );
13785
14593
  session.addRecording({
13786
14594
  device: `Audio
13787
14595
  Sample Rate: ${settingsAudio.sampleRate}
@@ -13789,7 +14597,7 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13789
14597
 
13790
14598
  Video:
13791
14599
  ${JSON.stringify(this.recorderOptions)}`,
13792
- file: fileWithDuration,
14600
+ arrayBuffer: await fixedBlob.arrayBuffer(),
13793
14601
  origin: "Camera" /* Camera */
13794
14602
  });
13795
14603
  }
@@ -13804,6 +14612,28 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13804
14612
  });
13805
14613
  });
13806
14614
  }
14615
+ /**
14616
+ * Verifica se a internet está estável para realizar o upload do vídeo na íntegra.
14617
+ */
14618
+ async checkInternetStability() {
14619
+ var _a2;
14620
+ if (!navigator.onLine) return false;
14621
+ try {
14622
+ const controller = new AbortController();
14623
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
14624
+ const baseUrl = (_a2 = this.backend) == null ? void 0 : _a2.getBaseUrl();
14625
+ if (!baseUrl) return true;
14626
+ const response = await fetch(`${baseUrl}/Client/health`, {
14627
+ method: "GET",
14628
+ signal: controller.signal
14629
+ });
14630
+ clearTimeout(timeoutId);
14631
+ return response.status < 500;
14632
+ } catch (e3) {
14633
+ console.warn("[CameraRecorder] Internet inst\xE1vel ou lenta detectada para upload integral.");
14634
+ return false;
14635
+ }
14636
+ }
13807
14637
  onNoiseDetected() {
13808
14638
  var _a2, _b, _c2;
13809
14639
  if (this.options.proctoringType === "REALTIME") return;
@@ -13828,6 +14658,14 @@ Setting: ${JSON.stringify(settings, null, 2)}`
13828
14658
  this.noiseWait++;
13829
14659
  }
13830
14660
  };
14661
+ // ========================
14662
+ // Chunk & Lifecycle
14663
+ // ========================
14664
+ /** Intervalo de cada chunk em ms (padrão: 60 segundos) */
14665
+ _CameraRecorder.CHUNK_TIMESLICE_MS = 6e4;
14666
+ /** Chave do localStorage para persistir estado da sessão */
14667
+ _CameraRecorder.LS_SESSION_KEY = "ep_proctoring_session";
14668
+ var CameraRecorder = _CameraRecorder;
13831
14669
 
13832
14670
  // src/new-flow/checkers/DeviceCheckerUI.ts
13833
14671
  var DeviceCheckerUI = class {
@@ -15744,11 +16582,17 @@ var ProctoringUploader = class {
15744
16582
  globalOnProgres(100);
15745
16583
  }
15746
16584
  }
16585
+ toFile(rec) {
16586
+ const suffix = rec.origin === "Screen" /* Screen */ ? "screen" : rec.origin === "Camera" /* Camera */ ? "camera" : "audio";
16587
+ const name = `EP_${this.session.id}_${suffix}_0.webm`;
16588
+ return new File([rec.arrayBuffer], name, { type: "video/webm" });
16589
+ }
15747
16590
  async uploadFile(rec, token, onProgress) {
16591
+ const file = this.toFile(rec);
15748
16592
  for await (const uploadService of this.uploadServices) {
15749
16593
  const result = await uploadService.upload(
15750
16594
  {
15751
- file: rec.file,
16595
+ file,
15752
16596
  onProgress: (progress) => {
15753
16597
  if (onProgress) onProgress(progress);
15754
16598
  }
@@ -15757,12 +16601,12 @@ var ProctoringUploader = class {
15757
16601
  ).catch(
15758
16602
  async (e3) => {
15759
16603
  console.log("Upload File Error", e3), trackers.registerError(this.proctoringId, `Upload File
15760
- Name: ${rec.file.name}
16604
+ Name: ${file.name}
15761
16605
  Error: ${e3.message}
15762
16606
  Size: ${e3.error}`);
15763
16607
  await uploadService.upload(
15764
16608
  {
15765
- file: rec.file,
16609
+ file,
15766
16610
  onProgress: (progress) => {
15767
16611
  if (onProgress) onProgress(progress);
15768
16612
  }
@@ -15773,17 +16617,17 @@ Error: ${e3.message}
15773
16617
  );
15774
16618
  if (result) {
15775
16619
  let fileType = "";
15776
- if (rec.file.name.search("camera") !== -1) {
16620
+ if (rec.origin === "Camera" /* Camera */) {
15777
16621
  fileType = "Camera";
15778
- } else if (rec.file.name.search("screen") !== -1) {
16622
+ } else if (rec.origin === "Screen" /* Screen */) {
15779
16623
  fileType = "Screen";
15780
- } else if (rec.file.name.search("audio") !== -1) {
16624
+ } else if (rec.origin === "Mic" /* Mic */) {
15781
16625
  fileType = "Audio";
15782
16626
  }
15783
16627
  trackers.registerUploadFile(this.proctoringId, `Upload File
15784
- Name: ${rec.file.name}
15785
- Type: ${rec.file.type}
15786
- Size: ${rec.file.size}`, fileType);
16628
+ Name: ${file.name}
16629
+ Type: ${file.type}
16630
+ Size: ${file.size}`, fileType);
15787
16631
  return result;
15788
16632
  }
15789
16633
  }
@@ -18593,14 +19437,9 @@ var ScreenRecorder = class {
18593
19437
  type: "video/webm"
18594
19438
  });
18595
19439
  const fixedBlob = await fixWebmDuration2(rawBlob, this.duration);
18596
- const file = new File(
18597
- [fixedBlob],
18598
- `EP_${session.id}_screen_0.webm`,
18599
- { type: rawBlob.type }
18600
- );
18601
19440
  session.addRecording({
18602
19441
  device: "",
18603
- file,
19442
+ arrayBuffer: await fixedBlob.arrayBuffer(),
18604
19443
  origin: "Screen" /* Screen */
18605
19444
  });
18606
19445
  }
@@ -22800,6 +23639,20 @@ var Proctoring = class {
22800
23639
  } catch (error) {
22801
23640
  throw SAFE_BROWSER_API_NOT_FOUND;
22802
23641
  }
23642
+ this.allRecorders.cameraRecorder.onVisibilityRestored = () => {
23643
+ console.log("[Proctoring] Usu\xE1rio retornou ao browser.");
23644
+ this.onVisibilityRestoredCallback();
23645
+ };
23646
+ if (this.sessionOptions.proctoringType === "REALTIME" && !isSafeBrowser()) {
23647
+ try {
23648
+ await BackgroundUploadService.recoverPendingUploads(
23649
+ this.backend,
23650
+ this.context.token
23651
+ );
23652
+ } catch (e3) {
23653
+ console.warn("[Proctoring] Erro ao recuperar chunks de sess\xE3o anterior:", e3);
23654
+ }
23655
+ }
22803
23656
  try {
22804
23657
  console.log("Starting recorders");
22805
23658
  await this.recorder.startAll();