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 +887 -34
- package/index.js +887 -34
- package/new-flow/chunk/BackgroundUploadService.d.ts +38 -0
- package/new-flow/chunk/ChunkStorageService.d.ts +25 -0
- package/new-flow/proctoring/ProctoringSession.d.ts +1 -1
- package/new-flow/proctoring/ProctoringUploader.d.ts +1 -0
- package/new-flow/recorders/CameraRecorder.d.ts +24 -0
- package/package.json +1 -1
- package/plugins/recorder.d.ts +5 -1
- package/unpkg/easyproctor.min.js +48 -45
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
|
-
|
|
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
|
|
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 = ((
|
|
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 (((
|
|
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 ((
|
|
32425
|
+
if ((_f = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _f.detectFace) {
|
|
31672
32426
|
await this.faceDetection.enableCam(this.cameraStream);
|
|
31673
32427
|
}
|
|
31674
|
-
if (((
|
|
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,20 +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 fileWithDuration = new File(
|
|
31878
|
-
[fixedBlob],
|
|
31879
|
-
`EP_${session.id}_camera_0.webm`,
|
|
31880
|
-
{ type: rawBlob.type }
|
|
31881
|
-
);
|
|
31882
32690
|
session.addRecording({
|
|
31883
32691
|
device: `Audio
|
|
31884
32692
|
Sample Rate: ${settingsAudio.sampleRate}
|
|
@@ -31886,7 +32694,7 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
31886
32694
|
|
|
31887
32695
|
Video:
|
|
31888
32696
|
${JSON.stringify(this.recorderOptions)}`,
|
|
31889
|
-
|
|
32697
|
+
arrayBuffer: await fixedBlob.arrayBuffer(),
|
|
31890
32698
|
origin: "Camera" /* Camera */
|
|
31891
32699
|
});
|
|
31892
32700
|
}
|
|
@@ -31901,6 +32709,28 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
31901
32709
|
});
|
|
31902
32710
|
});
|
|
31903
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
|
+
}
|
|
31904
32734
|
onNoiseDetected() {
|
|
31905
32735
|
var _a2, _b, _c2;
|
|
31906
32736
|
if (this.options.proctoringType === "REALTIME") return;
|
|
@@ -31925,6 +32755,14 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
31925
32755
|
this.noiseWait++;
|
|
31926
32756
|
}
|
|
31927
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;
|
|
31928
32766
|
|
|
31929
32767
|
// src/new-flow/checkers/DeviceCheckerUI.ts
|
|
31930
32768
|
var DeviceCheckerUI = class {
|
|
@@ -33841,11 +34679,17 @@ var ProctoringUploader = class {
|
|
|
33841
34679
|
globalOnProgres(100);
|
|
33842
34680
|
}
|
|
33843
34681
|
}
|
|
34682
|
+
toFile(rec) {
|
|
34683
|
+
const suffix = rec.origin === "Screen" /* Screen */ ? "screen" : rec.origin === "Camera" /* Camera */ ? "camera" : "audio";
|
|
34684
|
+
const name = `EP_${this.session.id}_${suffix}_0.webm`;
|
|
34685
|
+
return new File([rec.arrayBuffer], name, { type: "video/webm" });
|
|
34686
|
+
}
|
|
33844
34687
|
async uploadFile(rec, token, onProgress) {
|
|
34688
|
+
const file = this.toFile(rec);
|
|
33845
34689
|
for await (const uploadService of this.uploadServices) {
|
|
33846
34690
|
const result = await uploadService.upload(
|
|
33847
34691
|
{
|
|
33848
|
-
file
|
|
34692
|
+
file,
|
|
33849
34693
|
onProgress: (progress) => {
|
|
33850
34694
|
if (onProgress) onProgress(progress);
|
|
33851
34695
|
}
|
|
@@ -33854,12 +34698,12 @@ var ProctoringUploader = class {
|
|
|
33854
34698
|
).catch(
|
|
33855
34699
|
async (e3) => {
|
|
33856
34700
|
console.log("Upload File Error", e3), trackers.registerError(this.proctoringId, `Upload File
|
|
33857
|
-
Name: ${
|
|
34701
|
+
Name: ${file.name}
|
|
33858
34702
|
Error: ${e3.message}
|
|
33859
34703
|
Size: ${e3.error}`);
|
|
33860
34704
|
await uploadService.upload(
|
|
33861
34705
|
{
|
|
33862
|
-
file
|
|
34706
|
+
file,
|
|
33863
34707
|
onProgress: (progress) => {
|
|
33864
34708
|
if (onProgress) onProgress(progress);
|
|
33865
34709
|
}
|
|
@@ -33870,17 +34714,17 @@ Error: ${e3.message}
|
|
|
33870
34714
|
);
|
|
33871
34715
|
if (result) {
|
|
33872
34716
|
let fileType = "";
|
|
33873
|
-
if (rec.
|
|
34717
|
+
if (rec.origin === "Camera" /* Camera */) {
|
|
33874
34718
|
fileType = "Camera";
|
|
33875
|
-
} else if (rec.
|
|
34719
|
+
} else if (rec.origin === "Screen" /* Screen */) {
|
|
33876
34720
|
fileType = "Screen";
|
|
33877
|
-
} else if (rec.
|
|
34721
|
+
} else if (rec.origin === "Mic" /* Mic */) {
|
|
33878
34722
|
fileType = "Audio";
|
|
33879
34723
|
}
|
|
33880
34724
|
trackers.registerUploadFile(this.proctoringId, `Upload File
|
|
33881
|
-
Name: ${
|
|
33882
|
-
Type: ${
|
|
33883
|
-
Size: ${
|
|
34725
|
+
Name: ${file.name}
|
|
34726
|
+
Type: ${file.type}
|
|
34727
|
+
Size: ${file.size}`, fileType);
|
|
33884
34728
|
return result;
|
|
33885
34729
|
}
|
|
33886
34730
|
}
|
|
@@ -36690,14 +37534,9 @@ var ScreenRecorder = class {
|
|
|
36690
37534
|
type: "video/webm"
|
|
36691
37535
|
});
|
|
36692
37536
|
const fixedBlob = await fixWebmDuration2(rawBlob, this.duration);
|
|
36693
|
-
const file = new File(
|
|
36694
|
-
[fixedBlob],
|
|
36695
|
-
`EP_${session.id}_screen_0.webm`,
|
|
36696
|
-
{ type: rawBlob.type }
|
|
36697
|
-
);
|
|
36698
37537
|
session.addRecording({
|
|
36699
37538
|
device: "",
|
|
36700
|
-
|
|
37539
|
+
arrayBuffer: await fixedBlob.arrayBuffer(),
|
|
36701
37540
|
origin: "Screen" /* Screen */
|
|
36702
37541
|
});
|
|
36703
37542
|
}
|
|
@@ -38049,6 +38888,20 @@ var Proctoring = class {
|
|
|
38049
38888
|
} catch (error) {
|
|
38050
38889
|
throw SAFE_BROWSER_API_NOT_FOUND;
|
|
38051
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
|
+
}
|
|
38052
38905
|
try {
|
|
38053
38906
|
console.log("Starting recorders");
|
|
38054
38907
|
await this.recorder.startAll();
|