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