@jaak.ai/stamps 2.0.0 → 2.1.0-dev.2
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 +11 -1
- package/dist/cjs/jaak-stamps-webcomponent.cjs.js +1 -1
- package/dist/cjs/jaak-stamps.cjs.entry.js +759 -623
- package/dist/cjs/jaak-stamps.cjs.entry.js.map +1 -1
- package/dist/cjs/jaak-stamps.entry.cjs.js.map +1 -1
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/collection/components/my-component/my-component.css +178 -11
- package/dist/collection/components/my-component/my-component.js +566 -250
- package/dist/collection/components/my-component/my-component.js.map +1 -1
- package/dist/collection/services/CameraService.js +156 -344
- package/dist/collection/services/CameraService.js.map +1 -1
- package/dist/collection/services/DetectionService.js +68 -38
- package/dist/collection/services/DetectionService.js.map +1 -1
- package/dist/collection/services/EventBusService.js +1 -1
- package/dist/collection/services/EventBusService.js.map +1 -1
- package/dist/collection/services/ServiceContainer.js +4 -13
- package/dist/collection/services/ServiceContainer.js.map +1 -1
- package/dist/collection/services/interfaces/ICameraService.js.map +1 -1
- package/dist/collection/types/component-types.js.map +1 -1
- package/dist/components/jaak-stamps.js +763 -626
- package/dist/components/jaak-stamps.js.map +1 -1
- package/dist/esm/jaak-stamps-webcomponent.js +1 -1
- package/dist/esm/jaak-stamps.entry.js +759 -623
- package/dist/esm/jaak-stamps.entry.js.map +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
- package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
- package/dist/jaak-stamps-webcomponent/p-41e88688.entry.js +2 -0
- package/dist/jaak-stamps-webcomponent/p-41e88688.entry.js.map +1 -0
- package/dist/types/components/my-component/my-component.d.ts +68 -17
- package/dist/types/components.d.ts +14 -8
- package/dist/types/services/CameraService.d.ts +12 -14
- package/dist/types/services/DetectionService.d.ts +10 -3
- package/dist/types/services/ServiceContainer.d.ts +1 -2
- package/dist/types/services/interfaces/ICameraService.d.ts +3 -12
- package/dist/types/types/component-types.d.ts +3 -0
- package/package.json +2 -2
- package/dist/collection/services/LoggerService.js +0 -40
- package/dist/collection/services/LoggerService.js.map +0 -1
- package/dist/collection/services/interfaces/ILogger.js +0 -2
- package/dist/collection/services/interfaces/ILogger.js.map +0 -1
- package/dist/jaak-stamps-webcomponent/p-2264b5b4.entry.js +0 -2
- package/dist/jaak-stamps-webcomponent/p-2264b5b4.entry.js.map +0 -1
- package/dist/types/services/LoggerService.d.ts +0 -12
- package/dist/types/services/interfaces/ILogger.d.ts +0 -8
|
@@ -2,46 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
var index = require('./index-BfhtOB0D.js');
|
|
4
4
|
|
|
5
|
-
class LoggerService {
|
|
6
|
-
debugMode;
|
|
7
|
-
constructor(debug = false) {
|
|
8
|
-
this.debugMode = debug;
|
|
9
|
-
}
|
|
10
|
-
setDebugMode(debug) {
|
|
11
|
-
this.debugMode = debug;
|
|
12
|
-
}
|
|
13
|
-
info(...args) {
|
|
14
|
-
if (this.debugMode) {
|
|
15
|
-
console.log(`[JAAK-STAMPS] [INFO] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
warn(...args) {
|
|
19
|
-
if (this.debugMode) {
|
|
20
|
-
console.warn(`[JAAK-STAMPS] [WARN] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
error(...args) {
|
|
24
|
-
if (this.debugMode) {
|
|
25
|
-
console.error(`[JAAK-STAMPS] [ERROR] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
debug(...args) {
|
|
29
|
-
if (this.debugMode) {
|
|
30
|
-
console.debug(`[JAAK-STAMPS] [DEBUG] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
state(state, data) {
|
|
34
|
-
if (this.debugMode) {
|
|
35
|
-
console.log(`[JAAK-STAMPS] [STATE] [${new Date().toLocaleTimeString()}] ${state}`, data || '');
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
performance(operation, duration) {
|
|
39
|
-
if (this.debugMode) {
|
|
40
|
-
console.log(`[JAAK-STAMPS] [PERF] [${new Date().toLocaleTimeString()}] ${operation}: ${duration}ms`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
5
|
class EventBusService {
|
|
46
6
|
events = new Map();
|
|
47
7
|
on(event, callback) {
|
|
@@ -67,7 +27,7 @@ class EventBusService {
|
|
|
67
27
|
callback(data);
|
|
68
28
|
}
|
|
69
29
|
catch (error) {
|
|
70
|
-
|
|
30
|
+
// Silently handle callback errors
|
|
71
31
|
}
|
|
72
32
|
});
|
|
73
33
|
}
|
|
@@ -194,16 +154,20 @@ class StateManagerService {
|
|
|
194
154
|
}
|
|
195
155
|
|
|
196
156
|
class CameraService {
|
|
197
|
-
logger;
|
|
198
157
|
eventBus;
|
|
199
158
|
availableCameras = [];
|
|
200
159
|
selectedCameraId = null;
|
|
201
160
|
deviceType = 'desktop';
|
|
202
161
|
preferredCameraFacing = null;
|
|
203
162
|
preferredCamera = 'auto';
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
163
|
+
// Optimization: Static caches for performance
|
|
164
|
+
static cameraCapabilitiesCache = new Map();
|
|
165
|
+
static deviceEnumerationCache = null;
|
|
166
|
+
static CACHE_DURATION = 30000; // 30 seconds
|
|
167
|
+
// Stream management for optimization
|
|
168
|
+
currentStreamPromise;
|
|
169
|
+
lastStreamConstraints;
|
|
170
|
+
constructor(eventBus, preferredCamera = 'auto') {
|
|
207
171
|
this.eventBus = eventBus;
|
|
208
172
|
this.preferredCamera = preferredCamera;
|
|
209
173
|
}
|
|
@@ -220,18 +184,21 @@ class CameraService {
|
|
|
220
184
|
else {
|
|
221
185
|
this.deviceType = 'desktop';
|
|
222
186
|
}
|
|
223
|
-
this.logger.state('DISPOSITIVO_DETECTADO', {
|
|
224
|
-
deviceType: this.deviceType,
|
|
225
|
-
userAgent: navigator.userAgent,
|
|
226
|
-
screenDimensions: { width: window.innerWidth, height: window.innerHeight }
|
|
227
|
-
});
|
|
228
187
|
return this.deviceType;
|
|
229
188
|
}
|
|
230
189
|
async enumerateDevices() {
|
|
231
190
|
try {
|
|
191
|
+
// Check cache first for optimization
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
if (CameraService.deviceEnumerationCache &&
|
|
194
|
+
(now - CameraService.deviceEnumerationCache.timestamp) < CameraService.CACHE_DURATION) {
|
|
195
|
+
this.availableCameras = CameraService.deviceEnumerationCache.devices;
|
|
196
|
+
await this.setInitialCameraPreference();
|
|
197
|
+
this.eventBus.emit('camera-changed', this.selectedCameraId);
|
|
198
|
+
return this.availableCameras;
|
|
199
|
+
}
|
|
232
200
|
const permissionStatus = await this.checkCameraPermission();
|
|
233
201
|
if (permissionStatus === 'denied') {
|
|
234
|
-
this.logger.error('Permiso de cámara denegado por el usuario');
|
|
235
202
|
return [];
|
|
236
203
|
}
|
|
237
204
|
if (permissionStatus === 'prompt') {
|
|
@@ -240,24 +207,16 @@ class CameraService {
|
|
|
240
207
|
}
|
|
241
208
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
242
209
|
this.availableCameras = devices.filter(device => device.kind === 'videoinput');
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
});
|
|
250
|
-
this.setInitialCameraPreference();
|
|
251
|
-
this.logger.state('PREFERENCIA_INICIAL_APLICADA', {
|
|
252
|
-
selectedCameraId: this.selectedCameraId,
|
|
253
|
-
preferredCameraFacing: this.preferredCameraFacing,
|
|
254
|
-
preferredCamera: this.preferredCamera
|
|
255
|
-
});
|
|
210
|
+
// Cache the results for optimization
|
|
211
|
+
CameraService.deviceEnumerationCache = {
|
|
212
|
+
devices: this.availableCameras,
|
|
213
|
+
timestamp: now
|
|
214
|
+
};
|
|
215
|
+
await this.setInitialCameraPreference();
|
|
256
216
|
this.eventBus.emit('camera-changed', this.selectedCameraId);
|
|
257
217
|
return this.availableCameras;
|
|
258
218
|
}
|
|
259
219
|
catch (error) {
|
|
260
|
-
this.logger.error('Error al enumerar cámaras disponibles:', error);
|
|
261
220
|
this.handleCameraPermissionError(error);
|
|
262
221
|
return [];
|
|
263
222
|
}
|
|
@@ -278,8 +237,6 @@ class CameraService {
|
|
|
278
237
|
}
|
|
279
238
|
this.selectedCameraId = cameraId;
|
|
280
239
|
this.updatePreferredFacing(camera);
|
|
281
|
-
this.isManuallySelected = true; // Mark as manually selected
|
|
282
|
-
this.savePreference();
|
|
283
240
|
this.eventBus.emit('camera-changed', cameraId);
|
|
284
241
|
}
|
|
285
242
|
getPreferredFacing() {
|
|
@@ -288,14 +245,20 @@ class CameraService {
|
|
|
288
245
|
async setupCamera(constraints) {
|
|
289
246
|
try {
|
|
290
247
|
const finalConstraints = constraints || await this.getMaxResolution();
|
|
291
|
-
|
|
248
|
+
// Optimization: Check if we can reuse existing stream with same constraints
|
|
249
|
+
if (this.currentStreamPromise && this.constraintsEqual(finalConstraints, this.lastStreamConstraints)) {
|
|
250
|
+
return await this.currentStreamPromise;
|
|
251
|
+
}
|
|
252
|
+
// Create new stream promise for lazy loading
|
|
253
|
+
this.currentStreamPromise = navigator.mediaDevices.getUserMedia({
|
|
292
254
|
video: finalConstraints,
|
|
293
255
|
audio: false
|
|
294
256
|
});
|
|
257
|
+
this.lastStreamConstraints = finalConstraints;
|
|
258
|
+
const stream = await this.currentStreamPromise;
|
|
295
259
|
return stream;
|
|
296
260
|
}
|
|
297
261
|
catch (error) {
|
|
298
|
-
this.logger.error('Error en setupCamera, intentando con restricciones básicas:', error);
|
|
299
262
|
// Fallback to basic constraints if getMaxResolution fails
|
|
300
263
|
const basicConstraints = {
|
|
301
264
|
width: { ideal: 1280 },
|
|
@@ -305,65 +268,21 @@ class CameraService {
|
|
|
305
268
|
if (this.deviceType !== 'desktop' && this.preferredCameraFacing) {
|
|
306
269
|
basicConstraints.facingMode = this.preferredCameraFacing;
|
|
307
270
|
}
|
|
308
|
-
this.
|
|
309
|
-
return await navigator.mediaDevices.getUserMedia({
|
|
271
|
+
this.currentStreamPromise = navigator.mediaDevices.getUserMedia({
|
|
310
272
|
video: basicConstraints,
|
|
311
273
|
audio: false
|
|
312
274
|
});
|
|
275
|
+
this.lastStreamConstraints = basicConstraints;
|
|
276
|
+
return await this.currentStreamPromise;
|
|
313
277
|
}
|
|
314
278
|
}
|
|
315
279
|
async switchCamera(cameraId) {
|
|
316
280
|
const selectedCamera = this.availableCameras.find(cam => cam.deviceId === cameraId);
|
|
317
281
|
if (!selectedCamera) {
|
|
318
|
-
this.logger.warn('Cámara seleccionada no encontrada, re-enumerando dispositivos...');
|
|
319
282
|
await this.enumerateDevices();
|
|
320
283
|
return;
|
|
321
284
|
}
|
|
322
285
|
await this.setSelectedCamera(cameraId);
|
|
323
|
-
this.logger.state('CAMARA_CAMBIADA', {
|
|
324
|
-
label: selectedCamera.label,
|
|
325
|
-
deviceId: selectedCamera.deviceId,
|
|
326
|
-
isManuallySelected: this.isManuallySelected
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
async flipToNextCamera() {
|
|
330
|
-
if (!this.isMultipleCamerasAvailable())
|
|
331
|
-
return;
|
|
332
|
-
const currentIndex = this.availableCameras.findIndex(camera => camera.deviceId === this.selectedCameraId);
|
|
333
|
-
const nextIndex = (currentIndex + 1) % this.availableCameras.length;
|
|
334
|
-
const nextCamera = this.availableCameras[nextIndex];
|
|
335
|
-
await this.switchCamera(nextCamera.deviceId);
|
|
336
|
-
}
|
|
337
|
-
savePreference() {
|
|
338
|
-
try {
|
|
339
|
-
const preference = {
|
|
340
|
-
cameraId: this.selectedCameraId,
|
|
341
|
-
facing: this.preferredCameraFacing,
|
|
342
|
-
timestamp: Date.now()
|
|
343
|
-
};
|
|
344
|
-
localStorage.setItem('jaak-stamps-camera-preference', JSON.stringify(preference));
|
|
345
|
-
this.logger.state('PREFERENCIA_CAMARA_GUARDADA', preference);
|
|
346
|
-
}
|
|
347
|
-
catch (error) {
|
|
348
|
-
this.logger.warn('Error al guardar preferencia de cámara:', error);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
loadPreference() {
|
|
352
|
-
try {
|
|
353
|
-
const saved = localStorage.getItem('jaak-stamps-camera-preference');
|
|
354
|
-
if (saved) {
|
|
355
|
-
const preference = JSON.parse(saved);
|
|
356
|
-
const isStillAvailable = this.availableCameras.some(camera => camera.deviceId === preference.cameraId);
|
|
357
|
-
if (isStillAvailable) {
|
|
358
|
-
this.selectedCameraId = preference.cameraId;
|
|
359
|
-
this.preferredCameraFacing = preference.facing;
|
|
360
|
-
this.logger.state('PREFERENCIA_CAMARA_CARGADA', preference);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
catch (error) {
|
|
365
|
-
this.logger.warn('Error al cargar preferencia de cámara:', error);
|
|
366
|
-
}
|
|
367
286
|
}
|
|
368
287
|
isRearCamera(stream) {
|
|
369
288
|
const videoTrack = stream.getVideoTracks()[0];
|
|
@@ -372,13 +291,18 @@ class CameraService {
|
|
|
372
291
|
const settings = videoTrack.getSettings();
|
|
373
292
|
return settings.facingMode === 'environment';
|
|
374
293
|
}
|
|
375
|
-
getCameraInfo() {
|
|
376
|
-
|
|
377
|
-
|
|
294
|
+
async getCameraInfo() {
|
|
295
|
+
const availableCamerasWithAutofocus = await Promise.all(this.availableCameras.map(async (camera) => {
|
|
296
|
+
const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
|
|
297
|
+
return {
|
|
378
298
|
id: camera.deviceId,
|
|
379
299
|
label: camera.label || 'Unknown Camera',
|
|
380
|
-
selected: camera.deviceId === this.selectedCameraId
|
|
381
|
-
|
|
300
|
+
selected: camera.deviceId === this.selectedCameraId,
|
|
301
|
+
hasAutofocus
|
|
302
|
+
};
|
|
303
|
+
}));
|
|
304
|
+
return {
|
|
305
|
+
availableCameras: availableCamerasWithAutofocus,
|
|
382
306
|
selectedCameraId: this.selectedCameraId,
|
|
383
307
|
deviceType: this.deviceType,
|
|
384
308
|
isMultipleCamerasAvailable: this.isMultipleCamerasAvailable(),
|
|
@@ -394,7 +318,6 @@ class CameraService {
|
|
|
394
318
|
return permission.state;
|
|
395
319
|
}
|
|
396
320
|
catch (error) {
|
|
397
|
-
this.logger.warn('No se pudo verificar permisos de cámara:', error);
|
|
398
321
|
return 'prompt';
|
|
399
322
|
}
|
|
400
323
|
}
|
|
@@ -402,35 +325,48 @@ class CameraService {
|
|
|
402
325
|
this.availableCameras = [];
|
|
403
326
|
this.eventBus.emit('error', new Error(`Camera permission error: ${error.message}`));
|
|
404
327
|
}
|
|
405
|
-
setInitialCameraPreference() {
|
|
328
|
+
async setInitialCameraPreference() {
|
|
406
329
|
if (this.availableCameras.length === 0)
|
|
407
330
|
return;
|
|
408
|
-
// Clear any existing localStorage preferences to ensure fresh start
|
|
409
|
-
localStorage.removeItem('jaak-stamps-camera-preference');
|
|
410
|
-
this.logger.state('APLICANDO_PREFERENCIA_INICIAL', {
|
|
411
|
-
preferredCamera: this.preferredCamera,
|
|
412
|
-
availableCamerasCount: this.availableCameras.length
|
|
413
|
-
});
|
|
414
|
-
this.isManuallySelected = false; // Reset manual selection flag
|
|
415
331
|
if (this.preferredCamera === 'front') {
|
|
416
|
-
this.selectFrontCamera();
|
|
332
|
+
await this.selectFrontCamera();
|
|
417
333
|
}
|
|
418
334
|
else if (this.preferredCamera === 'back') {
|
|
419
|
-
this.selectBackCamera();
|
|
335
|
+
await this.selectBackCamera();
|
|
420
336
|
}
|
|
421
337
|
else {
|
|
422
|
-
this.selectAutoCamera();
|
|
338
|
+
await this.selectAutoCamera();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async checkCameraAutofocus(deviceId) {
|
|
342
|
+
try {
|
|
343
|
+
// Check cache first for optimization
|
|
344
|
+
if (CameraService.cameraCapabilitiesCache.has(deviceId)) {
|
|
345
|
+
const cached = CameraService.cameraCapabilitiesCache.get(deviceId);
|
|
346
|
+
return typeof cached === 'boolean' ? cached : false;
|
|
347
|
+
}
|
|
348
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
349
|
+
video: { deviceId: { exact: deviceId } }
|
|
350
|
+
});
|
|
351
|
+
const videoTrack = stream.getVideoTracks()[0];
|
|
352
|
+
const capabilities = videoTrack.getCapabilities();
|
|
353
|
+
stream.getTracks().forEach(track => track.stop());
|
|
354
|
+
const hasFocusMode = capabilities.focusMode && Array.isArray(capabilities.focusMode) && capabilities.focusMode.length > 0;
|
|
355
|
+
const hasAutofocus = hasFocusMode && (capabilities.focusMode.includes('continuous') ||
|
|
356
|
+
capabilities.focusMode.includes('auto'));
|
|
357
|
+
// Cache the result for future use
|
|
358
|
+
CameraService.cameraCapabilitiesCache.set(deviceId, hasAutofocus);
|
|
359
|
+
return hasAutofocus;
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
// Cache negative result to avoid repeated failures
|
|
363
|
+
CameraService.cameraCapabilitiesCache.set(deviceId, false);
|
|
364
|
+
return false;
|
|
423
365
|
}
|
|
424
366
|
}
|
|
425
|
-
selectFrontCamera() {
|
|
367
|
+
async selectFrontCamera() {
|
|
426
368
|
this.preferredCameraFacing = 'user';
|
|
427
|
-
this.
|
|
428
|
-
availableCameras: this.availableCameras.map(cam => ({
|
|
429
|
-
id: cam.deviceId,
|
|
430
|
-
label: cam.label
|
|
431
|
-
}))
|
|
432
|
-
});
|
|
433
|
-
const frontCamera = this.availableCameras.find(camera => {
|
|
369
|
+
const frontCameras = this.availableCameras.filter(camera => {
|
|
434
370
|
const label = camera.label.toLowerCase();
|
|
435
371
|
return label.includes('front') ||
|
|
436
372
|
label.includes('user') ||
|
|
@@ -438,24 +374,24 @@ class CameraService {
|
|
|
438
374
|
label.includes('frontal') ||
|
|
439
375
|
(!label.includes('back') && !label.includes('rear') && !label.includes('environment'));
|
|
440
376
|
});
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
377
|
+
let selectedCamera = null;
|
|
378
|
+
if (frontCameras.length > 0) {
|
|
379
|
+
for (const camera of frontCameras) {
|
|
380
|
+
const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
|
|
381
|
+
if (hasAutofocus) {
|
|
382
|
+
selectedCamera = camera;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (!selectedCamera) {
|
|
387
|
+
selectedCamera = frontCameras[0];
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
this.selectedCameraId = selectedCamera ? selectedCamera.deviceId : this.availableCameras[0].deviceId;
|
|
448
391
|
}
|
|
449
|
-
selectBackCamera() {
|
|
392
|
+
async selectBackCamera() {
|
|
450
393
|
this.preferredCameraFacing = 'environment';
|
|
451
|
-
this.
|
|
452
|
-
availableCameras: this.availableCameras.map(cam => ({
|
|
453
|
-
id: cam.deviceId,
|
|
454
|
-
label: cam.label
|
|
455
|
-
}))
|
|
456
|
-
});
|
|
457
|
-
// Try to find rear camera with multiple detection methods
|
|
458
|
-
let backCamera = this.availableCameras.find(camera => {
|
|
394
|
+
const backCameras = this.availableCameras.filter(camera => {
|
|
459
395
|
const label = camera.label.toLowerCase();
|
|
460
396
|
return label.includes('back') ||
|
|
461
397
|
label.includes('rear') ||
|
|
@@ -463,34 +399,33 @@ class CameraService {
|
|
|
463
399
|
label.includes('trasera') ||
|
|
464
400
|
label.includes('posterior');
|
|
465
401
|
});
|
|
466
|
-
|
|
467
|
-
if (
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
402
|
+
let selectedCamera = null;
|
|
403
|
+
if (backCameras.length > 0) {
|
|
404
|
+
for (const camera of backCameras) {
|
|
405
|
+
const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
|
|
406
|
+
if (hasAutofocus) {
|
|
407
|
+
selectedCamera = camera;
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (!selectedCamera) {
|
|
412
|
+
selectedCamera = backCameras[0];
|
|
413
|
+
}
|
|
473
414
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
415
|
+
else if (this.availableCameras.length > 1) {
|
|
416
|
+
const lastCamera = this.availableCameras[this.availableCameras.length - 1];
|
|
417
|
+
const hasAutofocus = await this.checkCameraAutofocus(lastCamera.deviceId);
|
|
418
|
+
selectedCamera = hasAutofocus ? lastCamera : this.availableCameras[this.availableCameras.length - 1];
|
|
419
|
+
}
|
|
420
|
+
this.selectedCameraId = selectedCamera ? selectedCamera.deviceId : this.availableCameras[0].deviceId;
|
|
480
421
|
}
|
|
481
|
-
selectAutoCamera() {
|
|
422
|
+
async selectAutoCamera() {
|
|
482
423
|
if (this.deviceType === 'mobile' || this.deviceType === 'tablet') {
|
|
483
|
-
this.selectBackCamera();
|
|
484
|
-
this.logger.state('CAMARA_AUTO_SELECCIONADA_MOBILE', { type: 'rear' });
|
|
424
|
+
await this.selectBackCamera();
|
|
485
425
|
}
|
|
486
426
|
else {
|
|
487
427
|
// For desktop, prefer front camera (usually built-in webcam)
|
|
488
|
-
this.selectFrontCamera();
|
|
489
|
-
this.logger.state('CAMARA_AUTO_SELECCIONADA_DESKTOP', {
|
|
490
|
-
type: 'front',
|
|
491
|
-
label: this.availableCameras[0].label,
|
|
492
|
-
deviceId: this.selectedCameraId
|
|
493
|
-
});
|
|
428
|
+
await this.selectFrontCamera();
|
|
494
429
|
}
|
|
495
430
|
}
|
|
496
431
|
updatePreferredFacing(camera) {
|
|
@@ -506,40 +441,19 @@ class CameraService {
|
|
|
506
441
|
await this.detectDeviceType();
|
|
507
442
|
}
|
|
508
443
|
const videoConstraints = {};
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
preferredCameraFacing: this.preferredCameraFacing,
|
|
512
|
-
preferredCamera: this.preferredCamera,
|
|
513
|
-
deviceType: this.deviceType,
|
|
514
|
-
isManuallySelected: this.isManuallySelected
|
|
515
|
-
});
|
|
516
|
-
if (this.isManuallySelected && this.selectedCameraId) {
|
|
517
|
-
// When manually selected, use deviceId for exact camera selection
|
|
444
|
+
// Priority 1: Always use deviceId if available (ensures specific camera with autofocus)
|
|
445
|
+
if (this.selectedCameraId) {
|
|
518
446
|
videoConstraints.deviceId = { exact: this.selectedCameraId };
|
|
519
|
-
this.logger.state('USANDO_CAMARA_MANUAL', {
|
|
520
|
-
deviceId: this.selectedCameraId
|
|
521
|
-
});
|
|
522
447
|
}
|
|
448
|
+
// Priority 2: Use facingMode as fallback when no specific camera is selected
|
|
523
449
|
else if (this.preferredCameraFacing) {
|
|
524
|
-
// Use facingMode for initial preference-based selection
|
|
525
|
-
// Use 'ideal' instead of 'exact' to avoid OverconstrainedError on desktop
|
|
526
450
|
const facingConstraint = this.deviceType === 'desktop'
|
|
527
451
|
? { ideal: this.preferredCameraFacing }
|
|
528
452
|
: { exact: this.preferredCameraFacing };
|
|
529
453
|
videoConstraints.facingMode = facingConstraint;
|
|
530
|
-
this.logger.state('USANDO_FACING_CONSTRAINT', {
|
|
531
|
-
facingMode: this.preferredCameraFacing,
|
|
532
|
-
constraintType: this.deviceType === 'desktop' ? 'ideal' : 'exact'
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
else if (this.selectedCameraId) {
|
|
536
|
-
videoConstraints.deviceId = { exact: this.selectedCameraId };
|
|
537
|
-
this.logger.state('USANDO_CAMARA_SELECCIONADA', {
|
|
538
|
-
deviceId: this.selectedCameraId
|
|
539
|
-
});
|
|
540
454
|
}
|
|
455
|
+
// Priority 3: Fallback to preferredCamera setting
|
|
541
456
|
else {
|
|
542
|
-
// Use preferredCamera setting to determine default facing mode
|
|
543
457
|
if (this.preferredCamera === 'front') {
|
|
544
458
|
videoConstraints.facingMode = "user";
|
|
545
459
|
}
|
|
@@ -547,24 +461,25 @@ class CameraService {
|
|
|
547
461
|
videoConstraints.facingMode = "environment";
|
|
548
462
|
}
|
|
549
463
|
else {
|
|
550
|
-
// Auto mode: use environment for mobile/tablet, user for desktop
|
|
551
464
|
videoConstraints.facingMode = (this.deviceType === 'mobile' || this.deviceType === 'tablet') ? "environment" : "user";
|
|
552
465
|
}
|
|
553
|
-
this.logger.state('USANDO_PREFERENCIA_CONFIGURADA', {
|
|
554
|
-
facingMode: videoConstraints.facingMode,
|
|
555
|
-
preferredCamera: this.preferredCamera,
|
|
556
|
-
deviceType: this.deviceType
|
|
557
|
-
});
|
|
558
466
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
467
|
+
// Optimization: Use cached capabilities if available
|
|
468
|
+
let capabilities;
|
|
469
|
+
if (this.selectedCameraId && CameraService.cameraCapabilitiesCache.has(this.selectedCameraId + '_caps')) {
|
|
470
|
+
capabilities = JSON.parse(CameraService.cameraCapabilitiesCache.get(this.selectedCameraId + '_caps'));
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
const tempStream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints });
|
|
474
|
+
const videoTrack = tempStream.getVideoTracks()[0];
|
|
475
|
+
capabilities = videoTrack.getCapabilities();
|
|
476
|
+
tempStream.getTracks().forEach(track => track.stop());
|
|
477
|
+
// Cache capabilities for future use
|
|
478
|
+
if (this.selectedCameraId) {
|
|
479
|
+
CameraService.cameraCapabilitiesCache.set(this.selectedCameraId + '_caps', JSON.stringify(capabilities));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
565
482
|
const constraints = { ...videoConstraints };
|
|
566
|
-
// Enhanced focus and image quality settings
|
|
567
|
-
this.applyAdvancedCameraSettings(constraints, capabilities);
|
|
568
483
|
if (capabilities.width && capabilities.height) {
|
|
569
484
|
const maxWidth = Math.min(capabilities.width.max, 1920);
|
|
570
485
|
const maxHeight = Math.min(capabilities.height.max, 1080);
|
|
@@ -578,15 +493,20 @@ class CameraService {
|
|
|
578
493
|
constraints.height = { ideal: maxHeight };
|
|
579
494
|
}
|
|
580
495
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
496
|
+
// Configure autofocus if camera has the capability
|
|
497
|
+
const capabilitiesAny = capabilities;
|
|
498
|
+
if (capabilitiesAny.focusMode && Array.isArray(capabilitiesAny.focusMode)) {
|
|
499
|
+
if (capabilitiesAny.focusMode.includes('continuous')) {
|
|
500
|
+
constraints.focusMode = 'continuous';
|
|
501
|
+
}
|
|
502
|
+
else if (capabilitiesAny.focusMode.includes('auto')) {
|
|
503
|
+
constraints.focusMode = 'auto';
|
|
504
|
+
}
|
|
505
|
+
}
|
|
585
506
|
return constraints;
|
|
586
507
|
}
|
|
587
508
|
catch (err) {
|
|
588
|
-
|
|
589
|
-
// Ensure device type is detected before determining constraints
|
|
509
|
+
// Fallback logic with minimal temporary streams
|
|
590
510
|
if (!this.deviceType || this.deviceType === 'desktop') {
|
|
591
511
|
await this.detectDeviceType();
|
|
592
512
|
}
|
|
@@ -595,25 +515,16 @@ class CameraService {
|
|
|
595
515
|
width: { ideal: isTablet ? 1280 : 1920 },
|
|
596
516
|
height: { ideal: isTablet ? 720 : 1080 }
|
|
597
517
|
};
|
|
598
|
-
|
|
599
|
-
this.applyBasicFocusSettings(fallbackConstraints);
|
|
600
|
-
if (this.isManuallySelected && this.selectedCameraId) {
|
|
601
|
-
// When manually selected, use deviceId for exact camera selection
|
|
518
|
+
if (this.selectedCameraId) {
|
|
602
519
|
fallbackConstraints.deviceId = { exact: this.selectedCameraId };
|
|
603
520
|
}
|
|
604
521
|
else if (this.preferredCameraFacing) {
|
|
605
|
-
// Use facingMode for initial preference-based selection
|
|
606
|
-
// Use 'ideal' instead of 'exact' to avoid OverconstrainedError on desktop
|
|
607
522
|
const facingConstraint = this.deviceType === 'desktop'
|
|
608
523
|
? { ideal: this.preferredCameraFacing }
|
|
609
524
|
: { exact: this.preferredCameraFacing };
|
|
610
525
|
fallbackConstraints.facingMode = facingConstraint;
|
|
611
526
|
}
|
|
612
|
-
else if (this.selectedCameraId) {
|
|
613
|
-
fallbackConstraints.deviceId = { exact: this.selectedCameraId };
|
|
614
|
-
}
|
|
615
527
|
else {
|
|
616
|
-
// Use preferredCamera setting to determine default facing mode
|
|
617
528
|
if (this.preferredCamera === 'front') {
|
|
618
529
|
fallbackConstraints.facingMode = "user";
|
|
619
530
|
}
|
|
@@ -621,165 +532,26 @@ class CameraService {
|
|
|
621
532
|
fallbackConstraints.facingMode = "environment";
|
|
622
533
|
}
|
|
623
534
|
else {
|
|
624
|
-
// Auto mode: use environment for mobile/tablet, user for desktop
|
|
625
535
|
fallbackConstraints.facingMode = (this.deviceType === 'mobile' || this.deviceType === 'tablet') ? "environment" : "user";
|
|
626
536
|
}
|
|
627
537
|
}
|
|
628
538
|
return fallbackConstraints;
|
|
629
539
|
}
|
|
630
540
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if (capabilities.frameRate) {
|
|
635
|
-
constraints.frameRate = {
|
|
636
|
-
ideal: 30,
|
|
637
|
-
min: 15,
|
|
638
|
-
max: 60
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
|
-
// Width and height constraints for optimal resolution
|
|
642
|
-
if (capabilities.width && capabilities.height) {
|
|
643
|
-
constraints.width = {
|
|
644
|
-
ideal: Math.min(1920, capabilities.width.max || 1920),
|
|
645
|
-
min: 640
|
|
646
|
-
};
|
|
647
|
-
constraints.height = {
|
|
648
|
-
ideal: Math.min(1080, capabilities.height.max || 1080),
|
|
649
|
-
min: 480
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
// Apply enhanced constraints through advanced array for better browser compatibility
|
|
653
|
-
const advancedConstraints = [];
|
|
654
|
-
// Try to set optimal settings through advanced constraints
|
|
655
|
-
const advanced = {};
|
|
656
|
-
// Focus mode - use 'any' type to bypass TypeScript limitations
|
|
657
|
-
if (capabilities.focusMode) {
|
|
658
|
-
if (capabilities.focusMode.includes('continuous')) {
|
|
659
|
-
advanced.focusMode = 'continuous';
|
|
660
|
-
}
|
|
661
|
-
else if (capabilities.focusMode.includes('single-shot')) {
|
|
662
|
-
advanced.focusMode = 'single-shot';
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
// Focus distance for close objects
|
|
666
|
-
if (capabilities.focusDistance) {
|
|
667
|
-
advanced.focusDistance = {
|
|
668
|
-
ideal: 0.4, // 40cm - optimal for document capture
|
|
669
|
-
min: 0.2,
|
|
670
|
-
max: 1.0
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
// Zoom control
|
|
674
|
-
if (capabilities.zoom) {
|
|
675
|
-
advanced.zoom = {
|
|
676
|
-
ideal: 1.0,
|
|
677
|
-
min: capabilities.zoom.min,
|
|
678
|
-
max: Math.min(capabilities.zoom.max, 3.0)
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
// Add advanced constraints if any were set
|
|
682
|
-
if (Object.keys(advanced).length > 0) {
|
|
683
|
-
advancedConstraints.push(advanced);
|
|
684
|
-
}
|
|
685
|
-
if (advancedConstraints.length > 0) {
|
|
686
|
-
constraints.advanced = advancedConstraints;
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
applyBasicFocusSettings(constraints) {
|
|
690
|
-
// Apply basic focus settings when capabilities are not available
|
|
691
|
-
// Use advanced constraints for non-standard properties
|
|
692
|
-
const advanced = {
|
|
693
|
-
focusMode: 'continuous',
|
|
694
|
-
exposureMode: 'continuous',
|
|
695
|
-
whiteBalanceMode: 'continuous'
|
|
696
|
-
};
|
|
697
|
-
if (!constraints.advanced) {
|
|
698
|
-
constraints.advanced = [];
|
|
699
|
-
}
|
|
700
|
-
constraints.advanced.push(advanced);
|
|
701
|
-
constraints.frameRate = { ideal: 30 };
|
|
702
|
-
}
|
|
703
|
-
getCapabilitiesSummary(capabilities) {
|
|
704
|
-
return {
|
|
705
|
-
hasAutoFocus: !!capabilities.focusMode,
|
|
706
|
-
hasFocusDistance: !!capabilities.focusDistance,
|
|
707
|
-
hasExposureControl: !!capabilities.exposureMode,
|
|
708
|
-
hasWhiteBalance: !!capabilities.whiteBalanceMode,
|
|
709
|
-
hasZoom: !!capabilities.zoom,
|
|
710
|
-
hasTorch: capabilities.torch !== undefined,
|
|
711
|
-
hasImageControls: !!(capabilities.brightness || capabilities.contrast || capabilities.saturation || capabilities.sharpness),
|
|
712
|
-
resolutionRange: capabilities.width && capabilities.height ?
|
|
713
|
-
`${capabilities.width.min}x${capabilities.height.min} - ${capabilities.width.max}x${capabilities.height.max}` : 'unknown'
|
|
714
|
-
};
|
|
715
|
-
}
|
|
716
|
-
// New method to enable torch/flash for better illumination
|
|
717
|
-
async setTorchEnabled(enabled, stream) {
|
|
718
|
-
try {
|
|
719
|
-
if (!stream) {
|
|
720
|
-
this.logger.warn('No hay stream disponible para controlar el flash');
|
|
721
|
-
return false;
|
|
722
|
-
}
|
|
723
|
-
const videoTrack = stream.getVideoTracks()[0];
|
|
724
|
-
if (!videoTrack) {
|
|
725
|
-
this.logger.warn('No hay track de video disponible');
|
|
726
|
-
return false;
|
|
727
|
-
}
|
|
728
|
-
const capabilities = videoTrack.getCapabilities();
|
|
729
|
-
if (capabilities.torch === undefined) {
|
|
730
|
-
this.logger.warn('El dispositivo no soporta control de flash');
|
|
731
|
-
return false;
|
|
732
|
-
}
|
|
733
|
-
await videoTrack.applyConstraints({
|
|
734
|
-
advanced: [{ torch: enabled }]
|
|
735
|
-
});
|
|
736
|
-
this.logger.state('FLASH_CONTROLADO', { enabled });
|
|
737
|
-
return true;
|
|
738
|
-
}
|
|
739
|
-
catch (error) {
|
|
740
|
-
this.logger.error('Error al controlar el flash:', error);
|
|
541
|
+
// Optimization helper methods
|
|
542
|
+
constraintsEqual(a, b) {
|
|
543
|
+
if (!a || !b)
|
|
741
544
|
return false;
|
|
742
|
-
|
|
545
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
743
546
|
}
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
if (!videoTrack) {
|
|
753
|
-
this.logger.warn('No hay track de video disponible');
|
|
754
|
-
return false;
|
|
755
|
-
}
|
|
756
|
-
const capabilities = videoTrack.getCapabilities();
|
|
757
|
-
if (!capabilities.focusMode || !capabilities.focusMode.includes('single-shot')) {
|
|
758
|
-
this.logger.warn('El dispositivo no soporta enfoque manual');
|
|
759
|
-
return false;
|
|
760
|
-
}
|
|
761
|
-
// Trigger single-shot focus
|
|
762
|
-
await videoTrack.applyConstraints({
|
|
763
|
-
advanced: [{ focusMode: 'single-shot' }]
|
|
764
|
-
});
|
|
765
|
-
// Return to continuous focus after a short delay
|
|
766
|
-
setTimeout(async () => {
|
|
767
|
-
try {
|
|
768
|
-
await videoTrack.applyConstraints({
|
|
769
|
-
advanced: [{ focusMode: 'continuous' }]
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
catch (error) {
|
|
773
|
-
this.logger.warn('Error al volver a enfoque continuo:', error);
|
|
774
|
-
}
|
|
775
|
-
}, 1000);
|
|
776
|
-
this.logger.state('ENFOQUE_MANUAL_ACTIVADO', { x, y });
|
|
777
|
-
return true;
|
|
778
|
-
}
|
|
779
|
-
catch (error) {
|
|
780
|
-
this.logger.error('Error al enfocar manualmente:', error);
|
|
781
|
-
return false;
|
|
782
|
-
}
|
|
547
|
+
// Cleanup method for optimization
|
|
548
|
+
static clearCaches() {
|
|
549
|
+
CameraService.cameraCapabilitiesCache.clear();
|
|
550
|
+
CameraService.deviceEnumerationCache = null;
|
|
551
|
+
}
|
|
552
|
+
// Method to invalidate device cache when needed
|
|
553
|
+
invalidateDeviceCache() {
|
|
554
|
+
CameraService.deviceEnumerationCache = null;
|
|
783
555
|
}
|
|
784
556
|
}
|
|
785
557
|
|
|
@@ -858,9 +630,9 @@ class DeviceStrategyFactory {
|
|
|
858
630
|
}
|
|
859
631
|
|
|
860
632
|
class DetectionService {
|
|
861
|
-
logger;
|
|
862
633
|
debug;
|
|
863
634
|
useDocumentClassification;
|
|
635
|
+
useDocumentDetector;
|
|
864
636
|
session;
|
|
865
637
|
mobileNetSession;
|
|
866
638
|
mobileNetClassMap;
|
|
@@ -875,30 +647,32 @@ class DetectionService {
|
|
|
875
647
|
preprocessCanvas;
|
|
876
648
|
preprocessCtx;
|
|
877
649
|
captureCanvas;
|
|
878
|
-
|
|
879
|
-
|
|
650
|
+
// Static canvas pool for reuse across instances
|
|
651
|
+
static canvasPool = new Map();
|
|
652
|
+
static MAX_POOL_SIZE = 5;
|
|
653
|
+
constructor(debug = false, useDocumentClassification = false, useDocumentDetector = true) {
|
|
880
654
|
this.debug = debug;
|
|
881
655
|
this.useDocumentClassification = useDocumentClassification;
|
|
656
|
+
this.useDocumentDetector = useDocumentDetector;
|
|
882
657
|
this.deviceStrategy = DeviceStrategyFactory.createStrategy();
|
|
883
658
|
this.initializeCanvasPool();
|
|
884
659
|
}
|
|
885
660
|
async loadModel() {
|
|
886
661
|
if (this.modelLoaded || this.session) {
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (!this.useDocumentDetector) {
|
|
665
|
+
// Skip loading the detection model but mark as "loaded" for compatibility
|
|
666
|
+
this.modelLoaded = true;
|
|
891
667
|
return;
|
|
892
668
|
}
|
|
893
669
|
try {
|
|
894
|
-
this.logger.state('PRECARGANDO_MODELO_DETECCION', { modelPath: this.MODEL_PATH });
|
|
895
670
|
const sessionOptions = this.deviceStrategy.getSessionOptions(this.debug);
|
|
896
671
|
try {
|
|
897
672
|
this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, sessionOptions);
|
|
898
673
|
}
|
|
899
674
|
catch (error) {
|
|
900
675
|
if (error.message.includes('failed to allocate a buffer')) {
|
|
901
|
-
this.logger.warn('Fallo en asignación de buffer durante precarga, intentando con configuración mínima');
|
|
902
676
|
const fallbackOptions = {
|
|
903
677
|
executionProviders: ['wasm'],
|
|
904
678
|
graphOptimizationLevel: 'disabled',
|
|
@@ -916,10 +690,8 @@ class DetectionService {
|
|
|
916
690
|
}
|
|
917
691
|
}
|
|
918
692
|
this.modelLoaded = true;
|
|
919
|
-
this.logger.state('MODELO_DETECCION_CARGADO_EXITOSAMENTE', { sessionCreated: !!this.session });
|
|
920
693
|
}
|
|
921
694
|
catch (error) {
|
|
922
|
-
this.logger.error('Error al precargar modelo de detección:', error);
|
|
923
695
|
throw error;
|
|
924
696
|
}
|
|
925
697
|
}
|
|
@@ -928,19 +700,14 @@ class DetectionService {
|
|
|
928
700
|
return;
|
|
929
701
|
}
|
|
930
702
|
try {
|
|
931
|
-
this.logger.state('CARGANDO_MODELO_MOBILENET', { path: this.MOBILENET_MODEL_PATH });
|
|
932
703
|
// Try to load class map (optional - SqueezeNet may not need it for basic classification)
|
|
933
704
|
try {
|
|
934
705
|
const classResponse = await fetch(this.MOBILENET_CLASSES_PATH);
|
|
935
706
|
if (classResponse.ok) {
|
|
936
707
|
this.mobileNetClassMap = await classResponse.json();
|
|
937
|
-
this.logger.state('CLASES_MOBILENET_CARGADAS', {
|
|
938
|
-
classCount: Object.keys(this.mobileNetClassMap).length
|
|
939
|
-
});
|
|
940
708
|
}
|
|
941
709
|
}
|
|
942
710
|
catch (error) {
|
|
943
|
-
this.logger.warn('No se pudo cargar el mapa de clases de MobileNet, usando clasificación básica');
|
|
944
711
|
// Create a basic class map for document types
|
|
945
712
|
this.mobileNetClassMap = {
|
|
946
713
|
"0": "document",
|
|
@@ -957,7 +724,6 @@ class DetectionService {
|
|
|
957
724
|
}
|
|
958
725
|
catch (error) {
|
|
959
726
|
if (error.message.includes('failed to allocate a buffer')) {
|
|
960
|
-
this.logger.warn('Fallo en asignación de buffer de MobileNet, intentando con configuración mínima');
|
|
961
727
|
const fallbackOptions = {
|
|
962
728
|
executionProviders: ['wasm'],
|
|
963
729
|
graphOptimizationLevel: 'disabled',
|
|
@@ -974,16 +740,16 @@ class DetectionService {
|
|
|
974
740
|
throw error;
|
|
975
741
|
}
|
|
976
742
|
}
|
|
977
|
-
this.logger.state('MODELO_MOBILENET_CARGADO_EXITOSAMENTE', {
|
|
978
|
-
sessionCreated: !!this.mobileNetSession
|
|
979
|
-
});
|
|
980
743
|
}
|
|
981
744
|
catch (error) {
|
|
982
|
-
this.logger.error('Error al cargar modelo MobileNet:', error);
|
|
983
745
|
throw error;
|
|
984
746
|
}
|
|
985
747
|
}
|
|
986
748
|
isModelLoaded() {
|
|
749
|
+
if (!this.useDocumentDetector) {
|
|
750
|
+
// If detector is disabled, consider it "loaded" for compatibility
|
|
751
|
+
return this.modelLoaded;
|
|
752
|
+
}
|
|
987
753
|
return this.modelLoaded && !!this.session;
|
|
988
754
|
}
|
|
989
755
|
preprocess(video) {
|
|
@@ -1008,6 +774,10 @@ class DetectionService {
|
|
|
1008
774
|
return new window.ort.Tensor("float16", float16Data, [1, 3, this.INPUT_SIZE, this.INPUT_SIZE]);
|
|
1009
775
|
}
|
|
1010
776
|
async runInference(inputTensor) {
|
|
777
|
+
if (!this.useDocumentDetector) {
|
|
778
|
+
// Return empty array when document detector is disabled
|
|
779
|
+
return [];
|
|
780
|
+
}
|
|
1011
781
|
if (!this.session) {
|
|
1012
782
|
throw new Error('Detection model not loaded');
|
|
1013
783
|
}
|
|
@@ -1032,11 +802,9 @@ class DetectionService {
|
|
|
1032
802
|
}
|
|
1033
803
|
async classifyDocument(canvas) {
|
|
1034
804
|
if (!this.mobileNetSession || !this.mobileNetClassMap) {
|
|
1035
|
-
this.logger.warn('Modelo MobileNet no está cargado, saltando clasificación');
|
|
1036
805
|
return null;
|
|
1037
806
|
}
|
|
1038
807
|
try {
|
|
1039
|
-
this.logger.state('CLASIFICANDO_DOCUMENTO', { timestamp: Date.now() });
|
|
1040
808
|
const inputTensor = this.preprocessMobileNet(canvas);
|
|
1041
809
|
const feeds = { [this.mobileNetSession.inputNames[0]]: inputTensor };
|
|
1042
810
|
const results = await this.mobileNetSession.run(feeds);
|
|
@@ -1044,13 +812,6 @@ class DetectionService {
|
|
|
1044
812
|
const maxIdx = output.reduce((bestIdx, val, idx, arr) => val > arr[bestIdx] ? idx : bestIdx, 0);
|
|
1045
813
|
const confidence = output[maxIdx];
|
|
1046
814
|
const className = this.mobileNetClassMap[maxIdx.toString()] || "unknown";
|
|
1047
|
-
this.logger.state('DOCUMENTO_CLASIFICADO', {
|
|
1048
|
-
class: className,
|
|
1049
|
-
confidence: confidence.toFixed(3),
|
|
1050
|
-
classIndex: maxIdx,
|
|
1051
|
-
timestamp: Date.now(),
|
|
1052
|
-
results
|
|
1053
|
-
});
|
|
1054
815
|
return {
|
|
1055
816
|
class: className,
|
|
1056
817
|
confidence: confidence,
|
|
@@ -1058,7 +819,6 @@ class DetectionService {
|
|
|
1058
819
|
};
|
|
1059
820
|
}
|
|
1060
821
|
catch (error) {
|
|
1061
|
-
this.logger.error('Error al clasificar documento:', error);
|
|
1062
822
|
return null;
|
|
1063
823
|
}
|
|
1064
824
|
}
|
|
@@ -1171,7 +931,6 @@ class DetectionService {
|
|
|
1171
931
|
}
|
|
1172
932
|
this.mobileNetClassMap = undefined;
|
|
1173
933
|
this.modelLoaded = false;
|
|
1174
|
-
this.logger.state('DETECCION_SERVICE_LIMPIADO', { timestamp: Date.now() });
|
|
1175
934
|
}
|
|
1176
935
|
initializeCanvasPool() {
|
|
1177
936
|
this.preprocessCanvas = document.createElement('canvas');
|
|
@@ -1182,9 +941,6 @@ class DetectionService {
|
|
|
1182
941
|
willReadFrequently: true
|
|
1183
942
|
});
|
|
1184
943
|
this.captureCanvas = document.createElement('canvas');
|
|
1185
|
-
this.logger.state('CANVAS_POOL_INICIALIZADO', {
|
|
1186
|
-
preprocessCanvasSize: this.INPUT_SIZE
|
|
1187
|
-
});
|
|
1188
944
|
}
|
|
1189
945
|
cleanupCanvasPool() {
|
|
1190
946
|
if (this.preprocessCanvas) {
|
|
@@ -1220,10 +976,11 @@ class DetectionService {
|
|
|
1220
976
|
return (sign << 15) | (newExp << 10) | (frac >> 13);
|
|
1221
977
|
}
|
|
1222
978
|
preprocessMobileNet(canvas) {
|
|
1223
|
-
|
|
1224
|
-
tempCanvas
|
|
1225
|
-
tempCanvas.height = 224;
|
|
979
|
+
// Use pooled canvas instead of creating new one
|
|
980
|
+
const tempCanvas = this.getCanvas(224, 224);
|
|
1226
981
|
const tempCtx = tempCanvas.getContext('2d');
|
|
982
|
+
// Clear canvas before use
|
|
983
|
+
tempCtx.clearRect(0, 0, 224, 224);
|
|
1227
984
|
// Resize image to 224x224 for SqueezeNet (using MobileNet interface)
|
|
1228
985
|
tempCtx.drawImage(canvas, 0, 0, 224, 224);
|
|
1229
986
|
const imageData = tempCtx.getImageData(0, 0, 224, 224);
|
|
@@ -1239,8 +996,53 @@ class DetectionService {
|
|
|
1239
996
|
// Blue channel
|
|
1240
997
|
arr[2 * hw + i] = data[i * 4 + 2] / 255.0;
|
|
1241
998
|
}
|
|
999
|
+
// Return canvas to pool after use
|
|
1000
|
+
this.returnCanvas(tempCanvas);
|
|
1242
1001
|
return new window.ort.Tensor('float32', arr, [1, 3, 224, 224]);
|
|
1243
1002
|
}
|
|
1003
|
+
// Canvas pool management methods for optimization
|
|
1004
|
+
getCanvas(width, height) {
|
|
1005
|
+
const key = `${width}x${height}`;
|
|
1006
|
+
if (!DetectionService.canvasPool.has(key)) {
|
|
1007
|
+
DetectionService.canvasPool.set(key, []);
|
|
1008
|
+
}
|
|
1009
|
+
const pool = DetectionService.canvasPool.get(key);
|
|
1010
|
+
let canvas = pool.pop();
|
|
1011
|
+
if (!canvas) {
|
|
1012
|
+
canvas = document.createElement('canvas');
|
|
1013
|
+
canvas.width = width;
|
|
1014
|
+
canvas.height = height;
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
// Clear the canvas before reuse
|
|
1018
|
+
const ctx = canvas.getContext('2d');
|
|
1019
|
+
ctx.clearRect(0, 0, width, height);
|
|
1020
|
+
}
|
|
1021
|
+
return canvas;
|
|
1022
|
+
}
|
|
1023
|
+
returnCanvas(canvas) {
|
|
1024
|
+
const key = `${canvas.width}x${canvas.height}`;
|
|
1025
|
+
if (!DetectionService.canvasPool.has(key)) {
|
|
1026
|
+
DetectionService.canvasPool.set(key, []);
|
|
1027
|
+
}
|
|
1028
|
+
const pool = DetectionService.canvasPool.get(key);
|
|
1029
|
+
// Only return to pool if not at max capacity
|
|
1030
|
+
if (pool.length < DetectionService.MAX_POOL_SIZE) {
|
|
1031
|
+
pool.push(canvas);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
// Static method to clear all canvas pools (useful for memory management)
|
|
1035
|
+
static clearCanvasPools() {
|
|
1036
|
+
DetectionService.canvasPool.clear();
|
|
1037
|
+
}
|
|
1038
|
+
// Method to get pool statistics for debugging
|
|
1039
|
+
getPoolStats() {
|
|
1040
|
+
const stats = {};
|
|
1041
|
+
DetectionService.canvasPool.forEach((pool, key) => {
|
|
1042
|
+
stats[key] = pool.length;
|
|
1043
|
+
});
|
|
1044
|
+
return stats;
|
|
1045
|
+
}
|
|
1244
1046
|
}
|
|
1245
1047
|
|
|
1246
1048
|
class ServiceContainer {
|
|
@@ -1251,13 +1053,11 @@ class ServiceContainer {
|
|
|
1251
1053
|
initializeServices(config) {
|
|
1252
1054
|
// Initialize services in dependency order
|
|
1253
1055
|
const eventBus = new EventBusService();
|
|
1254
|
-
const logger = new LoggerService(config.debug);
|
|
1255
1056
|
const stateManager = new StateManagerService(eventBus);
|
|
1256
|
-
const cameraService = new CameraService(
|
|
1257
|
-
const detectionService = new DetectionService(
|
|
1057
|
+
const cameraService = new CameraService(eventBus, config.preferredCamera);
|
|
1058
|
+
const detectionService = new DetectionService(config.debug, config.useDocumentClassification, config.useDocumentDetector);
|
|
1258
1059
|
// Register services
|
|
1259
1060
|
this.services.set('eventBus', eventBus);
|
|
1260
|
-
this.services.set('logger', logger);
|
|
1261
1061
|
this.services.set('stateManager', stateManager);
|
|
1262
1062
|
this.services.set('cameraService', cameraService);
|
|
1263
1063
|
this.services.set('detectionService', detectionService);
|
|
@@ -1269,9 +1069,6 @@ class ServiceContainer {
|
|
|
1269
1069
|
}
|
|
1270
1070
|
return service;
|
|
1271
1071
|
}
|
|
1272
|
-
getLogger() {
|
|
1273
|
-
return this.get('logger');
|
|
1274
|
-
}
|
|
1275
1072
|
getEventBus() {
|
|
1276
1073
|
return this.get('eventBus');
|
|
1277
1074
|
}
|
|
@@ -1285,10 +1082,6 @@ class ServiceContainer {
|
|
|
1285
1082
|
return this.get('detectionService');
|
|
1286
1083
|
}
|
|
1287
1084
|
updateConfig(config) {
|
|
1288
|
-
if (config.debug !== undefined) {
|
|
1289
|
-
const logger = this.services.get('logger');
|
|
1290
|
-
logger.setDebugMode(config.debug);
|
|
1291
|
-
}
|
|
1292
1085
|
}
|
|
1293
1086
|
cleanup() {
|
|
1294
1087
|
// Cleanup services in reverse order
|
|
@@ -1298,7 +1091,7 @@ class ServiceContainer {
|
|
|
1298
1091
|
}
|
|
1299
1092
|
}
|
|
1300
1093
|
|
|
1301
|
-
const myComponentCss = ":host{display:block;width:100%;height:100%;font-family:system-ui, -apple-system, sans-serif;color:#1a1a1a}.detector-container{display:flex;flex-direction:column;align-items:center;width:100%;height:100%}h1{font-size:24px;font-weight:500;color:#333;margin:0 0 24px 0}.video-container{position:relative;width:100%;height:100%;background:#333;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden}video,.detection-overlay{position:absolute;width:100%;height:100%;border-radius:8px}video.mirror,.detection-overlay.mirror{transform:rotateY(180deg)}.detection-overlay{z-index:1;pointer-events:none}video{z-index:0}.detection-box{transition:opacity 0.2s ease}.status{margin-top:16px;font-size:12px;color:#666}.overlay-mask{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10;display:block;pointer-events:none}.card-outline{position:absolute;top:var(--mask-center-y, 50%);left:var(--mask-center-x, 50%);transform:translate(-50%, -50%);width:var(--mask-width, 88%);height:var(--mask-height, 55%);border:none;border-radius:4px;background:transparent;opacity:0.8}.side{position:absolute;background:#999;transition:background-color 0.3s ease;border-radius:1px}.side-top.aligned{border-left-color:#28a745;border-top-color:#28a745}.side-right.aligned{border-right-color:#28a745;border-top-color:#28a745}.side-bottom.aligned{border-left-color:#28a745;border-bottom-color:#28a745}.side-left.aligned{border-right-color:#28a745;border-bottom-color:#28a745}.side-top{top:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-right{top:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-bottom{bottom:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.side-left{bottom:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.guide-text{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);color:#fff;font-size:14px;font-weight:600;text-align:center;white-space:normal;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(12px);padding:12px 20px;border-radius:8px;max-width:300px;z-index:20;border:1px solid rgba(255, 255, 255, 0.1)}.quality-indicator{position:absolute;top:20px;right:20px;display:flex;align-items:center;gap:8px;background:rgba(0, 0, 0, 0.8);padding:8px 12px;border-radius:20px;z-index:25;transition:all 0.3s ease}.quality-indicator.warning{background:rgba(255, 152, 0, 0.9)}.quality-indicator.good{background:rgba(76, 175, 80, 0.9)}.quality-score{color:#fff;font-size:12px;font-weight:600;min-width:30px;text-align:center}.quality-status{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;color:#fff;transition:all 0.3s ease}.quality-status.ready{background:#4CAF50}.quality-status.not-ready{background:#f44336}.card-outline.quality-warning{animation:qualityWarning 1s ease-in-out infinite alternate}@keyframes qualityWarning{0%{box-shadow:0 0 0 3px rgba(255, 152, 0, 0.6)}100%{box-shadow:0 0 0 6px rgba(255, 152, 0, 0.3)}}.capture-animation{position:absolute;top:0;left:0;width:100%;height:100%;background:#fff;opacity:0;z-index:30;pointer-events:none;animation:captureFlash 0.6s ease-out}@keyframes captureFlash{0%{opacity:0}15%{opacity:0.8}30%{opacity:0}45%{opacity:0.4}60%{opacity:0}100%{opacity:0}}.card-outline.capturing{animation:pulseGreen 0.6s ease-out}@keyframes pulseGreen{0%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}50%{border-color:#28a745;box-shadow:0 0 0 8px rgba(40, 167, 69, 0.6)}100%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}}.flip-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showFlipInstruction 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.id-card-icon{width:80px;height:50px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:8px;position:relative;animation:flipCard 2s ease-in-out infinite;box-shadow:0 4px 12px rgba(0, 0, 0, 0.3)}.id-card-icon::before{content:'';position:absolute;top:8px;left:8px;width:16px;height:12px;background:rgba(255, 255, 255, 0.9);border-radius:2px}.id-card-icon::after{content:'';position:absolute;top:25px;left:8px;width:64px;height:3px;background:rgba(255, 255, 255, 0.7);border-radius:1px;box-shadow:0 6px 0 rgba(255, 255, 255, 0.7), 0 12px 0 rgba(255, 255, 255, 0.7)}.flip-text{margin-top:12px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px}@keyframes showFlipInstruction{0%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}}@keyframes flipCard{0%{transform:rotateY(0deg)}50%{transform:rotateY(180deg)}100%{transform:rotateY(360deg)}}.success-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showSuccessMessage 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.check-icon{width:80px;height:80px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:50%;position:relative;animation:bounceSuccess 0.6s ease-out;box-shadow:0 4px 16px rgba(108, 117, 125, 0.4);display:flex;align-items:center;justify-content:center}.check-icon::before{content:'';position:absolute;width:20px;height:35px;border:4px solid #fff;border-top:none;border-left:none;transform:rotate(45deg);animation:drawCheck 0.4s ease-out 0.2s both}.success-text{margin-top:16px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px;animation:fadeInUp 0.5s ease-out 0.4s both}@keyframes showSuccessMessage{0%{opacity:0;transform:translate(-50%, -50%) scale(0.5)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.9)}}@keyframes bounceSuccess{0%{transform:scale(0)}50%{transform:scale(1.1)}100%{transform:scale(1)}}@keyframes drawCheck{0%{width:0;height:0}50%{width:20px;height:0}100%{width:20px;height:35px}}@keyframes fadeInUp{0%{opacity:0;transform:translateY(20px)}100%{opacity:1;transform:translateY(0)}}.skip-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:300px}.skip-explanation{background:rgba(0, 0, 0, 0.5);color:#ffffff;padding:10px 16px;border-radius:8px;font-size:14px;font-weight:500;text-align:center;margin-bottom:12px;box-shadow:0 2px 8px rgba(0, 0, 0, 0.2);backdrop-filter:blur(12px);border:1px solid rgba(255, 255, 255, 0.1);animation:fadeIn 0.3s ease-in-out}@keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.skip-button{pointer-events:auto;background:#fff;color:#333;border:none;border-radius:25px;padding:12px 24px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.3s ease}.skip-button:hover{background:#f8f9fa}.skip-button:active{background:#e9ecef;transform:translateY(1px)}.camera-controls{position:absolute;top:16px;right:16px;z-index:25;display:flex;gap:8px;pointer-events:auto}.camera-selector-button{height:32px;padding:0 10px;border:none;border-radius:8px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(12px);color:#ffffff;font-size:12px;font-weight:500;cursor:pointer;transition:all 0.2s ease;display:flex;align-items:center;justify-content:center;border:1px solid rgba(255, 255, 255, 0.1);white-space:nowrap;min-width:fit-content}.camera-selector-button:hover{background:rgba(0, 0, 0, 0.8);border-color:rgba(255, 255, 255, 0.2);transform:translateY(-1px)}.camera-selector-button:active{transform:translateY(0);background:rgba(0, 0, 0, 0.9)}.camera-selector-button:disabled,.camera-selector-button.loading{opacity:0.7;cursor:not-allowed;pointer-events:none}.camera-selector-button:disabled:hover,.camera-selector-button.loading:hover{transform:none;background:rgba(0, 0, 0, 0.6);border-color:rgba(255, 255, 255, 0.1)}.button-spinner{width:16px;height:16px;border:2px solid rgba(255, 255, 255, 0.3);border-top:2px solid #ffffff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.camera-selector-dropdown{position:absolute;top:56px;right:16px;z-index:30;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;min-width:260px;max-width:300px;border:1px solid rgba(255, 255, 255, 0.1);overflow:hidden;pointer-events:auto;animation:slideInFromTop 0.3s ease-out}@keyframes slideInFromTop{0%{opacity:0;transform:translateY(-8px) scale(0.95)}100%{opacity:1;transform:translateY(0) scale(1)}}.camera-selector-header{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border-bottom:1px solid rgba(255, 255, 255, 0.1)}.camera-selector-header span{font-weight:500;color:#ffffff;font-size:13px;opacity:0.9}.close-selector{width:20px;height:20px;border:none;background:none;color:rgba(255, 255, 255, 0.7);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:all 0.2s ease}.close-selector:hover{background:rgba(255, 255, 255, 0.1);color:#ffffff}.camera-list{padding:4px 0;max-height:240px;overflow-y:auto}.camera-option{width:100%;padding:8px 12px;border:none;background:none;text-align:left;cursor:pointer;transition:all 0.2s ease;display:flex;justify-content:space-between;align-items:center;color:rgba(255, 255, 255, 0.9)}.camera-option:hover{background:rgba(255, 255, 255, 0.08)}.camera-option.selected{background:rgba(255, 255, 255, 0.12);color:#ffffff}.camera-label{font-size:13px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-right:8px;font-weight:400}.selected-indicator{font-size:14px;color:#ffffff;opacity:0.9}.device-info{padding:6px 12px;border-top:1px solid rgba(255, 255, 255, 0.1);background:rgba(255, 255, 255, 0.05)}.device-info small{color:rgba(255, 255, 255, 0.6);font-size:11px;text-transform:capitalize;font-weight:400}@media (max-width: 480px){.camera-controls{top:12px;right:12px;gap:6px}.camera-selector-button{height:36px;padding:0 12px;font-size:11px;border-radius:6px}.camera-selector-dropdown{right:12px;top:48px;min-width:240px;max-width:calc(100vw - 24px)}.camera-selector-header{padding:6px 10px}.camera-option{padding:6px 10px}.device-info{padding:4px 10px}}.watermark{position:absolute;bottom:12px;right:12px;z-index:15;pointer-events:none;opacity:0.7}.watermark img{height:24px;width:auto;filter:drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3))}.component-status{position:absolute;top:16px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);display:flex;align-items:center;gap:6px;padding:6px 10px;z-index:40;height:32px;width:fit-content;animation:slideInFromTop 0.3s ease-out}.status-spinner{width:16px;height:16px;border:1px solid rgba(255, 255, 255, 0.3);border-top:1px solid #ffffff;border-radius:50%;animation:statusSpin 1s linear infinite;flex-shrink:0}.status-content{display:flex;flex-direction:column;gap:1px}.status-message{color:#ffffff;font-size:12px;font-weight:500;margin:0}.status-description{color:rgba(255, 255, 255, 0.7);font-size:10px;line-height:1.2;margin:0}@keyframes statusSpin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}@media (max-width: 480px){.component-status{top:12px;left:12px;padding:4px 8px;gap:4px;height:28px;border-radius:8px}.status-spinner{width:14px;height:14px}.status-message{font-size:11px}.status-description{font-size:9px}}.loading-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0, 0, 0, 0.85);backdrop-filter:blur(4px);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:30;border-radius:8px}.loading-spinner{width:50px;height:50px;border:3px solid rgba(255, 255, 255, 0.15);border-top:3px solid #28a745;border-radius:50%;animation:spin 1s ease-in-out infinite;margin-bottom:16px}.loading-text{color:#ffffff;font-size:14px;font-weight:500;text-align:center;opacity:0.9;margin-bottom:4px}.loading-description{color:rgba(255, 255, 255, 0.7);font-size:12px;text-align:center;opacity:0.8}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.performance-monitor{position:absolute;top:70px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);z-index:35;width:fit-content;animation:slideInFromLeft 0.3s ease-out;transition:all 0.3s ease}@keyframes slideInFromLeft{0%{opacity:0;transform:translateX(-20px) scale(0.95)}100%{opacity:1;transform:translateX(0) scale(1)}}.performance-expanded{padding:6px 10px}.metrics-row{display:flex;gap:8px;margin-bottom:4px}.metrics-row:last-child{margin-bottom:0}.metric-compact{display:flex;flex-direction:column;align-items:center;gap:2px;min-width:50px}.metric-label{font-size:9px;color:rgba(255, 255, 255, 0.6);font-weight:500;text-transform:uppercase;letter-spacing:0.3px}.metric-value{font-size:10px;font-weight:600;font-family:'Monaco', 'Menlo', 'Consolas', monospace;padding:2px 4px;border-radius:3px;text-align:center;white-space:nowrap}.metric-value.good{color:#4ade80;background:rgba(74, 222, 128, 0.1);border:1px solid rgba(74, 222, 128, 0.2)}.metric-value.warning{color:#fbbf24;background:rgba(251, 191, 36, 0.1);border:1px solid rgba(251, 191, 36, 0.2)}.metric-value.danger{color:#f87171;background:rgba(248, 113, 113, 0.1);border:1px solid rgba(248, 113, 113, 0.2)}@media (max-width: 480px){.performance-monitor{top:60px;left:12px;width:fit-content}.performance-expanded{padding:4px 8px}.metrics-row{gap:6px;margin-bottom:3px}.metric-compact{min-width:45px;gap:1px}.metric-label{font-size:8px}.metric-value{font-size:9px;padding:1px 3px}}.quality-monitor{position:absolute;top:200px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);z-index:35;width:fit-content;animation:slideInFromLeft 0.3s ease-out;transition:all 0.3s ease}.quality-text{padding:6px 10px;font-size:11px;color:white;font-family:'Monaco', 'Menlo', 'Consolas', monospace;line-height:1.4}.quality-fail{color:#ff9999}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.7}}.metric-value.danger{animation:pulse 2s infinite}@media (max-width: 480px){.quality-monitor{top:160px;left:12px}.quality-text{padding:4px 8px;font-size:10px}}";
|
|
1094
|
+
const myComponentCss = ":host{display:block;width:100%;height:100%;font-family:system-ui, -apple-system, sans-serif;color:#1a1a1a}.detector-container{display:flex;flex-direction:column;align-items:center;width:100%;height:100%}h1{font-size:24px;font-weight:500;color:#333;margin:0 0 24px 0}.video-container{position:relative;width:100%;height:100%;background:#333;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden}video,.detection-overlay{position:absolute;width:100%;height:100%;border-radius:8px}video.mirror,.detection-overlay.mirror{transform:rotateY(180deg)}.detection-overlay{z-index:1;pointer-events:none}video{z-index:0}.detection-box{transition:opacity 0.2s ease}.status{margin-top:16px;font-size:12px;color:#666}.overlay-mask{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10;display:block;pointer-events:none}.card-outline{position:absolute;top:var(--mask-center-y, 50%);left:var(--mask-center-x, 50%);transform:translate(-50%, -50%);width:var(--mask-width, 88%);height:var(--mask-height, 55%);border:none;border-radius:4px;background:transparent;opacity:0.8}.side{position:absolute;background:#999;transition:background-color 0.3s ease;border-radius:1px}.side-top.aligned{border-left-color:#28a745;border-top-color:#28a745}.side-right.aligned{border-right-color:#28a745;border-top-color:#28a745}.side-bottom.aligned{border-left-color:#28a745;border-bottom-color:#28a745}.side-left.aligned{border-right-color:#28a745;border-bottom-color:#28a745}.side-top{top:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-right{top:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-bottom{bottom:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.side-left{bottom:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.guide-text{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);color:#ffffff;font-size:12px;font-weight:500;text-align:center;white-space:normal;padding:13px;max-width:300px;z-index:20;height:fit-content;width:fit-content;animation:slideInFromTopCentered 0.3s ease-out}.quality-indicator{position:absolute;top:20px;right:20px;display:flex;align-items:center;gap:8px;background:rgba(0, 0, 0, 0.8);padding:8px 12px;border-radius:20px;z-index:25;transition:all 0.3s ease}.quality-indicator.warning{background:rgba(255, 152, 0, 0.9)}.quality-indicator.good{background:rgba(76, 175, 80, 0.9)}.quality-score{color:#fff;font-size:12px;font-weight:600;min-width:30px;text-align:center}.quality-status{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;color:#fff;transition:all 0.3s ease}.quality-status.ready{background:#4CAF50}.quality-status.not-ready{background:#f44336}.card-outline.quality-warning{animation:qualityWarning 1s ease-in-out infinite alternate}@keyframes qualityWarning{0%{box-shadow:0 0 0 3px rgba(255, 152, 0, 0.6)}100%{box-shadow:0 0 0 6px rgba(255, 152, 0, 0.3)}}.capture-animation{position:absolute;top:0;left:0;width:100%;height:100%;background:#fff;opacity:0;z-index:30;pointer-events:none;animation:captureFlash 0.6s ease-out}@keyframes captureFlash{0%{opacity:0}15%{opacity:0.8}30%{opacity:0}45%{opacity:0.4}60%{opacity:0}100%{opacity:0}}.card-outline.capturing{animation:pulseGreen 0.6s ease-out}@keyframes pulseGreen{0%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}50%{border-color:#28a745;box-shadow:0 0 0 8px rgba(40, 167, 69, 0.6)}100%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}}.flip-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showFlipInstruction 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.id-card-icon{width:80px;height:50px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:8px;position:relative;animation:flipCard 2s ease-in-out infinite;box-shadow:0 4px 12px rgba(0, 0, 0, 0.3)}.id-card-icon::before{content:'';position:absolute;top:8px;left:8px;width:16px;height:12px;background:rgba(255, 255, 255, 0.9);border-radius:2px}.id-card-icon::after{content:'';position:absolute;top:25px;left:8px;width:64px;height:3px;background:rgba(255, 255, 255, 0.7);border-radius:1px;box-shadow:0 6px 0 rgba(255, 255, 255, 0.7), 0 12px 0 rgba(255, 255, 255, 0.7)}.flip-text{margin-top:12px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px}@keyframes showFlipInstruction{0%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}}@keyframes flipCard{0%{transform:rotateY(0deg)}50%{transform:rotateY(180deg)}100%{transform:rotateY(360deg)}}.success-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showSuccessMessage 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.check-icon{width:80px;height:80px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:50%;position:relative;animation:bounceSuccess 0.6s ease-out;box-shadow:0 4px 16px rgba(108, 117, 125, 0.4);display:flex;align-items:center;justify-content:center}.check-icon::before{content:'';position:absolute;width:20px;height:35px;border:4px solid #fff;border-top:none;border-left:none;transform:rotate(45deg);animation:drawCheck 0.4s ease-out 0.2s both}.success-text{margin-top:16px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px;animation:fadeInUp 0.5s ease-out 0.4s both}@keyframes showSuccessMessage{0%{opacity:0;transform:translate(-50%, -50%) scale(0.5)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.9)}}@keyframes bounceSuccess{0%{transform:scale(0)}50%{transform:scale(1.1)}100%{transform:scale(1)}}@keyframes drawCheck{0%{width:0;height:0}50%{width:20px;height:0}100%{width:20px;height:35px}}@keyframes fadeInUp{0%{opacity:0;transform:translateY(20px)}100%{opacity:1;transform:translateY(0)}}.skip-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:300px}.back-capture-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:320px}.back-capture-buttons{display:flex;gap:12px;align-items:center;justify-content:center;flex-wrap:wrap}.capture-button{pointer-events:auto;background:#fff;color:#333;border:none;border-radius:25px;padding:12px 24px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.3s ease}.capture-button:hover{background:#f8f9fa}.capture-button:active{background:#e9ecef;transform:translateY(1px)}.capture-button:disabled{background:#e9ecef;color:#6c757d;cursor:not-allowed;transform:none}@media (max-width: 480px){.back-capture-section{bottom:16px;max-width:300px}.back-capture-buttons{flex-direction:column;gap:8px;width:100%}.capture-button,.back-capture-buttons .skip-button{width:100%;max-width:200px;padding:10px 20px;font-size:13px}}.skip-explanation{background:rgba(0, 0, 0, 0.5);color:#ffffff;padding:10px 16px;border-radius:8px;font-size:14px;font-weight:500;text-align:center;margin-bottom:12px;box-shadow:0 2px 8px rgba(0, 0, 0, 0.2);backdrop-filter:blur(12px);border:1px solid rgba(255, 255, 255, 0.1);animation:fadeIn 0.3s ease-in-out}@keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.skip-button{pointer-events:auto;background:#fff;color:#333;border:none;border-radius:25px;padding:12px 24px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.3s ease}.skip-button:hover{background:#f8f9fa}.skip-button:active{background:#e9ecef;transform:translateY(1px)}.camera-controls{position:absolute;top:16px;right:16px;z-index:25;display:flex;gap:8px;pointer-events:auto;animation:slideInFromTop 0.3s ease-out}.camera-selector-button{height:32px;padding:0 10px;border:none;border-radius:8px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(12px);color:#ffffff;font-size:12px;font-weight:500;cursor:pointer;transition:all 0.2s ease;display:flex;align-items:center;justify-content:center;border:1px solid rgba(255, 255, 255, 0.1);white-space:nowrap;min-width:fit-content}.camera-selector-button:hover{background:rgba(0, 0, 0, 0.8);border-color:rgba(255, 255, 255, 0.2);transform:translateY(-1px)}.camera-selector-button:active{transform:translateY(0);background:rgba(0, 0, 0, 0.9)}.camera-selector-button:disabled,.camera-selector-button.loading{opacity:0.7;cursor:not-allowed;pointer-events:none}.camera-selector-button:disabled:hover,.camera-selector-button.loading:hover{transform:none;background:rgba(0, 0, 0, 0.6);border-color:rgba(255, 255, 255, 0.1)}.button-spinner{width:16px;height:16px;border:2px solid rgba(255, 255, 255, 0.3);border-top:2px solid #ffffff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.camera-selector-dropdown{position:absolute;top:56px;right:16px;z-index:30;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;min-width:260px;max-width:300px;border:1px solid rgba(255, 255, 255, 0.1);overflow:hidden;pointer-events:auto;animation:slideInFromTop 0.3s ease-out}@keyframes slideInFromTop{0%{opacity:0;transform:translateY(-8px) scale(0.95)}100%{opacity:1;transform:translateY(0) scale(1)}}@keyframes slideInFromTopCentered{0%{opacity:0;transform:translate(-50%, -50%) translateY(-8px) scale(0.95)}100%{opacity:1;transform:translate(-50%, -50%) translateY(0) scale(1)}}.camera-selector-header{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border-bottom:1px solid rgba(255, 255, 255, 0.1)}.camera-selector-header span{font-weight:500;color:#ffffff;font-size:13px;opacity:0.9}.close-selector{width:20px;height:20px;border:none;background:none;color:rgba(255, 255, 255, 0.7);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:all 0.2s ease}.close-selector:hover{background:rgba(255, 255, 255, 0.1);color:#ffffff}.camera-list{padding:4px 0;max-height:240px;overflow-y:auto}.camera-option{width:100%;padding:8px 12px;border:none;background:none;text-align:left;cursor:pointer;transition:all 0.2s ease;display:flex;justify-content:flex-start;align-items:center;gap:8px;color:rgba(255, 255, 255, 0.9)}.camera-option:hover{background:rgba(255, 255, 255, 0.08)}.camera-option.selected{background:rgba(255, 255, 255, 0.12);color:#ffffff}.camera-label{font-size:13px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-right:0;font-weight:400;display:flex;align-items:center;gap:6px}.camera-label .autofocus-icon{font-size:12px;color:#ffffff !important;opacity:0.8;flex-shrink:0}.selected-indicator{font-size:14px;color:#ffffff;opacity:0.9;flex-shrink:0}.device-info{padding:6px 12px;border-top:1px solid rgba(255, 255, 255, 0.1);background:rgba(255, 255, 255, 0.05)}.device-info small{color:rgba(255, 255, 255, 0.6);font-size:11px;text-transform:capitalize;font-weight:400}@media (max-width: 480px){.camera-controls{top:12px;right:12px;gap:6px}.camera-selector-button{height:36px;padding:0 12px;font-size:11px;border-radius:6px}.camera-selector-dropdown{right:12px;top:48px;min-width:240px;max-width:calc(100vw - 24px)}.camera-selector-header{padding:6px 10px}.camera-option{padding:6px 10px}.device-info{padding:4px 10px}}.watermark{position:absolute;bottom:12px;right:12px;z-index:15;pointer-events:none;opacity:0.7}.watermark img{height:24px;width:auto;filter:drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3))}.component-status{position:absolute;top:16px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);display:flex;align-items:center;gap:6px;padding:6px 10px;z-index:40;height:32px;width:fit-content;animation:slideInFromTop 0.3s ease-out}.status-spinner{width:16px;height:16px;border:1px solid rgba(255, 255, 255, 0.3);border-top:1px solid #ffffff;border-radius:50%;animation:statusSpin 1s linear infinite;flex-shrink:0}.status-content{display:flex;flex-direction:column;gap:1px}.status-message{color:#ffffff;font-size:12px;font-weight:500;margin:0}.status-description{color:rgba(255, 255, 255, 0.7);font-size:10px;line-height:1.2;margin:0}@keyframes statusSpin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}@media (max-width: 480px){.component-status{top:12px;left:12px;padding:4px 8px;gap:4px;height:28px;border-radius:8px}.status-spinner{width:14px;height:14px}.status-message{font-size:11px}.status-description{font-size:9px}.guide-text{font-size:11px;padding:4px 8px;border-radius:8px}}.loading-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0, 0, 0, 0.85);backdrop-filter:blur(4px);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:30;border-radius:8px}.loading-spinner{width:50px;height:50px;border:3px solid rgba(255, 255, 255, 0.15);border-top:3px solid #28a745;border-radius:50%;animation:spin 1s ease-in-out infinite;margin-bottom:16px}.loading-text{color:#ffffff;font-size:14px;font-weight:500;text-align:center;opacity:0.9;margin-bottom:4px}.loading-description{color:rgba(255, 255, 255, 0.7);font-size:12px;text-align:center;opacity:0.8}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.performance-monitor{position:absolute;top:70px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);z-index:35;width:fit-content;animation:slideInFromTop 0.3s ease-out;transition:all 0.3s ease}@keyframes slideInFromLeft{0%{opacity:0;transform:translateX(-20px) scale(0.95)}100%{opacity:1;transform:translateX(0) scale(1)}}.performance-expanded{padding:6px 10px}.metrics-row{display:flex;gap:8px;margin-bottom:4px}.metrics-row:last-child{margin-bottom:0}.metric-compact{display:flex;flex-direction:column;align-items:center;gap:2px;min-width:50px}.metric-label{font-size:9px;color:rgba(255, 255, 255, 0.6);font-weight:500;text-transform:uppercase;letter-spacing:0.3px}.metric-value{font-size:10px;font-weight:600;font-family:'Monaco', 'Menlo', 'Consolas', monospace;padding:2px 4px;border-radius:3px;text-align:center;white-space:nowrap}.metric-value.good{color:#4ade80;background:rgba(74, 222, 128, 0.1);border:1px solid rgba(74, 222, 128, 0.2)}.metric-value.warning{color:#fbbf24;background:rgba(251, 191, 36, 0.1);border:1px solid rgba(251, 191, 36, 0.2)}.metric-value.danger{color:#f87171;background:rgba(248, 113, 113, 0.1);border:1px solid rgba(248, 113, 113, 0.2)}@media (max-width: 480px){.performance-monitor{top:60px;left:12px;width:fit-content}.performance-expanded{padding:4px 8px}.metrics-row{gap:6px;margin-bottom:3px}.metric-compact{min-width:45px;gap:1px}.metric-label{font-size:8px}.metric-value{font-size:9px;padding:1px 3px}}.quality-monitor{position:absolute;top:200px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);z-index:35;width:fit-content;animation:slideInFromLeft 0.3s ease-out;transition:all 0.3s ease}.quality-text{padding:6px 10px;font-size:11px;color:white;font-family:'Monaco', 'Menlo', 'Consolas', monospace;line-height:1.4}.quality-fail{color:#ff9999}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.7}}.metric-value.danger{animation:pulse 2s infinite}@media (max-width: 480px){.quality-monitor{top:160px;left:12px}.quality-text{padding:4px 8px;font-size:10px}}.manual-capture-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:300px}.manual-capture-button{pointer-events:auto;background:#fff;color:#333;border:none;border-radius:25px;padding:12px 24px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.3s ease}.manual-capture-button:hover{background:#f8f9fa}.manual-capture-button:active{background:#e9ecef;transform:translateY(1px)}.manual-capture-button:disabled{background:#e9ecef;color:#6c757d;cursor:not-allowed;transform:none}@media (max-width: 480px){.manual-capture-section{bottom:16px;max-width:280px}.manual-capture-button{padding:10px 20px;font-size:13px}}";
|
|
1302
1095
|
|
|
1303
1096
|
const JaakStamps = class {
|
|
1304
1097
|
constructor(hostRef) {
|
|
@@ -1312,6 +1105,7 @@ const JaakStamps = class {
|
|
|
1312
1105
|
maskSize = 90;
|
|
1313
1106
|
cropMargin = 20;
|
|
1314
1107
|
useDocumentClassification = false;
|
|
1108
|
+
useDocumentDetector = true;
|
|
1315
1109
|
preferredCamera = 'auto';
|
|
1316
1110
|
captureDelay = 1500;
|
|
1317
1111
|
enableBackDocumentTimer = false;
|
|
@@ -1328,9 +1122,16 @@ const JaakStamps = class {
|
|
|
1328
1122
|
showCameraSelector = false;
|
|
1329
1123
|
isSwitchingCamera = false;
|
|
1330
1124
|
hasDocumentDetected = false;
|
|
1125
|
+
cameraInfoWithAutofocus = {
|
|
1126
|
+
availableCameras: [],
|
|
1127
|
+
selectedCameraId: null,
|
|
1128
|
+
deviceType: 'desktop',
|
|
1129
|
+
isMultipleCamerasAvailable: false,
|
|
1130
|
+
preferredFacing: null
|
|
1131
|
+
};
|
|
1331
1132
|
currentStatus = {
|
|
1332
|
-
message: '
|
|
1333
|
-
description: 'Configurando
|
|
1133
|
+
message: 'Preparando capturador...',
|
|
1134
|
+
description: 'Configurando herramientas de captura',
|
|
1334
1135
|
type: 'initializing',
|
|
1335
1136
|
isInitialized: false
|
|
1336
1137
|
};
|
|
@@ -1345,9 +1146,9 @@ const JaakStamps = class {
|
|
|
1345
1146
|
detectionRate: 0
|
|
1346
1147
|
};
|
|
1347
1148
|
backDocumentTimerRemaining = 0;
|
|
1149
|
+
showManualCaptureButton = false;
|
|
1348
1150
|
// Services
|
|
1349
1151
|
serviceContainer;
|
|
1350
|
-
logger;
|
|
1351
1152
|
eventBus;
|
|
1352
1153
|
stateManager;
|
|
1353
1154
|
cameraService;
|
|
@@ -1378,19 +1179,25 @@ const JaakStamps = class {
|
|
|
1378
1179
|
lastUpdateTime: 0
|
|
1379
1180
|
};
|
|
1380
1181
|
performanceUpdateInterval;
|
|
1381
|
-
// Performance optimization
|
|
1182
|
+
// Performance optimization with adaptive frame skipping
|
|
1382
1183
|
frameSkipCounter = 0;
|
|
1383
|
-
|
|
1184
|
+
BASE_FRAME_SKIP = 2;
|
|
1185
|
+
MAX_FRAME_SKIP = 8;
|
|
1384
1186
|
consecutiveFailures = 0;
|
|
1385
1187
|
MAX_FAILURES = 30;
|
|
1386
1188
|
lastInferenceTime = 0;
|
|
1387
1189
|
MIN_INFERENCE_INTERVAL = 50;
|
|
1190
|
+
performanceHistory = [];
|
|
1191
|
+
PERFORMANCE_HISTORY_SIZE = 10;
|
|
1192
|
+
// Canvas pool for optimized screenshot capture
|
|
1193
|
+
canvasPool = [];
|
|
1194
|
+
MAX_CANVAS_POOL_SIZE = 3;
|
|
1388
1195
|
async componentDidLoad() {
|
|
1389
|
-
this.updateStatus('
|
|
1196
|
+
this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
|
|
1390
1197
|
await this.initializeServices();
|
|
1391
|
-
this.updateStatus('
|
|
1198
|
+
this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
|
|
1392
1199
|
await this.setupEventListeners();
|
|
1393
|
-
this.updateStatus('
|
|
1200
|
+
this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
|
|
1394
1201
|
await this.initializeComponent();
|
|
1395
1202
|
if (this.debug) {
|
|
1396
1203
|
this.initializePerformanceMonitor();
|
|
@@ -1403,37 +1210,28 @@ const JaakStamps = class {
|
|
|
1403
1210
|
maskSize: this.maskSize,
|
|
1404
1211
|
cropMargin: this.cropMargin,
|
|
1405
1212
|
useDocumentClassification: this.useDocumentClassification,
|
|
1213
|
+
useDocumentDetector: this.useDocumentDetector,
|
|
1406
1214
|
preferredCamera: this.preferredCamera,
|
|
1407
1215
|
captureDelay: this.captureDelay
|
|
1408
1216
|
};
|
|
1409
1217
|
this.serviceContainer = new ServiceContainer(config);
|
|
1410
|
-
this.logger = this.serviceContainer.getLogger();
|
|
1411
1218
|
this.eventBus = this.serviceContainer.getEventBus();
|
|
1412
1219
|
this.stateManager = this.serviceContainer.getStateManager();
|
|
1413
1220
|
this.cameraService = this.serviceContainer.getCameraService();
|
|
1414
1221
|
this.detectionService = this.serviceContainer.getDetectionService();
|
|
1415
|
-
this.logger.state('SERVICIOS_INICIALIZADOS', { timestamp: Date.now() });
|
|
1416
1222
|
}
|
|
1417
1223
|
async setupEventListeners() {
|
|
1418
1224
|
this.eventBus.on('state-changed', (data) => {
|
|
1419
1225
|
this.handleStateChange(data);
|
|
1420
1226
|
});
|
|
1421
|
-
this.eventBus.on('camera-changed', (
|
|
1422
|
-
|
|
1227
|
+
this.eventBus.on('camera-changed', () => {
|
|
1228
|
+
// Camera changed event
|
|
1423
1229
|
});
|
|
1424
|
-
this.eventBus.on('error', (
|
|
1425
|
-
|
|
1230
|
+
this.eventBus.on('error', () => {
|
|
1231
|
+
// Handle service errors
|
|
1426
1232
|
});
|
|
1427
1233
|
}
|
|
1428
1234
|
async initializeComponent() {
|
|
1429
|
-
this.logger.state('COMPONENTE_INICIALIZANDO', {
|
|
1430
|
-
debug: this.debug,
|
|
1431
|
-
maskSize: this.maskSize,
|
|
1432
|
-
cropMargin: this.cropMargin,
|
|
1433
|
-
useDocumentClassification: this.useDocumentClassification,
|
|
1434
|
-
preferredCamera: this.preferredCamera,
|
|
1435
|
-
captureDelay: this.captureDelay
|
|
1436
|
-
});
|
|
1437
1235
|
this.validateProps();
|
|
1438
1236
|
if (this.debug) {
|
|
1439
1237
|
this.stateManager.updateCaptureState({ isLoading: true });
|
|
@@ -1442,58 +1240,67 @@ const JaakStamps = class {
|
|
|
1442
1240
|
this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura', 'initializing');
|
|
1443
1241
|
await this.cameraService.detectDeviceType();
|
|
1444
1242
|
await this.cameraService.enumerateDevices();
|
|
1445
|
-
this.
|
|
1243
|
+
await this.updateCameraInfoWithAutofocus();
|
|
1244
|
+
this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'initializing');
|
|
1446
1245
|
await this.loadOnnxRuntime();
|
|
1447
1246
|
this.initializeResizeObserver();
|
|
1448
1247
|
}
|
|
1449
1248
|
validateProps() {
|
|
1450
1249
|
if (this.maskSize < 50 || this.maskSize > 100) {
|
|
1451
|
-
this.logger.warn(`Propiedad maskSize inválida. Valor: ${this.maskSize}, esperado: 50-100. Usando valor por defecto: 90`);
|
|
1452
1250
|
this.maskSize = 90;
|
|
1453
1251
|
}
|
|
1454
1252
|
if (this.cropMargin < 0 || this.cropMargin > 100) {
|
|
1455
|
-
this.logger.warn(`Propiedad cropMargin inválida. Valor: ${this.cropMargin}, esperado: 0-100. Usando valor por defecto: 0`);
|
|
1456
1253
|
this.cropMargin = 0;
|
|
1457
1254
|
}
|
|
1458
1255
|
const validOptions = ['auto', 'front', 'back'];
|
|
1459
1256
|
if (!validOptions.includes(this.preferredCamera)) {
|
|
1460
|
-
this.logger.warn(`Propiedad preferredCamera inválida. Valor: ${this.preferredCamera}, esperado: ${validOptions.join(', ')}. Usando valor por defecto: 'auto'`);
|
|
1461
1257
|
this.preferredCamera = 'auto';
|
|
1462
1258
|
}
|
|
1463
1259
|
if (this.captureDelay < 0 || this.captureDelay > 10000) {
|
|
1464
|
-
this.logger.warn(`Propiedad captureDelay inválida. Valor: ${this.captureDelay}, esperado: 0-10000. Usando valor por defecto: 1500`);
|
|
1465
1260
|
this.captureDelay = 1500;
|
|
1466
1261
|
}
|
|
1467
1262
|
}
|
|
1468
1263
|
async loadOnnxRuntime() {
|
|
1469
1264
|
if (!window.ort) {
|
|
1470
|
-
this.updateStatus('
|
|
1265
|
+
this.updateStatus('Preparando herramientas...', 'Configurando funciones de captura', 'initializing');
|
|
1471
1266
|
const script = document.createElement('script');
|
|
1472
1267
|
script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
|
|
1473
1268
|
document.head.appendChild(script);
|
|
1474
1269
|
await new Promise((resolve) => {
|
|
1475
1270
|
script.onload = () => {
|
|
1476
|
-
setTimeout(() => {
|
|
1477
|
-
this.finalizeInitialization();
|
|
1271
|
+
setTimeout(async () => {
|
|
1272
|
+
await this.finalizeInitialization();
|
|
1478
1273
|
resolve(undefined);
|
|
1479
1274
|
}, 300);
|
|
1480
1275
|
};
|
|
1481
1276
|
});
|
|
1482
1277
|
}
|
|
1483
1278
|
else {
|
|
1484
|
-
setTimeout(() => {
|
|
1485
|
-
this.finalizeInitialization();
|
|
1279
|
+
setTimeout(async () => {
|
|
1280
|
+
await this.finalizeInitialization();
|
|
1486
1281
|
}, 300);
|
|
1487
1282
|
}
|
|
1488
1283
|
}
|
|
1489
|
-
finalizeInitialization() {
|
|
1284
|
+
async finalizeInitialization() {
|
|
1490
1285
|
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1286
|
+
// Check camera permissions before showing "ready" status
|
|
1287
|
+
const hasPermissions = await this.checkCameraPermissions();
|
|
1288
|
+
if (hasPermissions) {
|
|
1289
|
+
this.currentStatus = {
|
|
1290
|
+
message: 'Listo para capturar',
|
|
1291
|
+
description: '',
|
|
1292
|
+
type: 'ready',
|
|
1293
|
+
isInitialized: true
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
this.currentStatus = {
|
|
1298
|
+
message: 'Permisos de cámara requeridos',
|
|
1299
|
+
description: 'Por favor, otorgue permisos de cámara para continuar',
|
|
1300
|
+
type: 'error',
|
|
1301
|
+
isInitialized: true
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1497
1304
|
this.emitReadyEvent();
|
|
1498
1305
|
}
|
|
1499
1306
|
updateStatus(message, description, type = 'loading') {
|
|
@@ -1507,11 +1314,26 @@ const JaakStamps = class {
|
|
|
1507
1314
|
emitReadyEvent() {
|
|
1508
1315
|
const isDocumentReady = !!window.ort && this.detectionService.isModelLoaded();
|
|
1509
1316
|
this.isReady.emit(isDocumentReady);
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1317
|
+
}
|
|
1318
|
+
async checkCameraPermissions() {
|
|
1319
|
+
try {
|
|
1320
|
+
if (!navigator.permissions) {
|
|
1321
|
+
// Fallback: try to access camera directly
|
|
1322
|
+
try {
|
|
1323
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
1324
|
+
stream.getTracks().forEach(track => track.stop());
|
|
1325
|
+
return true;
|
|
1326
|
+
}
|
|
1327
|
+
catch {
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
const permission = await navigator.permissions.query({ name: 'camera' });
|
|
1332
|
+
return permission.state === 'granted';
|
|
1333
|
+
}
|
|
1334
|
+
catch (error) {
|
|
1335
|
+
return false;
|
|
1336
|
+
}
|
|
1515
1337
|
}
|
|
1516
1338
|
handleStateChange(data) {
|
|
1517
1339
|
}
|
|
@@ -1528,9 +1350,31 @@ const JaakStamps = class {
|
|
|
1528
1350
|
const container = this.detectionContainer.parentElement;
|
|
1529
1351
|
const rect = container.getBoundingClientRect();
|
|
1530
1352
|
this.updateMaskDimensions(rect);
|
|
1531
|
-
this.logger.debug('Container redimensionado:', { width: rect.width, height: rect.height });
|
|
1532
1353
|
}
|
|
1533
1354
|
}
|
|
1355
|
+
getGuideText(step) {
|
|
1356
|
+
if (step === 'completed') {
|
|
1357
|
+
return 'Proceso completado';
|
|
1358
|
+
}
|
|
1359
|
+
if (step === 'front') {
|
|
1360
|
+
// Para el frente, si el detector manual está activo, mostrar instrucciones de captura manual
|
|
1361
|
+
if (this.showManualCaptureButton) {
|
|
1362
|
+
return 'Posicione el frente de su documento en el marco y presione el botón para capturar';
|
|
1363
|
+
}
|
|
1364
|
+
// Instrucción básica de alineación
|
|
1365
|
+
return 'Alinee su identificación con el marco';
|
|
1366
|
+
}
|
|
1367
|
+
else if (step === 'back') {
|
|
1368
|
+
// Para el reverso, mostrar instrucciones específicas según el modo
|
|
1369
|
+
if (this.useDocumentDetector) {
|
|
1370
|
+
return 'Si tu documento no tiene lado trasero, da clic en el botón para continuar con el proceso';
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
return 'Posicione el reverso de su documento en el marco y capture, o salte este paso';
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
return 'Alinee su identificación con el marco';
|
|
1377
|
+
}
|
|
1534
1378
|
updateMaskDimensions(containerRect) {
|
|
1535
1379
|
if (!this.videoRef)
|
|
1536
1380
|
return;
|
|
@@ -1582,16 +1426,38 @@ const JaakStamps = class {
|
|
|
1582
1426
|
this.el.style.setProperty('--mask-center-x', `${videoCenterXPercent}%`);
|
|
1583
1427
|
this.el.style.setProperty('--mask-center-y', `${videoCenterYPercent}%`);
|
|
1584
1428
|
this.isMaskReady = true;
|
|
1585
|
-
this.logger.state('DIMENSIONES_MASCARA_ACTUALIZADAS', {
|
|
1586
|
-
video: { width: videoWidth, height: videoHeight },
|
|
1587
|
-
displayed: { width: displayedVideoWidth, height: displayedVideoHeight },
|
|
1588
|
-
mask: { widthPercent: maskWidthPercent, heightPercent: maskHeightPercent },
|
|
1589
|
-
center: { x: videoCenterXPercent, y: videoCenterYPercent },
|
|
1590
|
-
offset: { x: videoOffsetX, y: videoOffsetY }
|
|
1591
|
-
});
|
|
1592
1429
|
}
|
|
1593
1430
|
// PUBLIC METHODS
|
|
1431
|
+
// Helper method to check if component is ready for operations
|
|
1432
|
+
isComponentReady() {
|
|
1433
|
+
if (!this.currentStatus.isInitialized) {
|
|
1434
|
+
return {
|
|
1435
|
+
ready: false,
|
|
1436
|
+
message: 'El componente aún se está inicializando',
|
|
1437
|
+
status: 'initializing'
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
if (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') {
|
|
1441
|
+
return {
|
|
1442
|
+
ready: false,
|
|
1443
|
+
message: 'El componente está cargando recursos',
|
|
1444
|
+
status: 'loading'
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
if (!this.serviceContainer || !this.stateManager || !this.cameraService || !this.detectionService) {
|
|
1448
|
+
return {
|
|
1449
|
+
ready: false,
|
|
1450
|
+
message: 'Los servicios del componente no están disponibles',
|
|
1451
|
+
status: 'error'
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
return { ready: true };
|
|
1455
|
+
}
|
|
1594
1456
|
async getCapturedImages() {
|
|
1457
|
+
const readyCheck = this.isComponentReady();
|
|
1458
|
+
if (!readyCheck.ready) {
|
|
1459
|
+
throw new Error(readyCheck.message);
|
|
1460
|
+
}
|
|
1595
1461
|
const state = this.stateManager.getCaptureState();
|
|
1596
1462
|
if (state.step !== 'completed') {
|
|
1597
1463
|
throw new Error('El proceso de captura no ha sido completado');
|
|
@@ -1599,47 +1465,128 @@ const JaakStamps = class {
|
|
|
1599
1465
|
return this.stateManager.getCapturedImages();
|
|
1600
1466
|
}
|
|
1601
1467
|
async isProcessCompleted() {
|
|
1468
|
+
const readyCheck = this.isComponentReady();
|
|
1469
|
+
if (!readyCheck.ready) {
|
|
1470
|
+
return false; // Return false instead of error for this method
|
|
1471
|
+
}
|
|
1602
1472
|
return this.stateManager.isProcessCompleted();
|
|
1603
1473
|
}
|
|
1604
1474
|
async startCapture() {
|
|
1605
|
-
|
|
1475
|
+
const readyCheck = this.isComponentReady();
|
|
1476
|
+
if (!readyCheck.ready) {
|
|
1477
|
+
this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
|
|
1478
|
+
return { success: false, error: readyCheck.message };
|
|
1479
|
+
}
|
|
1480
|
+
try {
|
|
1481
|
+
await this.startDetection();
|
|
1482
|
+
return { success: true };
|
|
1483
|
+
}
|
|
1484
|
+
catch (error) {
|
|
1485
|
+
this.updateStatus('Error al iniciar captura', error.message, 'error');
|
|
1486
|
+
return { success: false, error: error.message };
|
|
1487
|
+
}
|
|
1606
1488
|
}
|
|
1607
1489
|
async stopCapture() {
|
|
1608
|
-
this.
|
|
1490
|
+
const readyCheck = this.isComponentReady();
|
|
1491
|
+
if (!readyCheck.ready) {
|
|
1492
|
+
return { success: false, error: readyCheck.message };
|
|
1493
|
+
}
|
|
1494
|
+
try {
|
|
1495
|
+
this.exitSession();
|
|
1496
|
+
return { success: true };
|
|
1497
|
+
}
|
|
1498
|
+
catch (error) {
|
|
1499
|
+
return { success: false, error: error.message };
|
|
1500
|
+
}
|
|
1609
1501
|
}
|
|
1610
1502
|
async resetCapture() {
|
|
1611
|
-
this.
|
|
1503
|
+
const readyCheck = this.isComponentReady();
|
|
1504
|
+
if (!readyCheck.ready) {
|
|
1505
|
+
return { success: false, error: readyCheck.message };
|
|
1506
|
+
}
|
|
1507
|
+
try {
|
|
1508
|
+
this.resetDetection();
|
|
1509
|
+
return { success: true };
|
|
1510
|
+
}
|
|
1511
|
+
catch (error) {
|
|
1512
|
+
return { success: false, error: error.message };
|
|
1513
|
+
}
|
|
1612
1514
|
}
|
|
1613
1515
|
async skipBackCapture() {
|
|
1614
|
-
const
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1516
|
+
const readyCheck = this.isComponentReady();
|
|
1517
|
+
if (!readyCheck.ready) {
|
|
1518
|
+
return { success: false, error: readyCheck.message };
|
|
1519
|
+
}
|
|
1520
|
+
try {
|
|
1521
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1522
|
+
const capturedImages = this.stateManager.getCapturedImages();
|
|
1523
|
+
if (captureState.step === 'back' && capturedImages.front.fullFrame) {
|
|
1524
|
+
this.completeProcess(true);
|
|
1525
|
+
return { success: true };
|
|
1526
|
+
}
|
|
1527
|
+
else {
|
|
1528
|
+
return { success: false, error: 'No se puede saltar el reverso en el estado actual' };
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
catch (error) {
|
|
1532
|
+
return { success: false, error: error.message };
|
|
1618
1533
|
}
|
|
1619
1534
|
}
|
|
1620
1535
|
async getStatus() {
|
|
1621
|
-
const
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1536
|
+
const readyCheck = this.isComponentReady();
|
|
1537
|
+
if (!readyCheck.ready) {
|
|
1538
|
+
return {
|
|
1539
|
+
isVideoActive: false,
|
|
1540
|
+
captureStep: 'front',
|
|
1541
|
+
hasImages: false,
|
|
1542
|
+
isProcessCompleted: false,
|
|
1543
|
+
isModelPreloaded: false,
|
|
1544
|
+
componentStatus: readyCheck.status,
|
|
1545
|
+
componentMessage: readyCheck.message
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
try {
|
|
1549
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1550
|
+
const capturedImages = this.stateManager.getCapturedImages();
|
|
1551
|
+
return {
|
|
1552
|
+
isVideoActive: captureState.isVideoActive,
|
|
1553
|
+
captureStep: captureState.step,
|
|
1554
|
+
hasImages: !!(capturedImages.front.fullFrame || capturedImages.back.fullFrame),
|
|
1555
|
+
isProcessCompleted: this.stateManager.isProcessCompleted(),
|
|
1556
|
+
isModelPreloaded: this.detectionService.isModelLoaded(),
|
|
1557
|
+
componentStatus: 'ready'
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
catch (error) {
|
|
1561
|
+
return {
|
|
1562
|
+
isVideoActive: false,
|
|
1563
|
+
captureStep: 'front',
|
|
1564
|
+
hasImages: false,
|
|
1565
|
+
isProcessCompleted: false,
|
|
1566
|
+
isModelPreloaded: false,
|
|
1567
|
+
componentStatus: 'error',
|
|
1568
|
+
componentMessage: error.message
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1630
1571
|
}
|
|
1631
1572
|
async preloadModel() {
|
|
1573
|
+
const readyCheck = this.isComponentReady();
|
|
1574
|
+
if (!readyCheck.ready) {
|
|
1575
|
+
this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
|
|
1576
|
+
return { success: false, error: readyCheck.message };
|
|
1577
|
+
}
|
|
1632
1578
|
if (this.detectionService.isModelLoaded()) {
|
|
1633
|
-
this.
|
|
1634
|
-
this.updateStatus('Modelos ya cargados', '', 'ready');
|
|
1579
|
+
this.updateStatus('Reconocimiento listo', '', 'ready');
|
|
1635
1580
|
return { success: true, message: 'Model already loaded' };
|
|
1636
1581
|
}
|
|
1637
1582
|
try {
|
|
1638
1583
|
const loadStartTime = performance.now();
|
|
1639
|
-
this.updateStatus('
|
|
1640
|
-
this.stateManager
|
|
1584
|
+
this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
|
|
1585
|
+
if (this.stateManager) {
|
|
1586
|
+
this.stateManager.updateCaptureState({ isLoading: true });
|
|
1587
|
+
}
|
|
1641
1588
|
await this.detectionService.loadModel();
|
|
1642
|
-
this.updateStatus('
|
|
1589
|
+
this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
|
|
1643
1590
|
await this.detectionService.loadClassificationModel();
|
|
1644
1591
|
const loadEndTime = performance.now();
|
|
1645
1592
|
const loadTime = loadEndTime - loadStartTime;
|
|
@@ -1647,80 +1594,118 @@ const JaakStamps = class {
|
|
|
1647
1594
|
if (this.debug) {
|
|
1648
1595
|
this.recordOnnxPerformance(loadTime, 0);
|
|
1649
1596
|
}
|
|
1650
|
-
this.updateStatus('
|
|
1597
|
+
this.updateStatus('Finalizando configuración...', 'Preparando herramientas para captura', 'loading');
|
|
1651
1598
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
1652
|
-
this.updateStatus('
|
|
1653
|
-
this.stateManager
|
|
1599
|
+
this.updateStatus('Reconocimiento preparado', '', 'ready');
|
|
1600
|
+
if (this.stateManager) {
|
|
1601
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1602
|
+
}
|
|
1654
1603
|
this.emitReadyEvent();
|
|
1655
|
-
this.logger.state('MODELOS_PRECARGADOS_EXITOSAMENTE', { loadTime: Math.round(loadTime) });
|
|
1656
1604
|
return { success: true, message: 'Models preloaded successfully' };
|
|
1657
1605
|
}
|
|
1658
1606
|
catch (error) {
|
|
1659
|
-
this.
|
|
1660
|
-
this.
|
|
1661
|
-
|
|
1607
|
+
this.updateStatus('Error de configuración', 'No se pudieron preparar las herramientas necesarias', 'error');
|
|
1608
|
+
if (this.stateManager) {
|
|
1609
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1610
|
+
}
|
|
1662
1611
|
return { success: false, error: error.message };
|
|
1663
1612
|
}
|
|
1664
1613
|
}
|
|
1665
1614
|
async getCameraInfo() {
|
|
1666
|
-
|
|
1615
|
+
const readyCheck = this.isComponentReady();
|
|
1616
|
+
if (!readyCheck.ready) {
|
|
1617
|
+
return {
|
|
1618
|
+
availableCameras: [],
|
|
1619
|
+
selectedCameraId: null,
|
|
1620
|
+
deviceType: 'desktop',
|
|
1621
|
+
isMultipleCamerasAvailable: false,
|
|
1622
|
+
preferredFacing: null,
|
|
1623
|
+
error: readyCheck.message
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
try {
|
|
1627
|
+
return await this.cameraService.getCameraInfo();
|
|
1628
|
+
}
|
|
1629
|
+
catch (error) {
|
|
1630
|
+
return {
|
|
1631
|
+
availableCameras: [],
|
|
1632
|
+
selectedCameraId: null,
|
|
1633
|
+
deviceType: 'desktop',
|
|
1634
|
+
isMultipleCamerasAvailable: false,
|
|
1635
|
+
preferredFacing: null,
|
|
1636
|
+
error: error.message
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1667
1639
|
}
|
|
1668
1640
|
async setPreferredCamera(camera) {
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1641
|
+
const readyCheck = this.isComponentReady();
|
|
1642
|
+
if (!readyCheck.ready) {
|
|
1643
|
+
return {
|
|
1644
|
+
success: false,
|
|
1645
|
+
error: readyCheck.message,
|
|
1646
|
+
selectedCamera: null,
|
|
1647
|
+
availableCameras: 0
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
try {
|
|
1651
|
+
this.preferredCamera = camera;
|
|
1652
|
+
this.serviceContainer.updateConfig({ preferredCamera: camera });
|
|
1653
|
+
await this.cameraService.enumerateDevices();
|
|
1654
|
+
await this.updateCameraInfoWithAutofocus();
|
|
1655
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1656
|
+
if (captureState.isVideoActive) {
|
|
1657
|
+
const selectedCameraId = this.cameraService.getSelectedCameraId();
|
|
1658
|
+
if (selectedCameraId) {
|
|
1659
|
+
await this.cameraService.switchCamera(selectedCameraId);
|
|
1660
|
+
}
|
|
1677
1661
|
}
|
|
1662
|
+
return {
|
|
1663
|
+
success: true,
|
|
1664
|
+
selectedCamera: this.cameraService.getSelectedCameraId(),
|
|
1665
|
+
availableCameras: this.cameraService.getAvailableCameras().length
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
catch (error) {
|
|
1669
|
+
return {
|
|
1670
|
+
success: false,
|
|
1671
|
+
error: error.message,
|
|
1672
|
+
selectedCamera: null,
|
|
1673
|
+
availableCameras: 0
|
|
1674
|
+
};
|
|
1678
1675
|
}
|
|
1679
|
-
return {
|
|
1680
|
-
success: true,
|
|
1681
|
-
selectedCamera: this.cameraService.getSelectedCameraId(),
|
|
1682
|
-
availableCameras: this.cameraService.getAvailableCameras().length
|
|
1683
|
-
};
|
|
1684
1676
|
}
|
|
1685
1677
|
async setCaptureDelay(delay) {
|
|
1686
1678
|
if (delay < 0 || delay > 10000) {
|
|
1687
|
-
|
|
1679
|
+
return {
|
|
1680
|
+
success: false,
|
|
1681
|
+
error: 'Capture delay must be between 0 and 10000 milliseconds',
|
|
1682
|
+
captureDelay: this.captureDelay
|
|
1683
|
+
};
|
|
1688
1684
|
}
|
|
1685
|
+
// Allow setting capture delay even during initialization
|
|
1689
1686
|
this.captureDelay = delay;
|
|
1690
|
-
this.serviceContainer
|
|
1687
|
+
if (this.serviceContainer) {
|
|
1688
|
+
this.serviceContainer.updateConfig({ captureDelay: delay });
|
|
1689
|
+
}
|
|
1691
1690
|
return {
|
|
1692
1691
|
success: true,
|
|
1693
1692
|
captureDelay: this.captureDelay
|
|
1694
1693
|
};
|
|
1695
1694
|
}
|
|
1696
1695
|
async getCaptureDelay() {
|
|
1696
|
+
// Always allow getting capture delay, even during initialization
|
|
1697
1697
|
return this.captureDelay;
|
|
1698
1698
|
}
|
|
1699
|
-
async setTorchEnabled(enabled) {
|
|
1700
|
-
const torchEnabled = await this.cameraService.setTorchEnabled(enabled, this.videoStream);
|
|
1701
|
-
return {
|
|
1702
|
-
success: torchEnabled,
|
|
1703
|
-
enabled: torchEnabled ? enabled : false
|
|
1704
|
-
};
|
|
1705
|
-
}
|
|
1706
|
-
async focusAtPoint(x, y) {
|
|
1707
|
-
const focused = await this.cameraService.focusAtPoint(x, y, this.videoStream);
|
|
1708
|
-
return {
|
|
1709
|
-
success: focused,
|
|
1710
|
-
coordinates: { x, y }
|
|
1711
|
-
};
|
|
1712
|
-
}
|
|
1713
1699
|
// DETECTION METHODS
|
|
1714
1700
|
async startDetection() {
|
|
1715
|
-
this.logger.state('INICIANDO_DETECCION');
|
|
1716
1701
|
try {
|
|
1717
1702
|
// Paso 1: Verificar y cargar modelos si es necesario
|
|
1718
1703
|
if (!this.detectionService.isModelLoaded()) {
|
|
1719
1704
|
const loadStartTime = performance.now();
|
|
1720
|
-
this.updateStatus('
|
|
1705
|
+
this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
|
|
1721
1706
|
this.stateManager.updateCaptureState({ isLoading: true });
|
|
1722
1707
|
await this.detectionService.loadModel();
|
|
1723
|
-
this.updateStatus('
|
|
1708
|
+
this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
|
|
1724
1709
|
await this.detectionService.loadClassificationModel();
|
|
1725
1710
|
const loadEndTime = performance.now();
|
|
1726
1711
|
const loadTime = loadEndTime - loadStartTime;
|
|
@@ -1728,16 +1713,21 @@ const JaakStamps = class {
|
|
|
1728
1713
|
if (this.debug) {
|
|
1729
1714
|
this.recordOnnxPerformance(loadTime, 0);
|
|
1730
1715
|
}
|
|
1731
|
-
this.logger.state('MODELOS_CARGADOS_EN_DETECCION', { loadTime: Math.round(loadTime) });
|
|
1732
1716
|
}
|
|
1733
1717
|
// Paso 2: Detectar y configurar dispositivos
|
|
1734
|
-
this.updateStatus('
|
|
1718
|
+
this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
|
|
1735
1719
|
await this.cameraService.enumerateDevices();
|
|
1720
|
+
await this.updateCameraInfoWithAutofocus();
|
|
1736
1721
|
// Paso 3: Configurar cámara seleccionada
|
|
1737
|
-
this.updateStatus('
|
|
1722
|
+
this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
|
|
1738
1723
|
const stream = await this.cameraService.setupCamera();
|
|
1739
1724
|
// Paso 4: Inicializar video y ocultar estado inmediatamente
|
|
1740
|
-
this.
|
|
1725
|
+
if (this.useDocumentDetector) {
|
|
1726
|
+
this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
|
|
1727
|
+
}
|
|
1728
|
+
else {
|
|
1729
|
+
this.updateStatus('Captura activa', 'Posicione su documento y use el botón para capturar', 'active');
|
|
1730
|
+
}
|
|
1741
1731
|
await this.initializeVideoStream(stream);
|
|
1742
1732
|
// Paso 5: Calibrar máscara
|
|
1743
1733
|
if (this.detectionContainer) {
|
|
@@ -1748,11 +1738,14 @@ const JaakStamps = class {
|
|
|
1748
1738
|
// Finalizar e iniciar captura
|
|
1749
1739
|
this.startTime = Date.now();
|
|
1750
1740
|
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1741
|
+
// Mostrar botón manual si el detector está deshabilitado
|
|
1742
|
+
if (!this.useDocumentDetector) {
|
|
1743
|
+
this.showManualCaptureButton = true;
|
|
1744
|
+
}
|
|
1751
1745
|
this.detectFrame();
|
|
1752
1746
|
}
|
|
1753
1747
|
catch (err) {
|
|
1754
|
-
this.
|
|
1755
|
-
this.updateStatus('Error al iniciar captura', 'No se pudo completar la inicialización', 'error');
|
|
1748
|
+
this.updateStatus('Error al iniciar', 'No se pudo preparar la captura', 'error');
|
|
1756
1749
|
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1757
1750
|
}
|
|
1758
1751
|
}
|
|
@@ -1762,10 +1755,6 @@ const JaakStamps = class {
|
|
|
1762
1755
|
this.videoStream = stream;
|
|
1763
1756
|
const isRear = this.cameraService.isRearCamera(stream);
|
|
1764
1757
|
this.shouldMirrorVideo = !isRear;
|
|
1765
|
-
this.logger.state('CAMARA_CONFIGURADA', {
|
|
1766
|
-
isRearCamera: isRear,
|
|
1767
|
-
shouldMirrorVideo: this.shouldMirrorVideo
|
|
1768
|
-
});
|
|
1769
1758
|
return new Promise((resolve) => {
|
|
1770
1759
|
this.videoRef.onloadedmetadata = async () => {
|
|
1771
1760
|
await this.videoRef.play();
|
|
@@ -1784,15 +1773,28 @@ const JaakStamps = class {
|
|
|
1784
1773
|
const captureState = this.stateManager.getCaptureState();
|
|
1785
1774
|
if (!this.videoRef || !this.detectionContainer || !this.detectionService.isModelLoaded())
|
|
1786
1775
|
return;
|
|
1776
|
+
// Show manual capture button when document detector is disabled
|
|
1777
|
+
if (!this.useDocumentDetector) {
|
|
1778
|
+
this.showManualCaptureButton = true;
|
|
1779
|
+
// Continue the loop for UI updates but skip detection logic
|
|
1780
|
+
if (captureState.step !== 'completed') {
|
|
1781
|
+
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
1782
|
+
}
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
else {
|
|
1786
|
+
this.showManualCaptureButton = false;
|
|
1787
|
+
}
|
|
1787
1788
|
if (captureState.isDetectionPaused) {
|
|
1788
1789
|
if (captureState.step !== 'completed') {
|
|
1789
1790
|
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
1790
1791
|
}
|
|
1791
1792
|
return;
|
|
1792
1793
|
}
|
|
1793
|
-
//
|
|
1794
|
+
// Adaptive frame skipping for performance
|
|
1794
1795
|
this.frameSkipCounter++;
|
|
1795
|
-
|
|
1796
|
+
const currentFrameSkip = this.getAdaptiveFrameSkip();
|
|
1797
|
+
if (this.frameSkipCounter <= currentFrameSkip) {
|
|
1796
1798
|
if (captureState.step !== 'completed') {
|
|
1797
1799
|
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
1798
1800
|
}
|
|
@@ -1868,7 +1870,6 @@ const JaakStamps = class {
|
|
|
1868
1870
|
}
|
|
1869
1871
|
}
|
|
1870
1872
|
catch (e) {
|
|
1871
|
-
this.logger.error('Error en inferencia de modelo:', e);
|
|
1872
1873
|
const captureState = this.stateManager.getCaptureState();
|
|
1873
1874
|
if (captureState.step !== 'completed') {
|
|
1874
1875
|
setTimeout(() => this.detectFrame(), 100);
|
|
@@ -1895,6 +1896,8 @@ const JaakStamps = class {
|
|
|
1895
1896
|
clearInterval(this.performanceUpdateInterval);
|
|
1896
1897
|
this.performanceUpdateInterval = undefined;
|
|
1897
1898
|
}
|
|
1899
|
+
// Clear canvas pool
|
|
1900
|
+
this.canvasPool = [];
|
|
1898
1901
|
this.detectionBoxes = [];
|
|
1899
1902
|
this.alignmentStartTime = undefined;
|
|
1900
1903
|
this.hasDocumentDetected = false;
|
|
@@ -1908,11 +1911,8 @@ const JaakStamps = class {
|
|
|
1908
1911
|
step: 'front',
|
|
1909
1912
|
isCapturing: false
|
|
1910
1913
|
};
|
|
1911
|
-
const cameraInfo = this.
|
|
1912
|
-
|
|
1913
|
-
selectedCameraId: null,
|
|
1914
|
-
deviceType: 'desktop'};
|
|
1915
|
-
return (index.h("div", { key: '9d4d77042218ab3d0bc4fb1ce879db09dc3e78b4', class: "detector-container" }, index.h("div", { key: '51ac8ccb1275637282b038ad8364d19f14ef61b1', class: "video-container" }, index.h("video", { key: 'a34f0481da20ec5b7b62b21aa275ec5f6dfec5d1', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), index.h("div", { key: '43b71dfe3eaf20ffa66a4139e24b1af6a2074631', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index$1) => (index.h("div", { key: index$1, class: "detection-box", style: {
|
|
1914
|
+
const cameraInfo = this.cameraInfoWithAutofocus;
|
|
1915
|
+
return (index.h("div", { key: 'e0b32b1b41b58e3a58b552199384080e8f2c069b', class: "detector-container" }, index.h("div", { key: '03c77af311ff12d936994d629d58550cbaae190c', class: "video-container" }, index.h("video", { key: '94f15b34f34367a10d17816a31cd147adb57bc90', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), index.h("div", { key: '982be192792dfaf644eb173bfee9b26ebd229288', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index$1) => (index.h("div", { key: index$1, class: "detection-box", style: {
|
|
1916
1916
|
position: 'absolute',
|
|
1917
1917
|
left: `${box.x}px`,
|
|
1918
1918
|
top: `${box.y}px`,
|
|
@@ -1921,9 +1921,9 @@ const JaakStamps = class {
|
|
|
1921
1921
|
border: '2px solid #32406C',
|
|
1922
1922
|
pointerEvents: 'none',
|
|
1923
1923
|
boxSizing: 'border-box'
|
|
1924
|
-
} })))), this.isMaskReady && (index.h("div", { key: '
|
|
1924
|
+
} })))), this.isMaskReady && (index.h("div", { key: 'f5212ff70db65d2b836a3ac456a1701701630660', class: "overlay-mask" }, index.h("div", { key: '5080217f4962c2c7152def39cc978ba1c40a3a1c', class: "card-outline" }, index.h("div", { key: 'd37f1ca8723d79b10d1c6a3afaf54e2f2657ae79', class: "side side-top" }), index.h("div", { key: 'c5bb4f7d646a991f43598688ffa909c04697bb06', class: "side side-right" }), index.h("div", { key: '4a5040b4a236367dcdf72f4e72d497b8b6dee6c0', class: "side side-bottom" }), index.h("div", { key: 'c74ecef56eee8e92cd9491d693fa89a6c01bdbbd', class: "side side-left" })), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("div", { key: '2a2932b0c03835193ebf86ddb5f13173b1dba367', class: "guide-text" }, this.getGuideText(captureState.step))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("div", { key: '41fcf2d79106d3ed928fc2842cb6a6725ba4d4f6', class: "back-capture-section" }, index.h("div", { key: '98dbe1bbe454bad42c3e155247f25a9151aaf1f9', class: "back-capture-buttons" }, !this.useDocumentDetector && (index.h("button", { key: '01693bc0d6915a4d02f318268020dfbcf3a70b94', class: "capture-button primary-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: captureState.isCapturing }, captureState.isCapturing ? 'Capturando...' : 'Capturar Reverso')), index.h("button", { key: '0a4ba55cbbea867b936a83a16a622a3c36a225f9', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
|
|
1925
1925
|
? `Saltar reverso (${this.backDocumentTimerRemaining}s)`
|
|
1926
|
-
: 'Saltar reverso'))), captureState.isVideoActive && (index.h("div", { key: '
|
|
1926
|
+
: 'Saltar reverso')))), captureState.isVideoActive && (index.h("div", { key: '1db208a53dea37211bdd88254e575142b7abf91f', class: "camera-controls" }, index.h("button", { key: '90755839358f997c3ce2f4c36a8d17d39ea5f8ad', class: `camera-selector-button ${this.isSwitchingCamera ? 'loading' : ''}`, onClick: () => this.toggleCameraSelector(), type: "button", title: "Seleccionar c\u00E1mara", disabled: this.isSwitchingCamera }, this.isSwitchingCamera ? (index.h("div", { class: "button-spinner" })) : ('Cámaras')))), this.showCameraSelector && cameraInfo.availableCameras.length > 0 && (index.h("div", { key: 'ef7889ab82f9eaabcabec3e3dc3ebb7850818dd4', class: "camera-selector-dropdown" }, index.h("div", { key: 'e28fb005316a742318be3cf8582f520392b71bba', class: "camera-selector-header" }, index.h("span", { key: '97338030b1aedb50c5f5a588ea03c0f19bcb0ddd' }, "Seleccionar C\u00E1mara"), index.h("button", { key: '1537c907dfab17c232a256f9fc9210cbd0125cc6', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), index.h("div", { key: '509178119e6eed5fe0a6ea287904a53bd9fbecbc', class: "camera-list" }, cameraInfo.availableCameras.map((camera) => (index.h("button", { key: camera.id, class: `camera-option ${cameraInfo.selectedCameraId === camera.id ? 'selected' : ''}`, onClick: () => this.handleCameraSwitch(camera.id), type: "button" }, index.h("span", { class: "camera-label" }, camera.label || `Cámara ${cameraInfo.availableCameras.indexOf(camera) + 1}`, camera.hasAutofocus && (index.h("span", { class: "autofocus-icon", title: "Autofoco disponible" }, "\u29BF"))), cameraInfo.selectedCameraId === camera.id && (index.h("span", { class: "selected-indicator" }, "\u2713")))))), index.h("div", { key: '485f7e1a97c43ed280a106bde3c6ee910625025c', class: "device-info" }, index.h("small", { key: 'd76edba943f4e06c14024923496555fa1fe6d845' }, "Dispositivo: ", cameraInfo.deviceType)))), this.showManualCaptureButton && captureState.step === 'front' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("div", { key: 'c5184dd93aa41cff61b88ea2046b481efc87c1f2', class: "manual-capture-section" }, index.h("button", { key: '6e45d9a0fec60469eb926ff353cb1a440d59f5fe', class: "manual-capture-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: captureState.isCapturing }, captureState.isCapturing ? 'Capturando...' : 'Capturar Frente'))))), captureState.isCapturing && (index.h("div", { key: '9e296a942ba51cc1fd152e76ae406620c84ed721', class: "capture-animation" })), captureState.showFlipAnimation && (index.h("div", { key: '517db50f7273788d94d5e6db6c40f65e00e18784', class: "flip-animation" }, index.h("div", { key: 'fbf6a26c4cc0539bd16b45899d19984af3449610', class: "id-card-icon" }), index.h("div", { key: 'fb3a201864d04531a078074458aa6e7f6580d565', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (index.h("div", { key: '10c694e233d26523d538ff0d2edbf6dcfb8e37c8', class: "success-animation" }, index.h("div", { key: '87e545650825ab805e887667281ecdf8bdb34c45', class: "check-icon" }), index.h("div", { key: '359da71f391d68e9437b749b949ee90685b96b3f', class: "success-text" }, "\u00A1Proceso completado!"))), index.h("div", { key: '7c5533bb875eb3d9cdcc6cfdfc20e4d5bd98d103', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (index.h("div", { key: 'f32df46ab83279387de606eadf53cd4a4009f449', class: "status-spinner" })), index.h("div", { key: 'dd640de5a3b64f5f99363318e2fc70e32ccbc7db', class: "status-content" }, index.h("div", { key: '618c0ab9cb0a24b0af3f6946052beab0d660fe6d', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (index.h("div", { key: 'e52c2a4945c7f5a190a446ac588aa1668c21f26d', class: "status-description" }, this.currentStatus.description)))), this.debug && (index.h("div", { key: '61cea10bc3438fb74aa176d71c4238b070cb0f09', class: "performance-monitor" }, index.h("div", { key: '44f6b1accb56d99a236b22c0769f870638d9c768', class: "performance-expanded" }, index.h("div", { key: '6febc85e319752b0aebc0a4e4050ea6d597375a7', class: "metrics-row" }, index.h("div", { key: '96b81895ad3cd742c96308ed53a2126af76eb619', class: "metric-compact" }, index.h("span", { key: '774a11c6fed6a87db7673b018b3cdc378c5252ef', class: "metric-label" }, "FPS"), index.h("span", { key: 'cb866dcbea661e5f715b82c4dd760f786fd5bb2a', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), index.h("div", { key: 'a935aaadb83ea400bfaa984d1d845bdfd87becc8', class: "metric-compact" }, index.h("span", { key: 'ffacaa2cb7838403a297145b70586e255cc845c7', class: "metric-label" }, "MEM"), index.h("span", { key: 'a1d7a7abec97dc0544d50b11b372f699f750b76f', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), index.h("div", { key: 'bb3d156a46569a8c290bc518c42ec7957bc5b94e', class: "metrics-row" }, index.h("div", { key: '4cadd8f845e4c12120ed549e4a73e02c6cbd9257', class: "metric-compact" }, index.h("span", { key: '1fc5661222f495b79fd7eb126e04f52eecab6fb3', class: "metric-label" }, "INF"), index.h("span", { key: 'eca1f59d1c41ab99158eec81592bc7150bfeabe1', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), index.h("div", { key: 'f708e2757adcb03e633111d96bd6786fb6b80ea6', class: "metric-compact" }, index.h("span", { key: 'a13099ded64180996d6f65bf215483ba83daccbc', class: "metric-label" }, "FRAME"), index.h("span", { key: '2765cd1f7c1a49df9de64639cc1b777c79f89f4c', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), index.h("div", { key: '13aaf85a1b4b66294c4204f02d956b64665aa766', class: "metrics-row" }, index.h("div", { key: '40012f3f646d70c535a606ebbe581a2fe6fd2e50', class: "metric-compact" }, index.h("span", { key: '03f8b17746fe404485820d06a50173678db2fd25', class: "metric-label" }, "DET"), index.h("span", { key: 'c4f87430f142355054d9361330d6a5d508974b70', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), index.h("div", { key: 'cf366a41d7f7cda6046c32cc7c514eb7b46b05d1', class: "metric-compact" }, index.h("span", { key: '925e7de15b96ba4dbbcdbf8a9f0e3173bae49573', class: "metric-label" }, "RATE"), index.h("span", { key: '9701e60c7793f2c205f3c9ffd254b6a115b8a68c', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), index.h("div", { key: '636aeb9b2b8dd992d8a28a8db44b213e12514096', class: "watermark" }, index.h("img", { key: '0407b550d962a6113e05ad75e25a1a1bbd76d9ce', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
|
|
1927
1927
|
}
|
|
1928
1928
|
// Utility methods
|
|
1929
1929
|
updateDetectionBoxes(boxes) {
|
|
@@ -2004,35 +2004,18 @@ const JaakStamps = class {
|
|
|
2004
2004
|
if (allSidesAligned && bestBox) {
|
|
2005
2005
|
cardOutline?.classList.add('perfect-match');
|
|
2006
2006
|
corners?.forEach(corner => corner.classList.add('perfect-match'));
|
|
2007
|
-
// Debug logging
|
|
2008
|
-
this.logger.state('CAPTURE_EVALUATION', {
|
|
2009
|
-
allSidesAligned: true,
|
|
2010
|
-
hasScreenshotTaken: this.hasScreenshotTaken,
|
|
2011
|
-
captureDelay: this.captureDelay,
|
|
2012
|
-
alignmentStartTime: this.alignmentStartTime
|
|
2013
|
-
});
|
|
2014
2007
|
if (!this.hasScreenshotTaken) {
|
|
2015
2008
|
const currentTime = Date.now();
|
|
2016
2009
|
// Initialize alignment start time if not set
|
|
2017
2010
|
if (!this.alignmentStartTime) {
|
|
2018
2011
|
this.alignmentStartTime = currentTime;
|
|
2019
|
-
this.logger.state('ALIGNMENT_TIMER_STARTED', { startTime: currentTime });
|
|
2020
2012
|
}
|
|
2021
2013
|
// Check if document has been aligned for the configured delay
|
|
2022
2014
|
const alignmentDuration = currentTime - this.alignmentStartTime;
|
|
2023
|
-
this.logger.state('ALIGNMENT_DURATION_CHECK', {
|
|
2024
|
-
alignmentDuration,
|
|
2025
|
-
captureDelay: this.captureDelay,
|
|
2026
|
-
readyToCapture: alignmentDuration >= this.captureDelay
|
|
2027
|
-
});
|
|
2028
2015
|
if (alignmentDuration >= this.captureDelay) {
|
|
2029
|
-
this.logger.state('TRIGGERING_CAPTURE', {
|
|
2030
|
-
alignmentDuration,
|
|
2031
|
-
captureDelay: this.captureDelay
|
|
2032
|
-
});
|
|
2033
2016
|
this.lastDetectedBox = bestBox;
|
|
2034
|
-
this.takeScreenshot().catch(
|
|
2035
|
-
|
|
2017
|
+
this.takeScreenshot().catch(() => {
|
|
2018
|
+
// Handle screenshot error silently
|
|
2036
2019
|
});
|
|
2037
2020
|
this.hasScreenshotTaken = true;
|
|
2038
2021
|
this.alignmentStartTime = undefined;
|
|
@@ -2055,24 +2038,122 @@ const JaakStamps = class {
|
|
|
2055
2038
|
}
|
|
2056
2039
|
}
|
|
2057
2040
|
}
|
|
2041
|
+
async takeManualScreenshot() {
|
|
2042
|
+
if (!this.videoRef)
|
|
2043
|
+
return;
|
|
2044
|
+
// When using manual capture, use mask coordinates for cropping
|
|
2045
|
+
await this.takeScreenshotWithMaskCoordinates();
|
|
2046
|
+
}
|
|
2047
|
+
async takeScreenshotWithMaskCoordinates() {
|
|
2048
|
+
if (!this.videoRef || !this.detectionContainer)
|
|
2049
|
+
return;
|
|
2050
|
+
this.stateManager.updateCaptureState({ isCapturing: true });
|
|
2051
|
+
this.triggerCaptureAnimation();
|
|
2052
|
+
// Use pooled canvas for optimization
|
|
2053
|
+
const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
|
|
2054
|
+
const captureCtx = captureCanvas.getContext('2d', { alpha: false });
|
|
2055
|
+
captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
|
|
2056
|
+
captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
|
|
2057
|
+
// Calculate mask coordinates for cropping
|
|
2058
|
+
const container = this.detectionContainer.parentElement;
|
|
2059
|
+
const containerRect = container.getBoundingClientRect();
|
|
2060
|
+
// Get mask dimensions from CSS properties
|
|
2061
|
+
const maskWidthPercent = parseFloat(this.el.style.getPropertyValue('--mask-width').replace('%', '')) || 90;
|
|
2062
|
+
const maskHeightPercent = parseFloat(this.el.style.getPropertyValue('--mask-height').replace('%', '')) || 56.25;
|
|
2063
|
+
const maskCenterXPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-x').replace('%', '')) || 50;
|
|
2064
|
+
const maskCenterYPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-y').replace('%', '')) || 50;
|
|
2065
|
+
// Convert percentages to actual video coordinates
|
|
2066
|
+
const videoWidth = this.videoRef.videoWidth;
|
|
2067
|
+
const videoHeight = this.videoRef.videoHeight;
|
|
2068
|
+
const videoAspectRatio = videoWidth / videoHeight;
|
|
2069
|
+
const containerAspectRatio = containerRect.width / containerRect.height;
|
|
2070
|
+
let displayedVideoWidth, displayedVideoHeight;
|
|
2071
|
+
let videoOffsetX = 0, videoOffsetY = 0;
|
|
2072
|
+
if (videoAspectRatio > containerAspectRatio) {
|
|
2073
|
+
displayedVideoWidth = containerRect.width;
|
|
2074
|
+
displayedVideoHeight = containerRect.width / videoAspectRatio;
|
|
2075
|
+
videoOffsetY = (containerRect.height - displayedVideoHeight) / 2;
|
|
2076
|
+
}
|
|
2077
|
+
else {
|
|
2078
|
+
displayedVideoHeight = containerRect.height;
|
|
2079
|
+
displayedVideoWidth = containerRect.height * videoAspectRatio;
|
|
2080
|
+
videoOffsetX = (containerRect.width - displayedVideoWidth) / 2;
|
|
2081
|
+
}
|
|
2082
|
+
// Calculate mask coordinates in video space
|
|
2083
|
+
const maskWidthInContainer = (maskWidthPercent / 100) * containerRect.width;
|
|
2084
|
+
const maskHeightInContainer = (maskHeightPercent / 100) * containerRect.height;
|
|
2085
|
+
const maskCenterXInContainer = (maskCenterXPercent / 100) * containerRect.width;
|
|
2086
|
+
const maskCenterYInContainer = (maskCenterYPercent / 100) * containerRect.height;
|
|
2087
|
+
// Convert to video coordinates
|
|
2088
|
+
const scaleX = videoWidth / displayedVideoWidth;
|
|
2089
|
+
const scaleY = videoHeight / displayedVideoHeight;
|
|
2090
|
+
const maskCenterXInVideo = (maskCenterXInContainer - videoOffsetX) * scaleX;
|
|
2091
|
+
const maskCenterYInVideo = (maskCenterYInContainer - videoOffsetY) * scaleY;
|
|
2092
|
+
const maskWidthInVideo = maskWidthInContainer * scaleX;
|
|
2093
|
+
const maskHeightInVideo = maskHeightInContainer * scaleY;
|
|
2094
|
+
// Calculate crop coordinates
|
|
2095
|
+
const cropX = Math.max(0, maskCenterXInVideo - (maskWidthInVideo / 2) - this.cropMargin);
|
|
2096
|
+
const cropY = Math.max(0, maskCenterYInVideo - (maskHeightInVideo / 2) - this.cropMargin);
|
|
2097
|
+
const cropWidth = Math.min(maskWidthInVideo + (2 * this.cropMargin), videoWidth - cropX);
|
|
2098
|
+
const cropHeight = Math.min(maskHeightInVideo + (2 * this.cropMargin), videoHeight - cropY);
|
|
2099
|
+
// Use pooled canvas for cropped version
|
|
2100
|
+
const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
|
|
2101
|
+
const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
|
|
2102
|
+
croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
|
|
2103
|
+
croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
|
|
2104
|
+
const captureState = this.stateManager.getCaptureState();
|
|
2105
|
+
if (captureState.step === 'front') {
|
|
2106
|
+
this.stateManager.setCapturedImages({
|
|
2107
|
+
front: {
|
|
2108
|
+
fullFrame: captureCanvas.toDataURL('image/png'),
|
|
2109
|
+
cropped: croppedCanvas.toDataURL('image/png')
|
|
2110
|
+
}
|
|
2111
|
+
});
|
|
2112
|
+
// Check if document classification is enabled (independent of detector)
|
|
2113
|
+
if (this.useDocumentClassification) {
|
|
2114
|
+
const classification = await this.detectionService.classifyDocument(croppedCanvas);
|
|
2115
|
+
if (classification && classification.class === 'passport') {
|
|
2116
|
+
this.completeProcess(true);
|
|
2117
|
+
this.returnCanvasToPool(captureCanvas);
|
|
2118
|
+
this.returnCanvasToPool(croppedCanvas);
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
this.stateManager.updateCaptureState({
|
|
2123
|
+
step: 'back',
|
|
2124
|
+
isDetectionPaused: true,
|
|
2125
|
+
showFlipAnimation: true
|
|
2126
|
+
});
|
|
2127
|
+
setTimeout(() => {
|
|
2128
|
+
this.stateManager.updateCaptureState({
|
|
2129
|
+
showFlipAnimation: false,
|
|
2130
|
+
isDetectionPaused: false
|
|
2131
|
+
});
|
|
2132
|
+
this.startBackDocumentTimer();
|
|
2133
|
+
}, 3000);
|
|
2134
|
+
}
|
|
2135
|
+
else if (captureState.step === 'back') {
|
|
2136
|
+
this.stateManager.setCapturedImages({
|
|
2137
|
+
back: {
|
|
2138
|
+
fullFrame: captureCanvas.toDataURL('image/png'),
|
|
2139
|
+
cropped: croppedCanvas.toDataURL('image/png')
|
|
2140
|
+
}
|
|
2141
|
+
});
|
|
2142
|
+
this.completeProcess(false);
|
|
2143
|
+
}
|
|
2144
|
+
// Return canvases to pool after use
|
|
2145
|
+
this.returnCanvasToPool(captureCanvas);
|
|
2146
|
+
this.returnCanvasToPool(croppedCanvas);
|
|
2147
|
+
}
|
|
2058
2148
|
async takeScreenshot() {
|
|
2059
2149
|
if (!this.videoRef || !this.lastDetectedBox)
|
|
2060
2150
|
return;
|
|
2061
|
-
this.logger.state('INICIANDO_CAPTURA', {
|
|
2062
|
-
captureStep: this.stateManager.getCaptureState().step,
|
|
2063
|
-
detectedBox: this.lastDetectedBox,
|
|
2064
|
-
videoResolution: {
|
|
2065
|
-
width: this.videoRef.videoWidth,
|
|
2066
|
-
height: this.videoRef.videoHeight
|
|
2067
|
-
}
|
|
2068
|
-
});
|
|
2069
2151
|
this.stateManager.updateCaptureState({ isCapturing: true });
|
|
2070
2152
|
this.triggerCaptureAnimation();
|
|
2071
|
-
//
|
|
2072
|
-
const captureCanvas =
|
|
2073
|
-
captureCanvas.width = this.videoRef.videoWidth;
|
|
2074
|
-
captureCanvas.height = this.videoRef.videoHeight;
|
|
2153
|
+
// Use pooled canvas for optimization
|
|
2154
|
+
const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
|
|
2075
2155
|
const captureCtx = captureCanvas.getContext('2d', { alpha: false });
|
|
2156
|
+
captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
|
|
2076
2157
|
captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
|
|
2077
2158
|
// Calculate crop coordinates
|
|
2078
2159
|
const INPUT_SIZE = 320;
|
|
@@ -2082,11 +2163,10 @@ const JaakStamps = class {
|
|
|
2082
2163
|
const cropY = Math.max(0, (this.lastDetectedBox.y * scaleY) - this.cropMargin);
|
|
2083
2164
|
const cropWidth = Math.min((this.lastDetectedBox.w * scaleX) + (2 * this.cropMargin), this.videoRef.videoWidth - cropX);
|
|
2084
2165
|
const cropHeight = Math.min((this.lastDetectedBox.h * scaleY) + (2 * this.cropMargin), this.videoRef.videoHeight - cropY);
|
|
2085
|
-
//
|
|
2086
|
-
const croppedCanvas =
|
|
2087
|
-
croppedCanvas.width = cropWidth;
|
|
2088
|
-
croppedCanvas.height = cropHeight;
|
|
2166
|
+
// Use pooled canvas for cropped version
|
|
2167
|
+
const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
|
|
2089
2168
|
const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
|
|
2169
|
+
croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
|
|
2090
2170
|
croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
|
|
2091
2171
|
const captureState = this.stateManager.getCaptureState();
|
|
2092
2172
|
if (captureState.step === 'front') {
|
|
@@ -2100,7 +2180,6 @@ const JaakStamps = class {
|
|
|
2100
2180
|
if (this.useDocumentClassification) {
|
|
2101
2181
|
const classification = await this.detectionService.classifyDocument(croppedCanvas);
|
|
2102
2182
|
if (classification && classification.class === 'passport') {
|
|
2103
|
-
this.logger.state('PASAPORTE_DETECTADO_SALTANDO_REVERSO', { classification: classification?.class });
|
|
2104
2183
|
this.completeProcess(true);
|
|
2105
2184
|
return;
|
|
2106
2185
|
}
|
|
@@ -2127,6 +2206,9 @@ const JaakStamps = class {
|
|
|
2127
2206
|
});
|
|
2128
2207
|
this.completeProcess(false);
|
|
2129
2208
|
}
|
|
2209
|
+
// Return canvases to pool after use
|
|
2210
|
+
this.returnCanvasToPool(captureCanvas);
|
|
2211
|
+
this.returnCanvasToPool(croppedCanvas);
|
|
2130
2212
|
}
|
|
2131
2213
|
triggerCaptureAnimation() {
|
|
2132
2214
|
const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
|
|
@@ -2155,11 +2237,6 @@ const JaakStamps = class {
|
|
|
2155
2237
|
setTimeout(() => {
|
|
2156
2238
|
this.stateManager.updateCaptureState({ showSuccessAnimation: false });
|
|
2157
2239
|
}, 3000);
|
|
2158
|
-
this.logger.state('PROCESO_COMPLETADO', {
|
|
2159
|
-
skippedBack,
|
|
2160
|
-
totalImages: capturedImages.metadata.totalImages,
|
|
2161
|
-
timestamp: new Date().toISOString()
|
|
2162
|
-
});
|
|
2163
2240
|
}
|
|
2164
2241
|
stopDetection() {
|
|
2165
2242
|
if (this.animationId) {
|
|
@@ -2168,7 +2245,6 @@ const JaakStamps = class {
|
|
|
2168
2245
|
}
|
|
2169
2246
|
this.clearBackDocumentTimer();
|
|
2170
2247
|
this.detectionBoxes = [];
|
|
2171
|
-
this.logger.state('DETECTOR_DETENIDO', { timestamp: Date.now() });
|
|
2172
2248
|
}
|
|
2173
2249
|
startBackDocumentTimer() {
|
|
2174
2250
|
if (!this.enableBackDocumentTimer)
|
|
@@ -2197,6 +2273,15 @@ const JaakStamps = class {
|
|
|
2197
2273
|
return; // Don't toggle if switching camera
|
|
2198
2274
|
this.showCameraSelector = !this.showCameraSelector;
|
|
2199
2275
|
}
|
|
2276
|
+
async updateCameraInfoWithAutofocus() {
|
|
2277
|
+
try {
|
|
2278
|
+
const cameraInfo = await this.cameraService.getCameraInfo();
|
|
2279
|
+
this.cameraInfoWithAutofocus = cameraInfo;
|
|
2280
|
+
}
|
|
2281
|
+
catch (error) {
|
|
2282
|
+
// Keep existing state if update fails
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2200
2285
|
async handleCameraSwitch(cameraId) {
|
|
2201
2286
|
if (this.isSwitchingCamera)
|
|
2202
2287
|
return; // Prevent multiple simultaneous switches
|
|
@@ -2204,10 +2289,6 @@ const JaakStamps = class {
|
|
|
2204
2289
|
// Close the selector immediately when user selects a camera
|
|
2205
2290
|
this.showCameraSelector = false;
|
|
2206
2291
|
this.isSwitchingCamera = true;
|
|
2207
|
-
this.logger.state('INICIANDO_CAMBIO_CAMARA', {
|
|
2208
|
-
from: this.cameraService.getSelectedCameraId(),
|
|
2209
|
-
to: cameraId
|
|
2210
|
-
});
|
|
2211
2292
|
// Stop current video stream
|
|
2212
2293
|
if (this.videoStream) {
|
|
2213
2294
|
this.videoStream.getTracks().forEach(track => track.stop());
|
|
@@ -2218,21 +2299,17 @@ const JaakStamps = class {
|
|
|
2218
2299
|
const newStream = await this.cameraService.setupCamera();
|
|
2219
2300
|
// Update video element
|
|
2220
2301
|
await this.initializeVideoStream(newStream);
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
isRearCamera: this.cameraService.isRearCamera(newStream)
|
|
2224
|
-
});
|
|
2302
|
+
// Update camera info with autofocus data
|
|
2303
|
+
await this.updateCameraInfoWithAutofocus();
|
|
2225
2304
|
}
|
|
2226
2305
|
catch (error) {
|
|
2227
|
-
this.logger.error('Error al cambiar cámara:', error);
|
|
2228
2306
|
// Try to restore previous camera if switch failed
|
|
2229
2307
|
try {
|
|
2230
2308
|
const fallbackStream = await this.cameraService.setupCamera();
|
|
2231
2309
|
await this.initializeVideoStream(fallbackStream);
|
|
2232
2310
|
}
|
|
2233
2311
|
catch (fallbackError) {
|
|
2234
|
-
this.
|
|
2235
|
-
this.updateStatus('Error al cambiar cámara', 'No se pudo completar el cambio de dispositivo', 'error');
|
|
2312
|
+
this.updateStatus('Error al cambiar cámara', 'No se pudo cambiar el dispositivo', 'error');
|
|
2236
2313
|
}
|
|
2237
2314
|
}
|
|
2238
2315
|
finally {
|
|
@@ -2273,7 +2350,7 @@ const JaakStamps = class {
|
|
|
2273
2350
|
this.stateManager.updateCaptureState({ isVideoActive: false, isLoading: false });
|
|
2274
2351
|
}
|
|
2275
2352
|
this.isMaskReady = false;
|
|
2276
|
-
this.updateStatus('
|
|
2353
|
+
this.updateStatus('Captura finalizada', '', 'ready');
|
|
2277
2354
|
this.detectionBoxes = [];
|
|
2278
2355
|
this.cleanup();
|
|
2279
2356
|
}
|
|
@@ -2284,7 +2361,6 @@ const JaakStamps = class {
|
|
|
2284
2361
|
this.performanceUpdateInterval = window.setInterval(() => {
|
|
2285
2362
|
this.updatePerformanceMetrics();
|
|
2286
2363
|
}, 500);
|
|
2287
|
-
this.logger.debug('Monitor de performance inicializado');
|
|
2288
2364
|
}
|
|
2289
2365
|
updatePerformanceMetrics() {
|
|
2290
2366
|
const currentTime = performance.now();
|
|
@@ -2327,6 +2403,66 @@ const JaakStamps = class {
|
|
|
2327
2403
|
if (detectionsFound > 0) {
|
|
2328
2404
|
this.performanceMetrics.successfulDetections++;
|
|
2329
2405
|
}
|
|
2406
|
+
// Update performance history for adaptive frame skipping
|
|
2407
|
+
this.performanceHistory.push(processingTime);
|
|
2408
|
+
if (this.performanceHistory.length > this.PERFORMANCE_HISTORY_SIZE) {
|
|
2409
|
+
this.performanceHistory.shift();
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
// Adaptive frame skipping based on performance
|
|
2413
|
+
getAdaptiveFrameSkip() {
|
|
2414
|
+
// Base conditions for high frame skip
|
|
2415
|
+
if (this.consecutiveFailures > 20)
|
|
2416
|
+
return this.MAX_FRAME_SKIP;
|
|
2417
|
+
if (this.performanceMetrics.inferenceTime > 200)
|
|
2418
|
+
return 6;
|
|
2419
|
+
if (this.performanceMetrics.memoryUsage > 200)
|
|
2420
|
+
return 5;
|
|
2421
|
+
// Calculate average processing time
|
|
2422
|
+
if (this.performanceHistory.length > 0) {
|
|
2423
|
+
const avgProcessingTime = this.performanceHistory.reduce((a, b) => a + b, 0) / this.performanceHistory.length;
|
|
2424
|
+
if (avgProcessingTime > 100)
|
|
2425
|
+
return 4;
|
|
2426
|
+
if (avgProcessingTime > 80)
|
|
2427
|
+
return 3;
|
|
2428
|
+
if (avgProcessingTime > 60)
|
|
2429
|
+
return this.BASE_FRAME_SKIP + 1;
|
|
2430
|
+
}
|
|
2431
|
+
// Low memory devices get higher frame skip
|
|
2432
|
+
if (this.performanceMetrics.memoryUsage > 150)
|
|
2433
|
+
return 4;
|
|
2434
|
+
// Performance is good, use base frame skip
|
|
2435
|
+
return this.BASE_FRAME_SKIP;
|
|
2436
|
+
}
|
|
2437
|
+
// Canvas pool management for screenshots
|
|
2438
|
+
getPooledCanvas(width, height) {
|
|
2439
|
+
// Try to find a canvas with matching dimensions
|
|
2440
|
+
const matchingIndex = this.canvasPool.findIndex(canvas => canvas.width === width && canvas.height === height);
|
|
2441
|
+
if (matchingIndex !== -1) {
|
|
2442
|
+
return this.canvasPool.splice(matchingIndex, 1)[0];
|
|
2443
|
+
}
|
|
2444
|
+
// If no matching canvas, try to reuse any canvas and resize
|
|
2445
|
+
if (this.canvasPool.length > 0) {
|
|
2446
|
+
const canvas = this.canvasPool.pop();
|
|
2447
|
+
canvas.width = width;
|
|
2448
|
+
canvas.height = height;
|
|
2449
|
+
return canvas;
|
|
2450
|
+
}
|
|
2451
|
+
// Create new canvas if pool is empty
|
|
2452
|
+
const canvas = document.createElement('canvas');
|
|
2453
|
+
canvas.width = width;
|
|
2454
|
+
canvas.height = height;
|
|
2455
|
+
return canvas;
|
|
2456
|
+
}
|
|
2457
|
+
returnCanvasToPool(canvas) {
|
|
2458
|
+
// Only return to pool if not at max capacity
|
|
2459
|
+
if (this.canvasPool.length < this.MAX_CANVAS_POOL_SIZE) {
|
|
2460
|
+
// Clear the canvas before returning to pool
|
|
2461
|
+
const ctx = canvas.getContext('2d');
|
|
2462
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
2463
|
+
this.canvasPool.push(canvas);
|
|
2464
|
+
}
|
|
2465
|
+
// If pool is full, let the canvas be garbage collected
|
|
2330
2466
|
}
|
|
2331
2467
|
};
|
|
2332
2468
|
JaakStamps.style = myComponentCss;
|