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/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
|
-
|
|
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
|
|
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 = ((
|
|
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 (((
|
|
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 ((
|
|
14328
|
+
if ((_f = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _f.detectFace) {
|
|
13575
14329
|
await this.faceDetection.enableCam(this.cameraStream);
|
|
13576
14330
|
}
|
|
13577
|
-
if (((
|
|
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
|
-
|
|
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
|
|
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: ${
|
|
16604
|
+
Name: ${file.name}
|
|
15761
16605
|
Error: ${e3.message}
|
|
15762
16606
|
Size: ${e3.error}`);
|
|
15763
16607
|
await uploadService.upload(
|
|
15764
16608
|
{
|
|
15765
|
-
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.
|
|
16620
|
+
if (rec.origin === "Camera" /* Camera */) {
|
|
15777
16621
|
fileType = "Camera";
|
|
15778
|
-
} else if (rec.
|
|
16622
|
+
} else if (rec.origin === "Screen" /* Screen */) {
|
|
15779
16623
|
fileType = "Screen";
|
|
15780
|
-
} else if (rec.
|
|
16624
|
+
} else if (rec.origin === "Mic" /* Mic */) {
|
|
15781
16625
|
fileType = "Audio";
|
|
15782
16626
|
}
|
|
15783
16627
|
trackers.registerUploadFile(this.proctoringId, `Upload File
|
|
15784
|
-
Name: ${
|
|
15785
|
-
Type: ${
|
|
15786
|
-
Size: ${
|
|
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
|
-
|
|
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();
|