easyproctor-hml 2.6.0 → 2.7.1
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/README.md +31 -0
- package/esm/index.js +1152 -276
- package/extension/{extensionEasyProctor.d.ts → extension.d.ts} +1 -1
- package/index.js +1152 -276
- package/modules/BaseDetection.d.ts +1 -1
- package/new-flow/backend/BackendService.d.ts +7 -2
- package/new-flow/chunk/BackgroundUploadService.d.ts +38 -0
- package/new-flow/chunk/ChunkStorageService.d.ts +25 -0
- package/new-flow/proctoring/ProctoringSession.d.ts +1 -0
- package/new-flow/recorders/CameraRecorder.d.ts +27 -1
- package/new-flow/recorders/NoiseRecorder.d.ts +1 -1
- package/package.json +3 -2
- package/plugins/recorder.d.ts +5 -1
- package/proctoring/proctoring.d.ts +6 -5
- package/proctoring/useProctoring.d.ts +2 -2
- package/unpkg/easyproctor.min.js +45 -42
- package/utils/browserInformations.d.ts +1 -0
- package/extension/extensionEasyCatcher.d.ts +0 -11
package/esm/index.js
CHANGED
|
@@ -5044,14 +5044,17 @@ var require_browser = __commonJS({
|
|
|
5044
5044
|
|
|
5045
5045
|
// src/modules/checkPermissions.ts
|
|
5046
5046
|
async function checkPermissions() {
|
|
5047
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
5048
|
+
return false;
|
|
5049
|
+
}
|
|
5047
5050
|
try {
|
|
5048
5051
|
const constraints = {
|
|
5049
5052
|
audio: true,
|
|
5050
5053
|
video: true
|
|
5051
5054
|
};
|
|
5052
5055
|
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
5053
|
-
stream.getTracks().forEach((
|
|
5054
|
-
|
|
5056
|
+
stream.getTracks().forEach((track) => {
|
|
5057
|
+
track.stop();
|
|
5055
5058
|
});
|
|
5056
5059
|
return true;
|
|
5057
5060
|
} catch (error) {
|
|
@@ -9145,7 +9148,6 @@ var BaseDetection = class {
|
|
|
9145
9148
|
// objectDetection
|
|
9146
9149
|
runningMode: this.runningMode
|
|
9147
9150
|
});
|
|
9148
|
-
console.log("BaseDetection initializeDetector", this.detectorType);
|
|
9149
9151
|
}
|
|
9150
9152
|
stopDetection() {
|
|
9151
9153
|
this.animationFrameId && clearTimeout(this.animationFrameId);
|
|
@@ -9155,7 +9157,7 @@ var BaseDetection = class {
|
|
|
9155
9157
|
this.createdVideo && this.video && document.body.removeChild(this.video);
|
|
9156
9158
|
this.createdVideo = false;
|
|
9157
9159
|
}
|
|
9158
|
-
enableCam(cameraStream) {
|
|
9160
|
+
enableCam(cameraStream, delay = 1e4) {
|
|
9159
9161
|
var _a2;
|
|
9160
9162
|
if (!this.detector) {
|
|
9161
9163
|
console.log("Wait! Detector not loaded yet.");
|
|
@@ -9185,7 +9187,7 @@ var BaseDetection = class {
|
|
|
9185
9187
|
(_a2 = this.video) == null ? void 0 : _a2.addEventListener("loadeddata", () => {
|
|
9186
9188
|
this.animationFrameId = setTimeout(() => {
|
|
9187
9189
|
that.predictWebcam();
|
|
9188
|
-
},
|
|
9190
|
+
}, delay);
|
|
9189
9191
|
});
|
|
9190
9192
|
const style = document.createElement("style");
|
|
9191
9193
|
style.type = "text/css";
|
|
@@ -9202,7 +9204,6 @@ var BaseDetection = class {
|
|
|
9202
9204
|
}
|
|
9203
9205
|
`;
|
|
9204
9206
|
document.getElementsByTagName("head")[0].appendChild(style);
|
|
9205
|
-
console.log("BaseDetection enableCam OK");
|
|
9206
9207
|
}
|
|
9207
9208
|
async predictWebcam() {
|
|
9208
9209
|
if (this.detecting == false) return;
|
|
@@ -9332,7 +9333,6 @@ var FaceDetection = class extends BaseDetection {
|
|
|
9332
9333
|
);
|
|
9333
9334
|
this.emmitedPositionAlert = false;
|
|
9334
9335
|
this.emmitedFaceAlert = false;
|
|
9335
|
-
console.log("FaceDetection constructor");
|
|
9336
9336
|
this.numFacesSent = -1;
|
|
9337
9337
|
}
|
|
9338
9338
|
stopDetection() {
|
|
@@ -12093,13 +12093,28 @@ var {
|
|
|
12093
12093
|
var DEV_BASE_URL = "https://proctoring-api-dev.easyproctor.tech/api";
|
|
12094
12094
|
var HOMOL_BASE_URL = "https://proctoring-api-hml.easyproctor.tech/api";
|
|
12095
12095
|
var PROD_BASE_URL = "https://proctoring-api.easyproctor.tech/api";
|
|
12096
|
+
var REALTIME_DEV_BASE_URL = "https://easyproctor-realtime-api-dev.easyproctor.tech/api";
|
|
12097
|
+
var REALTIME_HOMOL_BASE_URL = "https://easyproctor-realtime-api-hml.easyproctor.tech/api";
|
|
12098
|
+
var REALTIME_PROD_BASE_URL = "https://easyproctor-realtime-api.easyproctor.tech/api";
|
|
12096
12099
|
var BackendService = class {
|
|
12097
12100
|
constructor(options) {
|
|
12098
12101
|
this.options = options;
|
|
12099
|
-
this.baseUrl = this.selectBaseUrl(options.type);
|
|
12102
|
+
this.baseUrl = this.selectBaseUrl(options.type, options.isRealtime);
|
|
12100
12103
|
this.token = options.token;
|
|
12101
12104
|
}
|
|
12102
|
-
|
|
12105
|
+
getBaseUrl() {
|
|
12106
|
+
return this.baseUrl;
|
|
12107
|
+
}
|
|
12108
|
+
selectBaseUrl(type, isRealtime) {
|
|
12109
|
+
if (isRealtime) {
|
|
12110
|
+
if (type === "dev") {
|
|
12111
|
+
return REALTIME_DEV_BASE_URL;
|
|
12112
|
+
} else if (type === "homol") {
|
|
12113
|
+
return REALTIME_HOMOL_BASE_URL;
|
|
12114
|
+
} else {
|
|
12115
|
+
return REALTIME_PROD_BASE_URL;
|
|
12116
|
+
}
|
|
12117
|
+
}
|
|
12103
12118
|
if (type === "dev") {
|
|
12104
12119
|
return DEV_BASE_URL;
|
|
12105
12120
|
} else if (type === "homol") {
|
|
@@ -12110,6 +12125,9 @@ var BackendService = class {
|
|
|
12110
12125
|
return PROD_BASE_URL;
|
|
12111
12126
|
}
|
|
12112
12127
|
}
|
|
12128
|
+
setRealtime(isRealtime) {
|
|
12129
|
+
this.baseUrl = this.selectBaseUrl(this.options.type, isRealtime);
|
|
12130
|
+
}
|
|
12113
12131
|
getSocketUrl() {
|
|
12114
12132
|
return this.baseUrl.replace("/api", "/hub/sockethub");
|
|
12115
12133
|
}
|
|
@@ -12220,6 +12238,18 @@ var BackendService = class {
|
|
|
12220
12238
|
});
|
|
12221
12239
|
return url.data;
|
|
12222
12240
|
}
|
|
12241
|
+
async initiateUpload(token, objectName, contentType) {
|
|
12242
|
+
const url = await this.makeRequestAxios({
|
|
12243
|
+
path: `/upload/initiate-upload`,
|
|
12244
|
+
method: "POST",
|
|
12245
|
+
jwt: token,
|
|
12246
|
+
body: {
|
|
12247
|
+
objectName,
|
|
12248
|
+
contentType
|
|
12249
|
+
}
|
|
12250
|
+
});
|
|
12251
|
+
return url.data;
|
|
12252
|
+
}
|
|
12223
12253
|
async saveAlerts(proctoringOptions, proctoringSession) {
|
|
12224
12254
|
await this.makeRequest({
|
|
12225
12255
|
path: "/proctoring/save-alerts",
|
|
@@ -12323,6 +12353,22 @@ var BackendService = class {
|
|
|
12323
12353
|
});
|
|
12324
12354
|
return result.data;
|
|
12325
12355
|
}
|
|
12356
|
+
async checkUpload(token, objectName, contentType) {
|
|
12357
|
+
try {
|
|
12358
|
+
const result = await this.makeRequestAxios({
|
|
12359
|
+
path: `/Upload/check`,
|
|
12360
|
+
method: "POST",
|
|
12361
|
+
jwt: token,
|
|
12362
|
+
body: {
|
|
12363
|
+
objectName,
|
|
12364
|
+
contentType
|
|
12365
|
+
}
|
|
12366
|
+
});
|
|
12367
|
+
return result.data === true;
|
|
12368
|
+
} catch (e3) {
|
|
12369
|
+
return false;
|
|
12370
|
+
}
|
|
12371
|
+
}
|
|
12326
12372
|
async getServerHour(token) {
|
|
12327
12373
|
return await this.makeRequest({
|
|
12328
12374
|
path: `/Proctoring/server-hour`,
|
|
@@ -12523,7 +12569,8 @@ var SpyCam = class {
|
|
|
12523
12569
|
this.context = context;
|
|
12524
12570
|
this.backend = new BackendService({
|
|
12525
12571
|
type: (context == null ? void 0 : context.type) || "prod",
|
|
12526
|
-
token: context.token
|
|
12572
|
+
token: context.token,
|
|
12573
|
+
isRealtime: false
|
|
12527
12574
|
});
|
|
12528
12575
|
this.currentIsPlugged = true;
|
|
12529
12576
|
}
|
|
@@ -12675,6 +12722,14 @@ var getDefaultProctoringVideoOptions = {
|
|
|
12675
12722
|
minHeight: 480
|
|
12676
12723
|
};
|
|
12677
12724
|
|
|
12725
|
+
// src/utils/verifyVersion.ts
|
|
12726
|
+
function versionVerify() {
|
|
12727
|
+
const agentStr = window.navigator.userAgent.split("SEB/");
|
|
12728
|
+
if (agentStr.length > 1)
|
|
12729
|
+
return agentStr[1];
|
|
12730
|
+
else return "1.0.0.0";
|
|
12731
|
+
}
|
|
12732
|
+
|
|
12678
12733
|
// src/utils/browserInformations.ts
|
|
12679
12734
|
function fnBrowserDetect() {
|
|
12680
12735
|
const userAgent = navigator.userAgent;
|
|
@@ -12703,13 +12758,16 @@ function isMobileDevice() {
|
|
|
12703
12758
|
}
|
|
12704
12759
|
return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
12705
12760
|
}
|
|
12761
|
+
function isSafeBrowser() {
|
|
12762
|
+
return versionVerify() !== "1.0.0.0";
|
|
12763
|
+
}
|
|
12706
12764
|
|
|
12707
12765
|
// src/plugins/recorder.ts
|
|
12708
12766
|
var proctoringId;
|
|
12709
12767
|
function setRecorderProctoringId(id) {
|
|
12710
12768
|
proctoringId = id;
|
|
12711
12769
|
}
|
|
12712
|
-
function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCallback, audio = false) {
|
|
12770
|
+
function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCallback, audio = false, recorderOpts) {
|
|
12713
12771
|
let resolvePromise;
|
|
12714
12772
|
let onBufferSizeInterval;
|
|
12715
12773
|
let lastEvent;
|
|
@@ -12717,6 +12775,7 @@ function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCa
|
|
|
12717
12775
|
bufferSize = 0;
|
|
12718
12776
|
let startTime;
|
|
12719
12777
|
let duration = 0;
|
|
12778
|
+
let chunkIndex = 0;
|
|
12720
12779
|
let recorderOptions = {
|
|
12721
12780
|
// eslint-disable-next-line no-useless-escape
|
|
12722
12781
|
mimeType: "video/webm",
|
|
@@ -12751,6 +12810,10 @@ function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCa
|
|
|
12751
12810
|
mediaRecorder2.ondataavailable = (e3) => {
|
|
12752
12811
|
bufferSize = bufferSize + e3.data.size;
|
|
12753
12812
|
if (e3.data.size > 0) {
|
|
12813
|
+
if (recorderOpts == null ? void 0 : recorderOpts.onChunkAvailable) {
|
|
12814
|
+
recorderOpts.onChunkAvailable(e3.data, chunkIndex);
|
|
12815
|
+
chunkIndex++;
|
|
12816
|
+
}
|
|
12754
12817
|
buffer.push(e3.data);
|
|
12755
12818
|
}
|
|
12756
12819
|
};
|
|
@@ -12782,7 +12845,13 @@ function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCa
|
|
|
12782
12845
|
};
|
|
12783
12846
|
try {
|
|
12784
12847
|
console.log("State antes do start:", recorder2.state);
|
|
12785
|
-
|
|
12848
|
+
chunkIndex = 0;
|
|
12849
|
+
if ((recorderOpts == null ? void 0 : recorderOpts.timeslice) && (recorderOpts == null ? void 0 : recorderOpts.timeslice) > 0) {
|
|
12850
|
+
recorder2.start(recorderOpts.timeslice);
|
|
12851
|
+
} else {
|
|
12852
|
+
recorder2.start(1e4);
|
|
12853
|
+
}
|
|
12854
|
+
bufferSize = 0;
|
|
12786
12855
|
startTime = new Date(Date.now());
|
|
12787
12856
|
} catch (e3) {
|
|
12788
12857
|
console.error("Recorder erro ao chamar start event:", e3);
|
|
@@ -12824,6 +12893,9 @@ function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCa
|
|
|
12824
12893
|
console.log("recorder onstop");
|
|
12825
12894
|
duration = Date.now() - startTime.getTime() || 0;
|
|
12826
12895
|
console.log("duration no onstop", duration);
|
|
12896
|
+
stream.getTracks().forEach((el) => {
|
|
12897
|
+
el.stop();
|
|
12898
|
+
});
|
|
12827
12899
|
resolvePromise && resolvePromise();
|
|
12828
12900
|
};
|
|
12829
12901
|
mediaRecorder.stop();
|
|
@@ -12833,9 +12905,6 @@ function recorder(stream, buffer, onBufferSizeError = false, onBufferSizeErrorCa
|
|
|
12833
12905
|
console.log("stopRecording Recorder n\xE3o est\xE1 em estado recording");
|
|
12834
12906
|
resolve();
|
|
12835
12907
|
}
|
|
12836
|
-
stream.getTracks().forEach((el) => {
|
|
12837
|
-
el.stop();
|
|
12838
|
-
});
|
|
12839
12908
|
});
|
|
12840
12909
|
}
|
|
12841
12910
|
function pauseRecording() {
|
|
@@ -12929,36 +12998,34 @@ var UploadService = class {
|
|
|
12929
12998
|
this.proctoringId = proctoringId2;
|
|
12930
12999
|
}
|
|
12931
13000
|
async uploadPackage(data, token) {
|
|
12932
|
-
const { file
|
|
13001
|
+
const { file } = data;
|
|
12933
13002
|
try {
|
|
12934
|
-
|
|
12935
|
-
|
|
12936
|
-
|
|
12937
|
-
|
|
12938
|
-
|
|
12939
|
-
|
|
13003
|
+
console.log("Upload service: uploadPackage");
|
|
13004
|
+
var uploadUrl = "";
|
|
13005
|
+
await this.backend.getSignedUrl(token, file, this.proctoringId).then((result) => uploadUrl = result).catch((error) => {
|
|
13006
|
+
throw error;
|
|
13007
|
+
});
|
|
13008
|
+
console.log("Upload service: uploadUrl", uploadUrl);
|
|
13009
|
+
await axios_default.request({
|
|
12940
13010
|
url: uploadUrl,
|
|
12941
13011
|
method: "PUT",
|
|
12942
13012
|
headers: {
|
|
12943
13013
|
"Content-Type": file.type,
|
|
12944
13014
|
"x-ms-blob-type": "BlockBlob"
|
|
12945
13015
|
},
|
|
12946
|
-
data: file
|
|
12947
|
-
|
|
12948
|
-
|
|
12949
|
-
|
|
12950
|
-
|
|
12951
|
-
|
|
12952
|
-
|
|
12953
|
-
url: uploadUrl,
|
|
12954
|
-
uploaded
|
|
12955
|
-
};
|
|
13016
|
+
data: file
|
|
13017
|
+
}).then(() => {
|
|
13018
|
+
return true;
|
|
13019
|
+
}).catch((error) => {
|
|
13020
|
+
throw error;
|
|
13021
|
+
});
|
|
13022
|
+
return true;
|
|
12956
13023
|
} catch (err) {
|
|
12957
|
-
trackers.registerError(this.proctoringId, `Failed to upload
|
|
13024
|
+
trackers.registerError(this.proctoringId, `Failed to upload package ${err}
|
|
12958
13025
|
File name: ${file.name}
|
|
12959
13026
|
File type: ${file.type}
|
|
12960
13027
|
File size: ${file.size}`);
|
|
12961
|
-
throw
|
|
13028
|
+
throw err;
|
|
12962
13029
|
}
|
|
12963
13030
|
}
|
|
12964
13031
|
async uploadImages(data, token, packSize) {
|
|
@@ -13245,10 +13312,647 @@ var VolumeMeter = class {
|
|
|
13245
13312
|
}
|
|
13246
13313
|
};
|
|
13247
13314
|
|
|
13315
|
+
// src/new-flow/chunk/ChunkStorageService.ts
|
|
13316
|
+
var _ChunkStorageService = class _ChunkStorageService {
|
|
13317
|
+
constructor() {
|
|
13318
|
+
this.db = null;
|
|
13319
|
+
}
|
|
13320
|
+
/**
|
|
13321
|
+
* Abre a conexão com o IndexedDB, criando o banco e o object store se necessário.
|
|
13322
|
+
*/
|
|
13323
|
+
async connect() {
|
|
13324
|
+
if (this.db) return this.db;
|
|
13325
|
+
return new Promise((resolve, reject) => {
|
|
13326
|
+
const request = window.indexedDB.open(
|
|
13327
|
+
_ChunkStorageService.DB_NAME,
|
|
13328
|
+
_ChunkStorageService.DB_VERSION
|
|
13329
|
+
);
|
|
13330
|
+
request.onerror = () => {
|
|
13331
|
+
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
|
+
/** v2: índices numéricos; v3: payload como ArrayBuffer em vez de Blob (Safari iOS). */
|
|
13555
|
+
_ChunkStorageService.DB_VERSION = 3;
|
|
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 byteLen = c3.arrayBuffer.byteLength;
|
|
13738
|
+
const start = virtualStart;
|
|
13739
|
+
const end = start + byteLen - 1;
|
|
13740
|
+
virtualStart += byteLen;
|
|
13741
|
+
return { chunk: c3, start, end };
|
|
13742
|
+
});
|
|
13743
|
+
const sliceParts = [];
|
|
13744
|
+
let lastProcessedChunkId = null;
|
|
13745
|
+
let finalChunkIndex = 0;
|
|
13746
|
+
let mimeType = pendingChunks[0].mimeType;
|
|
13747
|
+
for (const meta of chunksWithMeta) {
|
|
13748
|
+
if (this.currentOffset > meta.end) continue;
|
|
13749
|
+
const sliceStart = Math.max(0, this.currentOffset - meta.start);
|
|
13750
|
+
const view = new Uint8Array(meta.chunk.arrayBuffer);
|
|
13751
|
+
sliceParts.push(view.subarray(sliceStart));
|
|
13752
|
+
lastProcessedChunkId = meta.chunk.id;
|
|
13753
|
+
finalChunkIndex = meta.chunk.chunkIndex;
|
|
13754
|
+
}
|
|
13755
|
+
if (sliceParts.length === 0 && !isFinal) {
|
|
13756
|
+
this.isProcessing = false;
|
|
13757
|
+
return;
|
|
13758
|
+
}
|
|
13759
|
+
const combinedLength = sliceParts.reduce((acc, p3) => acc + p3.length, 0);
|
|
13760
|
+
const combined = new Uint8Array(combinedLength);
|
|
13761
|
+
{
|
|
13762
|
+
let off = 0;
|
|
13763
|
+
for (const p3 of sliceParts) {
|
|
13764
|
+
combined.set(p3, off);
|
|
13765
|
+
off += p3.length;
|
|
13766
|
+
}
|
|
13767
|
+
}
|
|
13768
|
+
let sendableSize = combined.byteLength;
|
|
13769
|
+
let totalSizeForHeader = void 0;
|
|
13770
|
+
if (!isFinal) {
|
|
13771
|
+
sendableSize = Math.floor(combined.byteLength / this.GCS_CHUNK_SIZE) * this.GCS_CHUNK_SIZE;
|
|
13772
|
+
if (sendableSize === 0) {
|
|
13773
|
+
console.log("[BackgroundUpload] Dados insuficientes para atingir 256KB. Aguardando novo chunk...");
|
|
13774
|
+
this.isProcessing = false;
|
|
13775
|
+
return;
|
|
13776
|
+
}
|
|
13777
|
+
} else {
|
|
13778
|
+
totalSizeForHeader = virtualStart;
|
|
13779
|
+
}
|
|
13780
|
+
const payload = sendableSize === combined.byteLength ? combined : combined.subarray(0, sendableSize);
|
|
13781
|
+
try {
|
|
13782
|
+
await this.uploadData(
|
|
13783
|
+
payload.byteLength > 0 ? payload : null,
|
|
13784
|
+
mimeType,
|
|
13785
|
+
finalChunkIndex,
|
|
13786
|
+
totalSizeForHeader
|
|
13787
|
+
);
|
|
13788
|
+
for (const meta of chunksWithMeta) {
|
|
13789
|
+
if (meta.chunk.uploaded === 0 && meta.end < this.currentOffset) {
|
|
13790
|
+
await this.chunkStorage.markAsUploaded(meta.chunk.id);
|
|
13791
|
+
this.retryCount.delete(meta.chunk.id);
|
|
13792
|
+
(_a2 = this.onChunkUploaded) == null ? void 0 : _a2.call(this, meta.chunk.id, meta.chunk.chunkIndex);
|
|
13793
|
+
console.log(`[BackgroundUpload] Chunk ${meta.chunk.chunkIndex} marcado como enviado.`);
|
|
13794
|
+
}
|
|
13795
|
+
}
|
|
13796
|
+
if (this.config.cleanAfterUpload) {
|
|
13797
|
+
const chunksToClear = chunksWithMeta.filter((meta) => meta.chunk.uploaded === 1 || meta.chunk.uploaded === 0 && meta.end < this.currentOffset);
|
|
13798
|
+
const sizePurged = chunksToClear.reduce(
|
|
13799
|
+
(acc, meta) => acc + meta.chunk.arrayBuffer.byteLength,
|
|
13800
|
+
0
|
|
13801
|
+
);
|
|
13802
|
+
await this.chunkStorage.clearUploadedChunks(this.proctoringId);
|
|
13803
|
+
if (sizePurged > 0) {
|
|
13804
|
+
this.totalBytesPurged += sizePurged;
|
|
13805
|
+
this.saveSessionState();
|
|
13806
|
+
console.log(`[BackgroundUpload] ${sizePurged} bytes limpos do armazenamento local. Total purgado: ${this.totalBytesPurged}`);
|
|
13807
|
+
}
|
|
13808
|
+
}
|
|
13809
|
+
if (isFinal) {
|
|
13810
|
+
this.clearSessionState();
|
|
13811
|
+
}
|
|
13812
|
+
} catch (error) {
|
|
13813
|
+
console.error("[BackgroundUpload] Falha no upload:", error);
|
|
13814
|
+
(_b = this.onUploadError) == null ? void 0 : _b.call(this, lastProcessedChunkId || 0, error);
|
|
13815
|
+
}
|
|
13816
|
+
} catch (error) {
|
|
13817
|
+
console.error("[BackgroundUpload] Erro ao processar fila:", error);
|
|
13818
|
+
} finally {
|
|
13819
|
+
this.isProcessing = false;
|
|
13820
|
+
}
|
|
13821
|
+
}
|
|
13822
|
+
/**
|
|
13823
|
+
* Faz o upload bruto de dados para a sessão GCS.
|
|
13824
|
+
*/
|
|
13825
|
+
async uploadData(body, mimeType, chunkIndex, totalSize) {
|
|
13826
|
+
var _a2;
|
|
13827
|
+
const fileName = `EP_${this.proctoringId}_camera_0.webm`;
|
|
13828
|
+
if (!this.sessionUrl) {
|
|
13829
|
+
const initiateUrl = await this.backend.initiateUpload(this.token, `${this.proctoringId}/${fileName}`, mimeType);
|
|
13830
|
+
const startResponse = await fetch(initiateUrl, {
|
|
13831
|
+
method: "POST",
|
|
13832
|
+
headers: { "x-goog-resumable": "start", "Content-Type": mimeType }
|
|
13833
|
+
});
|
|
13834
|
+
if (!startResponse.ok) throw new Error(`Falha ao iniciar: ${startResponse.status}`);
|
|
13835
|
+
this.sessionUrl = startResponse.headers.get("Location");
|
|
13836
|
+
if (!this.sessionUrl) throw new Error("Location header ausente");
|
|
13837
|
+
try {
|
|
13838
|
+
const urlObj = new URL(this.sessionUrl);
|
|
13839
|
+
const pathParts = urlObj.pathname.split("/");
|
|
13840
|
+
let bucket = pathParts[1];
|
|
13841
|
+
let object = decodeURIComponent(pathParts.slice(2).join("/"));
|
|
13842
|
+
if (pathParts.includes("b") && pathParts.includes("o")) {
|
|
13843
|
+
const bIdx = pathParts.indexOf("b") + 1;
|
|
13844
|
+
const oIdx = pathParts.indexOf("o") + 1;
|
|
13845
|
+
bucket = pathParts[bIdx];
|
|
13846
|
+
object = decodeURIComponent(pathParts.slice(oIdx).join("/"));
|
|
13847
|
+
}
|
|
13848
|
+
console.log(`[BackgroundUpload] Sess\xE3o Iniciada -> Bucket: ${bucket}, Objeto: ${object}`);
|
|
13849
|
+
} catch (e3) {
|
|
13850
|
+
console.log(`[BackgroundUpload] Sess\xE3o Iniciada. URL: ${this.sessionUrl}`);
|
|
13851
|
+
}
|
|
13852
|
+
this.currentOffset = 0;
|
|
13853
|
+
this.saveSessionState();
|
|
13854
|
+
} else {
|
|
13855
|
+
console.log(`[BackgroundUpload] Usando sess\xE3o GCS existente: ${this.sessionUrl}`);
|
|
13856
|
+
}
|
|
13857
|
+
const size = (_a2 = body == null ? void 0 : body.byteLength) != null ? _a2 : 0;
|
|
13858
|
+
const start = this.currentOffset;
|
|
13859
|
+
const end = start + size - 1;
|
|
13860
|
+
const totalHeader = totalSize !== void 0 ? totalSize.toString() : "*";
|
|
13861
|
+
const contentRangeHeader = size === 0 && totalSize !== void 0 ? `bytes */${totalHeader}` : `bytes ${start}-${end}/${totalHeader}`;
|
|
13862
|
+
console.log(
|
|
13863
|
+
`[BackgroundUpload] Enviando ${size > 0 ? "dados" : "finaliza\xE7\xE3o"}: ${contentRangeHeader} (Size: ${size})`
|
|
13864
|
+
);
|
|
13865
|
+
let uploadBody = null;
|
|
13866
|
+
if (size > 0 && body) {
|
|
13867
|
+
const raw = body.buffer;
|
|
13868
|
+
if (raw instanceof ArrayBuffer) {
|
|
13869
|
+
uploadBody = body.byteOffset === 0 && body.byteLength === raw.byteLength ? raw : raw.slice(body.byteOffset, body.byteOffset + body.byteLength);
|
|
13870
|
+
} else {
|
|
13871
|
+
uploadBody = new ArrayBuffer(body.byteLength);
|
|
13872
|
+
new Uint8Array(uploadBody).set(body);
|
|
13873
|
+
}
|
|
13874
|
+
}
|
|
13875
|
+
const response = await fetch(this.sessionUrl, {
|
|
13876
|
+
method: "PUT",
|
|
13877
|
+
headers: { "Content-Range": contentRangeHeader },
|
|
13878
|
+
body: uploadBody
|
|
13879
|
+
});
|
|
13880
|
+
console.log(`[BackgroundUpload] Resposta GCS (uploadData): ${response.status}`);
|
|
13881
|
+
if (response.status !== 200 && response.status !== 201 && response.status !== 308) {
|
|
13882
|
+
const errorText = await response.text();
|
|
13883
|
+
console.error(`[BackgroundUpload] Erro GCS: ${errorText}`);
|
|
13884
|
+
throw new Error(`Status HTTP inesperado: ${response.status}`);
|
|
13885
|
+
}
|
|
13886
|
+
const rangeHeader = response.headers.get("Range");
|
|
13887
|
+
if (rangeHeader) {
|
|
13888
|
+
const lastByte = parseInt(rangeHeader.split("-")[1], 10);
|
|
13889
|
+
this.currentOffset = lastByte + 1;
|
|
13890
|
+
} else {
|
|
13891
|
+
this.currentOffset += size;
|
|
13892
|
+
}
|
|
13893
|
+
this.saveSessionState();
|
|
13894
|
+
trackers.registerUploadFile(
|
|
13895
|
+
this.proctoringId,
|
|
13896
|
+
`GCS Stream Upload
|
|
13897
|
+
Size: ${size}
|
|
13898
|
+
Range: ${start}-${end}
|
|
13899
|
+
Last Index: ${chunkIndex}`,
|
|
13900
|
+
"CameraChunk"
|
|
13901
|
+
);
|
|
13902
|
+
}
|
|
13903
|
+
/**
|
|
13904
|
+
* Método estático para recuperação pós-crash.
|
|
13905
|
+
* Verifica o IndexedDB em busca de chunks pendentes de qualquer sessão
|
|
13906
|
+
* e tenta enviar.
|
|
13907
|
+
*/
|
|
13908
|
+
static async recoverPendingUploads(backend, token) {
|
|
13909
|
+
const chunkStorage = new ChunkStorageService();
|
|
13910
|
+
const recoveredIds = [];
|
|
13911
|
+
try {
|
|
13912
|
+
const pendingIds = await chunkStorage.getPendingProctoringIds();
|
|
13913
|
+
if (pendingIds.length === 0) {
|
|
13914
|
+
console.log("[BackgroundUpload] Nenhum chunk pendente encontrado para recupera\xE7\xE3o.");
|
|
13915
|
+
return recoveredIds;
|
|
13916
|
+
}
|
|
13917
|
+
console.log(
|
|
13918
|
+
`[BackgroundUpload] Recupera\xE7\xE3o p\xF3s-crash: ${pendingIds.length} sess\xE3o(\xF5es) com chunks pendentes.`
|
|
13919
|
+
);
|
|
13920
|
+
for (const proctoringId2 of pendingIds) {
|
|
13921
|
+
try {
|
|
13922
|
+
const service = new _BackgroundUploadService(
|
|
13923
|
+
proctoringId2,
|
|
13924
|
+
token,
|
|
13925
|
+
backend,
|
|
13926
|
+
chunkStorage,
|
|
13927
|
+
{ cleanAfterUpload: true }
|
|
13928
|
+
);
|
|
13929
|
+
await service.flush();
|
|
13930
|
+
recoveredIds.push(proctoringId2);
|
|
13931
|
+
console.log(
|
|
13932
|
+
`[BackgroundUpload] Chunks da sess\xE3o ${proctoringId2} recuperados com sucesso.`
|
|
13933
|
+
);
|
|
13934
|
+
} catch (error) {
|
|
13935
|
+
console.error(
|
|
13936
|
+
`[BackgroundUpload] Erro ao recuperar chunks da sess\xE3o ${proctoringId2}:`,
|
|
13937
|
+
error
|
|
13938
|
+
);
|
|
13939
|
+
}
|
|
13940
|
+
}
|
|
13941
|
+
} catch (error) {
|
|
13942
|
+
console.error("[BackgroundUpload] Erro geral na recupera\xE7\xE3o:", error);
|
|
13943
|
+
}
|
|
13944
|
+
return recoveredIds;
|
|
13945
|
+
}
|
|
13946
|
+
sleep(ms2) {
|
|
13947
|
+
return new Promise((resolve) => setTimeout(resolve, ms2));
|
|
13948
|
+
}
|
|
13949
|
+
};
|
|
13950
|
+
|
|
13248
13951
|
// src/new-flow/recorders/CameraRecorder.ts
|
|
13249
13952
|
var import_jszip_min = __toESM(require_jszip_min());
|
|
13250
|
-
var
|
|
13251
|
-
var
|
|
13953
|
+
var pkg = require_fix_webm_duration();
|
|
13954
|
+
var fixWebmDuration = pkg.default || pkg;
|
|
13955
|
+
var _CameraRecorder = class _CameraRecorder {
|
|
13252
13956
|
constructor(options, videoOptions, paramsConfig, backend, backendToken) {
|
|
13253
13957
|
this.blobs = [];
|
|
13254
13958
|
this.paramsConfig = {
|
|
@@ -13293,6 +13997,7 @@ var CameraRecorder = class {
|
|
|
13293
13997
|
this.blobsRTC = [];
|
|
13294
13998
|
this.imageCount = 0;
|
|
13295
13999
|
this.filesToUpload = [];
|
|
14000
|
+
this.pendingPackages = [];
|
|
13296
14001
|
this.animationFrameId = null;
|
|
13297
14002
|
this.isCanvasLoopActive = false;
|
|
13298
14003
|
this.hardwareStream = null;
|
|
@@ -13300,8 +14005,16 @@ var CameraRecorder = class {
|
|
|
13300
14005
|
this.videoElement = null;
|
|
13301
14006
|
this.duration = 0;
|
|
13302
14007
|
this.stopped = false;
|
|
14008
|
+
this.backgroundUpload = null;
|
|
14009
|
+
this.chunkIndex = 0;
|
|
14010
|
+
/** Lista de promises de chunks sendo salvos no IndexedDB para evitar race conditions no stop */
|
|
14011
|
+
this.pendingChunkSaves = [];
|
|
14012
|
+
// Handlers bound para poder remover os listeners depois
|
|
14013
|
+
this.boundVisibilityHandler = null;
|
|
14014
|
+
this.boundPageHideHandler = null;
|
|
13303
14015
|
this.currentRetries = 0;
|
|
13304
14016
|
this.packageCount = 0;
|
|
14017
|
+
this.failedUploads = 0;
|
|
13305
14018
|
this.noiseWait = 20;
|
|
13306
14019
|
this.options = options;
|
|
13307
14020
|
this.videoOptions = videoOptions;
|
|
@@ -13309,10 +14022,122 @@ var CameraRecorder = class {
|
|
|
13309
14022
|
this.backendToken = backendToken;
|
|
13310
14023
|
paramsConfig && (this.paramsConfig = paramsConfig);
|
|
13311
14024
|
}
|
|
14025
|
+
/**
|
|
14026
|
+
* Determina se o fluxo de chunks e lifecycle deve estar ativo.
|
|
14027
|
+
* Retorna true se:
|
|
14028
|
+
* 1. O proctoringId já foi definido (ou seja, estamos em uma sessão real, NÃO no checkDevices)
|
|
14029
|
+
* 2. E (`useChunkRecording` foi explicitamente setado como true OU o dispositivo é mobile)
|
|
14030
|
+
*/
|
|
14031
|
+
get isChunkEnabled() {
|
|
14032
|
+
return !!this.proctoringId && this.options.proctoringType === "REALTIME" && !isSafeBrowser();
|
|
14033
|
+
}
|
|
13312
14034
|
setProctoringId(proctoringId2) {
|
|
13313
14035
|
this.proctoringId = proctoringId2;
|
|
13314
14036
|
this.proctoringId && this.backend && (this.upload = new UploadService(this.proctoringId, this.backend));
|
|
13315
14037
|
setRecorderProctoringId(proctoringId2);
|
|
14038
|
+
if (this.isChunkEnabled) {
|
|
14039
|
+
this.chunkStorage = new ChunkStorageService();
|
|
14040
|
+
if (this.backend && this.backendToken) {
|
|
14041
|
+
this.backgroundUpload = new BackgroundUploadService(
|
|
14042
|
+
this.proctoringId,
|
|
14043
|
+
this.backendToken,
|
|
14044
|
+
this.backend,
|
|
14045
|
+
this.chunkStorage,
|
|
14046
|
+
{ pollInterval: 5e3, maxRetries: 5, cleanAfterUpload: true }
|
|
14047
|
+
);
|
|
14048
|
+
}
|
|
14049
|
+
this.persistSessionState("IN_PROGRESS");
|
|
14050
|
+
console.log(
|
|
14051
|
+
`[CameraRecorder] Chunk recording ATIVO (type: ${this.options.proctoringType}, mobile: ${isMobileDevice()})`
|
|
14052
|
+
);
|
|
14053
|
+
} else {
|
|
14054
|
+
console.log(
|
|
14055
|
+
`[CameraRecorder] Chunk recording INATIVO (type: ${this.options.proctoringType}) \u2014 modo cl\xE1ssico.`
|
|
14056
|
+
);
|
|
14057
|
+
}
|
|
14058
|
+
}
|
|
14059
|
+
// ========================
|
|
14060
|
+
// Session State Persistence (localStorage)
|
|
14061
|
+
// ========================
|
|
14062
|
+
persistSessionState(status) {
|
|
14063
|
+
try {
|
|
14064
|
+
const data = {
|
|
14065
|
+
proctoringId: this.proctoringId,
|
|
14066
|
+
status,
|
|
14067
|
+
timestamp: Date.now()
|
|
14068
|
+
};
|
|
14069
|
+
localStorage.setItem(_CameraRecorder.LS_SESSION_KEY, JSON.stringify(data));
|
|
14070
|
+
} catch (e3) {
|
|
14071
|
+
console.warn("[CameraRecorder] N\xE3o foi poss\xEDvel salvar estado no localStorage:", e3);
|
|
14072
|
+
}
|
|
14073
|
+
}
|
|
14074
|
+
clearSessionState() {
|
|
14075
|
+
try {
|
|
14076
|
+
localStorage.removeItem(_CameraRecorder.LS_SESSION_KEY);
|
|
14077
|
+
} catch (e3) {
|
|
14078
|
+
console.warn("[CameraRecorder] N\xE3o foi poss\xEDvel limpar estado do localStorage:", e3);
|
|
14079
|
+
}
|
|
14080
|
+
}
|
|
14081
|
+
/**
|
|
14082
|
+
* Verifica se existe uma sessão ativa anterior no localStorage.
|
|
14083
|
+
* Retorna os dados da sessão se ela estiver em IN_PROGRESS, null caso contrário.
|
|
14084
|
+
*/
|
|
14085
|
+
static checkForActiveSession() {
|
|
14086
|
+
try {
|
|
14087
|
+
const raw = localStorage.getItem(_CameraRecorder.LS_SESSION_KEY);
|
|
14088
|
+
if (!raw) return null;
|
|
14089
|
+
const data = JSON.parse(raw);
|
|
14090
|
+
if (data.status === "IN_PROGRESS") return data;
|
|
14091
|
+
return null;
|
|
14092
|
+
} catch (e3) {
|
|
14093
|
+
return null;
|
|
14094
|
+
}
|
|
14095
|
+
}
|
|
14096
|
+
// ========================
|
|
14097
|
+
// Page Lifecycle Management
|
|
14098
|
+
// ========================
|
|
14099
|
+
setupLifecycleListeners() {
|
|
14100
|
+
this.boundVisibilityHandler = () => this.handleVisibilityChange();
|
|
14101
|
+
this.boundPageHideHandler = () => this.handlePageHide();
|
|
14102
|
+
document.addEventListener("visibilitychange", this.boundVisibilityHandler);
|
|
14103
|
+
window.addEventListener("pagehide", this.boundPageHideHandler);
|
|
14104
|
+
}
|
|
14105
|
+
removeLifecycleListeners() {
|
|
14106
|
+
if (this.boundVisibilityHandler) {
|
|
14107
|
+
document.removeEventListener("visibilitychange", this.boundVisibilityHandler);
|
|
14108
|
+
this.boundVisibilityHandler = null;
|
|
14109
|
+
}
|
|
14110
|
+
if (this.boundPageHideHandler) {
|
|
14111
|
+
window.removeEventListener("pagehide", this.boundPageHideHandler);
|
|
14112
|
+
this.boundPageHideHandler = null;
|
|
14113
|
+
}
|
|
14114
|
+
}
|
|
14115
|
+
handleVisibilityChange() {
|
|
14116
|
+
var _a2;
|
|
14117
|
+
if (document.visibilityState === "hidden") {
|
|
14118
|
+
console.log("[CameraRecorder] P\xE1gina ficou invis\xEDvel \u2014 sess\xE3o potencialmente interrompida.");
|
|
14119
|
+
this.persistSessionState("INTERRUPTED");
|
|
14120
|
+
this.proctoringId && trackers.registerError(
|
|
14121
|
+
this.proctoringId,
|
|
14122
|
+
"Visibility API: P\xE1gina ficou oculta (hidden). Poss\xEDvel troca de app ou minimiza\xE7\xE3o."
|
|
14123
|
+
);
|
|
14124
|
+
} else if (document.visibilityState === "visible") {
|
|
14125
|
+
console.log("[CameraRecorder] P\xE1gina vis\xEDvel novamente \u2014 verificando estado da grava\xE7\xE3o.");
|
|
14126
|
+
this.persistSessionState("IN_PROGRESS");
|
|
14127
|
+
this.proctoringId && trackers.registerError(
|
|
14128
|
+
this.proctoringId,
|
|
14129
|
+
"Visibility API: P\xE1gina voltou a ficar vis\xEDvel. Usu\xE1rio retornou."
|
|
14130
|
+
);
|
|
14131
|
+
(_a2 = this.onVisibilityRestored) == null ? void 0 : _a2.call(this);
|
|
14132
|
+
}
|
|
14133
|
+
}
|
|
14134
|
+
handlePageHide() {
|
|
14135
|
+
console.log("[CameraRecorder] pagehide detectado \u2014 persistindo estado.");
|
|
14136
|
+
this.persistSessionState("INTERRUPTED");
|
|
14137
|
+
this.proctoringId && trackers.registerError(
|
|
14138
|
+
this.proctoringId,
|
|
14139
|
+
"Page Lifecycle: pagehide event detectado. P\xE1gina est\xE1 sendo descarregada."
|
|
14140
|
+
);
|
|
13316
14141
|
}
|
|
13317
14142
|
async initializeDetectors() {
|
|
13318
14143
|
var _a2, _b, _c2;
|
|
@@ -13465,10 +14290,15 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
13465
14290
|
await new Promise((r3) => setTimeout(r3, 300));
|
|
13466
14291
|
}
|
|
13467
14292
|
async startRecording() {
|
|
13468
|
-
var _a2, _b, _c2, _d, _e3, _f, _g;
|
|
13469
|
-
console.log("CameraRecorder startRecording");
|
|
14293
|
+
var _a2, _b, _c2, _d, _e3, _f, _g, _h;
|
|
13470
14294
|
await this.startStream();
|
|
13471
14295
|
await this.attachAndWarmup(this.cameraStream);
|
|
14296
|
+
const recorderOpts = this.isChunkEnabled ? {
|
|
14297
|
+
timeslice: _CameraRecorder.CHUNK_TIMESLICE_MS,
|
|
14298
|
+
onChunkAvailable: (blob, idx) => {
|
|
14299
|
+
this.handleNewChunk(blob, idx);
|
|
14300
|
+
}
|
|
14301
|
+
} : {};
|
|
13472
14302
|
const {
|
|
13473
14303
|
startRecording,
|
|
13474
14304
|
stopRecording,
|
|
@@ -13484,7 +14314,8 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
13484
14314
|
this.blobs,
|
|
13485
14315
|
this.options.onBufferSizeError,
|
|
13486
14316
|
(e3) => this.bufferError(e3),
|
|
13487
|
-
false
|
|
14317
|
+
false,
|
|
14318
|
+
recorderOpts
|
|
13488
14319
|
);
|
|
13489
14320
|
this.recordingStart = startRecording;
|
|
13490
14321
|
this.recordingStop = stopRecording;
|
|
@@ -13494,13 +14325,18 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
13494
14325
|
this.getBufferSize = getBufferSize;
|
|
13495
14326
|
this.getStartTime = getStartTime;
|
|
13496
14327
|
this.getDuration = getDuration;
|
|
14328
|
+
this.chunkIndex = 0;
|
|
14329
|
+
if (this.isChunkEnabled) {
|
|
14330
|
+
(_a2 = this.backgroundUpload) == null ? void 0 : _a2.start();
|
|
14331
|
+
this.setupLifecycleListeners();
|
|
14332
|
+
}
|
|
13497
14333
|
try {
|
|
13498
14334
|
await new Promise((r3) => setTimeout(r3, 500));
|
|
13499
14335
|
await this.recordingStart();
|
|
13500
14336
|
} catch (error) {
|
|
13501
14337
|
console.log("Camera Recorder error", error);
|
|
13502
14338
|
this.stopRecording();
|
|
13503
|
-
const maxRetries = ((
|
|
14339
|
+
const maxRetries = ((_b = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _b.maxRetries) || 3;
|
|
13504
14340
|
if (this.currentRetries < maxRetries) {
|
|
13505
14341
|
console.log("Camera Recorder retry", this.currentRetries);
|
|
13506
14342
|
this.currentRetries++;
|
|
@@ -13510,16 +14346,17 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
13510
14346
|
}
|
|
13511
14347
|
}
|
|
13512
14348
|
this.stopped = false;
|
|
13513
|
-
if (((
|
|
14349
|
+
if (((_c2 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _c2.detectPerson) || ((_d = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _d.detectCellPhone) || ((_e3 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _e3.detectFace)) {
|
|
13514
14350
|
await this.initializeDetectors();
|
|
13515
14351
|
}
|
|
13516
|
-
if ((
|
|
14352
|
+
if ((_f = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _f.detectFace) {
|
|
13517
14353
|
await this.faceDetection.enableCam(this.cameraStream);
|
|
13518
14354
|
}
|
|
13519
|
-
if (((
|
|
14355
|
+
if (((_g = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _g.detectPerson) || ((_h = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _h.detectCellPhone)) {
|
|
13520
14356
|
await this.objectDetection.enableCam(this.cameraStream);
|
|
13521
14357
|
}
|
|
13522
14358
|
this.filesToUpload = [];
|
|
14359
|
+
this.pendingPackages = [];
|
|
13523
14360
|
if (this.options.proctoringType == "REALTIME") {
|
|
13524
14361
|
await this.startRealtimeCapture();
|
|
13525
14362
|
}
|
|
@@ -13539,6 +14376,7 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
13539
14376
|
this.intervalNoiseDetection && clearInterval(this.intervalNoiseDetection);
|
|
13540
14377
|
this.recordingStop && await this.recordingStop();
|
|
13541
14378
|
this.duration = this.getDuration ? this.getDuration() : 0;
|
|
14379
|
+
await new Promise((r3) => setTimeout(r3, 200));
|
|
13542
14380
|
try {
|
|
13543
14381
|
if (this.animationFrameId) {
|
|
13544
14382
|
cancelAnimationFrame(this.animationFrameId);
|
|
@@ -13569,9 +14407,55 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
13569
14407
|
console.error("Erro ao parar os streams de m\xEDdia.");
|
|
13570
14408
|
}
|
|
13571
14409
|
if (this.options.proctoringType == "REALTIME" && this.upload && this.backendToken) {
|
|
13572
|
-
|
|
14410
|
+
this.pendingPackages.push(this.filesToUpload.slice(0, this.filesToUpload.length));
|
|
14411
|
+
await this.sendPackage();
|
|
13573
14412
|
await this.filesToUpload.splice(0, this.filesToUpload.length);
|
|
13574
14413
|
}
|
|
14414
|
+
if (this.isChunkEnabled) {
|
|
14415
|
+
if (this.backgroundUpload) {
|
|
14416
|
+
try {
|
|
14417
|
+
if (this.pendingChunkSaves.length > 0) {
|
|
14418
|
+
console.log(`[CameraRecorder] Aguardando ${this.pendingChunkSaves.length} salvamentos de chunks pendentes...`);
|
|
14419
|
+
await Promise.all(this.pendingChunkSaves);
|
|
14420
|
+
}
|
|
14421
|
+
await this.backgroundUpload.flush();
|
|
14422
|
+
} catch (e3) {
|
|
14423
|
+
console.warn("[CameraRecorder] Erro ao fazer flush dos chunks:", e3);
|
|
14424
|
+
}
|
|
14425
|
+
this.backgroundUpload.stop();
|
|
14426
|
+
}
|
|
14427
|
+
this.removeLifecycleListeners();
|
|
14428
|
+
this.persistSessionState("FINISHED");
|
|
14429
|
+
}
|
|
14430
|
+
}
|
|
14431
|
+
/**
|
|
14432
|
+
* Callback chamado pelo recorder a cada novo chunk de vídeo disponível.
|
|
14433
|
+
* Salva o chunk no IndexedDB para persistência e recuperação.
|
|
14434
|
+
*/
|
|
14435
|
+
async handleNewChunk(blob, idx) {
|
|
14436
|
+
if (!this.proctoringId || !this.chunkStorage) return;
|
|
14437
|
+
const savePromise = (async () => {
|
|
14438
|
+
var _a2;
|
|
14439
|
+
try {
|
|
14440
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
14441
|
+
await this.chunkStorage.saveChunk({
|
|
14442
|
+
proctoringId: this.proctoringId,
|
|
14443
|
+
chunkIndex: this.chunkIndex,
|
|
14444
|
+
arrayBuffer,
|
|
14445
|
+
timestamp: Date.now(),
|
|
14446
|
+
uploaded: 0,
|
|
14447
|
+
mimeType: ((_a2 = this.recorderOptions) == null ? void 0 : _a2.mimeType) || "video/webm"
|
|
14448
|
+
});
|
|
14449
|
+
this.chunkIndex++;
|
|
14450
|
+
console.log(`[CameraRecorder] Chunk ${this.chunkIndex - 1} salvo no IndexedDB.`);
|
|
14451
|
+
} catch (error) {
|
|
14452
|
+
console.error("[CameraRecorder] Erro ao salvar chunk no IndexedDB:", error);
|
|
14453
|
+
}
|
|
14454
|
+
})();
|
|
14455
|
+
this.pendingChunkSaves.push(savePromise);
|
|
14456
|
+
savePromise.finally(() => {
|
|
14457
|
+
this.pendingChunkSaves = this.pendingChunkSaves.filter((p3) => p3 !== savePromise);
|
|
14458
|
+
});
|
|
13575
14459
|
}
|
|
13576
14460
|
async pauseRecording() {
|
|
13577
14461
|
await this.recordingPause();
|
|
@@ -13618,9 +14502,9 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
13618
14502
|
if (this.proctoringId == void 0) return;
|
|
13619
14503
|
if (packSize == this.imageCount) {
|
|
13620
14504
|
this.imageCount = 0;
|
|
13621
|
-
|
|
13622
|
-
this.sendPackage(
|
|
13623
|
-
await this.filesToUpload.splice(0,
|
|
14505
|
+
this.pendingPackages.push(this.filesToUpload.slice(0, packSize));
|
|
14506
|
+
this.sendPackage();
|
|
14507
|
+
await this.filesToUpload.splice(0, packSize);
|
|
13624
14508
|
}
|
|
13625
14509
|
let imageName = `${this.proctoringId}_${this.imageCount + 1}.jpg`;
|
|
13626
14510
|
imageFile = await this.getFile(image_data_url, imageName, "image/jpeg");
|
|
@@ -13634,54 +14518,61 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
13634
14518
|
var _a2;
|
|
13635
14519
|
this.configImageCapture();
|
|
13636
14520
|
this.imageCount = 0;
|
|
14521
|
+
this.pendingPackages = [];
|
|
13637
14522
|
await this.captureFrame();
|
|
13638
14523
|
this.imageInterval = setInterval(async () => {
|
|
13639
14524
|
await this.captureFrame();
|
|
13640
14525
|
}, ((_a2 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _a2.realtimeCaptureInterval) * 1e3);
|
|
13641
14526
|
}
|
|
13642
14527
|
// envia pacote de imagens
|
|
13643
|
-
async sendPackage(
|
|
14528
|
+
async sendPackage() {
|
|
13644
14529
|
var _a2, _b;
|
|
13645
|
-
let pending = false;
|
|
13646
|
-
let undeliveredPackagesCount = 0;
|
|
13647
14530
|
const packSize = (_a2 = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _a2.realtimePackageSize;
|
|
13648
14531
|
const packCaptureInterval = (_b = this.paramsConfig.videoBehaviourParameters) == null ? void 0 : _b.realtimeCaptureInterval;
|
|
13649
|
-
if (this.upload && this.backendToken &&
|
|
13650
|
-
|
|
13651
|
-
|
|
13652
|
-
const
|
|
13653
|
-
|
|
13654
|
-
|
|
13655
|
-
|
|
13656
|
-
|
|
13657
|
-
|
|
13658
|
-
|
|
13659
|
-
|
|
13660
|
-
|
|
13661
|
-
|
|
13662
|
-
|
|
13663
|
-
|
|
13664
|
-
{
|
|
13665
|
-
|
|
13666
|
-
|
|
13667
|
-
|
|
13668
|
-
|
|
13669
|
-
|
|
13670
|
-
|
|
13671
|
-
|
|
13672
|
-
|
|
14532
|
+
if (this.upload && this.backendToken && this.pendingPackages.length > 0) {
|
|
14533
|
+
let packagesToDelete = [];
|
|
14534
|
+
let packageIndex = 0;
|
|
14535
|
+
for (const packageToSend of this.pendingPackages) {
|
|
14536
|
+
const zip = new import_jszip_min.default();
|
|
14537
|
+
for (const file of packageToSend) {
|
|
14538
|
+
zip.file(file.name, file);
|
|
14539
|
+
}
|
|
14540
|
+
const blob = await zip.generateAsync({ type: "blob" });
|
|
14541
|
+
let packageName = "realtime_package_" + packSize * packCaptureInterval * this.packageCount + ".zip";
|
|
14542
|
+
const myPackage = new File(
|
|
14543
|
+
[blob],
|
|
14544
|
+
packageName,
|
|
14545
|
+
{ type: "application/zip" }
|
|
14546
|
+
);
|
|
14547
|
+
try {
|
|
14548
|
+
const uploadResult = await this.upload.uploadPackage(
|
|
14549
|
+
{
|
|
14550
|
+
file: myPackage
|
|
14551
|
+
},
|
|
14552
|
+
this.backendToken
|
|
14553
|
+
);
|
|
14554
|
+
if (uploadResult == true) {
|
|
14555
|
+
this.packageCount++;
|
|
14556
|
+
this.failedUploads = 0;
|
|
14557
|
+
packagesToDelete.push(packageIndex++);
|
|
14558
|
+
}
|
|
14559
|
+
} catch (error) {
|
|
14560
|
+
this.failedUploads++;
|
|
14561
|
+
if (this.failedUploads >= 2) {
|
|
14562
|
+
this.options.onRealtimeAlertsCallback({
|
|
14563
|
+
status: "ALERT",
|
|
14564
|
+
description: "Realtime n\xE3o est\xE1 enviando pacotes",
|
|
14565
|
+
type: "error_upload_package",
|
|
14566
|
+
category: "error_upload_package",
|
|
14567
|
+
begin: 0,
|
|
14568
|
+
end: 0
|
|
14569
|
+
});
|
|
14570
|
+
}
|
|
14571
|
+
break;
|
|
14572
|
+
}
|
|
13673
14573
|
}
|
|
13674
|
-
|
|
13675
|
-
|
|
13676
|
-
undeliveredPackagesCount++;
|
|
13677
|
-
if (undeliveredPackagesCount == 3) {
|
|
13678
|
-
undeliveredPackagesCount = 0;
|
|
13679
|
-
let newCanvasWidth = this.videoOptions.width / 2;
|
|
13680
|
-
let newCanvasHeight = this.videoOptions.height / 2;
|
|
13681
|
-
if (newCanvasWidth < 320) newCanvasWidth = 320;
|
|
13682
|
-
if (newCanvasHeight < 180) newCanvasHeight = 180;
|
|
13683
|
-
this.canvas.width = newCanvasWidth;
|
|
13684
|
-
this.canvas.height = newCanvasHeight;
|
|
14574
|
+
for (const packageToDelete of packagesToDelete) {
|
|
14575
|
+
await this.pendingPackages.splice(packageToDelete, 1);
|
|
13685
14576
|
}
|
|
13686
14577
|
}
|
|
13687
14578
|
}
|
|
@@ -13700,16 +14591,32 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
13700
14591
|
if (this.blobs != null)
|
|
13701
14592
|
trackers.registerSaveOnSession(
|
|
13702
14593
|
this.proctoringId,
|
|
13703
|
-
`Blobs Length: ${this.blobs.length} Buffer Size: ${this.getBufferSize()} `
|
|
14594
|
+
`Blobs Length: ${this.blobs.length} Buffer Size: ${this.getBufferSize()} ChunkEnabled: ${this.isChunkEnabled}`
|
|
13704
14595
|
);
|
|
13705
14596
|
const settings = this.cameraStream.getVideoTracks()[0].getSettings();
|
|
13706
14597
|
const settingsAudio = this.cameraStream.getAudioTracks()[0].getSettings();
|
|
13707
14598
|
if (this.options.proctoringType == "VIDEO" || this.options.proctoringType == "REALTIME" || this.options.proctoringType == "IMAGE" && ((_a2 = this.paramsConfig.imageBehaviourParameters) == null ? void 0 : _a2.saveVideo)) {
|
|
14599
|
+
let videoFile;
|
|
14600
|
+
if (this.isChunkEnabled) {
|
|
14601
|
+
const isStable = await this.checkInternetStability();
|
|
14602
|
+
if (isStable) {
|
|
14603
|
+
} else {
|
|
14604
|
+
if (this.backend && this.backendToken && this.proctoringId) {
|
|
14605
|
+
const fileName = `EP_${this.proctoringId}_camera_0.webm`;
|
|
14606
|
+
const objectName = `${this.proctoringId}/${fileName}`;
|
|
14607
|
+
const isUploaded = await this.backend.checkUpload(this.backendToken, objectName, "video/webm");
|
|
14608
|
+
if (isUploaded) {
|
|
14609
|
+
this.chunkStorage && await this.chunkStorage.clearAllChunks(session.id);
|
|
14610
|
+
return;
|
|
14611
|
+
}
|
|
14612
|
+
}
|
|
14613
|
+
}
|
|
14614
|
+
}
|
|
13708
14615
|
const rawBlob = new Blob(this.blobs, {
|
|
13709
14616
|
type: ((_b = this.recorderOptions) == null ? void 0 : _b.mimeType) || "video/webm"
|
|
13710
14617
|
});
|
|
13711
|
-
const fixedBlob = await (
|
|
13712
|
-
|
|
14618
|
+
const fixedBlob = await fixWebmDuration(rawBlob, this.duration);
|
|
14619
|
+
videoFile = new File(
|
|
13713
14620
|
[fixedBlob],
|
|
13714
14621
|
`EP_${session.id}_camera_0.webm`,
|
|
13715
14622
|
{ type: rawBlob.type }
|
|
@@ -13721,7 +14628,7 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
13721
14628
|
|
|
13722
14629
|
Video:
|
|
13723
14630
|
${JSON.stringify(this.recorderOptions)}`,
|
|
13724
|
-
file:
|
|
14631
|
+
file: videoFile,
|
|
13725
14632
|
origin: "Camera" /* Camera */
|
|
13726
14633
|
});
|
|
13727
14634
|
}
|
|
@@ -13736,6 +14643,28 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
13736
14643
|
});
|
|
13737
14644
|
});
|
|
13738
14645
|
}
|
|
14646
|
+
/**
|
|
14647
|
+
* Verifica se a internet está estável para realizar o upload do vídeo na íntegra.
|
|
14648
|
+
*/
|
|
14649
|
+
async checkInternetStability() {
|
|
14650
|
+
var _a2;
|
|
14651
|
+
if (!navigator.onLine) return false;
|
|
14652
|
+
try {
|
|
14653
|
+
const controller = new AbortController();
|
|
14654
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
14655
|
+
const baseUrl = (_a2 = this.backend) == null ? void 0 : _a2.getBaseUrl();
|
|
14656
|
+
if (!baseUrl) return true;
|
|
14657
|
+
const response = await fetch(`${baseUrl}/Client/health`, {
|
|
14658
|
+
method: "GET",
|
|
14659
|
+
signal: controller.signal
|
|
14660
|
+
});
|
|
14661
|
+
clearTimeout(timeoutId);
|
|
14662
|
+
return response.status < 500;
|
|
14663
|
+
} catch (e3) {
|
|
14664
|
+
console.warn("[CameraRecorder] Internet inst\xE1vel ou lenta detectada para upload integral.");
|
|
14665
|
+
return false;
|
|
14666
|
+
}
|
|
14667
|
+
}
|
|
13739
14668
|
onNoiseDetected() {
|
|
13740
14669
|
var _a2, _b, _c2;
|
|
13741
14670
|
if (this.options.proctoringType === "REALTIME") return;
|
|
@@ -13748,7 +14677,6 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
13748
14677
|
}
|
|
13749
14678
|
const volume = (_b = (_a2 = this.volumeMeter) == null ? void 0 : _a2.getVolume()) != null ? _b : 0;
|
|
13750
14679
|
if (volume >= (((_c2 = this.paramsConfig.audioBehaviourParameters) == null ? void 0 : _c2.noiseLimit) || 40)) {
|
|
13751
|
-
console.log("entrou" + this.noiseWait);
|
|
13752
14680
|
if (this.noiseWait >= 20) {
|
|
13753
14681
|
this.options.onRealtimeAlertsCallback({
|
|
13754
14682
|
status: "ALERT",
|
|
@@ -13761,6 +14689,14 @@ Setting: ${JSON.stringify(settings, null, 2)}`
|
|
|
13761
14689
|
this.noiseWait++;
|
|
13762
14690
|
}
|
|
13763
14691
|
};
|
|
14692
|
+
// ========================
|
|
14693
|
+
// Chunk & Lifecycle
|
|
14694
|
+
// ========================
|
|
14695
|
+
/** Intervalo de cada chunk em ms (padrão: 60 segundos) */
|
|
14696
|
+
_CameraRecorder.CHUNK_TIMESLICE_MS = 6e4;
|
|
14697
|
+
/** Chave do localStorage para persistir estado da sessão */
|
|
14698
|
+
_CameraRecorder.LS_SESSION_KEY = "ep_proctoring_session";
|
|
14699
|
+
var CameraRecorder = _CameraRecorder;
|
|
13764
14700
|
|
|
13765
14701
|
// src/new-flow/checkers/DeviceCheckerUI.ts
|
|
13766
14702
|
var DeviceCheckerUI = class {
|
|
@@ -14833,7 +15769,8 @@ var _DeviceCheckerService = class _DeviceCheckerService {
|
|
|
14833
15769
|
this.context = context;
|
|
14834
15770
|
this.backend = new BackendService({
|
|
14835
15771
|
type: (context == null ? void 0 : context.type) || "prod",
|
|
14836
|
-
token: context.token
|
|
15772
|
+
token: context.token,
|
|
15773
|
+
isRealtime: false
|
|
14837
15774
|
});
|
|
14838
15775
|
}
|
|
14839
15776
|
getDeviceCheckResult() {
|
|
@@ -14994,7 +15931,7 @@ var _DeviceCheckerService = class _DeviceCheckerService {
|
|
|
14994
15931
|
videoDeviceInterface(stream) {
|
|
14995
15932
|
this.DeviceCheckerUI && this.DeviceCheckerUI.videoDeviceInterfaceUI(stream);
|
|
14996
15933
|
this.isUnderResolution();
|
|
14997
|
-
this.faceDetection.enableCam(stream);
|
|
15934
|
+
this.faceDetection.enableCam(stream, 1e3);
|
|
14998
15935
|
}
|
|
14999
15936
|
audioDeviceInterface(stream) {
|
|
15000
15937
|
this.volumeMeter = new VolumeMeter(this.cameraRecorder.cameraStream);
|
|
@@ -15446,8 +16383,8 @@ var CapturePhoto = class {
|
|
|
15446
16383
|
}
|
|
15447
16384
|
};
|
|
15448
16385
|
|
|
15449
|
-
// src/extension/
|
|
15450
|
-
var
|
|
16386
|
+
// src/extension/extension.ts
|
|
16387
|
+
var Extension = class {
|
|
15451
16388
|
constructor() {
|
|
15452
16389
|
this.hasExtension = false;
|
|
15453
16390
|
this.tryes = 0;
|
|
@@ -15492,89 +16429,6 @@ var ExtensionEasyProctor = class {
|
|
|
15492
16429
|
}
|
|
15493
16430
|
};
|
|
15494
16431
|
|
|
15495
|
-
// src/extension/extensionEasyCatcher.ts
|
|
15496
|
-
var ExtensionEasyCatcher = class {
|
|
15497
|
-
constructor(options) {
|
|
15498
|
-
this.hasExtension = false;
|
|
15499
|
-
this.tryes = 0;
|
|
15500
|
-
this.responseStart = false;
|
|
15501
|
-
this.options = options || {};
|
|
15502
|
-
}
|
|
15503
|
-
/**
|
|
15504
|
-
* Verifica se a extensão está instalada e ativa.
|
|
15505
|
-
* Retorna o número da versão se encontrada, ou lança erro após timeout.
|
|
15506
|
-
*/
|
|
15507
|
-
checkExtensionInstalled(timeoutMs = 2e3) {
|
|
15508
|
-
return new Promise((resolve, reject) => {
|
|
15509
|
-
let handled = false;
|
|
15510
|
-
const handler = (event) => {
|
|
15511
|
-
if (event.source === window && event.data.sender === "easyproctor-extension" && event.data.message_name === "version") {
|
|
15512
|
-
handled = true;
|
|
15513
|
-
window.removeEventListener("message", handler);
|
|
15514
|
-
resolve(event.data.message);
|
|
15515
|
-
}
|
|
15516
|
-
};
|
|
15517
|
-
window.addEventListener("message", handler);
|
|
15518
|
-
window.postMessage({
|
|
15519
|
-
type: "easycatcher",
|
|
15520
|
-
func: "verifyExtensionEasycatcher"
|
|
15521
|
-
}, "*");
|
|
15522
|
-
setTimeout(() => {
|
|
15523
|
-
if (!handled) {
|
|
15524
|
-
window.removeEventListener("message", handler);
|
|
15525
|
-
reject(new Error("Extens\xE3o n\xE3o detectada ou n\xE3o respondeu."));
|
|
15526
|
-
}
|
|
15527
|
-
}, timeoutMs);
|
|
15528
|
-
});
|
|
15529
|
-
}
|
|
15530
|
-
/**
|
|
15531
|
-
* Solicita o JSON da sessão atual capturado pela extensão.
|
|
15532
|
-
*/
|
|
15533
|
-
getSessionData(timeoutMs = 5e3) {
|
|
15534
|
-
return new Promise((resolve, reject) => {
|
|
15535
|
-
let handled = false;
|
|
15536
|
-
const handler = (event) => {
|
|
15537
|
-
if (event.source === window && event.data.sender === "easyproctor-extension" && event.data.message_name === "data_response") {
|
|
15538
|
-
handled = true;
|
|
15539
|
-
window.removeEventListener("message", handler);
|
|
15540
|
-
resolve(event.data.payload);
|
|
15541
|
-
}
|
|
15542
|
-
};
|
|
15543
|
-
window.addEventListener("message", handler);
|
|
15544
|
-
window.postMessage({
|
|
15545
|
-
type: "easycatcher",
|
|
15546
|
-
func: "getDataExtensionEasycatcher"
|
|
15547
|
-
}, "*");
|
|
15548
|
-
setTimeout(() => {
|
|
15549
|
-
if (!handled) {
|
|
15550
|
-
window.removeEventListener("message", handler);
|
|
15551
|
-
reject(new Error("Timeout ao aguardar dados da extens\xE3o."));
|
|
15552
|
-
}
|
|
15553
|
-
}, timeoutMs);
|
|
15554
|
-
});
|
|
15555
|
-
}
|
|
15556
|
-
start() {
|
|
15557
|
-
return new Promise((resolve, reject) => {
|
|
15558
|
-
let handled = false;
|
|
15559
|
-
const handler = (event) => {
|
|
15560
|
-
if (event.source === window && event.data.sender === "easyproctor-extension" && event.data.message_name === "started_confirmed") {
|
|
15561
|
-
handled = true;
|
|
15562
|
-
window.removeEventListener("message", handler);
|
|
15563
|
-
resolve(true);
|
|
15564
|
-
}
|
|
15565
|
-
};
|
|
15566
|
-
window.addEventListener("message", handler);
|
|
15567
|
-
window.postMessage({ type: "easycatcher", func: "startExtensionEasycatcher" }, "*");
|
|
15568
|
-
setTimeout(() => {
|
|
15569
|
-
if (!handled) {
|
|
15570
|
-
window.removeEventListener("message", handler);
|
|
15571
|
-
reject(new Error("Timeout: Extens\xE3o n\xE3o confirmou o in\xEDcio."));
|
|
15572
|
-
}
|
|
15573
|
-
}, 3e3);
|
|
15574
|
-
});
|
|
15575
|
-
}
|
|
15576
|
-
};
|
|
15577
|
-
|
|
15578
16432
|
// src/modules/onChangeDevices.ts
|
|
15579
16433
|
var onChangeDevices = class {
|
|
15580
16434
|
constructor(repositoryDevices, proctoringId2, sessionOptions, allRecorders) {
|
|
@@ -18260,6 +19114,7 @@ var NoiseRecorder = class {
|
|
|
18260
19114
|
this.MAX_PRE_ROLL_CHUNKS = 4;
|
|
18261
19115
|
this.lastNoiseTime = 0;
|
|
18262
19116
|
this.SILENCE_THRESHOLD = 3e3;
|
|
19117
|
+
this.filesToUpload = [];
|
|
18263
19118
|
this.optionsProctoring = optionsProctoring;
|
|
18264
19119
|
this.proctoringSession = proctoringSession;
|
|
18265
19120
|
this.paramsConfig = paramsConfig;
|
|
@@ -18351,7 +19206,7 @@ var NoiseRecorder = class {
|
|
|
18351
19206
|
}
|
|
18352
19207
|
}
|
|
18353
19208
|
async stopSoundRecord() {
|
|
18354
|
-
var _a2;
|
|
19209
|
+
var _a2, _b;
|
|
18355
19210
|
if (!this.recordingInProgress && this.recordingChunks.length === 0) return;
|
|
18356
19211
|
this.recordingEndTime = Date.now() - (((_a2 = this.cameraRecorder.getStartTime()) == null ? void 0 : _a2.getTime()) || 0);
|
|
18357
19212
|
if (this.optionsProctoring.proctoringType !== "REALTIME") return;
|
|
@@ -18365,7 +19220,33 @@ var NoiseRecorder = class {
|
|
|
18365
19220
|
type: "audio/wav"
|
|
18366
19221
|
}
|
|
18367
19222
|
);
|
|
18368
|
-
this.
|
|
19223
|
+
let filesToSend = [...this.filesToUpload];
|
|
19224
|
+
for (const myFile of filesToSend) {
|
|
19225
|
+
try {
|
|
19226
|
+
await ((_b = this.upload) == null ? void 0 : _b.upload(
|
|
19227
|
+
{
|
|
19228
|
+
file: myFile
|
|
19229
|
+
},
|
|
19230
|
+
this.backendToken
|
|
19231
|
+
));
|
|
19232
|
+
this.filesToUpload.splice(this.filesToUpload.indexOf(myFile), 1);
|
|
19233
|
+
} catch (error) {
|
|
19234
|
+
break;
|
|
19235
|
+
}
|
|
19236
|
+
}
|
|
19237
|
+
try {
|
|
19238
|
+
if (file && this.upload && this.backendToken) {
|
|
19239
|
+
this.upload.upload(
|
|
19240
|
+
{
|
|
19241
|
+
file
|
|
19242
|
+
},
|
|
19243
|
+
this.backendToken
|
|
19244
|
+
);
|
|
19245
|
+
}
|
|
19246
|
+
} catch (error) {
|
|
19247
|
+
console.log("error Noise recorder adicionando na fila", error);
|
|
19248
|
+
this.filesToUpload.push(file);
|
|
19249
|
+
}
|
|
18369
19250
|
this.recordingChunks = [];
|
|
18370
19251
|
this.recordingInProgress = false;
|
|
18371
19252
|
}
|
|
@@ -18379,16 +19260,6 @@ var NoiseRecorder = class {
|
|
|
18379
19260
|
a3.click();
|
|
18380
19261
|
window.URL.revokeObjectURL(url);
|
|
18381
19262
|
}
|
|
18382
|
-
uploadRecord(file) {
|
|
18383
|
-
if (file && this.upload && this.backendToken) {
|
|
18384
|
-
this.upload.upload(
|
|
18385
|
-
{
|
|
18386
|
-
file
|
|
18387
|
-
},
|
|
18388
|
-
this.backendToken
|
|
18389
|
-
);
|
|
18390
|
-
}
|
|
18391
|
-
}
|
|
18392
19263
|
// CLASSIFIER -< Media Pipe
|
|
18393
19264
|
// Verify if has speech in the classifier array
|
|
18394
19265
|
hasDesiredResult(array) {
|
|
@@ -18495,7 +19366,8 @@ registerProcessor("audio-processor", AudioProcessor);
|
|
|
18495
19366
|
`;
|
|
18496
19367
|
|
|
18497
19368
|
// src/new-flow/recorders/ScreenRecorder.ts
|
|
18498
|
-
var
|
|
19369
|
+
var pkg2 = require_fix_webm_duration();
|
|
19370
|
+
var fixWebmDuration2 = pkg2.default || pkg2;
|
|
18499
19371
|
var ScreenRecorder = class {
|
|
18500
19372
|
constructor(options) {
|
|
18501
19373
|
this.blobs = [];
|
|
@@ -18591,7 +19463,7 @@ var ScreenRecorder = class {
|
|
|
18591
19463
|
const rawBlob = new Blob(this.blobs, {
|
|
18592
19464
|
type: "video/webm"
|
|
18593
19465
|
});
|
|
18594
|
-
const fixedBlob = await (
|
|
19466
|
+
const fixedBlob = await fixWebmDuration2(rawBlob, this.duration);
|
|
18595
19467
|
const file = new File(
|
|
18596
19468
|
[fixedBlob],
|
|
18597
19469
|
`EP_${session.id}_screen_0.webm`,
|
|
@@ -18696,14 +19568,6 @@ function getGeolocation() {
|
|
|
18696
19568
|
});
|
|
18697
19569
|
}
|
|
18698
19570
|
|
|
18699
|
-
// src/utils/verifyVersion.ts
|
|
18700
|
-
function versionVerify() {
|
|
18701
|
-
const agentStr = window.navigator.userAgent.split("SEB/");
|
|
18702
|
-
if (agentStr.length > 1)
|
|
18703
|
-
return agentStr[1];
|
|
18704
|
-
else return "1.0.0.0";
|
|
18705
|
-
}
|
|
18706
|
-
|
|
18707
19571
|
// src/proctoring/Auth.ts
|
|
18708
19572
|
var Auth = class {
|
|
18709
19573
|
constructor(cpf, backend) {
|
|
@@ -21697,7 +22561,8 @@ var _ExternalCameraChecker = class _ExternalCameraChecker {
|
|
|
21697
22561
|
this.onRealtimeAlertsCallback = onRealtimeAlertsCallback;
|
|
21698
22562
|
this.backend = new BackendService({
|
|
21699
22563
|
type: (context == null ? void 0 : context.type) || "prod",
|
|
21700
|
-
token: context.token
|
|
22564
|
+
token: context.token,
|
|
22565
|
+
isRealtime: false
|
|
21701
22566
|
});
|
|
21702
22567
|
this.currentStep = -1 /* WAITING */;
|
|
21703
22568
|
}
|
|
@@ -22430,6 +23295,12 @@ var Proctoring = class {
|
|
|
22430
23295
|
this.serviceType = "Upload" /* Upload */;
|
|
22431
23296
|
this.onStopSharingScreenCallback = () => {
|
|
22432
23297
|
};
|
|
23298
|
+
/** Callback notificando que o usuário saiu do browser (minimizou/trocou de aba) */
|
|
23299
|
+
this.onVisibilityLostCallback = () => {
|
|
23300
|
+
};
|
|
23301
|
+
/** Callback notificando que o usuário retornou ao browser */
|
|
23302
|
+
this.onVisibilityRestoredCallback = () => {
|
|
23303
|
+
};
|
|
22433
23304
|
this.onLostFocusCallback = () => {
|
|
22434
23305
|
};
|
|
22435
23306
|
this.onLostFocusAlertRecorderCallback = (response) => {
|
|
@@ -22445,13 +23316,16 @@ var Proctoring = class {
|
|
|
22445
23316
|
await this.internalOnRealtimeAlerts(response);
|
|
22446
23317
|
return;
|
|
22447
23318
|
};
|
|
23319
|
+
this.realtimeAlertsToSend = [];
|
|
22448
23320
|
this.onBufferSizeErrorCallback = (cameraStream) => {
|
|
22449
23321
|
return;
|
|
22450
23322
|
};
|
|
22451
23323
|
var _a2;
|
|
22452
23324
|
this.backend = new BackendService({
|
|
22453
23325
|
type: context.type || "prod",
|
|
22454
|
-
token: context.token
|
|
23326
|
+
token: context.token,
|
|
23327
|
+
isRealtime: false
|
|
23328
|
+
// Default false, atualizado no start() via backend.setRealtime()
|
|
22455
23329
|
});
|
|
22456
23330
|
this.repository = new IndexDbSessionRepository("EasyProctorDb", "exams2");
|
|
22457
23331
|
this.repositoryDevices = new IndexDbSessionRepository(
|
|
@@ -22461,6 +23335,12 @@ var Proctoring = class {
|
|
|
22461
23335
|
((_a2 = this.context.credentials) == null ? void 0 : _a2.cpf) && (this.auth = new Auth(this.context.credentials.cpf, this.backend));
|
|
22462
23336
|
this.appChecker = new ExternalCameraChecker(this.context, (response) => this.onRealtimeAlertsCallback(response));
|
|
22463
23337
|
}
|
|
23338
|
+
setOnVisibilityLostCallback(cb) {
|
|
23339
|
+
this.onVisibilityLostCallback = cb;
|
|
23340
|
+
}
|
|
23341
|
+
setOnVisibilityRestoredCallback(cb) {
|
|
23342
|
+
this.onVisibilityRestoredCallback = cb;
|
|
23343
|
+
}
|
|
22464
23344
|
setOnStopSharingScreenCallback(cb) {
|
|
22465
23345
|
this.onStopSharingScreenCallback = async () => {
|
|
22466
23346
|
var _a2, _b, _c2, _d;
|
|
@@ -22540,6 +23420,8 @@ var Proctoring = class {
|
|
|
22540
23420
|
return 25 /* FocusOff */;
|
|
22541
23421
|
case "focus":
|
|
22542
23422
|
return 25 /* FocusOff */;
|
|
23423
|
+
case "error_upload_package":
|
|
23424
|
+
return 44 /* RealtimeOffline */;
|
|
22543
23425
|
default:
|
|
22544
23426
|
return null;
|
|
22545
23427
|
}
|
|
@@ -22580,19 +23462,53 @@ var Proctoring = class {
|
|
|
22580
23462
|
};
|
|
22581
23463
|
await verifyFace(1);
|
|
22582
23464
|
}
|
|
23465
|
+
async sendPendingRealtimeAlerts() {
|
|
23466
|
+
let alertsToSend = [...this.realtimeAlertsToSend];
|
|
23467
|
+
for (const alert of alertsToSend) {
|
|
23468
|
+
try {
|
|
23469
|
+
if (alert.status === "ALERT") {
|
|
23470
|
+
await this.backend.startRealtimeAlert({
|
|
23471
|
+
proctoringId: this.proctoringId,
|
|
23472
|
+
begin: alert.begin,
|
|
23473
|
+
end: alert.end,
|
|
23474
|
+
alert: this.convertRealtimeCategoryToAlertCategory(alert.category)
|
|
23475
|
+
});
|
|
23476
|
+
} else if (alert.status === "OK") {
|
|
23477
|
+
await this.stopRealtimeAlert(alert);
|
|
23478
|
+
}
|
|
23479
|
+
this.realtimeAlertsToSend.splice(this.realtimeAlertsToSend.indexOf(alert), 1);
|
|
23480
|
+
} catch (error) {
|
|
23481
|
+
console.log("error sendPendingRealtimeAlerts", error);
|
|
23482
|
+
this.realtimeAlertsToSend.push(alert);
|
|
23483
|
+
break;
|
|
23484
|
+
}
|
|
23485
|
+
}
|
|
23486
|
+
}
|
|
22583
23487
|
async internalOnRealtimeAlerts(response) {
|
|
22584
|
-
if (this.sessionOptions.proctoringType === "REALTIME" && (response.type === "face_detection_on_stream" || response.type === "person_detection_on_stream" || response.type === "lost_focus" || response.type === "focus")) {
|
|
23488
|
+
if (this.sessionOptions.proctoringType === "REALTIME" && (response.type === "face_detection_on_stream" || response.type === "person_detection_on_stream" || response.type === "lost_focus" || response.type === "focus" || response.type === "error_upload_package")) {
|
|
23489
|
+
await this.sendPendingRealtimeAlerts();
|
|
22585
23490
|
if (response.status === "ALERT") {
|
|
22586
23491
|
if (this.allRecorders.cameraRecorder.stopped) return;
|
|
22587
|
-
|
|
22588
|
-
|
|
22589
|
-
|
|
22590
|
-
|
|
22591
|
-
|
|
22592
|
-
|
|
23492
|
+
try {
|
|
23493
|
+
await this.backend.startRealtimeAlert({
|
|
23494
|
+
proctoringId: this.proctoringId,
|
|
23495
|
+
begin: response.begin,
|
|
23496
|
+
end: response.end,
|
|
23497
|
+
alert: this.convertRealtimeCategoryToAlertCategory(response.category)
|
|
23498
|
+
});
|
|
23499
|
+
} catch (error) {
|
|
23500
|
+
console.log("error startRealtimeAlert " + response.category);
|
|
23501
|
+
console.log("error startRealtimeAlert adicionando na fila", error);
|
|
23502
|
+
this.realtimeAlertsToSend.push(response);
|
|
23503
|
+
}
|
|
22593
23504
|
} else if (response.status === "OK") {
|
|
22594
23505
|
if (this.allRecorders.cameraRecorder.stopped && response.description !== "face_stop") return;
|
|
22595
|
-
|
|
23506
|
+
try {
|
|
23507
|
+
await this.stopRealtimeAlert(response);
|
|
23508
|
+
} catch (error) {
|
|
23509
|
+
console.log("error stopRealtimeAlert adicionando na fila", error);
|
|
23510
|
+
this.realtimeAlertsToSend.push(response);
|
|
23511
|
+
}
|
|
22596
23512
|
}
|
|
22597
23513
|
}
|
|
22598
23514
|
}
|
|
@@ -22600,8 +23516,8 @@ var Proctoring = class {
|
|
|
22600
23516
|
this.setOnLostFocusAlertRecorderCallback();
|
|
22601
23517
|
this.setOnFocusAlertRecorderCallback();
|
|
22602
23518
|
this.onRealtimeAlertsCallback = async (response) => {
|
|
22603
|
-
await this.internalOnRealtimeAlerts(response);
|
|
22604
23519
|
options.data && options.data(response);
|
|
23520
|
+
await this.internalOnRealtimeAlerts(response);
|
|
22605
23521
|
};
|
|
22606
23522
|
}
|
|
22607
23523
|
setOnBufferSizeErrorCallback(cb) {
|
|
@@ -22687,18 +23603,17 @@ var Proctoring = class {
|
|
|
22687
23603
|
if (this.context.token === void 0) {
|
|
22688
23604
|
throw TOKEN_MISSING;
|
|
22689
23605
|
}
|
|
22690
|
-
|
|
22691
|
-
this.extensionEasycatcher = new ExtensionEasyCatcher();
|
|
22692
|
-
}
|
|
22693
|
-
this.extension = new ExtensionEasyProctor();
|
|
23606
|
+
this.extension = new Extension();
|
|
22694
23607
|
this.extension.addEventListener();
|
|
22695
23608
|
const baseURL = this.backend.selectBaseUrl(this.context.type);
|
|
22696
23609
|
const devices = await enumarateDevices();
|
|
22697
23610
|
await this.repositoryDevices.save({ ...devices, id: "devices" });
|
|
22698
23611
|
this.sessionOptions = { ...getDefaultProctoringOptions, ...options };
|
|
22699
23612
|
this.videoOptions = validatePartialVideoOptions(_videoOptions);
|
|
23613
|
+
if (this.sessionOptions.proctoringType === "REALTIME") {
|
|
23614
|
+
this.backend.setRealtime(true);
|
|
23615
|
+
}
|
|
22700
23616
|
await this.initConfig(options.useGeolocation);
|
|
22701
|
-
await this.verifyBrowser();
|
|
22702
23617
|
this.sessionOptions.captureScreen && await this.verifyMultipleMonitors(this.sessionOptions);
|
|
22703
23618
|
try {
|
|
22704
23619
|
if (options == null ? void 0 : options.useSpyScan) {
|
|
@@ -22756,6 +23671,20 @@ var Proctoring = class {
|
|
|
22756
23671
|
} catch (error) {
|
|
22757
23672
|
throw SAFE_BROWSER_API_NOT_FOUND;
|
|
22758
23673
|
}
|
|
23674
|
+
this.allRecorders.cameraRecorder.onVisibilityRestored = () => {
|
|
23675
|
+
console.log("[Proctoring] Usu\xE1rio retornou ao browser.");
|
|
23676
|
+
this.onVisibilityRestoredCallback();
|
|
23677
|
+
};
|
|
23678
|
+
if (this.sessionOptions.proctoringType === "REALTIME" && !isSafeBrowser()) {
|
|
23679
|
+
try {
|
|
23680
|
+
await BackgroundUploadService.recoverPendingUploads(
|
|
23681
|
+
this.backend,
|
|
23682
|
+
this.context.token
|
|
23683
|
+
);
|
|
23684
|
+
} catch (e3) {
|
|
23685
|
+
console.warn("[Proctoring] Erro ao recuperar chunks de sess\xE3o anterior:", e3);
|
|
23686
|
+
}
|
|
23687
|
+
}
|
|
22759
23688
|
try {
|
|
22760
23689
|
console.log("Starting recorders");
|
|
22761
23690
|
await this.recorder.startAll();
|
|
@@ -22835,6 +23764,7 @@ Error: ${error}`
|
|
|
22835
23764
|
this.spyCam && this.spyCam.stopCheckSpyCam();
|
|
22836
23765
|
this.appChecker && await this.appChecker.disconnectWebSocket();
|
|
22837
23766
|
await this.recorder.saveAllOnSession();
|
|
23767
|
+
await this.sendPendingRealtimeAlerts();
|
|
22838
23768
|
await this.repository.save(this.proctoringSession);
|
|
22839
23769
|
let uploader;
|
|
22840
23770
|
let uploaderServices;
|
|
@@ -23057,61 +23987,6 @@ Error: ` + error
|
|
|
23057
23987
|
_screenStream: (_a2 = this.allRecorders.screenRecorder) == null ? void 0 : _a2.screenStream
|
|
23058
23988
|
};
|
|
23059
23989
|
}
|
|
23060
|
-
async startChallenge(templateId) {
|
|
23061
|
-
var _a2;
|
|
23062
|
-
if (!this.sessionOptions.useChallenge) {
|
|
23063
|
-
throw new Error("useChallenge is set as false on start method");
|
|
23064
|
-
}
|
|
23065
|
-
await this.extensionEasycatcher.checkExtensionInstalled().catch((err) => {
|
|
23066
|
-
throw new Error("EasyCatcher Extension is not installed");
|
|
23067
|
-
});
|
|
23068
|
-
this.extensionEasycatcher.start();
|
|
23069
|
-
const start = Date.now() - ((_a2 = this.allRecorders.cameraRecorder.getStartTime()) == null ? void 0 : _a2.getTime()) || 0;
|
|
23070
|
-
await this.backend.startChallenge({
|
|
23071
|
-
proctoringId: this.proctoringId,
|
|
23072
|
-
templateId,
|
|
23073
|
-
start
|
|
23074
|
-
}).then((resp) => {
|
|
23075
|
-
console.log(resp);
|
|
23076
|
-
this.challengeId = resp.id;
|
|
23077
|
-
}).catch((reason) => {
|
|
23078
|
-
trackers.registerError(
|
|
23079
|
-
this.proctoringId,
|
|
23080
|
-
"N\xE3o foi poss\xEDvel iniciar desafio!"
|
|
23081
|
-
);
|
|
23082
|
-
throw reason;
|
|
23083
|
-
});
|
|
23084
|
-
this.isChallengeRunning = true;
|
|
23085
|
-
}
|
|
23086
|
-
async stopChallenge() {
|
|
23087
|
-
var _a2;
|
|
23088
|
-
if (!this.isChallengeRunning) {
|
|
23089
|
-
throw new Error("Challenge not started");
|
|
23090
|
-
}
|
|
23091
|
-
try {
|
|
23092
|
-
const sessionData = await this.extensionEasycatcher.getSessionData();
|
|
23093
|
-
const end = Date.now() - ((_a2 = this.allRecorders.cameraRecorder.getStartTime()) == null ? void 0 : _a2.getTime()) || 0;
|
|
23094
|
-
await this.backend.stopChallenge(
|
|
23095
|
-
this.challengeId,
|
|
23096
|
-
{
|
|
23097
|
-
end,
|
|
23098
|
-
data: sessionData
|
|
23099
|
-
}
|
|
23100
|
-
).catch((reason) => {
|
|
23101
|
-
trackers.registerError(
|
|
23102
|
-
this.proctoringId,
|
|
23103
|
-
"N\xE3o foi poss\xEDvel finalizar o desafio no backend!"
|
|
23104
|
-
);
|
|
23105
|
-
return void 0;
|
|
23106
|
-
});
|
|
23107
|
-
this.isChallengeRunning = false;
|
|
23108
|
-
} catch (error) {
|
|
23109
|
-
trackers.registerError(
|
|
23110
|
-
this.proctoringId,
|
|
23111
|
-
"Erro ao recuperar dados da extens\xE3o: " + error.message
|
|
23112
|
-
);
|
|
23113
|
-
}
|
|
23114
|
-
}
|
|
23115
23990
|
};
|
|
23116
23991
|
|
|
23117
23992
|
// src/proctoring/SignTerm.ts
|
|
@@ -23119,7 +23994,8 @@ var _SignTerm = class _SignTerm {
|
|
|
23119
23994
|
constructor(context) {
|
|
23120
23995
|
this.backend = new BackendService({
|
|
23121
23996
|
type: (context == null ? void 0 : context.type) || "prod",
|
|
23122
|
-
token: context.token
|
|
23997
|
+
token: context.token,
|
|
23998
|
+
isRealtime: false
|
|
23123
23999
|
});
|
|
23124
24000
|
}
|
|
23125
24001
|
async signInTerms() {
|
|
@@ -23339,8 +24215,6 @@ function useProctoring(proctoringOptions, enviromentConfig = "prod") {
|
|
|
23339
24215
|
return originalStart(parameters2, videoOptions);
|
|
23340
24216
|
};
|
|
23341
24217
|
const finish = proctoring.finish.bind(proctoring);
|
|
23342
|
-
const startChallenge = proctoring.startChallenge.bind(proctoring);
|
|
23343
|
-
const stopChallenge = proctoring.stopChallenge.bind(proctoring);
|
|
23344
24218
|
const pause = proctoring.pause.bind(proctoring);
|
|
23345
24219
|
const resume = proctoring.resume.bind(proctoring);
|
|
23346
24220
|
const onFocus = proctoring.setOnFocusCallback.bind(proctoring);
|
|
@@ -23349,6 +24223,8 @@ function useProctoring(proctoringOptions, enviromentConfig = "prod") {
|
|
|
23349
24223
|
const onBufferSizeError = proctoring.setOnBufferSizeErrorCallback.bind(proctoring);
|
|
23350
24224
|
const onStopSharingScreen = proctoring.setOnStopSharingScreenCallback.bind(proctoring);
|
|
23351
24225
|
const onRealtimeAlerts = proctoring.onRealtimeAlerts.bind(proctoring);
|
|
24226
|
+
const onVisibilityLost = proctoring.setOnVisibilityLostCallback.bind(proctoring);
|
|
24227
|
+
const onVisibilityRestored = proctoring.setOnVisibilityRestoredCallback.bind(proctoring);
|
|
23352
24228
|
const signInTerms = signTerm.signInTerms.bind(signTerm);
|
|
23353
24229
|
const checkDevices = checker.checkDevices.bind(checker);
|
|
23354
24230
|
const checkExternalCamera = proctoring.appChecker.checkExternalCamera.bind(proctoring.appChecker);
|
|
@@ -23363,8 +24239,6 @@ function useProctoring(proctoringOptions, enviromentConfig = "prod") {
|
|
|
23363
24239
|
login,
|
|
23364
24240
|
start,
|
|
23365
24241
|
finish,
|
|
23366
|
-
startChallenge,
|
|
23367
|
-
stopChallenge,
|
|
23368
24242
|
onFocus,
|
|
23369
24243
|
onLostFocus,
|
|
23370
24244
|
onChangeDevices: onChangeDevices2,
|
|
@@ -23386,7 +24260,9 @@ function useProctoring(proctoringOptions, enviromentConfig = "prod") {
|
|
|
23386
24260
|
startExternalCameraSession,
|
|
23387
24261
|
takeExternalCameraPicture,
|
|
23388
24262
|
goToExternalCameraPositionStep,
|
|
23389
|
-
startExternalCameraTransmission
|
|
24263
|
+
startExternalCameraTransmission,
|
|
24264
|
+
onVisibilityLost,
|
|
24265
|
+
onVisibilityRestored
|
|
23390
24266
|
};
|
|
23391
24267
|
}
|
|
23392
24268
|
|