easyproctor-hml 2.7.13 → 2.8.0
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 +1078 -85
- package/extension/extensionEasyCatcher.d.ts +11 -0
- package/extension/{extension.d.ts → extensionEasyProctor.d.ts} +1 -1
- package/index.js +1078 -85
- package/new-flow/checkers/DeviceCheckerUI.d.ts +11 -0
- package/new-flow/chunk/BackgroundUploadService.d.ts +38 -0
- package/new-flow/chunk/ChunkStorageService.d.ts +25 -0
- package/new-flow/recorders/CameraRecorder.d.ts +23 -2
- package/package.json +1 -1
- package/proctoring/options/ProctoringOptions.d.ts +1 -0
- package/proctoring/proctoring.d.ts +5 -0
- package/proctoring/useProctoring.d.ts +2 -0
- package/unpkg/easyproctor.min.js +44 -41
package/esm/index.js
CHANGED
|
@@ -9340,6 +9340,9 @@ var FaceDetection = class extends BaseDetection {
|
|
|
9340
9340
|
if (this.emmitedFaceAlert) {
|
|
9341
9341
|
this.handleOk("face_stop", "face_detection_on_stream");
|
|
9342
9342
|
}
|
|
9343
|
+
this.numFacesSent = -1;
|
|
9344
|
+
this.emmitedPositionAlert = false;
|
|
9345
|
+
this.emmitedFaceAlert = false;
|
|
9343
9346
|
}
|
|
9344
9347
|
// displayVideoDetections(result: { detections: any; }) {
|
|
9345
9348
|
// // console.log(result);
|
|
@@ -12699,7 +12702,8 @@ var getDefaultProctoringOptions = {
|
|
|
12699
12702
|
useSpyScan: false,
|
|
12700
12703
|
useExternalCamera: false,
|
|
12701
12704
|
useChallenge: false,
|
|
12702
|
-
screenRecorderOptions: { width: 1280, height: 720 }
|
|
12705
|
+
screenRecorderOptions: { width: 1280, height: 720 },
|
|
12706
|
+
auto: false
|
|
12703
12707
|
};
|
|
12704
12708
|
|
|
12705
12709
|
// src/proctoring/options/ProctoringVideoOptions.ts
|
|
@@ -12758,6 +12762,9 @@ function isMobileDevice() {
|
|
|
12758
12762
|
}
|
|
12759
12763
|
return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
12760
12764
|
}
|
|
12765
|
+
function isSafeBrowser() {
|
|
12766
|
+
return versionVerify() !== "1.0.0.0";
|
|
12767
|
+
}
|
|
12761
12768
|
|
|
12762
12769
|
// src/plugins/recorder.ts
|
|
12763
12770
|
var proctoringId;
|
|
@@ -13251,61 +13258,623 @@ var ObjectDetection = class extends BaseDetection {
|
|
|
13251
13258
|
}
|
|
13252
13259
|
};
|
|
13253
13260
|
|
|
13254
|
-
// src/new-flow/recorders/
|
|
13255
|
-
var
|
|
13256
|
-
|
|
13257
|
-
|
|
13258
|
-
|
|
13259
|
-
|
|
13261
|
+
// src/new-flow/recorders/CameraRecorder.ts
|
|
13262
|
+
var import_jszip_min = __toESM(require_jszip_min());
|
|
13263
|
+
|
|
13264
|
+
// src/new-flow/chunk/ChunkStorageService.ts
|
|
13265
|
+
var _ChunkStorageService = class _ChunkStorageService {
|
|
13266
|
+
constructor() {
|
|
13267
|
+
this.db = null;
|
|
13260
13268
|
}
|
|
13261
|
-
|
|
13269
|
+
/**
|
|
13270
|
+
* Abre a conexão com o IndexedDB, criando o banco e o object store se necessário.
|
|
13271
|
+
*/
|
|
13272
|
+
async connect() {
|
|
13273
|
+
if (this.db) return this.db;
|
|
13262
13274
|
return new Promise((resolve, reject) => {
|
|
13263
|
-
|
|
13264
|
-
|
|
13265
|
-
|
|
13266
|
-
|
|
13267
|
-
|
|
13268
|
-
|
|
13269
|
-
|
|
13270
|
-
|
|
13271
|
-
|
|
13272
|
-
|
|
13273
|
-
|
|
13274
|
-
|
|
13275
|
-
|
|
13276
|
-
|
|
13277
|
-
|
|
13275
|
+
const request = window.indexedDB.open(
|
|
13276
|
+
_ChunkStorageService.DB_NAME,
|
|
13277
|
+
_ChunkStorageService.DB_VERSION
|
|
13278
|
+
);
|
|
13279
|
+
request.onerror = () => {
|
|
13280
|
+
reject(new Error("N\xE3o foi poss\xEDvel conectar ao IndexedDB para chunks."));
|
|
13281
|
+
};
|
|
13282
|
+
request.onupgradeneeded = () => {
|
|
13283
|
+
const db = request.result;
|
|
13284
|
+
if (db.objectStoreNames.contains(_ChunkStorageService.STORE_NAME)) {
|
|
13285
|
+
db.deleteObjectStore(_ChunkStorageService.STORE_NAME);
|
|
13286
|
+
}
|
|
13287
|
+
const store = db.createObjectStore(_ChunkStorageService.STORE_NAME, {
|
|
13288
|
+
keyPath: "id",
|
|
13289
|
+
autoIncrement: true
|
|
13290
|
+
});
|
|
13291
|
+
store.createIndex("proctoringId", "proctoringId", { unique: false });
|
|
13292
|
+
store.createIndex("uploaded", "uploaded", { unique: false });
|
|
13293
|
+
store.createIndex("proctoringId_uploaded", ["proctoringId", "uploaded"], {
|
|
13294
|
+
unique: false
|
|
13295
|
+
});
|
|
13296
|
+
};
|
|
13297
|
+
request.onsuccess = () => {
|
|
13298
|
+
this.db = request.result;
|
|
13299
|
+
resolve(this.db);
|
|
13300
|
+
};
|
|
13301
|
+
});
|
|
13302
|
+
}
|
|
13303
|
+
/**
|
|
13304
|
+
* Salva um chunk de vídeo no IndexedDB.
|
|
13305
|
+
*/
|
|
13306
|
+
async saveChunk(chunk) {
|
|
13307
|
+
const db = await this.connect();
|
|
13308
|
+
return new Promise((resolve, reject) => {
|
|
13309
|
+
const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
|
|
13310
|
+
const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
|
|
13311
|
+
const request = store.add(chunk);
|
|
13312
|
+
request.onsuccess = () => {
|
|
13313
|
+
resolve(request.result);
|
|
13314
|
+
};
|
|
13315
|
+
request.onerror = () => {
|
|
13316
|
+
var _a2;
|
|
13317
|
+
reject(new Error(`Erro ao salvar chunk no IndexedDB: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
|
|
13318
|
+
};
|
|
13319
|
+
});
|
|
13320
|
+
}
|
|
13321
|
+
/**
|
|
13322
|
+
* Retorna todos os chunks pendentes (não enviados) de um proctoringId específico.
|
|
13323
|
+
*/
|
|
13324
|
+
async getPendingChunks(proctoringId2) {
|
|
13325
|
+
const db = await this.connect();
|
|
13326
|
+
return new Promise((resolve, reject) => {
|
|
13327
|
+
const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
|
|
13328
|
+
const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
|
|
13329
|
+
const index = store.index("proctoringId_uploaded");
|
|
13330
|
+
const range = IDBKeyRange.only([proctoringId2, 0]);
|
|
13331
|
+
const request = index.getAll(range);
|
|
13332
|
+
request.onsuccess = () => {
|
|
13333
|
+
const chunks = request.result.sort(
|
|
13334
|
+
(a3, b3) => a3.chunkIndex - b3.chunkIndex
|
|
13335
|
+
);
|
|
13336
|
+
resolve(chunks);
|
|
13337
|
+
};
|
|
13338
|
+
request.onerror = () => {
|
|
13339
|
+
var _a2;
|
|
13340
|
+
reject(new Error(`Erro ao buscar chunks pendentes: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
|
|
13341
|
+
};
|
|
13342
|
+
});
|
|
13343
|
+
}
|
|
13344
|
+
/**
|
|
13345
|
+
* Retorna todos os chunks (enviados ou não) de um proctoringId específico.
|
|
13346
|
+
*/
|
|
13347
|
+
async getAllChunks(proctoringId2) {
|
|
13348
|
+
const db = await this.connect();
|
|
13349
|
+
return new Promise((resolve, reject) => {
|
|
13350
|
+
const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
|
|
13351
|
+
const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
|
|
13352
|
+
const index = store.index("proctoringId");
|
|
13353
|
+
const range = IDBKeyRange.only(proctoringId2);
|
|
13354
|
+
const request = index.getAll(range);
|
|
13355
|
+
request.onsuccess = () => {
|
|
13356
|
+
const chunks = request.result.sort(
|
|
13357
|
+
(a3, b3) => a3.chunkIndex - b3.chunkIndex
|
|
13358
|
+
);
|
|
13359
|
+
resolve(chunks);
|
|
13360
|
+
};
|
|
13361
|
+
request.onerror = () => {
|
|
13362
|
+
var _a2;
|
|
13363
|
+
reject(new Error(`Erro ao buscar todos os chunks: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
|
|
13364
|
+
};
|
|
13365
|
+
});
|
|
13366
|
+
}
|
|
13367
|
+
/**
|
|
13368
|
+
* Marca um chunk como enviado (uploaded = 1).
|
|
13369
|
+
*/
|
|
13370
|
+
async markAsUploaded(chunkId) {
|
|
13371
|
+
const db = await this.connect();
|
|
13372
|
+
return new Promise((resolve, reject) => {
|
|
13373
|
+
const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
|
|
13374
|
+
const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
|
|
13375
|
+
const getRequest = store.get(chunkId);
|
|
13376
|
+
getRequest.onsuccess = () => {
|
|
13377
|
+
const chunk = getRequest.result;
|
|
13378
|
+
if (!chunk) {
|
|
13379
|
+
resolve();
|
|
13380
|
+
return;
|
|
13381
|
+
}
|
|
13382
|
+
chunk.uploaded = 1;
|
|
13383
|
+
const putRequest = store.put(chunk);
|
|
13384
|
+
putRequest.onsuccess = () => resolve();
|
|
13385
|
+
putRequest.onerror = () => {
|
|
13386
|
+
var _a2;
|
|
13387
|
+
return reject(new Error(`Erro ao marcar chunk como enviado: ${(_a2 = putRequest.error) == null ? void 0 : _a2.message}`));
|
|
13278
13388
|
};
|
|
13279
|
-
|
|
13280
|
-
|
|
13281
|
-
|
|
13282
|
-
|
|
13283
|
-
|
|
13284
|
-
}
|
|
13389
|
+
};
|
|
13390
|
+
getRequest.onerror = () => {
|
|
13391
|
+
var _a2;
|
|
13392
|
+
return reject(new Error(`Erro ao buscar chunk para marcar: ${(_a2 = getRequest.error) == null ? void 0 : _a2.message}`));
|
|
13393
|
+
};
|
|
13285
13394
|
});
|
|
13286
13395
|
}
|
|
13396
|
+
/**
|
|
13397
|
+
* Remove todos os chunks já enviados de um proctoringId para liberar espaço.
|
|
13398
|
+
*/
|
|
13399
|
+
async clearUploadedChunks(proctoringId2) {
|
|
13400
|
+
const db = await this.connect();
|
|
13401
|
+
return new Promise((resolve, reject) => {
|
|
13402
|
+
const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
|
|
13403
|
+
const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
|
|
13404
|
+
const index = store.index("proctoringId_uploaded");
|
|
13405
|
+
const range = IDBKeyRange.only([proctoringId2, 1]);
|
|
13406
|
+
const request = index.openCursor(range);
|
|
13407
|
+
request.onsuccess = () => {
|
|
13408
|
+
const cursor = request.result;
|
|
13409
|
+
if (cursor) {
|
|
13410
|
+
cursor.delete();
|
|
13411
|
+
cursor.continue();
|
|
13412
|
+
} else {
|
|
13413
|
+
resolve();
|
|
13414
|
+
}
|
|
13415
|
+
};
|
|
13416
|
+
request.onerror = () => {
|
|
13417
|
+
var _a2;
|
|
13418
|
+
return reject(new Error(`Erro ao limpar chunks enviados: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
|
|
13419
|
+
};
|
|
13420
|
+
});
|
|
13421
|
+
}
|
|
13422
|
+
/**
|
|
13423
|
+
* Remove TODOS os chunks de um proctoringId (limpeza completa pós-finalização).
|
|
13424
|
+
*/
|
|
13425
|
+
async clearAllChunks(proctoringId2) {
|
|
13426
|
+
const db = await this.connect();
|
|
13427
|
+
return new Promise((resolve, reject) => {
|
|
13428
|
+
const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readwrite");
|
|
13429
|
+
const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
|
|
13430
|
+
const index = store.index("proctoringId");
|
|
13431
|
+
const range = IDBKeyRange.only(proctoringId2);
|
|
13432
|
+
const request = index.openCursor(range);
|
|
13433
|
+
request.onsuccess = () => {
|
|
13434
|
+
const cursor = request.result;
|
|
13435
|
+
if (cursor) {
|
|
13436
|
+
cursor.delete();
|
|
13437
|
+
cursor.continue();
|
|
13438
|
+
} else {
|
|
13439
|
+
resolve();
|
|
13440
|
+
}
|
|
13441
|
+
};
|
|
13442
|
+
request.onerror = () => {
|
|
13443
|
+
var _a2;
|
|
13444
|
+
return reject(new Error(`Erro ao limpar todos os chunks: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
|
|
13445
|
+
};
|
|
13446
|
+
});
|
|
13447
|
+
}
|
|
13448
|
+
/**
|
|
13449
|
+
* Verifica se existem chunks pendentes para qualquer proctoringId.
|
|
13450
|
+
* Útil na recuperação pós-crash.
|
|
13451
|
+
*/
|
|
13452
|
+
async hasAnyPendingChunks() {
|
|
13453
|
+
const db = await this.connect();
|
|
13454
|
+
return new Promise((resolve, reject) => {
|
|
13455
|
+
const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
|
|
13456
|
+
const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
|
|
13457
|
+
const index = store.index("uploaded");
|
|
13458
|
+
const range = IDBKeyRange.only(0);
|
|
13459
|
+
const request = index.count(range);
|
|
13460
|
+
request.onsuccess = () => {
|
|
13461
|
+
resolve(request.result > 0);
|
|
13462
|
+
};
|
|
13463
|
+
request.onerror = () => {
|
|
13464
|
+
var _a2;
|
|
13465
|
+
return reject(new Error(`Erro ao verificar chunks pendentes: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
|
|
13466
|
+
};
|
|
13467
|
+
});
|
|
13468
|
+
}
|
|
13469
|
+
/**
|
|
13470
|
+
* Retorna todos os proctoringIds que possuem chunks pendentes.
|
|
13471
|
+
* Útil na recuperação pós-crash para saber quais sessões precisam ser finalizadas.
|
|
13472
|
+
*/
|
|
13473
|
+
async getPendingProctoringIds() {
|
|
13474
|
+
const db = await this.connect();
|
|
13475
|
+
return new Promise((resolve, reject) => {
|
|
13476
|
+
const transaction = db.transaction(_ChunkStorageService.STORE_NAME, "readonly");
|
|
13477
|
+
const store = transaction.objectStore(_ChunkStorageService.STORE_NAME);
|
|
13478
|
+
const index = store.index("uploaded");
|
|
13479
|
+
const range = IDBKeyRange.only(0);
|
|
13480
|
+
const request = index.getAll(range);
|
|
13481
|
+
request.onsuccess = () => {
|
|
13482
|
+
const chunks = request.result;
|
|
13483
|
+
const ids = [...new Set(chunks.map((c3) => c3.proctoringId))];
|
|
13484
|
+
resolve(ids);
|
|
13485
|
+
};
|
|
13486
|
+
request.onerror = () => {
|
|
13487
|
+
var _a2;
|
|
13488
|
+
return reject(new Error(`Erro ao buscar proctoringIds pendentes: ${(_a2 = request.error) == null ? void 0 : _a2.message}`));
|
|
13489
|
+
};
|
|
13490
|
+
});
|
|
13491
|
+
}
|
|
13492
|
+
/**
|
|
13493
|
+
* Fecha a conexão com o banco.
|
|
13494
|
+
*/
|
|
13495
|
+
close() {
|
|
13496
|
+
if (this.db) {
|
|
13497
|
+
this.db.close();
|
|
13498
|
+
this.db = null;
|
|
13499
|
+
}
|
|
13500
|
+
}
|
|
13501
|
+
};
|
|
13502
|
+
_ChunkStorageService.DB_NAME = "EasyProctorChunksDb";
|
|
13503
|
+
/** v2: índices uploaded numéricos; v3: campo arrayBuffer em vez de blob */
|
|
13504
|
+
_ChunkStorageService.DB_VERSION = 3;
|
|
13505
|
+
_ChunkStorageService.STORE_NAME = "chunks";
|
|
13506
|
+
var ChunkStorageService = _ChunkStorageService;
|
|
13507
|
+
|
|
13508
|
+
// src/new-flow/chunk/BackgroundUploadService.ts
|
|
13509
|
+
var DEFAULT_CONFIG = {
|
|
13510
|
+
pollInterval: 5e3,
|
|
13511
|
+
maxRetries: 5,
|
|
13512
|
+
baseRetryDelay: 2e3,
|
|
13513
|
+
cleanAfterUpload: true
|
|
13514
|
+
};
|
|
13515
|
+
var BackgroundUploadService = class _BackgroundUploadService {
|
|
13516
|
+
constructor(proctoringId2, token, backend, chunkStorage, config) {
|
|
13517
|
+
this.pollTimer = null;
|
|
13518
|
+
this.isProcessing = false;
|
|
13519
|
+
this.isRunning = false;
|
|
13520
|
+
/** Mapa de chunkId -> número de tentativas já feitas */
|
|
13521
|
+
this.retryCount = /* @__PURE__ */ new Map();
|
|
13522
|
+
/** GCS Resumable Upload State */
|
|
13523
|
+
this.sessionUrl = null;
|
|
13524
|
+
this.currentOffset = 0;
|
|
13525
|
+
this.totalBytesPurged = 0;
|
|
13526
|
+
this.STORAGE_KEY_PREFIX = "ep_upload_session_";
|
|
13527
|
+
this.GCS_CHUNK_SIZE = 256 * 1024;
|
|
13528
|
+
this.proctoringId = proctoringId2.trim();
|
|
13529
|
+
this.token = token;
|
|
13530
|
+
this.backend = backend;
|
|
13531
|
+
this.chunkStorage = chunkStorage;
|
|
13532
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
13533
|
+
this.loadSessionState();
|
|
13534
|
+
}
|
|
13535
|
+
loadSessionState() {
|
|
13536
|
+
try {
|
|
13537
|
+
const stored = localStorage.getItem(`${this.STORAGE_KEY_PREFIX}${this.proctoringId}`);
|
|
13538
|
+
if (stored) {
|
|
13539
|
+
const { sessionUrl, currentOffset, totalBytesPurged } = JSON.parse(stored);
|
|
13540
|
+
this.sessionUrl = sessionUrl;
|
|
13541
|
+
this.currentOffset = currentOffset;
|
|
13542
|
+
this.totalBytesPurged = totalBytesPurged || 0;
|
|
13543
|
+
}
|
|
13544
|
+
} catch (e3) {
|
|
13545
|
+
console.warn("[BackgroundUpload] Erro ao carregar estado da sess\xE3o:", e3);
|
|
13546
|
+
}
|
|
13547
|
+
}
|
|
13548
|
+
saveSessionState() {
|
|
13549
|
+
try {
|
|
13550
|
+
localStorage.setItem(
|
|
13551
|
+
`${this.STORAGE_KEY_PREFIX}${this.proctoringId}`,
|
|
13552
|
+
JSON.stringify({
|
|
13553
|
+
sessionUrl: this.sessionUrl,
|
|
13554
|
+
currentOffset: this.currentOffset,
|
|
13555
|
+
totalBytesPurged: this.totalBytesPurged
|
|
13556
|
+
})
|
|
13557
|
+
);
|
|
13558
|
+
} catch (e3) {
|
|
13559
|
+
console.warn("[BackgroundUpload] Erro ao salvar estado da sess\xE3o:", e3);
|
|
13560
|
+
}
|
|
13561
|
+
}
|
|
13562
|
+
clearSessionState() {
|
|
13563
|
+
try {
|
|
13564
|
+
localStorage.removeItem(`${this.STORAGE_KEY_PREFIX}${this.proctoringId}`);
|
|
13565
|
+
this.sessionUrl = null;
|
|
13566
|
+
this.currentOffset = 0;
|
|
13567
|
+
this.totalBytesPurged = 0;
|
|
13568
|
+
} catch (e3) {
|
|
13569
|
+
console.warn("[BackgroundUpload] Erro ao limpar estado da sess\xE3o:", e3);
|
|
13570
|
+
}
|
|
13571
|
+
}
|
|
13572
|
+
/**
|
|
13573
|
+
* Inicia o serviço de upload em background. Faz polling periódico no IndexedDB
|
|
13574
|
+
* para enviar chunks pendentes.
|
|
13575
|
+
*/
|
|
13576
|
+
start() {
|
|
13577
|
+
if (this.isRunning) return;
|
|
13578
|
+
this.isRunning = true;
|
|
13579
|
+
console.log(`[BackgroundUpload] Iniciando servi\xE7o para proctoringId: ${this.proctoringId}`);
|
|
13580
|
+
this.processQueue();
|
|
13581
|
+
this.pollTimer = setInterval(() => {
|
|
13582
|
+
this.processQueue(false);
|
|
13583
|
+
}, this.config.pollInterval);
|
|
13584
|
+
}
|
|
13585
|
+
/**
|
|
13586
|
+
* Para o serviço de upload em background.
|
|
13587
|
+
*/
|
|
13287
13588
|
stop() {
|
|
13288
|
-
|
|
13289
|
-
if (this.
|
|
13290
|
-
|
|
13589
|
+
this.isRunning = false;
|
|
13590
|
+
if (this.pollTimer) {
|
|
13591
|
+
clearInterval(this.pollTimer);
|
|
13592
|
+
this.pollTimer = null;
|
|
13291
13593
|
}
|
|
13292
|
-
(
|
|
13293
|
-
(_b = this.microphone) == null ? void 0 : _b.disconnect();
|
|
13294
|
-
(_c2 = this.analyser) == null ? void 0 : _c2.disconnect();
|
|
13594
|
+
console.log(`[BackgroundUpload] Servi\xE7o parado para proctoringId: ${this.proctoringId}`);
|
|
13295
13595
|
}
|
|
13296
|
-
|
|
13297
|
-
|
|
13596
|
+
/**
|
|
13597
|
+
* Força o processamento de todos os chunks pendentes e encerra a sessão GCS.
|
|
13598
|
+
* Útil quando a gravação é finalizada.
|
|
13599
|
+
*/
|
|
13600
|
+
async flush() {
|
|
13601
|
+
console.log(`[BackgroundUpload] Flush: enviando todos os chunks pendentes e finalizando...`);
|
|
13602
|
+
let waitAttempts = 0;
|
|
13603
|
+
while (this.isProcessing && waitAttempts < 10) {
|
|
13604
|
+
await this.sleep(1e3);
|
|
13605
|
+
waitAttempts++;
|
|
13606
|
+
}
|
|
13607
|
+
let flushRetries = 0;
|
|
13608
|
+
const maxFlushRetries = 3;
|
|
13609
|
+
while (flushRetries < maxFlushRetries) {
|
|
13610
|
+
try {
|
|
13611
|
+
await this.processQueue(true);
|
|
13612
|
+
console.log(`[BackgroundUpload] Flush completado com sucesso.`);
|
|
13613
|
+
return;
|
|
13614
|
+
} catch (error) {
|
|
13615
|
+
flushRetries++;
|
|
13616
|
+
console.error(`[BackgroundUpload] Erro no flush (tentativa ${flushRetries}/${maxFlushRetries}):`, error);
|
|
13617
|
+
if (flushRetries < maxFlushRetries) {
|
|
13618
|
+
await this.sleep(2e3);
|
|
13619
|
+
}
|
|
13620
|
+
}
|
|
13621
|
+
}
|
|
13622
|
+
throw new Error(`[BackgroundUpload] Falha ao finalizar upload ap\xF3s ${maxFlushRetries} tentativas.`);
|
|
13298
13623
|
}
|
|
13299
|
-
|
|
13300
|
-
|
|
13624
|
+
/**
|
|
13625
|
+
* Sincroniza o offset local com o estado real no Google Cloud Storage.
|
|
13626
|
+
*/
|
|
13627
|
+
async syncOffset() {
|
|
13628
|
+
if (!this.sessionUrl) return 0;
|
|
13629
|
+
try {
|
|
13630
|
+
console.log(`[BackgroundUpload] Sincronizando offset com GCS...`);
|
|
13631
|
+
const response = await fetch(this.sessionUrl, {
|
|
13632
|
+
method: "PUT",
|
|
13633
|
+
headers: {
|
|
13634
|
+
"Content-Range": "bytes */*"
|
|
13635
|
+
}
|
|
13636
|
+
});
|
|
13637
|
+
console.log(`[BackgroundUpload] Status da sincroniza\xE7\xE3o (syncOffset): ${response.status}`);
|
|
13638
|
+
if (response.status === 308) {
|
|
13639
|
+
const range = response.headers.get("Range");
|
|
13640
|
+
if (range) {
|
|
13641
|
+
const lastByte = parseInt(range.split("-")[1], 10);
|
|
13642
|
+
this.currentOffset = lastByte + 1;
|
|
13643
|
+
this.saveSessionState();
|
|
13644
|
+
console.log(`[BackgroundUpload] Offset sincronizado: ${this.currentOffset}`);
|
|
13645
|
+
} else {
|
|
13646
|
+
this.currentOffset = 0;
|
|
13647
|
+
}
|
|
13648
|
+
} else if (response.ok || response.status === 201) {
|
|
13649
|
+
console.log("[BackgroundUpload] Sincroniza\xE7\xE3o indicou upload JA FINALIZADO.");
|
|
13650
|
+
this.currentOffset = -1;
|
|
13651
|
+
} else {
|
|
13652
|
+
console.warn(`[BackgroundUpload] Status inesperado na sincroniza\xE7\xE3o: ${response.status}`);
|
|
13653
|
+
}
|
|
13654
|
+
} catch (error) {
|
|
13655
|
+
console.warn("[BackgroundUpload] Erro ao sincronizar offset:", error);
|
|
13656
|
+
}
|
|
13657
|
+
return this.currentOffset;
|
|
13658
|
+
}
|
|
13659
|
+
/**
|
|
13660
|
+
* Verifica e envia chunks pendentes para o backend.
|
|
13661
|
+
* @param isFinal Se true, não alinha a 256KB e fecha a sessão com /TOTAL no header.
|
|
13662
|
+
*/
|
|
13663
|
+
async processQueue(isFinal = false) {
|
|
13664
|
+
var _a2, _b;
|
|
13665
|
+
if (this.isProcessing) return;
|
|
13666
|
+
this.isProcessing = true;
|
|
13667
|
+
try {
|
|
13668
|
+
if (this.sessionUrl) {
|
|
13669
|
+
await this.syncOffset();
|
|
13670
|
+
if (this.currentOffset === -1) {
|
|
13671
|
+
console.log("[BackgroundUpload] Sess\xE3o j\xE1 finalizada no servidor.");
|
|
13672
|
+
this.clearSessionState();
|
|
13673
|
+
this.isProcessing = false;
|
|
13674
|
+
return;
|
|
13675
|
+
}
|
|
13676
|
+
}
|
|
13677
|
+
const allChunks = await this.chunkStorage.getAllChunks(this.proctoringId);
|
|
13678
|
+
const pendingChunks = allChunks.filter((c3) => c3.uploaded === 0);
|
|
13679
|
+
if (pendingChunks.length === 0 && !isFinal) {
|
|
13680
|
+
this.isProcessing = false;
|
|
13681
|
+
return;
|
|
13682
|
+
}
|
|
13683
|
+
console.log(`[BackgroundUpload] ${pendingChunks.length} chunks pendentes encontrados. Modo final: ${isFinal}`);
|
|
13684
|
+
let virtualStart = this.totalBytesPurged;
|
|
13685
|
+
const chunksWithMeta = allChunks.map((c3) => {
|
|
13686
|
+
const start = virtualStart;
|
|
13687
|
+
const byteLength = c3.arrayBuffer.byteLength;
|
|
13688
|
+
const end = start + byteLength - 1;
|
|
13689
|
+
virtualStart += byteLength;
|
|
13690
|
+
return { chunk: c3, start, end };
|
|
13691
|
+
});
|
|
13692
|
+
let combinedBlobParts = [];
|
|
13693
|
+
let lastProcessedChunkId = null;
|
|
13694
|
+
let finalChunkIndex = 0;
|
|
13695
|
+
let mimeType = pendingChunks[0].mimeType;
|
|
13696
|
+
for (const meta of chunksWithMeta) {
|
|
13697
|
+
if (this.currentOffset > meta.end) continue;
|
|
13698
|
+
const sliceStart = Math.max(0, this.currentOffset - meta.start);
|
|
13699
|
+
const sliceBuf = meta.chunk.arrayBuffer.slice(sliceStart);
|
|
13700
|
+
combinedBlobParts.push(new Blob([sliceBuf]));
|
|
13701
|
+
lastProcessedChunkId = meta.chunk.id;
|
|
13702
|
+
finalChunkIndex = meta.chunk.chunkIndex;
|
|
13703
|
+
}
|
|
13704
|
+
if (combinedBlobParts.length === 0 && !isFinal) {
|
|
13705
|
+
this.isProcessing = false;
|
|
13706
|
+
return;
|
|
13707
|
+
}
|
|
13708
|
+
let fullBlob = new Blob(combinedBlobParts, { type: mimeType });
|
|
13709
|
+
let sendableSize = fullBlob.size;
|
|
13710
|
+
let totalSizeForHeader = void 0;
|
|
13711
|
+
if (!isFinal) {
|
|
13712
|
+
sendableSize = Math.floor(fullBlob.size / this.GCS_CHUNK_SIZE) * this.GCS_CHUNK_SIZE;
|
|
13713
|
+
if (sendableSize === 0) {
|
|
13714
|
+
console.log("[BackgroundUpload] Dados insuficientes para atingir 256KB. Aguardando novo chunk...");
|
|
13715
|
+
this.isProcessing = false;
|
|
13716
|
+
return;
|
|
13717
|
+
}
|
|
13718
|
+
} else {
|
|
13719
|
+
totalSizeForHeader = virtualStart;
|
|
13720
|
+
}
|
|
13721
|
+
const blobToSend = fullBlob.slice(0, sendableSize);
|
|
13722
|
+
try {
|
|
13723
|
+
await this.uploadData(blobToSend, mimeType, finalChunkIndex, totalSizeForHeader);
|
|
13724
|
+
for (const meta of chunksWithMeta) {
|
|
13725
|
+
if (meta.chunk.uploaded === 0 && meta.end < this.currentOffset) {
|
|
13726
|
+
await this.chunkStorage.markAsUploaded(meta.chunk.id);
|
|
13727
|
+
this.retryCount.delete(meta.chunk.id);
|
|
13728
|
+
(_a2 = this.onChunkUploaded) == null ? void 0 : _a2.call(this, meta.chunk.id, meta.chunk.chunkIndex);
|
|
13729
|
+
console.log(`[BackgroundUpload] Chunk ${meta.chunk.chunkIndex} marcado como enviado.`);
|
|
13730
|
+
}
|
|
13731
|
+
}
|
|
13732
|
+
if (this.config.cleanAfterUpload) {
|
|
13733
|
+
const chunksToClear = chunksWithMeta.filter((meta) => meta.chunk.uploaded === 1 || meta.chunk.uploaded === 0 && meta.end < this.currentOffset);
|
|
13734
|
+
const sizePurged = chunksToClear.reduce(
|
|
13735
|
+
(acc, meta) => acc + meta.chunk.arrayBuffer.byteLength,
|
|
13736
|
+
0
|
|
13737
|
+
);
|
|
13738
|
+
await this.chunkStorage.clearUploadedChunks(this.proctoringId);
|
|
13739
|
+
if (sizePurged > 0) {
|
|
13740
|
+
this.totalBytesPurged += sizePurged;
|
|
13741
|
+
this.saveSessionState();
|
|
13742
|
+
console.log(`[BackgroundUpload] ${sizePurged} bytes limpos do armazenamento local. Total purgado: ${this.totalBytesPurged}`);
|
|
13743
|
+
}
|
|
13744
|
+
}
|
|
13745
|
+
if (isFinal) {
|
|
13746
|
+
this.clearSessionState();
|
|
13747
|
+
}
|
|
13748
|
+
} catch (error) {
|
|
13749
|
+
console.error("[BackgroundUpload] Falha no upload:", error);
|
|
13750
|
+
(_b = this.onUploadError) == null ? void 0 : _b.call(this, lastProcessedChunkId || 0, error);
|
|
13751
|
+
}
|
|
13752
|
+
} catch (error) {
|
|
13753
|
+
console.error("[BackgroundUpload] Erro ao processar fila:", error);
|
|
13754
|
+
} finally {
|
|
13755
|
+
this.isProcessing = false;
|
|
13756
|
+
}
|
|
13757
|
+
}
|
|
13758
|
+
/**
|
|
13759
|
+
* Faz o upload bruto de dados para a sessão GCS.
|
|
13760
|
+
*/
|
|
13761
|
+
async uploadData(blob, mimeType, chunkIndex, totalSize) {
|
|
13762
|
+
const fileName = `EP_${this.proctoringId}_camera_0.webm`;
|
|
13763
|
+
if (!this.sessionUrl) {
|
|
13764
|
+
const initiateUrl = await this.backend.initiateUpload(this.token, `${this.proctoringId}/${fileName}`, mimeType);
|
|
13765
|
+
const startResponse = await fetch(initiateUrl, {
|
|
13766
|
+
method: "POST",
|
|
13767
|
+
headers: { "x-goog-resumable": "start", "Content-Type": mimeType }
|
|
13768
|
+
});
|
|
13769
|
+
if (!startResponse.ok) throw new Error(`Falha ao iniciar: ${startResponse.status}`);
|
|
13770
|
+
this.sessionUrl = startResponse.headers.get("Location");
|
|
13771
|
+
if (!this.sessionUrl) throw new Error("Location header ausente");
|
|
13772
|
+
try {
|
|
13773
|
+
const urlObj = new URL(this.sessionUrl);
|
|
13774
|
+
const pathParts = urlObj.pathname.split("/");
|
|
13775
|
+
let bucket = pathParts[1];
|
|
13776
|
+
let object = decodeURIComponent(pathParts.slice(2).join("/"));
|
|
13777
|
+
if (pathParts.includes("b") && pathParts.includes("o")) {
|
|
13778
|
+
const bIdx = pathParts.indexOf("b") + 1;
|
|
13779
|
+
const oIdx = pathParts.indexOf("o") + 1;
|
|
13780
|
+
bucket = pathParts[bIdx];
|
|
13781
|
+
object = decodeURIComponent(pathParts.slice(oIdx).join("/"));
|
|
13782
|
+
}
|
|
13783
|
+
console.log(`[BackgroundUpload] Sess\xE3o Iniciada -> Bucket: ${bucket}, Objeto: ${object}`);
|
|
13784
|
+
} catch (e3) {
|
|
13785
|
+
console.log(`[BackgroundUpload] Sess\xE3o Iniciada. URL: ${this.sessionUrl}`);
|
|
13786
|
+
}
|
|
13787
|
+
this.currentOffset = 0;
|
|
13788
|
+
this.saveSessionState();
|
|
13789
|
+
} else {
|
|
13790
|
+
console.log(`[BackgroundUpload] Usando sess\xE3o GCS existente: ${this.sessionUrl}`);
|
|
13791
|
+
}
|
|
13792
|
+
const start = this.currentOffset;
|
|
13793
|
+
const end = start + blob.size - 1;
|
|
13794
|
+
const totalHeader = totalSize !== void 0 ? totalSize.toString() : "*";
|
|
13795
|
+
const contentRangeHeader = blob.size === 0 && totalSize !== void 0 ? `bytes */${totalHeader}` : `bytes ${start}-${end}/${totalHeader}`;
|
|
13796
|
+
console.log(`[BackgroundUpload] Enviando ${blob.size > 0 ? "dados" : "finaliza\xE7\xE3o"}: ${contentRangeHeader} (Size: ${blob.size})`);
|
|
13797
|
+
const response = await fetch(this.sessionUrl, {
|
|
13798
|
+
method: "PUT",
|
|
13799
|
+
headers: { "Content-Range": contentRangeHeader },
|
|
13800
|
+
body: blob.size > 0 ? blob : null
|
|
13801
|
+
// Usa null para garantir corpo vazio se necessário
|
|
13802
|
+
});
|
|
13803
|
+
console.log(`[BackgroundUpload] Resposta GCS (uploadData): ${response.status}`);
|
|
13804
|
+
if (response.status !== 200 && response.status !== 201 && response.status !== 308) {
|
|
13805
|
+
const errorText = await response.text();
|
|
13806
|
+
console.error(`[BackgroundUpload] Erro GCS: ${errorText}`);
|
|
13807
|
+
throw new Error(`Status HTTP inesperado: ${response.status}`);
|
|
13808
|
+
}
|
|
13809
|
+
const rangeHeader = response.headers.get("Range");
|
|
13810
|
+
if (rangeHeader) {
|
|
13811
|
+
const lastByte = parseInt(rangeHeader.split("-")[1], 10);
|
|
13812
|
+
this.currentOffset = lastByte + 1;
|
|
13813
|
+
} else {
|
|
13814
|
+
this.currentOffset += blob.size;
|
|
13815
|
+
}
|
|
13816
|
+
this.saveSessionState();
|
|
13817
|
+
trackers.registerUploadFile(
|
|
13818
|
+
this.proctoringId,
|
|
13819
|
+
`GCS Stream Upload
|
|
13820
|
+
Size: ${blob.size}
|
|
13821
|
+
Range: ${start}-${end}
|
|
13822
|
+
Last Index: ${chunkIndex}`,
|
|
13823
|
+
"CameraChunk"
|
|
13824
|
+
);
|
|
13825
|
+
}
|
|
13826
|
+
/**
|
|
13827
|
+
* Método estático para recuperação pós-crash.
|
|
13828
|
+
* Verifica o IndexedDB em busca de chunks pendentes de qualquer sessão
|
|
13829
|
+
* e tenta enviar.
|
|
13830
|
+
*/
|
|
13831
|
+
static async recoverPendingUploads(backend, token) {
|
|
13832
|
+
const chunkStorage = new ChunkStorageService();
|
|
13833
|
+
const recoveredIds = [];
|
|
13834
|
+
try {
|
|
13835
|
+
const pendingIds = await chunkStorage.getPendingProctoringIds();
|
|
13836
|
+
if (pendingIds.length === 0) {
|
|
13837
|
+
console.log("[BackgroundUpload] Nenhum chunk pendente encontrado para recupera\xE7\xE3o.");
|
|
13838
|
+
return recoveredIds;
|
|
13839
|
+
}
|
|
13840
|
+
console.log(
|
|
13841
|
+
`[BackgroundUpload] Recupera\xE7\xE3o p\xF3s-crash: ${pendingIds.length} sess\xE3o(\xF5es) com chunks pendentes.`
|
|
13842
|
+
);
|
|
13843
|
+
for (const proctoringId2 of pendingIds) {
|
|
13844
|
+
try {
|
|
13845
|
+
const service = new _BackgroundUploadService(
|
|
13846
|
+
proctoringId2,
|
|
13847
|
+
token,
|
|
13848
|
+
backend,
|
|
13849
|
+
chunkStorage,
|
|
13850
|
+
{ cleanAfterUpload: true }
|
|
13851
|
+
);
|
|
13852
|
+
await service.flush();
|
|
13853
|
+
recoveredIds.push(proctoringId2);
|
|
13854
|
+
console.log(
|
|
13855
|
+
`[BackgroundUpload] Chunks da sess\xE3o ${proctoringId2} recuperados com sucesso.`
|
|
13856
|
+
);
|
|
13857
|
+
} catch (error) {
|
|
13858
|
+
console.error(
|
|
13859
|
+
`[BackgroundUpload] Erro ao recuperar chunks da sess\xE3o ${proctoringId2}:`,
|
|
13860
|
+
error
|
|
13861
|
+
);
|
|
13862
|
+
}
|
|
13863
|
+
}
|
|
13864
|
+
} catch (error) {
|
|
13865
|
+
console.error("[BackgroundUpload] Erro geral na recupera\xE7\xE3o:", error);
|
|
13866
|
+
}
|
|
13867
|
+
return recoveredIds;
|
|
13868
|
+
}
|
|
13869
|
+
sleep(ms2) {
|
|
13870
|
+
return new Promise((resolve) => setTimeout(resolve, ms2));
|
|
13301
13871
|
}
|
|
13302
13872
|
};
|
|
13303
13873
|
|
|
13304
13874
|
// src/new-flow/recorders/CameraRecorder.ts
|
|
13305
|
-
var import_jszip_min = __toESM(require_jszip_min());
|
|
13306
13875
|
var pkg = require_fix_webm_duration();
|
|
13307
13876
|
var fixWebmDuration = pkg.default || pkg;
|
|
13308
|
-
var
|
|
13877
|
+
var _CameraRecorder = class _CameraRecorder {
|
|
13309
13878
|
constructor(options, videoOptions, paramsConfig, backend, backendToken) {
|
|
13310
13879
|
this.blobs = [];
|
|
13311
13880
|
this.paramsConfig = {
|
|
@@ -13358,20 +13927,138 @@ var CameraRecorder = class {
|
|
|
13358
13927
|
this.videoElement = null;
|
|
13359
13928
|
this.duration = 0;
|
|
13360
13929
|
this.stopped = false;
|
|
13930
|
+
this.backgroundUpload = null;
|
|
13931
|
+
this.chunkIndex = 0;
|
|
13932
|
+
/** Lista de promises de chunks sendo salvos no IndexedDB para evitar race conditions no stop */
|
|
13933
|
+
this.pendingChunkSaves = [];
|
|
13934
|
+
// Handlers bound para poder remover os listeners depois
|
|
13935
|
+
this.boundVisibilityHandler = null;
|
|
13936
|
+
this.boundPageHideHandler = null;
|
|
13361
13937
|
this.currentRetries = 0;
|
|
13362
13938
|
this.packageCount = 0;
|
|
13363
13939
|
this.failedUploads = 0;
|
|
13364
|
-
this.noiseWait = 20;
|
|
13365
13940
|
this.options = options;
|
|
13366
13941
|
this.videoOptions = videoOptions;
|
|
13367
13942
|
this.backend = backend;
|
|
13368
13943
|
this.backendToken = backendToken;
|
|
13369
13944
|
paramsConfig && (this.paramsConfig = paramsConfig);
|
|
13370
13945
|
}
|
|
13946
|
+
/**
|
|
13947
|
+
* Determina se o fluxo de chunks e lifecycle deve estar ativo.
|
|
13948
|
+
* Retorna true se:
|
|
13949
|
+
* 1. O proctoringId já foi definido (ou seja, estamos em uma sessão real, NÃO no checkDevices)
|
|
13950
|
+
* 2. E (`useChunkRecording` foi explicitamente setado como true OU o dispositivo é mobile)
|
|
13951
|
+
*/
|
|
13952
|
+
get isChunkEnabled() {
|
|
13953
|
+
return !!this.proctoringId && this.options.proctoringType === "REALTIME" && !isSafeBrowser();
|
|
13954
|
+
}
|
|
13371
13955
|
setProctoringId(proctoringId2) {
|
|
13372
13956
|
this.proctoringId = proctoringId2;
|
|
13373
13957
|
this.proctoringId && this.backend && (this.upload = new UploadService(this.proctoringId, this.backend));
|
|
13374
13958
|
setRecorderProctoringId(proctoringId2);
|
|
13959
|
+
if (this.isChunkEnabled) {
|
|
13960
|
+
this.chunkStorage = new ChunkStorageService();
|
|
13961
|
+
if (this.backend && this.backendToken) {
|
|
13962
|
+
this.backgroundUpload = new BackgroundUploadService(
|
|
13963
|
+
this.proctoringId,
|
|
13964
|
+
this.backendToken,
|
|
13965
|
+
this.backend,
|
|
13966
|
+
this.chunkStorage,
|
|
13967
|
+
{ pollInterval: 5e3, maxRetries: 5, cleanAfterUpload: true }
|
|
13968
|
+
);
|
|
13969
|
+
}
|
|
13970
|
+
this.persistSessionState("IN_PROGRESS");
|
|
13971
|
+
console.log(
|
|
13972
|
+
`[CameraRecorder] Chunk recording ATIVO (type: ${this.options.proctoringType}, mobile: ${isMobileDevice()})`
|
|
13973
|
+
);
|
|
13974
|
+
} else {
|
|
13975
|
+
console.log(
|
|
13976
|
+
`[CameraRecorder] Chunk recording INATIVO (type: ${this.options.proctoringType}) \u2014 modo cl\xE1ssico.`
|
|
13977
|
+
);
|
|
13978
|
+
}
|
|
13979
|
+
}
|
|
13980
|
+
// ========================
|
|
13981
|
+
// Session State Persistence (localStorage)
|
|
13982
|
+
// ========================
|
|
13983
|
+
persistSessionState(status) {
|
|
13984
|
+
try {
|
|
13985
|
+
const data = {
|
|
13986
|
+
proctoringId: this.proctoringId,
|
|
13987
|
+
status,
|
|
13988
|
+
timestamp: Date.now()
|
|
13989
|
+
};
|
|
13990
|
+
localStorage.setItem(_CameraRecorder.LS_SESSION_KEY, JSON.stringify(data));
|
|
13991
|
+
} catch (e3) {
|
|
13992
|
+
console.warn("[CameraRecorder] N\xE3o foi poss\xEDvel salvar estado no localStorage:", e3);
|
|
13993
|
+
}
|
|
13994
|
+
}
|
|
13995
|
+
clearSessionState() {
|
|
13996
|
+
try {
|
|
13997
|
+
localStorage.removeItem(_CameraRecorder.LS_SESSION_KEY);
|
|
13998
|
+
} catch (e3) {
|
|
13999
|
+
console.warn("[CameraRecorder] N\xE3o foi poss\xEDvel limpar estado do localStorage:", e3);
|
|
14000
|
+
}
|
|
14001
|
+
}
|
|
14002
|
+
/**
|
|
14003
|
+
* Verifica se existe uma sessão ativa anterior no localStorage.
|
|
14004
|
+
* Retorna os dados da sessão se ela estiver em IN_PROGRESS, null caso contrário.
|
|
14005
|
+
*/
|
|
14006
|
+
static checkForActiveSession() {
|
|
14007
|
+
try {
|
|
14008
|
+
const raw = localStorage.getItem(_CameraRecorder.LS_SESSION_KEY);
|
|
14009
|
+
if (!raw) return null;
|
|
14010
|
+
const data = JSON.parse(raw);
|
|
14011
|
+
if (data.status === "IN_PROGRESS") return data;
|
|
14012
|
+
return null;
|
|
14013
|
+
} catch (e3) {
|
|
14014
|
+
return null;
|
|
14015
|
+
}
|
|
14016
|
+
}
|
|
14017
|
+
// ========================
|
|
14018
|
+
// Page Lifecycle Management
|
|
14019
|
+
// ========================
|
|
14020
|
+
setupLifecycleListeners() {
|
|
14021
|
+
this.boundVisibilityHandler = () => this.handleVisibilityChange();
|
|
14022
|
+
this.boundPageHideHandler = () => this.handlePageHide();
|
|
14023
|
+
document.addEventListener("visibilitychange", this.boundVisibilityHandler);
|
|
14024
|
+
window.addEventListener("pagehide", this.boundPageHideHandler);
|
|
14025
|
+
}
|
|
14026
|
+
removeLifecycleListeners() {
|
|
14027
|
+
if (this.boundVisibilityHandler) {
|
|
14028
|
+
document.removeEventListener("visibilitychange", this.boundVisibilityHandler);
|
|
14029
|
+
this.boundVisibilityHandler = null;
|
|
14030
|
+
}
|
|
14031
|
+
if (this.boundPageHideHandler) {
|
|
14032
|
+
window.removeEventListener("pagehide", this.boundPageHideHandler);
|
|
14033
|
+
this.boundPageHideHandler = null;
|
|
14034
|
+
}
|
|
14035
|
+
}
|
|
14036
|
+
handleVisibilityChange() {
|
|
14037
|
+
var _a2;
|
|
14038
|
+
if (document.visibilityState === "hidden") {
|
|
14039
|
+
console.log("[CameraRecorder] P\xE1gina ficou invis\xEDvel \u2014 sess\xE3o potencialmente interrompida.");
|
|
14040
|
+
this.persistSessionState("INTERRUPTED");
|
|
14041
|
+
this.proctoringId && trackers.registerError(
|
|
14042
|
+
this.proctoringId,
|
|
14043
|
+
"Visibility API: P\xE1gina ficou oculta (hidden). Poss\xEDvel troca de app ou minimiza\xE7\xE3o."
|
|
14044
|
+
);
|
|
14045
|
+
} else if (document.visibilityState === "visible") {
|
|
14046
|
+
console.log("[CameraRecorder] P\xE1gina vis\xEDvel novamente \u2014 verificando estado da grava\xE7\xE3o.");
|
|
14047
|
+
this.persistSessionState("IN_PROGRESS");
|
|
14048
|
+
this.proctoringId && trackers.registerError(
|
|
14049
|
+
this.proctoringId,
|
|
14050
|
+
"Visibility API: P\xE1gina voltou a ficar vis\xEDvel. Usu\xE1rio retornou."
|
|
14051
|
+
);
|
|
14052
|
+
(_a2 = this.onVisibilityRestored) == null ? void 0 : _a2.call(this);
|
|
14053
|
+
}
|
|
14054
|
+
}
|
|
14055
|
+
handlePageHide() {
|
|
14056
|
+
console.log("[CameraRecorder] pagehide detectado \u2014 persistindo estado.");
|
|
14057
|
+
this.persistSessionState("INTERRUPTED");
|
|
14058
|
+
this.proctoringId && trackers.registerError(
|
|
14059
|
+
this.proctoringId,
|
|
14060
|
+
"Page Lifecycle: pagehide event detectado. P\xE1gina est\xE1 sendo descarregada."
|
|
14061
|
+
);
|
|
13375
14062
|
}
|
|
13376
14063
|
async initializeDetectors() {
|
|
13377
14064
|
var _a2, _b, _c2;
|
|
@@ -13633,6 +14320,51 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
13633
14320
|
await this.sendPackage();
|
|
13634
14321
|
await this.filesToUpload.splice(0, this.filesToUpload.length);
|
|
13635
14322
|
}
|
|
14323
|
+
if (this.isChunkEnabled) {
|
|
14324
|
+
if (this.backgroundUpload) {
|
|
14325
|
+
try {
|
|
14326
|
+
if (this.pendingChunkSaves.length > 0) {
|
|
14327
|
+
console.log(`[CameraRecorder] Aguardando ${this.pendingChunkSaves.length} salvamentos de chunks pendentes...`);
|
|
14328
|
+
await Promise.all(this.pendingChunkSaves);
|
|
14329
|
+
}
|
|
14330
|
+
await this.backgroundUpload.flush();
|
|
14331
|
+
} catch (e3) {
|
|
14332
|
+
console.warn("[CameraRecorder] Erro ao fazer flush dos chunks:", e3);
|
|
14333
|
+
}
|
|
14334
|
+
this.backgroundUpload.stop();
|
|
14335
|
+
}
|
|
14336
|
+
this.removeLifecycleListeners();
|
|
14337
|
+
this.persistSessionState("FINISHED");
|
|
14338
|
+
}
|
|
14339
|
+
}
|
|
14340
|
+
/**
|
|
14341
|
+
* Callback chamado pelo recorder a cada novo chunk de vídeo disponível.
|
|
14342
|
+
* Salva o chunk no IndexedDB para persistência e recuperação.
|
|
14343
|
+
*/
|
|
14344
|
+
async handleNewChunk(blob, idx) {
|
|
14345
|
+
if (!this.proctoringId || !this.chunkStorage) return;
|
|
14346
|
+
const savePromise = (async () => {
|
|
14347
|
+
var _a2;
|
|
14348
|
+
try {
|
|
14349
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
14350
|
+
await this.chunkStorage.saveChunk({
|
|
14351
|
+
proctoringId: this.proctoringId,
|
|
14352
|
+
chunkIndex: this.chunkIndex,
|
|
14353
|
+
arrayBuffer,
|
|
14354
|
+
timestamp: Date.now(),
|
|
14355
|
+
uploaded: 0,
|
|
14356
|
+
mimeType: ((_a2 = this.recorderOptions) == null ? void 0 : _a2.mimeType) || "video/webm"
|
|
14357
|
+
});
|
|
14358
|
+
this.chunkIndex++;
|
|
14359
|
+
console.log(`[CameraRecorder] Chunk ${this.chunkIndex - 1} salvo no IndexedDB.`);
|
|
14360
|
+
} catch (error) {
|
|
14361
|
+
console.error("[CameraRecorder] Erro ao salvar chunk no IndexedDB:", error);
|
|
14362
|
+
}
|
|
14363
|
+
})();
|
|
14364
|
+
this.pendingChunkSaves.push(savePromise);
|
|
14365
|
+
savePromise.finally(() => {
|
|
14366
|
+
this.pendingChunkSaves = this.pendingChunkSaves.filter((p3) => p3 !== savePromise);
|
|
14367
|
+
});
|
|
13636
14368
|
}
|
|
13637
14369
|
async pauseRecording() {
|
|
13638
14370
|
await this.recordingPause();
|
|
@@ -13773,21 +14505,36 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
13773
14505
|
const settings = this.cameraStream.getVideoTracks()[0].getSettings();
|
|
13774
14506
|
const settingsAudio = this.cameraStream.getAudioTracks()[0].getSettings();
|
|
13775
14507
|
if (this.options.proctoringType == "VIDEO" || this.options.proctoringType == "REALTIME" || this.options.proctoringType == "IMAGE" && ((_a2 = this.paramsConfig.imageBehaviourParameters) == null ? void 0 : _a2.saveVideo)) {
|
|
13776
|
-
|
|
13777
|
-
|
|
13778
|
-
|
|
13779
|
-
|
|
13780
|
-
|
|
13781
|
-
|
|
13782
|
-
|
|
14508
|
+
let isUploaded = false;
|
|
14509
|
+
if (this.isChunkEnabled) {
|
|
14510
|
+
if (this.backend && this.backendToken && this.proctoringId) {
|
|
14511
|
+
const fileName = `EP_${this.proctoringId}_camera_0.webm`;
|
|
14512
|
+
const objectName = `${this.proctoringId}/${fileName}`;
|
|
14513
|
+
isUploaded = await this.backend.checkUpload(this.backendToken, objectName, "video/webm");
|
|
14514
|
+
this.chunkStorage && await this.chunkStorage.clearAllChunks(session.id);
|
|
14515
|
+
}
|
|
14516
|
+
}
|
|
14517
|
+
if (!isUploaded) {
|
|
14518
|
+
const rawBlob = new Blob(this.blobs, {
|
|
14519
|
+
type: ((_b = this.recorderOptions) == null ? void 0 : _b.mimeType) || "video/webm"
|
|
14520
|
+
});
|
|
14521
|
+
let finalBlob = rawBlob;
|
|
14522
|
+
if (typeof fixWebmDuration === "function") {
|
|
14523
|
+
finalBlob = await fixWebmDuration(rawBlob, this.duration);
|
|
14524
|
+
} else {
|
|
14525
|
+
console.warn("fixWebmDuration n\xE3o dispon\xEDvel");
|
|
14526
|
+
}
|
|
14527
|
+
session.addRecording({
|
|
14528
|
+
device: `Audio
|
|
13783
14529
|
Sample Rate: ${settingsAudio.sampleRate}
|
|
13784
14530
|
Sample Size: ${settingsAudio.sampleSize}
|
|
13785
14531
|
|
|
13786
14532
|
Video:
|
|
13787
14533
|
${JSON.stringify(this.recorderOptions)}`,
|
|
13788
|
-
|
|
13789
|
-
|
|
13790
|
-
|
|
14534
|
+
arrayBuffer: await finalBlob.arrayBuffer(),
|
|
14535
|
+
origin: "Camera" /* Camera */
|
|
14536
|
+
});
|
|
14537
|
+
}
|
|
13791
14538
|
}
|
|
13792
14539
|
}
|
|
13793
14540
|
async getFile(file, name, type) {
|
|
@@ -13800,28 +14547,63 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
13800
14547
|
});
|
|
13801
14548
|
});
|
|
13802
14549
|
}
|
|
13803
|
-
|
|
13804
|
-
|
|
13805
|
-
|
|
13806
|
-
|
|
13807
|
-
|
|
13808
|
-
|
|
13809
|
-
|
|
13810
|
-
|
|
13811
|
-
|
|
13812
|
-
|
|
13813
|
-
|
|
13814
|
-
|
|
13815
|
-
|
|
13816
|
-
|
|
13817
|
-
|
|
13818
|
-
|
|
13819
|
-
|
|
13820
|
-
|
|
13821
|
-
|
|
14550
|
+
};
|
|
14551
|
+
// ========================
|
|
14552
|
+
// Chunk & Lifecycle
|
|
14553
|
+
// ========================
|
|
14554
|
+
/** Intervalo de cada chunk em ms (padrão: 60 segundos) */
|
|
14555
|
+
_CameraRecorder.CHUNK_TIMESLICE_MS = 6e4;
|
|
14556
|
+
/** Chave do localStorage para persistir estado da sessão */
|
|
14557
|
+
_CameraRecorder.LS_SESSION_KEY = "ep_proctoring_session";
|
|
14558
|
+
var CameraRecorder = _CameraRecorder;
|
|
14559
|
+
|
|
14560
|
+
// src/new-flow/recorders/VolumeMeter.ts
|
|
14561
|
+
var VolumeMeter = class {
|
|
14562
|
+
constructor(stream) {
|
|
14563
|
+
this.volume = null;
|
|
14564
|
+
this.animationFrameId = null;
|
|
14565
|
+
this.stream = stream;
|
|
14566
|
+
}
|
|
14567
|
+
async start(options = {}) {
|
|
14568
|
+
return new Promise((resolve, reject) => {
|
|
14569
|
+
try {
|
|
14570
|
+
this.audioContext = new AudioContext();
|
|
14571
|
+
this.analyser = this.audioContext.createAnalyser();
|
|
14572
|
+
this.microphone = this.audioContext.createMediaStreamSource(this.stream);
|
|
14573
|
+
this.analyser.smoothingTimeConstant = 0.8;
|
|
14574
|
+
this.analyser.fftSize = 1024;
|
|
14575
|
+
this.microphone.connect(this.analyser);
|
|
14576
|
+
const processAudio = () => {
|
|
14577
|
+
const array = new Uint8Array(this.analyser.frequencyBinCount);
|
|
14578
|
+
this.analyser.getByteFrequencyData(array);
|
|
14579
|
+
const arraySum = array.reduce((a3, value) => a3 + value, 0);
|
|
14580
|
+
const average = arraySum / array.length;
|
|
14581
|
+
this.setVolume(average);
|
|
14582
|
+
options.setVolume && options.setVolume(average);
|
|
14583
|
+
this.animationFrameId = requestAnimationFrame(processAudio);
|
|
14584
|
+
};
|
|
14585
|
+
this.animationFrameId = requestAnimationFrame(processAudio);
|
|
14586
|
+
resolve(true);
|
|
14587
|
+
} catch (error) {
|
|
14588
|
+
this.stop();
|
|
14589
|
+
reject(`Error: ${error}`);
|
|
13822
14590
|
}
|
|
14591
|
+
});
|
|
14592
|
+
}
|
|
14593
|
+
stop() {
|
|
14594
|
+
var _a2, _b, _c2;
|
|
14595
|
+
if (this.animationFrameId !== null) {
|
|
14596
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
13823
14597
|
}
|
|
13824
|
-
this.
|
|
14598
|
+
(_a2 = this.audioContext) == null ? void 0 : _a2.close();
|
|
14599
|
+
(_b = this.microphone) == null ? void 0 : _b.disconnect();
|
|
14600
|
+
(_c2 = this.analyser) == null ? void 0 : _c2.disconnect();
|
|
14601
|
+
}
|
|
14602
|
+
getVolume() {
|
|
14603
|
+
return this.volume;
|
|
14604
|
+
}
|
|
14605
|
+
setVolume(value) {
|
|
14606
|
+
this.volume = value;
|
|
13825
14607
|
}
|
|
13826
14608
|
};
|
|
13827
14609
|
|
|
@@ -13829,6 +14611,8 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
13829
14611
|
var DeviceCheckerUI = class {
|
|
13830
14612
|
constructor(options = getDefaultProctoringOptions, _videoOptions) {
|
|
13831
14613
|
this.videoOptions = { width: 1080, height: 720, minWidth: 0, minHeight: 0 };
|
|
14614
|
+
this.autoConfirmTimer = null;
|
|
14615
|
+
this.autoConfirmSeconds = 5;
|
|
13832
14616
|
this.options = {
|
|
13833
14617
|
...getDefaultProctoringOptions,
|
|
13834
14618
|
...options,
|
|
@@ -14594,9 +15378,53 @@ var DeviceCheckerUI = class {
|
|
|
14594
15378
|
}));
|
|
14595
15379
|
}
|
|
14596
15380
|
closeModal() {
|
|
15381
|
+
this.stopAutoConfirm();
|
|
14597
15382
|
const checkDevices = document.querySelector("#checkDevices");
|
|
14598
15383
|
checkDevices == null ? void 0 : checkDevices.remove();
|
|
14599
15384
|
}
|
|
15385
|
+
verifyAutoConfirm(status) {
|
|
15386
|
+
var _a2;
|
|
15387
|
+
if (!((_a2 = this.options) == null ? void 0 : _a2.auto)) return;
|
|
15388
|
+
let isAllGreen = status.allowedResolution && status.allowedPositionFace && status.allowedAmbient && status.allowedMicrophone;
|
|
15389
|
+
if (this.options.useSpyScan) {
|
|
15390
|
+
isAllGreen = isAllGreen && status.allowedSpyScan === true;
|
|
15391
|
+
}
|
|
15392
|
+
if (isAllGreen) {
|
|
15393
|
+
if (!this.autoConfirmTimer) {
|
|
15394
|
+
this.startAutoConfirm();
|
|
15395
|
+
}
|
|
15396
|
+
} else {
|
|
15397
|
+
if (this.autoConfirmTimer) {
|
|
15398
|
+
this.stopAutoConfirm();
|
|
15399
|
+
}
|
|
15400
|
+
}
|
|
15401
|
+
}
|
|
15402
|
+
startAutoConfirm() {
|
|
15403
|
+
this.autoConfirmSeconds = 5;
|
|
15404
|
+
const btn = document.getElementById("confirmBtn");
|
|
15405
|
+
if (btn && !btn.disabled) {
|
|
15406
|
+
btn.innerText = `Continuando em ${this.autoConfirmSeconds}s...`;
|
|
15407
|
+
this.autoConfirmTimer = setInterval(() => {
|
|
15408
|
+
this.autoConfirmSeconds--;
|
|
15409
|
+
if (this.autoConfirmSeconds <= 0) {
|
|
15410
|
+
this.stopAutoConfirm();
|
|
15411
|
+
btn.click();
|
|
15412
|
+
} else {
|
|
15413
|
+
btn.innerText = `Continuando em ${this.autoConfirmSeconds}s...`;
|
|
15414
|
+
}
|
|
15415
|
+
}, 1e3);
|
|
15416
|
+
}
|
|
15417
|
+
}
|
|
15418
|
+
stopAutoConfirm() {
|
|
15419
|
+
if (this.autoConfirmTimer) {
|
|
15420
|
+
clearInterval(this.autoConfirmTimer);
|
|
15421
|
+
this.autoConfirmTimer = null;
|
|
15422
|
+
}
|
|
15423
|
+
const btn = document.getElementById("confirmBtn");
|
|
15424
|
+
if (btn) {
|
|
15425
|
+
btn.innerText = "Continuar";
|
|
15426
|
+
}
|
|
15427
|
+
}
|
|
14600
15428
|
modalActions(closeCheckDevices) {
|
|
14601
15429
|
const cancelBtn = document.getElementById("cancelBtn");
|
|
14602
15430
|
const confirmBtn = document.getElementById("confirmBtn");
|
|
@@ -14963,6 +15791,7 @@ var _DeviceCheckerService = class _DeviceCheckerService {
|
|
|
14963
15791
|
}
|
|
14964
15792
|
}
|
|
14965
15793
|
onUpdateCallback() {
|
|
15794
|
+
var _a2;
|
|
14966
15795
|
if (typeof this.onUpdateCb === "function") {
|
|
14967
15796
|
this.onUpdateCb({
|
|
14968
15797
|
allowedResolution: this.allowedResolution,
|
|
@@ -14973,6 +15802,15 @@ var _DeviceCheckerService = class _DeviceCheckerService {
|
|
|
14973
15802
|
faceDetectionAlerts: this.faceDetectionAlerts
|
|
14974
15803
|
});
|
|
14975
15804
|
}
|
|
15805
|
+
if (((_a2 = this.options) == null ? void 0 : _a2.auto) && this.DeviceCheckerUI) {
|
|
15806
|
+
this.DeviceCheckerUI.verifyAutoConfirm({
|
|
15807
|
+
allowedResolution: this.allowedResolution,
|
|
15808
|
+
allowedPositionFace: this.allowedPositionFace,
|
|
15809
|
+
allowedAmbient: this.allowedAmbient,
|
|
15810
|
+
allowedMicrophone: this.allowedMicrophone,
|
|
15811
|
+
allowedSpyScan: this.allowedSpyScan
|
|
15812
|
+
});
|
|
15813
|
+
}
|
|
14976
15814
|
}
|
|
14977
15815
|
async checkDevices(options = getDefaultProctoringOptions, _videoOptions = getDefaultProctoringVideoOptions) {
|
|
14978
15816
|
var _a2;
|
|
@@ -15179,16 +16017,19 @@ var _DeviceCheckerService = class _DeviceCheckerService {
|
|
|
15179
16017
|
}).finally(() => {
|
|
15180
16018
|
this.DeviceCheckerUI && this.DeviceCheckerUI.waitingSpyDevices(false);
|
|
15181
16019
|
this.DeviceCheckerUI && this.allowedSpyScan != null && this.DeviceCheckerUI.isSpyDevicesUI(this.allowedSpyScan);
|
|
16020
|
+
this.onUpdateCallback();
|
|
15182
16021
|
});
|
|
15183
16022
|
} else {
|
|
15184
16023
|
this.allowedSpyScan = false;
|
|
15185
16024
|
this.DeviceCheckerUI && this.DeviceCheckerUI.waitingSpyDevices(false);
|
|
15186
16025
|
this.DeviceCheckerUI && this.DeviceCheckerUI.isSpyDevicesUI(this.allowedSpyScan);
|
|
16026
|
+
this.onUpdateCallback();
|
|
15187
16027
|
}
|
|
15188
16028
|
} catch (error) {
|
|
15189
16029
|
this.allowedSpyScan = false;
|
|
15190
16030
|
this.DeviceCheckerUI && this.DeviceCheckerUI.waitingSpyDevices(false);
|
|
15191
16031
|
this.DeviceCheckerUI && this.DeviceCheckerUI.isSpyDevicesUI(this.allowedSpyScan);
|
|
16032
|
+
this.onUpdateCallback();
|
|
15192
16033
|
console.log(error);
|
|
15193
16034
|
}
|
|
15194
16035
|
}
|
|
@@ -15508,8 +16349,8 @@ var CapturePhoto = class {
|
|
|
15508
16349
|
}
|
|
15509
16350
|
};
|
|
15510
16351
|
|
|
15511
|
-
// src/extension/
|
|
15512
|
-
var
|
|
16352
|
+
// src/extension/extensionEasyProctor.ts
|
|
16353
|
+
var ExtensionEasyProctor = class {
|
|
15513
16354
|
constructor() {
|
|
15514
16355
|
this.hasExtension = false;
|
|
15515
16356
|
this.tryes = 0;
|
|
@@ -15554,6 +16395,89 @@ var Extension = class {
|
|
|
15554
16395
|
}
|
|
15555
16396
|
};
|
|
15556
16397
|
|
|
16398
|
+
// src/extension/extensionEasyCatcher.ts
|
|
16399
|
+
var ExtensionEasyCatcher = class {
|
|
16400
|
+
constructor(options) {
|
|
16401
|
+
this.hasExtension = false;
|
|
16402
|
+
this.tryes = 0;
|
|
16403
|
+
this.responseStart = false;
|
|
16404
|
+
this.options = options || {};
|
|
16405
|
+
}
|
|
16406
|
+
/**
|
|
16407
|
+
* Verifica se a extensão está instalada e ativa.
|
|
16408
|
+
* Retorna o número da versão se encontrada, ou lança erro após timeout.
|
|
16409
|
+
*/
|
|
16410
|
+
checkExtensionInstalled(timeoutMs = 2e3) {
|
|
16411
|
+
return new Promise((resolve, reject) => {
|
|
16412
|
+
let handled = false;
|
|
16413
|
+
const handler = (event) => {
|
|
16414
|
+
if (event.source === window && event.data.sender === "easyproctor-extension" && event.data.message_name === "version") {
|
|
16415
|
+
handled = true;
|
|
16416
|
+
window.removeEventListener("message", handler);
|
|
16417
|
+
resolve(event.data.message);
|
|
16418
|
+
}
|
|
16419
|
+
};
|
|
16420
|
+
window.addEventListener("message", handler);
|
|
16421
|
+
window.postMessage({
|
|
16422
|
+
type: "easycatcher",
|
|
16423
|
+
func: "verifyExtensionEasycatcher"
|
|
16424
|
+
}, "*");
|
|
16425
|
+
setTimeout(() => {
|
|
16426
|
+
if (!handled) {
|
|
16427
|
+
window.removeEventListener("message", handler);
|
|
16428
|
+
reject(new Error("Extens\xE3o n\xE3o detectada ou n\xE3o respondeu."));
|
|
16429
|
+
}
|
|
16430
|
+
}, timeoutMs);
|
|
16431
|
+
});
|
|
16432
|
+
}
|
|
16433
|
+
/**
|
|
16434
|
+
* Solicita o JSON da sessão atual capturado pela extensão.
|
|
16435
|
+
*/
|
|
16436
|
+
getSessionData(timeoutMs = 5e3) {
|
|
16437
|
+
return new Promise((resolve, reject) => {
|
|
16438
|
+
let handled = false;
|
|
16439
|
+
const handler = (event) => {
|
|
16440
|
+
if (event.source === window && event.data.sender === "easyproctor-extension" && event.data.message_name === "data_response") {
|
|
16441
|
+
handled = true;
|
|
16442
|
+
window.removeEventListener("message", handler);
|
|
16443
|
+
resolve(event.data.payload);
|
|
16444
|
+
}
|
|
16445
|
+
};
|
|
16446
|
+
window.addEventListener("message", handler);
|
|
16447
|
+
window.postMessage({
|
|
16448
|
+
type: "easycatcher",
|
|
16449
|
+
func: "getDataExtensionEasycatcher"
|
|
16450
|
+
}, "*");
|
|
16451
|
+
setTimeout(() => {
|
|
16452
|
+
if (!handled) {
|
|
16453
|
+
window.removeEventListener("message", handler);
|
|
16454
|
+
reject(new Error("Timeout ao aguardar dados da extens\xE3o."));
|
|
16455
|
+
}
|
|
16456
|
+
}, timeoutMs);
|
|
16457
|
+
});
|
|
16458
|
+
}
|
|
16459
|
+
start() {
|
|
16460
|
+
return new Promise((resolve, reject) => {
|
|
16461
|
+
let handled = false;
|
|
16462
|
+
const handler = (event) => {
|
|
16463
|
+
if (event.source === window && event.data.sender === "easyproctor-extension" && event.data.message_name === "started_confirmed") {
|
|
16464
|
+
handled = true;
|
|
16465
|
+
window.removeEventListener("message", handler);
|
|
16466
|
+
resolve(true);
|
|
16467
|
+
}
|
|
16468
|
+
};
|
|
16469
|
+
window.addEventListener("message", handler);
|
|
16470
|
+
window.postMessage({ type: "easycatcher", func: "startExtensionEasycatcher" }, "*");
|
|
16471
|
+
setTimeout(() => {
|
|
16472
|
+
if (!handled) {
|
|
16473
|
+
window.removeEventListener("message", handler);
|
|
16474
|
+
reject(new Error("Timeout: Extens\xE3o n\xE3o confirmou o in\xEDcio."));
|
|
16475
|
+
}
|
|
16476
|
+
}, 3e3);
|
|
16477
|
+
});
|
|
16478
|
+
}
|
|
16479
|
+
};
|
|
16480
|
+
|
|
15557
16481
|
// src/modules/onChangeDevices.ts
|
|
15558
16482
|
var onChangeDevices = class {
|
|
15559
16483
|
constructor(repositoryDevices, proctoringId2, sessionOptions, allRecorders) {
|
|
@@ -15785,15 +16709,18 @@ Error: ${e3.message}
|
|
|
15785
16709
|
);
|
|
15786
16710
|
});
|
|
15787
16711
|
if (result) {
|
|
15788
|
-
|
|
15789
|
-
|
|
15790
|
-
|
|
15791
|
-
|
|
16712
|
+
let fileType = "";
|
|
16713
|
+
if (rec.origin === "Camera" /* Camera */) {
|
|
16714
|
+
fileType = "Camera";
|
|
16715
|
+
} else if (rec.origin === "Screen" /* Screen */) {
|
|
16716
|
+
fileType = "Screen";
|
|
16717
|
+
} else if (rec.origin === "Mic" /* Mic */) {
|
|
16718
|
+
fileType = "Audio";
|
|
16719
|
+
}
|
|
16720
|
+
trackers.registerUploadFile(this.proctoringId, `Upload File
|
|
15792
16721
|
Name: ${file.name}
|
|
15793
16722
|
Type: ${file.type}
|
|
15794
|
-
Size: ${file.size}`,
|
|
15795
|
-
fileType
|
|
15796
|
-
);
|
|
16723
|
+
Size: ${file.size}`, fileType);
|
|
15797
16724
|
return result;
|
|
15798
16725
|
}
|
|
15799
16726
|
}
|
|
@@ -18602,11 +19529,15 @@ var ScreenRecorder = class {
|
|
|
18602
19529
|
const rawBlob = new Blob(this.blobs, {
|
|
18603
19530
|
type: "video/webm"
|
|
18604
19531
|
});
|
|
18605
|
-
|
|
18606
|
-
|
|
19532
|
+
let finalBlob = rawBlob;
|
|
19533
|
+
if (typeof fixWebmDuration2 === "function") {
|
|
19534
|
+
finalBlob = await fixWebmDuration2(rawBlob, this.duration);
|
|
19535
|
+
} else {
|
|
19536
|
+
console.warn("fixWebmDuration n\xE3o dispon\xEDvel");
|
|
19537
|
+
}
|
|
18607
19538
|
session.addRecording({
|
|
18608
19539
|
device: "",
|
|
18609
|
-
arrayBuffer,
|
|
19540
|
+
arrayBuffer: await finalBlob.arrayBuffer(),
|
|
18610
19541
|
origin: "Screen" /* Screen */
|
|
18611
19542
|
});
|
|
18612
19543
|
}
|
|
@@ -22738,7 +23669,10 @@ var Proctoring = class {
|
|
|
22738
23669
|
if (this.context.token === void 0) {
|
|
22739
23670
|
throw TOKEN_MISSING;
|
|
22740
23671
|
}
|
|
22741
|
-
|
|
23672
|
+
if (options.useChallenge) {
|
|
23673
|
+
this.extensionEasycatcher = new ExtensionEasyCatcher();
|
|
23674
|
+
}
|
|
23675
|
+
this.extension = new ExtensionEasyProctor();
|
|
22742
23676
|
this.extension.addEventListener();
|
|
22743
23677
|
const baseURL = this.backend.selectBaseUrl(this.context.type);
|
|
22744
23678
|
const devices = await enumarateDevices();
|
|
@@ -23110,6 +24044,61 @@ Error: ` + error
|
|
|
23110
24044
|
_screenStream: (_a2 = this.allRecorders.screenRecorder) == null ? void 0 : _a2.screenStream
|
|
23111
24045
|
};
|
|
23112
24046
|
}
|
|
24047
|
+
async startChallenge(templateId) {
|
|
24048
|
+
var _a2;
|
|
24049
|
+
if (!this.sessionOptions.useChallenge) {
|
|
24050
|
+
throw new Error("useChallenge is set as false on start method");
|
|
24051
|
+
}
|
|
24052
|
+
await this.extensionEasycatcher.checkExtensionInstalled().catch((err) => {
|
|
24053
|
+
throw new Error("EasyCatcher Extension is not installed");
|
|
24054
|
+
});
|
|
24055
|
+
this.extensionEasycatcher.start();
|
|
24056
|
+
const start = Date.now() - ((_a2 = this.allRecorders.cameraRecorder.getStartTime()) == null ? void 0 : _a2.getTime()) || 0;
|
|
24057
|
+
await this.backend.startChallenge({
|
|
24058
|
+
proctoringId: this.proctoringId,
|
|
24059
|
+
templateId,
|
|
24060
|
+
start
|
|
24061
|
+
}).then((resp) => {
|
|
24062
|
+
console.log(resp);
|
|
24063
|
+
this.challengeId = resp.id;
|
|
24064
|
+
}).catch((reason) => {
|
|
24065
|
+
trackers.registerError(
|
|
24066
|
+
this.proctoringId,
|
|
24067
|
+
"N\xE3o foi poss\xEDvel iniciar desafio!"
|
|
24068
|
+
);
|
|
24069
|
+
throw reason;
|
|
24070
|
+
});
|
|
24071
|
+
this.isChallengeRunning = true;
|
|
24072
|
+
}
|
|
24073
|
+
async stopChallenge() {
|
|
24074
|
+
var _a2;
|
|
24075
|
+
if (!this.isChallengeRunning) {
|
|
24076
|
+
throw new Error("Challenge not started");
|
|
24077
|
+
}
|
|
24078
|
+
try {
|
|
24079
|
+
const sessionData = await this.extensionEasycatcher.getSessionData();
|
|
24080
|
+
const end = Date.now() - ((_a2 = this.allRecorders.cameraRecorder.getStartTime()) == null ? void 0 : _a2.getTime()) || 0;
|
|
24081
|
+
await this.backend.stopChallenge(
|
|
24082
|
+
this.challengeId,
|
|
24083
|
+
{
|
|
24084
|
+
end,
|
|
24085
|
+
data: sessionData
|
|
24086
|
+
}
|
|
24087
|
+
).catch((reason) => {
|
|
24088
|
+
trackers.registerError(
|
|
24089
|
+
this.proctoringId,
|
|
24090
|
+
"N\xE3o foi poss\xEDvel finalizar o desafio no backend!"
|
|
24091
|
+
);
|
|
24092
|
+
return void 0;
|
|
24093
|
+
});
|
|
24094
|
+
this.isChallengeRunning = false;
|
|
24095
|
+
} catch (error) {
|
|
24096
|
+
trackers.registerError(
|
|
24097
|
+
this.proctoringId,
|
|
24098
|
+
"Erro ao recuperar dados da extens\xE3o: " + error.message
|
|
24099
|
+
);
|
|
24100
|
+
}
|
|
24101
|
+
}
|
|
23113
24102
|
};
|
|
23114
24103
|
|
|
23115
24104
|
// src/proctoring/SignTerm.ts
|
|
@@ -23338,6 +24327,8 @@ function useProctoring(proctoringOptions, enviromentConfig = "prod") {
|
|
|
23338
24327
|
return originalStart(parameters2, videoOptions);
|
|
23339
24328
|
};
|
|
23340
24329
|
const finish = proctoring.finish.bind(proctoring);
|
|
24330
|
+
const startChallenge = proctoring.startChallenge.bind(proctoring);
|
|
24331
|
+
const stopChallenge = proctoring.stopChallenge.bind(proctoring);
|
|
23341
24332
|
const pause = proctoring.pause.bind(proctoring);
|
|
23342
24333
|
const resume = proctoring.resume.bind(proctoring);
|
|
23343
24334
|
const onFocus = proctoring.setOnFocusCallback.bind(proctoring);
|
|
@@ -23362,6 +24353,8 @@ function useProctoring(proctoringOptions, enviromentConfig = "prod") {
|
|
|
23362
24353
|
login,
|
|
23363
24354
|
start,
|
|
23364
24355
|
finish,
|
|
24356
|
+
startChallenge,
|
|
24357
|
+
stopChallenge,
|
|
23365
24358
|
onFocus,
|
|
23366
24359
|
onLostFocus,
|
|
23367
24360
|
onChangeDevices: onChangeDevices2,
|