@jaak.ai/stamps 2.0.0 → 2.1.0-dev.10
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/app-globals-V2Kpy_OQ.js +8 -0
- package/dist/cjs/app-globals-V2Kpy_OQ.js.map +1 -0
- package/dist/cjs/{index-BfhtOB0D.js → index-Ga0t6BMe.js} +176 -119
- package/dist/cjs/index-Ga0t6BMe.js.map +1 -0
- package/dist/cjs/jaak-stamps-webcomponent.cjs.js +5 -4
- package/dist/cjs/jaak-stamps-webcomponent.cjs.js.map +1 -1
- package/dist/cjs/jaak-stamps.cjs.entry.js +1174 -655
- 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 +4 -3
- package/dist/cjs/loader.cjs.js.map +1 -1
- package/dist/collection/collection-manifest.json +1 -1
- package/dist/collection/components/my-component/my-component.css +224 -13
- package/dist/collection/components/my-component/my-component.js +950 -281
- 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 +100 -42
- 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/index.js +165 -113
- package/dist/components/index.js.map +1 -1
- package/dist/components/jaak-stamps.js +1182 -658
- package/dist/components/jaak-stamps.js.map +1 -1
- package/dist/esm/app-globals-DQuL1Twl.js +6 -0
- package/dist/esm/app-globals-DQuL1Twl.js.map +1 -0
- package/dist/esm/{index-BP1Q4KOg.js → index-Drbzcuq-.js} +177 -119
- package/dist/esm/index-Drbzcuq-.js.map +1 -0
- package/dist/esm/jaak-stamps-webcomponent.js +5 -4
- package/dist/esm/jaak-stamps-webcomponent.js.map +1 -1
- package/dist/esm/jaak-stamps.entry.js +1174 -655
- package/dist/esm/jaak-stamps.entry.js.map +1 -1
- package/dist/esm/loader.js +4 -3
- package/dist/esm/loader.js.map +1 -1
- package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
- package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js.map +1 -1
- package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
- package/dist/jaak-stamps-webcomponent/loader.esm.js.map +1 -1
- package/dist/jaak-stamps-webcomponent/p-5f7301f1.entry.js +2 -0
- package/dist/jaak-stamps-webcomponent/p-5f7301f1.entry.js.map +1 -0
- package/dist/jaak-stamps-webcomponent/p-DQuL1Twl.js +2 -0
- package/dist/jaak-stamps-webcomponent/p-DQuL1Twl.js.map +1 -0
- package/dist/jaak-stamps-webcomponent/p-Drbzcuq-.js +3 -0
- package/dist/jaak-stamps-webcomponent/p-Drbzcuq-.js.map +1 -0
- package/dist/types/components/my-component/my-component.d.ts +87 -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/stencil-public-runtime.d.ts +12 -5
- package/dist/types/types/component-types.d.ts +3 -0
- package/package.json +4 -2
- package/dist/cjs/index-BfhtOB0D.js.map +0 -1
- 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/esm/index-BP1Q4KOg.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/jaak-stamps-webcomponent/p-BP1Q4KOg.js +0 -3
- package/dist/jaak-stamps-webcomponent/p-BP1Q4KOg.js.map +0 -1
- package/dist/types/services/LoggerService.d.ts +0 -12
- package/dist/types/services/interfaces/ILogger.d.ts +0 -8
|
@@ -1,46 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var index = require('./index-
|
|
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
|
-
}
|
|
3
|
+
var index = require('./index-Ga0t6BMe.js');
|
|
44
4
|
|
|
45
5
|
class EventBusService {
|
|
46
6
|
events = new Map();
|
|
@@ -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();
|
|
423
339
|
}
|
|
424
340
|
}
|
|
425
|
-
|
|
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;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
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,37 @@ 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
|
-
|
|
901
|
-
|
|
675
|
+
// Múltiples tipos de errores de memoria que pueden ocurrir
|
|
676
|
+
const isMemoryError = error.message.includes('failed to allocate a buffer') ||
|
|
677
|
+
error.message.includes('Out of memory') ||
|
|
678
|
+
error.message.includes('RangeError: Out of memory') ||
|
|
679
|
+
error.message.includes('no available backend found');
|
|
680
|
+
if (isMemoryError) {
|
|
902
681
|
const fallbackOptions = {
|
|
903
682
|
executionProviders: ['wasm'],
|
|
904
683
|
graphOptimizationLevel: 'disabled',
|
|
@@ -908,18 +687,25 @@ class DetectionService {
|
|
|
908
687
|
executionMode: 'sequential',
|
|
909
688
|
interOpNumThreads: 1,
|
|
910
689
|
intraOpNumThreads: 1,
|
|
690
|
+
// Configuraciones adicionales para dispositivos con poca memoria
|
|
691
|
+
enableProfiling: false,
|
|
692
|
+
sessionLogSeverityLevel: 4,
|
|
693
|
+
sessionLogVerbosityLevel: 0,
|
|
911
694
|
};
|
|
912
|
-
|
|
695
|
+
try {
|
|
696
|
+
this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, fallbackOptions);
|
|
697
|
+
}
|
|
698
|
+
catch (fallbackError) {
|
|
699
|
+
throw new Error(`no available backend found. ERR: [wasm] RangeError: Out of memory`);
|
|
700
|
+
}
|
|
913
701
|
}
|
|
914
702
|
else {
|
|
915
703
|
throw error;
|
|
916
704
|
}
|
|
917
705
|
}
|
|
918
706
|
this.modelLoaded = true;
|
|
919
|
-
this.logger.state('MODELO_DETECCION_CARGADO_EXITOSAMENTE', { sessionCreated: !!this.session });
|
|
920
707
|
}
|
|
921
708
|
catch (error) {
|
|
922
|
-
this.logger.error('Error al precargar modelo de detección:', error);
|
|
923
709
|
throw error;
|
|
924
710
|
}
|
|
925
711
|
}
|
|
@@ -928,19 +714,14 @@ class DetectionService {
|
|
|
928
714
|
return;
|
|
929
715
|
}
|
|
930
716
|
try {
|
|
931
|
-
this.logger.state('CARGANDO_MODELO_MOBILENET', { path: this.MOBILENET_MODEL_PATH });
|
|
932
717
|
// Try to load class map (optional - SqueezeNet may not need it for basic classification)
|
|
933
718
|
try {
|
|
934
719
|
const classResponse = await fetch(this.MOBILENET_CLASSES_PATH);
|
|
935
720
|
if (classResponse.ok) {
|
|
936
721
|
this.mobileNetClassMap = await classResponse.json();
|
|
937
|
-
this.logger.state('CLASES_MOBILENET_CARGADAS', {
|
|
938
|
-
classCount: Object.keys(this.mobileNetClassMap).length
|
|
939
|
-
});
|
|
940
722
|
}
|
|
941
723
|
}
|
|
942
724
|
catch (error) {
|
|
943
|
-
this.logger.warn('No se pudo cargar el mapa de clases de MobileNet, usando clasificación básica');
|
|
944
725
|
// Create a basic class map for document types
|
|
945
726
|
this.mobileNetClassMap = {
|
|
946
727
|
"0": "document",
|
|
@@ -956,8 +737,12 @@ class DetectionService {
|
|
|
956
737
|
this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, sessionOptions);
|
|
957
738
|
}
|
|
958
739
|
catch (error) {
|
|
959
|
-
|
|
960
|
-
|
|
740
|
+
// Múltiples tipos de errores de memoria que pueden ocurrir
|
|
741
|
+
const isMemoryError = error.message.includes('failed to allocate a buffer') ||
|
|
742
|
+
error.message.includes('Out of memory') ||
|
|
743
|
+
error.message.includes('RangeError: Out of memory') ||
|
|
744
|
+
error.message.includes('no available backend found');
|
|
745
|
+
if (isMemoryError) {
|
|
961
746
|
const fallbackOptions = {
|
|
962
747
|
executionProviders: ['wasm'],
|
|
963
748
|
graphOptimizationLevel: 'disabled',
|
|
@@ -967,23 +752,32 @@ class DetectionService {
|
|
|
967
752
|
executionMode: 'sequential',
|
|
968
753
|
interOpNumThreads: 1,
|
|
969
754
|
intraOpNumThreads: 1,
|
|
755
|
+
// Configuraciones adicionales para dispositivos con poca memoria
|
|
756
|
+
enableProfiling: false,
|
|
757
|
+
sessionLogSeverityLevel: 4,
|
|
758
|
+
sessionLogVerbosityLevel: 0,
|
|
970
759
|
};
|
|
971
|
-
|
|
760
|
+
try {
|
|
761
|
+
this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, fallbackOptions);
|
|
762
|
+
}
|
|
763
|
+
catch (fallbackError) {
|
|
764
|
+
throw new Error(`no available backend found. ERR: [wasm] RangeError: Out of memory`);
|
|
765
|
+
}
|
|
972
766
|
}
|
|
973
767
|
else {
|
|
974
768
|
throw error;
|
|
975
769
|
}
|
|
976
770
|
}
|
|
977
|
-
this.logger.state('MODELO_MOBILENET_CARGADO_EXITOSAMENTE', {
|
|
978
|
-
sessionCreated: !!this.mobileNetSession
|
|
979
|
-
});
|
|
980
771
|
}
|
|
981
772
|
catch (error) {
|
|
982
|
-
this.logger.error('Error al cargar modelo MobileNet:', error);
|
|
983
773
|
throw error;
|
|
984
774
|
}
|
|
985
775
|
}
|
|
986
776
|
isModelLoaded() {
|
|
777
|
+
if (!this.useDocumentDetector) {
|
|
778
|
+
// If detector is disabled, consider it "loaded" for compatibility
|
|
779
|
+
return this.modelLoaded;
|
|
780
|
+
}
|
|
987
781
|
return this.modelLoaded && !!this.session;
|
|
988
782
|
}
|
|
989
783
|
preprocess(video) {
|
|
@@ -1008,6 +802,10 @@ class DetectionService {
|
|
|
1008
802
|
return new window.ort.Tensor("float16", float16Data, [1, 3, this.INPUT_SIZE, this.INPUT_SIZE]);
|
|
1009
803
|
}
|
|
1010
804
|
async runInference(inputTensor) {
|
|
805
|
+
if (!this.useDocumentDetector) {
|
|
806
|
+
// Return empty array when document detector is disabled
|
|
807
|
+
return [];
|
|
808
|
+
}
|
|
1011
809
|
if (!this.session) {
|
|
1012
810
|
throw new Error('Detection model not loaded');
|
|
1013
811
|
}
|
|
@@ -1032,11 +830,9 @@ class DetectionService {
|
|
|
1032
830
|
}
|
|
1033
831
|
async classifyDocument(canvas) {
|
|
1034
832
|
if (!this.mobileNetSession || !this.mobileNetClassMap) {
|
|
1035
|
-
this.logger.warn('Modelo MobileNet no está cargado, saltando clasificación');
|
|
1036
833
|
return null;
|
|
1037
834
|
}
|
|
1038
835
|
try {
|
|
1039
|
-
this.logger.state('CLASIFICANDO_DOCUMENTO', { timestamp: Date.now() });
|
|
1040
836
|
const inputTensor = this.preprocessMobileNet(canvas);
|
|
1041
837
|
const feeds = { [this.mobileNetSession.inputNames[0]]: inputTensor };
|
|
1042
838
|
const results = await this.mobileNetSession.run(feeds);
|
|
@@ -1044,13 +840,6 @@ class DetectionService {
|
|
|
1044
840
|
const maxIdx = output.reduce((bestIdx, val, idx, arr) => val > arr[bestIdx] ? idx : bestIdx, 0);
|
|
1045
841
|
const confidence = output[maxIdx];
|
|
1046
842
|
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
843
|
return {
|
|
1055
844
|
class: className,
|
|
1056
845
|
confidence: confidence,
|
|
@@ -1058,7 +847,6 @@ class DetectionService {
|
|
|
1058
847
|
};
|
|
1059
848
|
}
|
|
1060
849
|
catch (error) {
|
|
1061
|
-
this.logger.error('Error al clasificar documento:', error);
|
|
1062
850
|
return null;
|
|
1063
851
|
}
|
|
1064
852
|
}
|
|
@@ -1171,7 +959,6 @@ class DetectionService {
|
|
|
1171
959
|
}
|
|
1172
960
|
this.mobileNetClassMap = undefined;
|
|
1173
961
|
this.modelLoaded = false;
|
|
1174
|
-
this.logger.state('DETECCION_SERVICE_LIMPIADO', { timestamp: Date.now() });
|
|
1175
962
|
}
|
|
1176
963
|
initializeCanvasPool() {
|
|
1177
964
|
this.preprocessCanvas = document.createElement('canvas');
|
|
@@ -1182,9 +969,6 @@ class DetectionService {
|
|
|
1182
969
|
willReadFrequently: true
|
|
1183
970
|
});
|
|
1184
971
|
this.captureCanvas = document.createElement('canvas');
|
|
1185
|
-
this.logger.state('CANVAS_POOL_INICIALIZADO', {
|
|
1186
|
-
preprocessCanvasSize: this.INPUT_SIZE
|
|
1187
|
-
});
|
|
1188
972
|
}
|
|
1189
973
|
cleanupCanvasPool() {
|
|
1190
974
|
if (this.preprocessCanvas) {
|
|
@@ -1220,10 +1004,11 @@ class DetectionService {
|
|
|
1220
1004
|
return (sign << 15) | (newExp << 10) | (frac >> 13);
|
|
1221
1005
|
}
|
|
1222
1006
|
preprocessMobileNet(canvas) {
|
|
1223
|
-
|
|
1224
|
-
tempCanvas
|
|
1225
|
-
tempCanvas.height = 224;
|
|
1007
|
+
// Use pooled canvas instead of creating new one
|
|
1008
|
+
const tempCanvas = this.getCanvas(224, 224);
|
|
1226
1009
|
const tempCtx = tempCanvas.getContext('2d');
|
|
1010
|
+
// Clear canvas before use
|
|
1011
|
+
tempCtx.clearRect(0, 0, 224, 224);
|
|
1227
1012
|
// Resize image to 224x224 for SqueezeNet (using MobileNet interface)
|
|
1228
1013
|
tempCtx.drawImage(canvas, 0, 0, 224, 224);
|
|
1229
1014
|
const imageData = tempCtx.getImageData(0, 0, 224, 224);
|
|
@@ -1239,8 +1024,53 @@ class DetectionService {
|
|
|
1239
1024
|
// Blue channel
|
|
1240
1025
|
arr[2 * hw + i] = data[i * 4 + 2] / 255.0;
|
|
1241
1026
|
}
|
|
1027
|
+
// Return canvas to pool after use
|
|
1028
|
+
this.returnCanvas(tempCanvas);
|
|
1242
1029
|
return new window.ort.Tensor('float32', arr, [1, 3, 224, 224]);
|
|
1243
1030
|
}
|
|
1031
|
+
// Canvas pool management methods for optimization
|
|
1032
|
+
getCanvas(width, height) {
|
|
1033
|
+
const key = `${width}x${height}`;
|
|
1034
|
+
if (!DetectionService.canvasPool.has(key)) {
|
|
1035
|
+
DetectionService.canvasPool.set(key, []);
|
|
1036
|
+
}
|
|
1037
|
+
const pool = DetectionService.canvasPool.get(key);
|
|
1038
|
+
let canvas = pool.pop();
|
|
1039
|
+
if (!canvas) {
|
|
1040
|
+
canvas = document.createElement('canvas');
|
|
1041
|
+
canvas.width = width;
|
|
1042
|
+
canvas.height = height;
|
|
1043
|
+
}
|
|
1044
|
+
else {
|
|
1045
|
+
// Clear the canvas before reuse
|
|
1046
|
+
const ctx = canvas.getContext('2d');
|
|
1047
|
+
ctx.clearRect(0, 0, width, height);
|
|
1048
|
+
}
|
|
1049
|
+
return canvas;
|
|
1050
|
+
}
|
|
1051
|
+
returnCanvas(canvas) {
|
|
1052
|
+
const key = `${canvas.width}x${canvas.height}`;
|
|
1053
|
+
if (!DetectionService.canvasPool.has(key)) {
|
|
1054
|
+
DetectionService.canvasPool.set(key, []);
|
|
1055
|
+
}
|
|
1056
|
+
const pool = DetectionService.canvasPool.get(key);
|
|
1057
|
+
// Only return to pool if not at max capacity
|
|
1058
|
+
if (pool.length < DetectionService.MAX_POOL_SIZE) {
|
|
1059
|
+
pool.push(canvas);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
// Static method to clear all canvas pools (useful for memory management)
|
|
1063
|
+
static clearCanvasPools() {
|
|
1064
|
+
DetectionService.canvasPool.clear();
|
|
1065
|
+
}
|
|
1066
|
+
// Method to get pool statistics for debugging
|
|
1067
|
+
getPoolStats() {
|
|
1068
|
+
const stats = {};
|
|
1069
|
+
DetectionService.canvasPool.forEach((pool, key) => {
|
|
1070
|
+
stats[key] = pool.length;
|
|
1071
|
+
});
|
|
1072
|
+
return stats;
|
|
1073
|
+
}
|
|
1244
1074
|
}
|
|
1245
1075
|
|
|
1246
1076
|
class ServiceContainer {
|
|
@@ -1251,13 +1081,11 @@ class ServiceContainer {
|
|
|
1251
1081
|
initializeServices(config) {
|
|
1252
1082
|
// Initialize services in dependency order
|
|
1253
1083
|
const eventBus = new EventBusService();
|
|
1254
|
-
const logger = new LoggerService(config.debug);
|
|
1255
1084
|
const stateManager = new StateManagerService(eventBus);
|
|
1256
|
-
const cameraService = new CameraService(
|
|
1257
|
-
const detectionService = new DetectionService(
|
|
1085
|
+
const cameraService = new CameraService(eventBus, config.preferredCamera);
|
|
1086
|
+
const detectionService = new DetectionService(config.debug, config.useDocumentClassification, config.useDocumentDetector);
|
|
1258
1087
|
// Register services
|
|
1259
1088
|
this.services.set('eventBus', eventBus);
|
|
1260
|
-
this.services.set('logger', logger);
|
|
1261
1089
|
this.services.set('stateManager', stateManager);
|
|
1262
1090
|
this.services.set('cameraService', cameraService);
|
|
1263
1091
|
this.services.set('detectionService', detectionService);
|
|
@@ -1269,9 +1097,6 @@ class ServiceContainer {
|
|
|
1269
1097
|
}
|
|
1270
1098
|
return service;
|
|
1271
1099
|
}
|
|
1272
|
-
getLogger() {
|
|
1273
|
-
return this.get('logger');
|
|
1274
|
-
}
|
|
1275
1100
|
getEventBus() {
|
|
1276
1101
|
return this.get('eventBus');
|
|
1277
1102
|
}
|
|
@@ -1285,10 +1110,6 @@ class ServiceContainer {
|
|
|
1285
1110
|
return this.get('detectionService');
|
|
1286
1111
|
}
|
|
1287
1112
|
updateConfig(config) {
|
|
1288
|
-
if (config.debug !== undefined) {
|
|
1289
|
-
const logger = this.services.get('logger');
|
|
1290
|
-
logger.setDebugMode(config.debug);
|
|
1291
|
-
}
|
|
1292
1113
|
}
|
|
1293
1114
|
cleanup() {
|
|
1294
1115
|
// Cleanup services in reverse order
|
|
@@ -1298,7 +1119,7 @@ class ServiceContainer {
|
|
|
1298
1119
|
}
|
|
1299
1120
|
}
|
|
1300
1121
|
|
|
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}}";
|
|
1122
|
+
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,.capture-button.primary-button:disabled{cursor:not-allowed !important;transform:none !important;opacity:0.7 !important;transition:none !important}@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}.capture-button:disabled,.capture-button.primary-button:disabled,.skip-button:disabled{opacity:0.7 !important;cursor:not-allowed !important}}.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)}}.guide-text.performance-warning-text{animation:gentlePulse 2s ease-in-out infinite;background:rgba(255, 107, 107, 0.3);border:1px solid rgba(255, 107, 107, 0.5)}@keyframes gentlePulse{0%,100%{transform:translate(-50%, -50%) scale(1);background:rgba(255, 107, 107, 0.3)}50%{transform:translate(-50%, -50%) scale(1.02);background:rgba(255, 107, 107, 0.4)}}.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)}.skip-button:disabled{cursor:not-allowed !important;transform:none !important;opacity:0.7 !important;transition:none !important}.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(0, 0, 0, 0.2);border-top:2px solid #333333;border-radius:50%;animation:spin 1s linear infinite;display:inline-block;margin-right:8px;vertical-align:middle}@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{cursor:not-allowed !important;transform:none !important;opacity:0.7 !important;transition:none !important}@media (max-width: 480px){.manual-capture-section{bottom:16px;max-width:280px}.manual-capture-button{padding:10px 20px;font-size:13px}.manual-capture-button:disabled{opacity:0.7 !important;cursor:not-allowed !important}}";
|
|
1302
1123
|
|
|
1303
1124
|
const JaakStamps = class {
|
|
1304
1125
|
constructor(hostRef) {
|
|
@@ -1312,6 +1133,7 @@ const JaakStamps = class {
|
|
|
1312
1133
|
maskSize = 90;
|
|
1313
1134
|
cropMargin = 20;
|
|
1314
1135
|
useDocumentClassification = false;
|
|
1136
|
+
useDocumentDetector = true;
|
|
1315
1137
|
preferredCamera = 'auto';
|
|
1316
1138
|
captureDelay = 1500;
|
|
1317
1139
|
enableBackDocumentTimer = false;
|
|
@@ -1328,9 +1150,16 @@ const JaakStamps = class {
|
|
|
1328
1150
|
showCameraSelector = false;
|
|
1329
1151
|
isSwitchingCamera = false;
|
|
1330
1152
|
hasDocumentDetected = false;
|
|
1153
|
+
cameraInfoWithAutofocus = {
|
|
1154
|
+
availableCameras: [],
|
|
1155
|
+
selectedCameraId: null,
|
|
1156
|
+
deviceType: 'desktop',
|
|
1157
|
+
isMultipleCamerasAvailable: false,
|
|
1158
|
+
preferredFacing: null
|
|
1159
|
+
};
|
|
1331
1160
|
currentStatus = {
|
|
1332
|
-
message: '
|
|
1333
|
-
description: 'Configurando
|
|
1161
|
+
message: 'Preparando capturador...',
|
|
1162
|
+
description: 'Configurando herramientas de captura',
|
|
1334
1163
|
type: 'initializing',
|
|
1335
1164
|
isInitialized: false
|
|
1336
1165
|
};
|
|
@@ -1345,9 +1174,13 @@ const JaakStamps = class {
|
|
|
1345
1174
|
detectionRate: 0
|
|
1346
1175
|
};
|
|
1347
1176
|
backDocumentTimerRemaining = 0;
|
|
1177
|
+
showManualCaptureButton = false;
|
|
1178
|
+
performanceDegradedMode = false; // Auto-switched to manual mode due to performance
|
|
1179
|
+
showPerformanceMessage = false; // Show performance message temporarily
|
|
1180
|
+
captureStateVersion = 0; // Triggers re-render when StateManager changes
|
|
1181
|
+
processingButton = null; // Track which button is processing
|
|
1348
1182
|
// Services
|
|
1349
1183
|
serviceContainer;
|
|
1350
|
-
logger;
|
|
1351
1184
|
eventBus;
|
|
1352
1185
|
stateManager;
|
|
1353
1186
|
cameraService;
|
|
@@ -1364,12 +1197,12 @@ const JaakStamps = class {
|
|
|
1364
1197
|
alignmentStartTime;
|
|
1365
1198
|
alignmentTimer;
|
|
1366
1199
|
backDocumentTimer;
|
|
1200
|
+
orientationTimer;
|
|
1367
1201
|
// Performance monitoring
|
|
1368
1202
|
performanceMetrics = {
|
|
1369
1203
|
fps: 0,
|
|
1370
1204
|
inferenceTime: 0,
|
|
1371
1205
|
memoryUsage: 0,
|
|
1372
|
-
cpuUsage: 0,
|
|
1373
1206
|
onnxLoadTime: 0,
|
|
1374
1207
|
frameProcessingTime: 0,
|
|
1375
1208
|
totalDetections: 0,
|
|
@@ -1378,22 +1211,39 @@ const JaakStamps = class {
|
|
|
1378
1211
|
lastUpdateTime: 0
|
|
1379
1212
|
};
|
|
1380
1213
|
performanceUpdateInterval;
|
|
1381
|
-
// Performance optimization
|
|
1214
|
+
// Performance optimization with adaptive frame skipping
|
|
1382
1215
|
frameSkipCounter = 0;
|
|
1383
|
-
|
|
1216
|
+
BASE_FRAME_SKIP = 2;
|
|
1217
|
+
MAX_FRAME_SKIP = 8;
|
|
1384
1218
|
consecutiveFailures = 0;
|
|
1385
1219
|
MAX_FAILURES = 30;
|
|
1386
1220
|
lastInferenceTime = 0;
|
|
1387
1221
|
MIN_INFERENCE_INTERVAL = 50;
|
|
1222
|
+
performanceHistory = [];
|
|
1223
|
+
PERFORMANCE_HISTORY_SIZE = 10;
|
|
1224
|
+
// Sistema simplificado de monitoreo de rendimiento
|
|
1225
|
+
PERFORMANCE_THRESHOLD_MS = 2000; // Umbral único para todos los frames
|
|
1226
|
+
SLOW_FRAMES_TO_TRIGGER = 2; // Cuántos frames lentos consecutivos antes de cambiar
|
|
1227
|
+
slowFrameCount = 0; // Contador de frames consecutivos lentos
|
|
1228
|
+
processedFramesCount = 0; // Contador total de frames procesados
|
|
1229
|
+
hasAutoSwitchedToManual = false;
|
|
1230
|
+
// Canvas pool for optimized screenshot capture
|
|
1231
|
+
canvasPool = [];
|
|
1232
|
+
MAX_CANVAS_POOL_SIZE = 3;
|
|
1388
1233
|
async componentDidLoad() {
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
this.
|
|
1234
|
+
try {
|
|
1235
|
+
this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
|
|
1236
|
+
await this.initializeServices();
|
|
1237
|
+
this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
|
|
1238
|
+
await this.setupEventListeners();
|
|
1239
|
+
this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
|
|
1240
|
+
await this.initializeComponent();
|
|
1241
|
+
if (this.debug) {
|
|
1242
|
+
this.initializePerformanceMonitor();
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
catch (error) {
|
|
1246
|
+
this.updateStatus('Error de inicialización', error.message, 'error');
|
|
1397
1247
|
}
|
|
1398
1248
|
}
|
|
1399
1249
|
async initializeServices() {
|
|
@@ -1403,37 +1253,22 @@ const JaakStamps = class {
|
|
|
1403
1253
|
maskSize: this.maskSize,
|
|
1404
1254
|
cropMargin: this.cropMargin,
|
|
1405
1255
|
useDocumentClassification: this.useDocumentClassification,
|
|
1256
|
+
useDocumentDetector: this.useDocumentDetector,
|
|
1406
1257
|
preferredCamera: this.preferredCamera,
|
|
1407
1258
|
captureDelay: this.captureDelay
|
|
1408
1259
|
};
|
|
1409
1260
|
this.serviceContainer = new ServiceContainer(config);
|
|
1410
|
-
this.logger = this.serviceContainer.getLogger();
|
|
1411
1261
|
this.eventBus = this.serviceContainer.getEventBus();
|
|
1412
1262
|
this.stateManager = this.serviceContainer.getStateManager();
|
|
1413
1263
|
this.cameraService = this.serviceContainer.getCameraService();
|
|
1414
1264
|
this.detectionService = this.serviceContainer.getDetectionService();
|
|
1415
|
-
this.logger.state('SERVICIOS_INICIALIZADOS', { timestamp: Date.now() });
|
|
1416
1265
|
}
|
|
1417
1266
|
async setupEventListeners() {
|
|
1418
|
-
this.eventBus.on('state-changed', (
|
|
1419
|
-
this.handleStateChange(
|
|
1420
|
-
});
|
|
1421
|
-
this.eventBus.on('camera-changed', (cameraId) => {
|
|
1422
|
-
this.logger.state('CAMARA_CAMBIADA_EVENT', { cameraId });
|
|
1423
|
-
});
|
|
1424
|
-
this.eventBus.on('error', (error) => {
|
|
1425
|
-
this.logger.error('Error en servicio:', error);
|
|
1267
|
+
this.eventBus.on('state-changed', () => {
|
|
1268
|
+
this.handleStateChange();
|
|
1426
1269
|
});
|
|
1427
1270
|
}
|
|
1428
1271
|
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
1272
|
this.validateProps();
|
|
1438
1273
|
if (this.debug) {
|
|
1439
1274
|
this.stateManager.updateCaptureState({ isLoading: true });
|
|
@@ -1442,58 +1277,67 @@ const JaakStamps = class {
|
|
|
1442
1277
|
this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura', 'initializing');
|
|
1443
1278
|
await this.cameraService.detectDeviceType();
|
|
1444
1279
|
await this.cameraService.enumerateDevices();
|
|
1445
|
-
this.
|
|
1280
|
+
await this.updateCameraInfoWithAutofocus();
|
|
1281
|
+
this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'initializing');
|
|
1446
1282
|
await this.loadOnnxRuntime();
|
|
1447
1283
|
this.initializeResizeObserver();
|
|
1448
1284
|
}
|
|
1449
1285
|
validateProps() {
|
|
1450
1286
|
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
1287
|
this.maskSize = 90;
|
|
1453
1288
|
}
|
|
1454
1289
|
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
1290
|
this.cropMargin = 0;
|
|
1457
1291
|
}
|
|
1458
1292
|
const validOptions = ['auto', 'front', 'back'];
|
|
1459
1293
|
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
1294
|
this.preferredCamera = 'auto';
|
|
1462
1295
|
}
|
|
1463
1296
|
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
1297
|
this.captureDelay = 1500;
|
|
1466
1298
|
}
|
|
1467
1299
|
}
|
|
1468
1300
|
async loadOnnxRuntime() {
|
|
1469
1301
|
if (!window.ort) {
|
|
1470
|
-
this.updateStatus('
|
|
1302
|
+
this.updateStatus('Preparando herramientas...', 'Configurando funciones de captura', 'initializing');
|
|
1471
1303
|
const script = document.createElement('script');
|
|
1472
1304
|
script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
|
|
1473
1305
|
document.head.appendChild(script);
|
|
1474
1306
|
await new Promise((resolve) => {
|
|
1475
1307
|
script.onload = () => {
|
|
1476
|
-
setTimeout(() => {
|
|
1477
|
-
this.finalizeInitialization();
|
|
1308
|
+
setTimeout(async () => {
|
|
1309
|
+
await this.finalizeInitialization();
|
|
1478
1310
|
resolve(undefined);
|
|
1479
1311
|
}, 300);
|
|
1480
1312
|
};
|
|
1481
1313
|
});
|
|
1482
1314
|
}
|
|
1483
1315
|
else {
|
|
1484
|
-
setTimeout(() => {
|
|
1485
|
-
this.finalizeInitialization();
|
|
1316
|
+
setTimeout(async () => {
|
|
1317
|
+
await this.finalizeInitialization();
|
|
1486
1318
|
}, 300);
|
|
1487
1319
|
}
|
|
1488
1320
|
}
|
|
1489
|
-
finalizeInitialization() {
|
|
1321
|
+
async finalizeInitialization() {
|
|
1490
1322
|
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1323
|
+
// Check camera permissions before showing "ready" status
|
|
1324
|
+
const hasPermissions = await this.checkCameraPermissions();
|
|
1325
|
+
if (hasPermissions) {
|
|
1326
|
+
this.currentStatus = {
|
|
1327
|
+
message: 'Listo para capturar',
|
|
1328
|
+
description: '',
|
|
1329
|
+
type: 'ready',
|
|
1330
|
+
isInitialized: true
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
else {
|
|
1334
|
+
this.currentStatus = {
|
|
1335
|
+
message: 'Permisos de cámara requeridos',
|
|
1336
|
+
description: 'Por favor, otorgue permisos de cámara para continuar',
|
|
1337
|
+
type: 'error',
|
|
1338
|
+
isInitialized: true
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1497
1341
|
this.emitReadyEvent();
|
|
1498
1342
|
}
|
|
1499
1343
|
updateStatus(message, description, type = 'loading') {
|
|
@@ -1507,13 +1351,29 @@ const JaakStamps = class {
|
|
|
1507
1351
|
emitReadyEvent() {
|
|
1508
1352
|
const isDocumentReady = !!window.ort && this.detectionService.isModelLoaded();
|
|
1509
1353
|
this.isReady.emit(isDocumentReady);
|
|
1510
|
-
this.logger.state('COMPONENTE_LISTO', {
|
|
1511
|
-
ortLibraryLoaded: !!window.ort,
|
|
1512
|
-
modelPreloaded: this.detectionService.isModelLoaded(),
|
|
1513
|
-
isReady: isDocumentReady
|
|
1514
|
-
});
|
|
1515
1354
|
}
|
|
1516
|
-
|
|
1355
|
+
async checkCameraPermissions() {
|
|
1356
|
+
try {
|
|
1357
|
+
if (!navigator.permissions) {
|
|
1358
|
+
// Fallback: try to access camera directly
|
|
1359
|
+
try {
|
|
1360
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
1361
|
+
stream.getTracks().forEach(track => track.stop());
|
|
1362
|
+
return true;
|
|
1363
|
+
}
|
|
1364
|
+
catch {
|
|
1365
|
+
return false;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
const permission = await navigator.permissions.query({ name: 'camera' });
|
|
1369
|
+
return permission.state === 'granted';
|
|
1370
|
+
}
|
|
1371
|
+
catch (error) {
|
|
1372
|
+
return false;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
handleStateChange() {
|
|
1376
|
+
// Handle state changes from StateManager if needed
|
|
1517
1377
|
}
|
|
1518
1378
|
initializeResizeObserver() {
|
|
1519
1379
|
if ('ResizeObserver' in window && this.detectionContainer) {
|
|
@@ -1522,15 +1382,56 @@ const JaakStamps = class {
|
|
|
1522
1382
|
});
|
|
1523
1383
|
resizeObserver.observe(this.detectionContainer.parentElement);
|
|
1524
1384
|
}
|
|
1385
|
+
// Handle mobile device orientation changes
|
|
1386
|
+
window.addEventListener('orientationchange', () => {
|
|
1387
|
+
// Clear any existing orientation timer
|
|
1388
|
+
if (this.orientationTimer) {
|
|
1389
|
+
clearTimeout(this.orientationTimer);
|
|
1390
|
+
}
|
|
1391
|
+
// Add delay to ensure video dimensions are updated after rotation
|
|
1392
|
+
this.orientationTimer = window.setTimeout(() => {
|
|
1393
|
+
this.handleResize();
|
|
1394
|
+
this.orientationTimer = undefined;
|
|
1395
|
+
}, 300);
|
|
1396
|
+
});
|
|
1525
1397
|
}
|
|
1526
1398
|
handleResize() {
|
|
1527
1399
|
if (this.detectionContainer) {
|
|
1528
1400
|
const container = this.detectionContainer.parentElement;
|
|
1529
1401
|
const rect = container.getBoundingClientRect();
|
|
1530
1402
|
this.updateMaskDimensions(rect);
|
|
1531
|
-
this.logger.debug('Container redimensionado:', { width: rect.width, height: rect.height });
|
|
1532
1403
|
}
|
|
1533
1404
|
}
|
|
1405
|
+
triggerRerender() {
|
|
1406
|
+
this.captureStateVersion = this.captureStateVersion + 1;
|
|
1407
|
+
}
|
|
1408
|
+
getGuideText(step) {
|
|
1409
|
+
if (step === 'completed') {
|
|
1410
|
+
return 'Proceso completado';
|
|
1411
|
+
}
|
|
1412
|
+
// Check if we should show performance message
|
|
1413
|
+
if (this.showPerformanceMessage && step === 'front') {
|
|
1414
|
+
return 'Modo manual activado por rendimiento. Use el botón para capturar.';
|
|
1415
|
+
}
|
|
1416
|
+
if (step === 'front') {
|
|
1417
|
+
// Para el frente, si el detector manual está activo, mostrar instrucciones de captura manual
|
|
1418
|
+
if (this.showManualCaptureButton) {
|
|
1419
|
+
return 'Posicione el frente de su documento en el marco y presione el botón para capturar';
|
|
1420
|
+
}
|
|
1421
|
+
// Instrucción básica de alineación
|
|
1422
|
+
return 'Alinee su identificación con el marco';
|
|
1423
|
+
}
|
|
1424
|
+
else if (step === 'back') {
|
|
1425
|
+
// Para el reverso, mostrar instrucciones específicas según el modo
|
|
1426
|
+
if (this.useDocumentDetector) {
|
|
1427
|
+
return 'Si tu documento no tiene lado trasero, da clic en el botón para continuar con el proceso';
|
|
1428
|
+
}
|
|
1429
|
+
else {
|
|
1430
|
+
return 'Posicione el reverso de su documento en el marco y capture, o salte este paso';
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return 'Alinee su identificación con el marco';
|
|
1434
|
+
}
|
|
1534
1435
|
updateMaskDimensions(containerRect) {
|
|
1535
1436
|
if (!this.videoRef)
|
|
1536
1437
|
return;
|
|
@@ -1582,16 +1483,38 @@ const JaakStamps = class {
|
|
|
1582
1483
|
this.el.style.setProperty('--mask-center-x', `${videoCenterXPercent}%`);
|
|
1583
1484
|
this.el.style.setProperty('--mask-center-y', `${videoCenterYPercent}%`);
|
|
1584
1485
|
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
1486
|
}
|
|
1593
1487
|
// PUBLIC METHODS
|
|
1488
|
+
// Helper method to check if component is ready for operations
|
|
1489
|
+
isComponentReady() {
|
|
1490
|
+
if (!this.currentStatus.isInitialized) {
|
|
1491
|
+
return {
|
|
1492
|
+
ready: false,
|
|
1493
|
+
message: 'El componente aún se está inicializando',
|
|
1494
|
+
status: 'initializing'
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
if (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') {
|
|
1498
|
+
return {
|
|
1499
|
+
ready: false,
|
|
1500
|
+
message: 'El componente está cargando recursos',
|
|
1501
|
+
status: 'loading'
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
if (!this.serviceContainer || !this.stateManager || !this.cameraService || !this.detectionService) {
|
|
1505
|
+
return {
|
|
1506
|
+
ready: false,
|
|
1507
|
+
message: 'Los servicios del componente no están disponibles',
|
|
1508
|
+
status: 'error'
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
return { ready: true };
|
|
1512
|
+
}
|
|
1594
1513
|
async getCapturedImages() {
|
|
1514
|
+
const readyCheck = this.isComponentReady();
|
|
1515
|
+
if (!readyCheck.ready) {
|
|
1516
|
+
throw new Error(readyCheck.message);
|
|
1517
|
+
}
|
|
1595
1518
|
const state = this.stateManager.getCaptureState();
|
|
1596
1519
|
if (state.step !== 'completed') {
|
|
1597
1520
|
throw new Error('El proceso de captura no ha sido completado');
|
|
@@ -1599,146 +1522,359 @@ const JaakStamps = class {
|
|
|
1599
1522
|
return this.stateManager.getCapturedImages();
|
|
1600
1523
|
}
|
|
1601
1524
|
async isProcessCompleted() {
|
|
1525
|
+
const readyCheck = this.isComponentReady();
|
|
1526
|
+
if (!readyCheck.ready) {
|
|
1527
|
+
return false; // Return false instead of error for this method
|
|
1528
|
+
}
|
|
1602
1529
|
return this.stateManager.isProcessCompleted();
|
|
1603
1530
|
}
|
|
1604
1531
|
async startCapture() {
|
|
1605
|
-
|
|
1532
|
+
const readyCheck = this.isComponentReady();
|
|
1533
|
+
if (!readyCheck.ready) {
|
|
1534
|
+
this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
|
|
1535
|
+
return { success: false, error: readyCheck.message };
|
|
1536
|
+
}
|
|
1537
|
+
try {
|
|
1538
|
+
await this.startDetection();
|
|
1539
|
+
return { success: true };
|
|
1540
|
+
}
|
|
1541
|
+
catch (error) {
|
|
1542
|
+
this.updateStatus('Error al iniciar captura', error.message, 'error');
|
|
1543
|
+
return { success: false, error: error.message };
|
|
1544
|
+
}
|
|
1606
1545
|
}
|
|
1607
1546
|
async stopCapture() {
|
|
1608
|
-
this.
|
|
1547
|
+
const readyCheck = this.isComponentReady();
|
|
1548
|
+
if (!readyCheck.ready) {
|
|
1549
|
+
return { success: false, error: readyCheck.message };
|
|
1550
|
+
}
|
|
1551
|
+
try {
|
|
1552
|
+
this.exitSession();
|
|
1553
|
+
return { success: true };
|
|
1554
|
+
}
|
|
1555
|
+
catch (error) {
|
|
1556
|
+
return { success: false, error: error.message };
|
|
1557
|
+
}
|
|
1609
1558
|
}
|
|
1610
1559
|
async resetCapture() {
|
|
1611
|
-
this.
|
|
1560
|
+
const readyCheck = this.isComponentReady();
|
|
1561
|
+
if (!readyCheck.ready) {
|
|
1562
|
+
return { success: false, error: readyCheck.message };
|
|
1563
|
+
}
|
|
1564
|
+
try {
|
|
1565
|
+
this.resetDetection();
|
|
1566
|
+
return { success: true };
|
|
1567
|
+
}
|
|
1568
|
+
catch (error) {
|
|
1569
|
+
return { success: false, error: error.message };
|
|
1570
|
+
}
|
|
1612
1571
|
}
|
|
1613
1572
|
async skipBackCapture() {
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1573
|
+
console.log('🟡 Botón clicked: Saltar reverso');
|
|
1574
|
+
// Set processing state immediately
|
|
1575
|
+
this.processingButton = 'skip-back';
|
|
1576
|
+
// Add small delay to show spinner
|
|
1577
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1578
|
+
const readyCheck = this.isComponentReady();
|
|
1579
|
+
if (!readyCheck.ready) {
|
|
1580
|
+
this.processingButton = null;
|
|
1581
|
+
return { success: false, error: readyCheck.message };
|
|
1618
1582
|
}
|
|
1619
|
-
|
|
1583
|
+
try {
|
|
1584
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1585
|
+
const capturedImages = this.stateManager.getCapturedImages();
|
|
1586
|
+
if (captureState.step === 'back' && capturedImages.front.fullFrame) {
|
|
1587
|
+
this.completeProcess(true);
|
|
1588
|
+
return { success: true };
|
|
1589
|
+
}
|
|
1590
|
+
else {
|
|
1591
|
+
return { success: false, error: 'No se puede saltar el reverso en el estado actual' };
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
catch (error) {
|
|
1595
|
+
return { success: false, error: error.message };
|
|
1596
|
+
}
|
|
1597
|
+
finally {
|
|
1598
|
+
// Always clear processing state when done
|
|
1599
|
+
this.processingButton = null;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1620
1602
|
async getStatus() {
|
|
1621
|
-
const
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1603
|
+
const readyCheck = this.isComponentReady();
|
|
1604
|
+
if (!readyCheck.ready) {
|
|
1605
|
+
return {
|
|
1606
|
+
isVideoActive: false,
|
|
1607
|
+
captureStep: 'front',
|
|
1608
|
+
hasImages: false,
|
|
1609
|
+
isProcessCompleted: false,
|
|
1610
|
+
isModelPreloaded: false,
|
|
1611
|
+
componentStatus: readyCheck.status,
|
|
1612
|
+
componentMessage: readyCheck.message
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
try {
|
|
1616
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1617
|
+
const capturedImages = this.stateManager.getCapturedImages();
|
|
1618
|
+
return {
|
|
1619
|
+
isVideoActive: captureState.isVideoActive,
|
|
1620
|
+
captureStep: captureState.step,
|
|
1621
|
+
hasImages: !!(capturedImages.front.fullFrame || capturedImages.back.fullFrame),
|
|
1622
|
+
isProcessCompleted: this.stateManager.isProcessCompleted(),
|
|
1623
|
+
isModelPreloaded: this.detectionService.isModelLoaded(),
|
|
1624
|
+
componentStatus: 'ready'
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
catch (error) {
|
|
1628
|
+
return {
|
|
1629
|
+
isVideoActive: false,
|
|
1630
|
+
captureStep: 'front',
|
|
1631
|
+
hasImages: false,
|
|
1632
|
+
isProcessCompleted: false,
|
|
1633
|
+
isModelPreloaded: false,
|
|
1634
|
+
componentStatus: 'error',
|
|
1635
|
+
componentMessage: error.message
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1630
1638
|
}
|
|
1631
1639
|
async preloadModel() {
|
|
1640
|
+
const readyCheck = this.isComponentReady();
|
|
1641
|
+
if (!readyCheck.ready) {
|
|
1642
|
+
this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
|
|
1643
|
+
return { success: false, error: readyCheck.message };
|
|
1644
|
+
}
|
|
1632
1645
|
if (this.detectionService.isModelLoaded()) {
|
|
1633
|
-
this.
|
|
1634
|
-
this.updateStatus('Modelos ya cargados', '', 'ready');
|
|
1646
|
+
this.updateStatus('Reconocimiento listo', '', 'ready');
|
|
1635
1647
|
return { success: true, message: 'Model already loaded' };
|
|
1636
1648
|
}
|
|
1637
1649
|
try {
|
|
1638
1650
|
const loadStartTime = performance.now();
|
|
1639
|
-
this.updateStatus('
|
|
1640
|
-
this.stateManager
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1651
|
+
this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
|
|
1652
|
+
if (this.stateManager) {
|
|
1653
|
+
this.stateManager.updateCaptureState({ isLoading: true });
|
|
1654
|
+
}
|
|
1655
|
+
try {
|
|
1656
|
+
await this.detectionService.loadModel();
|
|
1657
|
+
}
|
|
1658
|
+
catch (modelError) {
|
|
1659
|
+
this.switchToManualModeWithError('Error al cargar modelo de detección');
|
|
1660
|
+
throw modelError;
|
|
1661
|
+
}
|
|
1662
|
+
this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
|
|
1663
|
+
try {
|
|
1664
|
+
await this.detectionService.loadClassificationModel();
|
|
1665
|
+
}
|
|
1666
|
+
catch (classificationError) {
|
|
1667
|
+
this.switchToManualModeWithError('Error al cargar modelo de clasificación');
|
|
1668
|
+
throw classificationError;
|
|
1669
|
+
}
|
|
1644
1670
|
const loadEndTime = performance.now();
|
|
1645
1671
|
const loadTime = loadEndTime - loadStartTime;
|
|
1646
1672
|
// Record ONNX load time
|
|
1647
1673
|
if (this.debug) {
|
|
1648
1674
|
this.recordOnnxPerformance(loadTime, 0);
|
|
1649
1675
|
}
|
|
1650
|
-
this.updateStatus('
|
|
1676
|
+
this.updateStatus('Finalizando configuración...', 'Preparando herramientas para captura', 'loading');
|
|
1651
1677
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
1652
|
-
this.updateStatus('
|
|
1653
|
-
this.stateManager
|
|
1678
|
+
this.updateStatus('Reconocimiento preparado', '', 'ready');
|
|
1679
|
+
if (this.stateManager) {
|
|
1680
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1681
|
+
}
|
|
1654
1682
|
this.emitReadyEvent();
|
|
1655
|
-
this.logger.state('MODELOS_PRECARGADOS_EXITOSAMENTE', { loadTime: Math.round(loadTime) });
|
|
1656
1683
|
return { success: true, message: 'Models preloaded successfully' };
|
|
1657
1684
|
}
|
|
1658
1685
|
catch (error) {
|
|
1659
|
-
this.
|
|
1660
|
-
this.
|
|
1661
|
-
|
|
1686
|
+
this.updateStatus('Error de configuración', 'No se pudieron preparar las herramientas necesarias', 'error');
|
|
1687
|
+
if (this.stateManager) {
|
|
1688
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1689
|
+
}
|
|
1662
1690
|
return { success: false, error: error.message };
|
|
1663
1691
|
}
|
|
1664
1692
|
}
|
|
1665
1693
|
async getCameraInfo() {
|
|
1666
|
-
|
|
1694
|
+
const readyCheck = this.isComponentReady();
|
|
1695
|
+
if (!readyCheck.ready) {
|
|
1696
|
+
return {
|
|
1697
|
+
availableCameras: [],
|
|
1698
|
+
selectedCameraId: null,
|
|
1699
|
+
deviceType: 'desktop',
|
|
1700
|
+
isMultipleCamerasAvailable: false,
|
|
1701
|
+
preferredFacing: null,
|
|
1702
|
+
error: readyCheck.message
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
try {
|
|
1706
|
+
return await this.cameraService.getCameraInfo();
|
|
1707
|
+
}
|
|
1708
|
+
catch (error) {
|
|
1709
|
+
return {
|
|
1710
|
+
availableCameras: [],
|
|
1711
|
+
selectedCameraId: null,
|
|
1712
|
+
deviceType: 'desktop',
|
|
1713
|
+
isMultipleCamerasAvailable: false,
|
|
1714
|
+
preferredFacing: null,
|
|
1715
|
+
error: error.message
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1667
1718
|
}
|
|
1668
1719
|
async setPreferredCamera(camera) {
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1720
|
+
const readyCheck = this.isComponentReady();
|
|
1721
|
+
if (!readyCheck.ready) {
|
|
1722
|
+
return {
|
|
1723
|
+
success: false,
|
|
1724
|
+
error: readyCheck.message,
|
|
1725
|
+
selectedCamera: null,
|
|
1726
|
+
availableCameras: 0
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
try {
|
|
1730
|
+
this.preferredCamera = camera;
|
|
1731
|
+
this.serviceContainer.updateConfig({ preferredCamera: camera });
|
|
1732
|
+
await this.cameraService.enumerateDevices();
|
|
1733
|
+
await this.updateCameraInfoWithAutofocus();
|
|
1734
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1735
|
+
if (captureState.isVideoActive) {
|
|
1736
|
+
const selectedCameraId = this.cameraService.getSelectedCameraId();
|
|
1737
|
+
if (selectedCameraId) {
|
|
1738
|
+
await this.cameraService.switchCamera(selectedCameraId);
|
|
1739
|
+
}
|
|
1677
1740
|
}
|
|
1741
|
+
return {
|
|
1742
|
+
success: true,
|
|
1743
|
+
selectedCamera: this.cameraService.getSelectedCameraId(),
|
|
1744
|
+
availableCameras: this.cameraService.getAvailableCameras().length
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
catch (error) {
|
|
1748
|
+
return {
|
|
1749
|
+
success: false,
|
|
1750
|
+
error: error.message,
|
|
1751
|
+
selectedCamera: null,
|
|
1752
|
+
availableCameras: 0
|
|
1753
|
+
};
|
|
1678
1754
|
}
|
|
1679
|
-
return {
|
|
1680
|
-
success: true,
|
|
1681
|
-
selectedCamera: this.cameraService.getSelectedCameraId(),
|
|
1682
|
-
availableCameras: this.cameraService.getAvailableCameras().length
|
|
1683
|
-
};
|
|
1684
1755
|
}
|
|
1685
1756
|
async setCaptureDelay(delay) {
|
|
1686
1757
|
if (delay < 0 || delay > 10000) {
|
|
1687
|
-
|
|
1758
|
+
return {
|
|
1759
|
+
success: false,
|
|
1760
|
+
error: 'Capture delay must be between 0 and 10000 milliseconds',
|
|
1761
|
+
captureDelay: this.captureDelay
|
|
1762
|
+
};
|
|
1688
1763
|
}
|
|
1764
|
+
// Allow setting capture delay even during initialization
|
|
1689
1765
|
this.captureDelay = delay;
|
|
1690
|
-
this.serviceContainer
|
|
1766
|
+
if (this.serviceContainer) {
|
|
1767
|
+
this.serviceContainer.updateConfig({ captureDelay: delay });
|
|
1768
|
+
}
|
|
1691
1769
|
return {
|
|
1692
1770
|
success: true,
|
|
1693
1771
|
captureDelay: this.captureDelay
|
|
1694
1772
|
};
|
|
1695
1773
|
}
|
|
1696
1774
|
async getCaptureDelay() {
|
|
1775
|
+
// Always allow getting capture delay, even during initialization
|
|
1697
1776
|
return this.captureDelay;
|
|
1698
1777
|
}
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
};
|
|
1705
|
-
}
|
|
1706
|
-
async focusAtPoint(x, y) {
|
|
1707
|
-
const focused = await this.cameraService.focusAtPoint(x, y, this.videoStream);
|
|
1778
|
+
// Memory and device capability detection
|
|
1779
|
+
checkDeviceCapabilities() {
|
|
1780
|
+
const deviceMemory = navigator.deviceMemory;
|
|
1781
|
+
// Siempre intentar usar ML, independientemente del dispositivo
|
|
1782
|
+
// Las restricciones anteriores no eran confiables
|
|
1708
1783
|
return {
|
|
1709
|
-
|
|
1710
|
-
|
|
1784
|
+
canUseML: true,
|
|
1785
|
+
reason: 'Dispositivo compatible con detección automática.',
|
|
1786
|
+
memoryMB: deviceMemory ? deviceMemory * 1024 : null
|
|
1711
1787
|
};
|
|
1712
1788
|
}
|
|
1713
1789
|
// DETECTION METHODS
|
|
1714
1790
|
async startDetection() {
|
|
1715
|
-
this.logger.state('INICIANDO_DETECCION');
|
|
1716
1791
|
try {
|
|
1792
|
+
// Verificar capacidades del dispositivo antes de cargar modelos
|
|
1793
|
+
const capabilities = this.checkDeviceCapabilities();
|
|
1794
|
+
// Si el dispositivo no puede usar ML o tenemos problemas de memoria, usar modo manual
|
|
1795
|
+
if (!capabilities.canUseML && this.useDocumentDetector) {
|
|
1796
|
+
this.switchToManualModeWithWarning(capabilities.reason);
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1717
1799
|
// Paso 1: Verificar y cargar modelos si es necesario
|
|
1718
1800
|
if (!this.detectionService.isModelLoaded()) {
|
|
1719
1801
|
const loadStartTime = performance.now();
|
|
1720
|
-
this.updateStatus('
|
|
1802
|
+
this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
|
|
1721
1803
|
this.stateManager.updateCaptureState({ isLoading: true });
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1804
|
+
try {
|
|
1805
|
+
await this.detectionService.loadModel();
|
|
1806
|
+
}
|
|
1807
|
+
catch (modelError) {
|
|
1808
|
+
// Si es un error de memoria, cambiar a modo manual con mensaje específico
|
|
1809
|
+
if (modelError.message.includes('Out of memory') || modelError.message.includes('no available backend')) {
|
|
1810
|
+
const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
|
|
1811
|
+
this.switchToManualModeWithWarning(`Memoria insuficiente para detección automática ${memoryInfo}. Modo manual activado - funciona igual de bien!`);
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
else {
|
|
1815
|
+
this.switchToManualModeWithError('Error al cargar modelo de detección');
|
|
1816
|
+
throw modelError;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
|
|
1820
|
+
try {
|
|
1821
|
+
await this.detectionService.loadClassificationModel();
|
|
1822
|
+
}
|
|
1823
|
+
catch (classificationError) {
|
|
1824
|
+
// Si es un error de memoria, cambiar a modo manual con mensaje específico
|
|
1825
|
+
if (classificationError.message.includes('Out of memory') || classificationError.message.includes('no available backend')) {
|
|
1826
|
+
const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
|
|
1827
|
+
this.switchToManualModeWithWarning(`Memoria insuficiente para clasificación automática ${memoryInfo}. Modo manual activado para optimizar rendimiento.`);
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
else {
|
|
1831
|
+
this.switchToManualModeWithError('Error al cargar modelo de clasificación');
|
|
1832
|
+
throw classificationError;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1725
1835
|
const loadEndTime = performance.now();
|
|
1726
1836
|
const loadTime = loadEndTime - loadStartTime;
|
|
1727
1837
|
// Record ONNX load time
|
|
1728
1838
|
if (this.debug) {
|
|
1729
1839
|
this.recordOnnxPerformance(loadTime, 0);
|
|
1730
1840
|
}
|
|
1731
|
-
this.logger.state('MODELOS_CARGADOS_EN_DETECCION', { loadTime: Math.round(loadTime) });
|
|
1732
1841
|
}
|
|
1733
1842
|
// Paso 2: Detectar y configurar dispositivos
|
|
1734
|
-
this.updateStatus('
|
|
1735
|
-
|
|
1843
|
+
this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
|
|
1844
|
+
try {
|
|
1845
|
+
await this.cameraService.enumerateDevices();
|
|
1846
|
+
}
|
|
1847
|
+
catch (enumerateError) {
|
|
1848
|
+
throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
|
|
1849
|
+
}
|
|
1850
|
+
try {
|
|
1851
|
+
await this.updateCameraInfoWithAutofocus();
|
|
1852
|
+
}
|
|
1853
|
+
catch (cameraInfoError) {
|
|
1854
|
+
throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
|
|
1855
|
+
}
|
|
1736
1856
|
// Paso 3: Configurar cámara seleccionada
|
|
1737
|
-
this.updateStatus('
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1857
|
+
this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
|
|
1858
|
+
let stream;
|
|
1859
|
+
try {
|
|
1860
|
+
stream = await this.cameraService.setupCamera();
|
|
1861
|
+
}
|
|
1862
|
+
catch (setupError) {
|
|
1863
|
+
throw new Error(`Error al configurar cámara: ${setupError.message}`);
|
|
1864
|
+
}
|
|
1865
|
+
// Paso 4: Inicializar video y mostrar estado de análisis inicial
|
|
1866
|
+
if (this.useDocumentDetector) {
|
|
1867
|
+
this.updateStatus('Analizando estabilidad...', 'Evaluando rendimiento del sistema', 'loading');
|
|
1868
|
+
}
|
|
1869
|
+
else {
|
|
1870
|
+
this.updateStatus('Captura activa', 'Posicione su documento y use el botón para capturar', 'active');
|
|
1871
|
+
}
|
|
1872
|
+
try {
|
|
1873
|
+
await this.initializeVideoStream(stream);
|
|
1874
|
+
}
|
|
1875
|
+
catch (videoError) {
|
|
1876
|
+
throw new Error(`Error al inicializar video: ${videoError.message}`);
|
|
1877
|
+
}
|
|
1742
1878
|
// Paso 5: Calibrar máscara
|
|
1743
1879
|
if (this.detectionContainer) {
|
|
1744
1880
|
const container = this.detectionContainer.parentElement;
|
|
@@ -1748,11 +1884,14 @@ const JaakStamps = class {
|
|
|
1748
1884
|
// Finalizar e iniciar captura
|
|
1749
1885
|
this.startTime = Date.now();
|
|
1750
1886
|
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1887
|
+
// Mostrar botón manual si el detector está deshabilitado
|
|
1888
|
+
if (!this.useDocumentDetector) {
|
|
1889
|
+
this.showManualCaptureButton = true;
|
|
1890
|
+
}
|
|
1751
1891
|
this.detectFrame();
|
|
1752
1892
|
}
|
|
1753
1893
|
catch (err) {
|
|
1754
|
-
this.
|
|
1755
|
-
this.updateStatus('Error al iniciar captura', 'No se pudo completar la inicialización', 'error');
|
|
1894
|
+
this.updateStatus('Error al iniciar', 'No se pudo preparar la captura', 'error');
|
|
1756
1895
|
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1757
1896
|
}
|
|
1758
1897
|
}
|
|
@@ -1762,14 +1901,16 @@ const JaakStamps = class {
|
|
|
1762
1901
|
this.videoStream = stream;
|
|
1763
1902
|
const isRear = this.cameraService.isRearCamera(stream);
|
|
1764
1903
|
this.shouldMirrorVideo = !isRear;
|
|
1765
|
-
this.logger.state('CAMARA_CONFIGURADA', {
|
|
1766
|
-
isRearCamera: isRear,
|
|
1767
|
-
shouldMirrorVideo: this.shouldMirrorVideo
|
|
1768
|
-
});
|
|
1769
1904
|
return new Promise((resolve) => {
|
|
1770
1905
|
this.videoRef.onloadedmetadata = async () => {
|
|
1771
1906
|
await this.videoRef.play();
|
|
1772
1907
|
this.stateManager.updateCaptureState({ isVideoActive: true });
|
|
1908
|
+
// Recalculate mask dimensions when new camera stream loads
|
|
1909
|
+
if (this.detectionContainer) {
|
|
1910
|
+
const container = this.detectionContainer.parentElement;
|
|
1911
|
+
const rect = container.getBoundingClientRect();
|
|
1912
|
+
this.updateMaskDimensions(rect);
|
|
1913
|
+
}
|
|
1773
1914
|
resolve();
|
|
1774
1915
|
};
|
|
1775
1916
|
});
|
|
@@ -1784,15 +1925,28 @@ const JaakStamps = class {
|
|
|
1784
1925
|
const captureState = this.stateManager.getCaptureState();
|
|
1785
1926
|
if (!this.videoRef || !this.detectionContainer || !this.detectionService.isModelLoaded())
|
|
1786
1927
|
return;
|
|
1928
|
+
// Show manual capture button when document detector is disabled or performance is degraded
|
|
1929
|
+
if (!this.useDocumentDetector || this.performanceDegradedMode) {
|
|
1930
|
+
this.showManualCaptureButton = true;
|
|
1931
|
+
// Continue the loop for UI updates but skip detection logic
|
|
1932
|
+
if (captureState.step !== 'completed') {
|
|
1933
|
+
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
1934
|
+
}
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
else if (!this.performanceDegradedMode) {
|
|
1938
|
+
this.showManualCaptureButton = false;
|
|
1939
|
+
}
|
|
1787
1940
|
if (captureState.isDetectionPaused) {
|
|
1788
1941
|
if (captureState.step !== 'completed') {
|
|
1789
1942
|
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
1790
1943
|
}
|
|
1791
1944
|
return;
|
|
1792
1945
|
}
|
|
1793
|
-
//
|
|
1946
|
+
// Adaptive frame skipping for performance
|
|
1794
1947
|
this.frameSkipCounter++;
|
|
1795
|
-
|
|
1948
|
+
const currentFrameSkip = this.getAdaptiveFrameSkip();
|
|
1949
|
+
if (this.frameSkipCounter <= currentFrameSkip) {
|
|
1796
1950
|
if (captureState.step !== 'completed') {
|
|
1797
1951
|
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
1798
1952
|
}
|
|
@@ -1810,10 +1964,27 @@ const JaakStamps = class {
|
|
|
1810
1964
|
this.lastInferenceTime = currentTime;
|
|
1811
1965
|
// Measure preprocessing and inference time
|
|
1812
1966
|
const inferenceStartTime = performance.now();
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1967
|
+
let inputTensor;
|
|
1968
|
+
let detections;
|
|
1969
|
+
let inferenceTime;
|
|
1970
|
+
try {
|
|
1971
|
+
inputTensor = this.detectionService.preprocess(this.videoRef);
|
|
1972
|
+
}
|
|
1973
|
+
catch (preprocessError) {
|
|
1974
|
+
this.switchToManualModeWithError('Error al procesar imagen');
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
try {
|
|
1978
|
+
// Standard detection without timeout (timeout is handled by frame counting)
|
|
1979
|
+
detections = await this.detectionService.runInference(inputTensor);
|
|
1980
|
+
inferenceTime = performance.now() - inferenceStartTime;
|
|
1981
|
+
}
|
|
1982
|
+
catch (inferenceError) {
|
|
1983
|
+
this.switchToManualModeWithError('Error en detección automática');
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
// Increment frame count
|
|
1987
|
+
this.processedFramesCount++;
|
|
1817
1988
|
// Record ONNX performance metrics
|
|
1818
1989
|
if (this.debug) {
|
|
1819
1990
|
this.recordOnnxPerformance(0, inferenceTime);
|
|
@@ -1852,9 +2023,30 @@ const JaakStamps = class {
|
|
|
1852
2023
|
}
|
|
1853
2024
|
this.updateMaskColor(detections);
|
|
1854
2025
|
// Record frame processing metrics
|
|
2026
|
+
const frameEndTime = performance.now();
|
|
2027
|
+
const totalFrameTime = frameEndTime - frameStartTime;
|
|
2028
|
+
// Sistema simplificado: Verificar rendimiento de cada frame (solo si no se ha cambiado a manual)
|
|
2029
|
+
if (this.useDocumentDetector && !this.hasAutoSwitchedToManual) {
|
|
2030
|
+
// Verificar si el frame es lento
|
|
2031
|
+
if (totalFrameTime >= this.PERFORMANCE_THRESHOLD_MS) {
|
|
2032
|
+
this.slowFrameCount++;
|
|
2033
|
+
// Si tenemos suficientes frames lentos consecutivos, cambiar a manual
|
|
2034
|
+
if (this.slowFrameCount >= this.SLOW_FRAMES_TO_TRIGGER) {
|
|
2035
|
+
this.switchToManualMode();
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
else {
|
|
2039
|
+
// Frame rápido, reiniciar contador
|
|
2040
|
+
if (this.slowFrameCount > 0) {
|
|
2041
|
+
this.slowFrameCount = 0;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
// Cambiar mensaje de estado después de analizar los primeros 2 frames
|
|
2045
|
+
if (this.processedFramesCount === 2) {
|
|
2046
|
+
this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
1855
2049
|
if (this.debug) {
|
|
1856
|
-
const frameEndTime = performance.now();
|
|
1857
|
-
const totalFrameTime = frameEndTime - frameStartTime;
|
|
1858
2050
|
this.recordFrameProcessing(totalFrameTime, detections.length);
|
|
1859
2051
|
}
|
|
1860
2052
|
// Continue detection loop
|
|
@@ -1868,7 +2060,6 @@ const JaakStamps = class {
|
|
|
1868
2060
|
}
|
|
1869
2061
|
}
|
|
1870
2062
|
catch (e) {
|
|
1871
|
-
this.logger.error('Error en inferencia de modelo:', e);
|
|
1872
2063
|
const captureState = this.stateManager.getCaptureState();
|
|
1873
2064
|
if (captureState.step !== 'completed') {
|
|
1874
2065
|
setTimeout(() => this.detectFrame(), 100);
|
|
@@ -1891,10 +2082,18 @@ const JaakStamps = class {
|
|
|
1891
2082
|
clearTimeout(this.alignmentTimer);
|
|
1892
2083
|
this.alignmentTimer = undefined;
|
|
1893
2084
|
}
|
|
2085
|
+
if (this.orientationTimer) {
|
|
2086
|
+
clearTimeout(this.orientationTimer);
|
|
2087
|
+
this.orientationTimer = undefined;
|
|
2088
|
+
}
|
|
2089
|
+
// Liberar recursos de ONNX al limpiar el componente
|
|
2090
|
+
this.releaseOnnxResources();
|
|
1894
2091
|
if (this.performanceUpdateInterval) {
|
|
1895
2092
|
clearInterval(this.performanceUpdateInterval);
|
|
1896
2093
|
this.performanceUpdateInterval = undefined;
|
|
1897
2094
|
}
|
|
2095
|
+
// Clear canvas pool
|
|
2096
|
+
this.canvasPool = [];
|
|
1898
2097
|
this.detectionBoxes = [];
|
|
1899
2098
|
this.alignmentStartTime = undefined;
|
|
1900
2099
|
this.hasDocumentDetected = false;
|
|
@@ -1908,11 +2107,8 @@ const JaakStamps = class {
|
|
|
1908
2107
|
step: 'front',
|
|
1909
2108
|
isCapturing: false
|
|
1910
2109
|
};
|
|
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: {
|
|
2110
|
+
const cameraInfo = this.cameraInfoWithAutofocus;
|
|
2111
|
+
return (index.h("div", { key: 'dda3e3054a18dac59fd6dab4bf722f405d1c1b0f', class: "detector-container" }, index.h("div", { key: 'c260bf52307e072a634cddcc273c1f55037d01ef', class: "video-container" }, index.h("video", { key: '5f20ce9d95c5ece7ab90a2da30f28034b86a57a0', 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: '6afd8a46c16592c67e6bb93f1655de1660338f30', 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
2112
|
position: 'absolute',
|
|
1917
2113
|
left: `${box.x}px`,
|
|
1918
2114
|
top: `${box.y}px`,
|
|
@@ -1921,9 +2117,9 @@ const JaakStamps = class {
|
|
|
1921
2117
|
border: '2px solid #32406C',
|
|
1922
2118
|
pointerEvents: 'none',
|
|
1923
2119
|
boxSizing: 'border-box'
|
|
1924
|
-
} })))), this.isMaskReady && (index.h("div", { key: '
|
|
2120
|
+
} })))), this.isMaskReady && (index.h("div", { key: '178f2022bed334f8c9c8c866a92b44c8cb0369b1', class: "overlay-mask" }, index.h("div", { key: 'acb1bc996409095afd29ffa5a038c3eeae6c7ed9', class: "card-outline" }, index.h("div", { key: 'dcc952d9011c5fd13b229f3a759cc74721a1ee11', class: "side side-top" }), index.h("div", { key: 'addb25f6c41ad10a09b6918e6357052294d14adf', class: "side side-right" }), index.h("div", { key: 'a1ec08f3bf1b16731a224c034c76dd8c6412597b', class: "side side-bottom" }), index.h("div", { key: '22f3c158f2c2bb1dc582f796c39ea60bc11505dd', class: "side side-left" })), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("div", { key: '5b150f7df5b0c5402e42bae4601936d4e5d1f5d4', class: `guide-text ${this.showPerformanceMessage ? 'performance-warning-text' : ''}` }, this.getGuideText(captureState.step))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("div", { key: '17b0a81437eb8691cb4dc5042c72bda01ac92be0', class: "back-capture-section" }, index.h("div", { key: 'a65ccbd5f53b06ec340b36a0a2348cf04920b3df', class: "back-capture-buttons" }, (!this.useDocumentDetector || this.performanceDegradedMode) && (index.h("button", { key: '383d7398079dbb8b1adfe695407cc598059b2a94', class: "capture-button primary-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'capture-back' && (index.h("span", { key: '86bddcfeb6f90da9b18926f4c6e1208296502dcf', class: "button-spinner" })), "Capturar Reverso")), index.h("button", { key: '5c9d116cbb9b5bc16481b2b90c8451182ad436c5', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'skip-back' && (index.h("span", { key: '881f00cfc3a24167bc6b8da5849cb404101b692d', class: "button-spinner" })), this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
|
|
1925
2121
|
? `Saltar reverso (${this.backDocumentTimerRemaining}s)`
|
|
1926
|
-
: 'Saltar reverso'))), captureState.isVideoActive && (index.h("div", { key: '
|
|
2122
|
+
: 'Saltar reverso')))), captureState.isVideoActive && (index.h("div", { key: 'd929fc0eeeeee944f3d722fde98c52642443e9a5', class: "camera-controls" }, index.h("button", { key: '1afcfae9d4a23aaede153f27ba6895a33182ada1', 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: 'f37feefd9def935417467dffb52b9fe8d8acea79', class: "camera-selector-dropdown" }, index.h("div", { key: 'a8479ec6a6c4076b5dc40677e1988295100e4d0d', class: "camera-selector-header" }, index.h("span", { key: '074f02967354293a1e5df4e3ac8a126faee33d04' }, "Seleccionar C\u00E1mara"), index.h("button", { key: 'd6ba0252fb269c3a04967fb0e5c075166f202d71', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), index.h("div", { key: '63f5f9be2b05da71f4fe5a4e38b11ab3e39fb1e0', 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: '4ca46fdebe0eaa078f1dae4d2f1af03321577c7f', class: "device-info" }, index.h("small", { key: '10c54ddc5a196b11a69077b045119187ed09f4be' }, "Dispositivo: ", cameraInfo.deviceType)))), this.showManualCaptureButton && captureState.step === 'front' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("div", { key: '25393018948798f4d9d5f44738e61c7a0a6be990', class: "manual-capture-section" }, index.h("button", { key: '7a6c8686c0db3e352f9689bc41f7fd3f612c800c', class: "manual-capture-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'capture-front' && (index.h("span", { key: '1916b97da3f8dce7d06b86b21f0361dd7ce10b95', class: "button-spinner" })), "Capturar Frente"))))), captureState.isCapturing && (index.h("div", { key: 'aed05a3280dc96121564ffea87945f441d4286f6', class: "capture-animation" })), captureState.showFlipAnimation && (index.h("div", { key: '63d262e5e10325d145fada8e91a878c47305f2b9', class: "flip-animation" }, index.h("div", { key: '2a3699f47e595c5e4ab00dfd11e1e66ab2e13039', class: "id-card-icon" }), index.h("div", { key: 'df195fe9a6db4ec3e14b04b4e932d55adb7c3f3c', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (index.h("div", { key: '670341c302a31c8017a9083ebf35e2988122ac3f', class: "success-animation" }, index.h("div", { key: '27b0b0d91de6a882182b6ccdebecc7c440997e36', class: "check-icon" }), index.h("div", { key: '71ee5327faf48ea26c2ae36a8a6906c448c3a389', class: "success-text" }, "\u00A1Proceso completado!"))), index.h("div", { key: '39c92aa0b82f6169058b1092894845fcba3f27b4', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (index.h("div", { key: '74b02ba81756f0e8bfb6e2feafdb2c44fb80afad', class: "status-spinner" })), index.h("div", { key: 'c7419efb2c6e2760019ecce619534e24ae3d7351', class: "status-content" }, index.h("div", { key: '57f315384630f17f6332eddfea628b333299e831', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (index.h("div", { key: 'ac8632fdd68c24ed00192cb04a13026ada1f953d', class: "status-description" }, this.currentStatus.description)))), this.debug && (index.h("div", { key: 'e51286a839795b18cfac2874e7e1f6e9f8e3bb4b', class: "performance-monitor" }, index.h("div", { key: 'f8cd289d260af9df2daf23bdfb8db70fe8bf6f45', class: "performance-expanded" }, index.h("div", { key: 'bf2becb2ee3ad3bffb82b0e90246a4385a19f567', class: "metrics-row" }, index.h("div", { key: 'f334c2b5de45df73aec6e06cd371151110789a45', class: "metric-compact" }, index.h("span", { key: 'cbf32f72eb3aaf1bf770d651c32123a43b81035c', class: "metric-label" }, "FPS"), index.h("span", { key: '9d2d2f46ba3fdb23c010f781044f00b1dd1250e6', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), index.h("div", { key: 'f29406e8a5b00f5838a144de9fda77ce53177d43', class: "metric-compact" }, index.h("span", { key: '8f44e29f6134c340f0a58f8aa321612c8a9bbfb8', class: "metric-label" }, "MEM"), index.h("span", { key: '7a58e164d292329d95054b2679a90e031f6fe4c2', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), index.h("div", { key: 'd0284a0e65fc2879557d73fc2e03295c29f512ae', class: "metrics-row" }, index.h("div", { key: '78dcbce552e65c95fcd01158f3aa8916ef0e7175', class: "metric-compact" }, index.h("span", { key: '673cfbc8c9dabb020a3d3c18ee07558a61da9b51', class: "metric-label" }, "INF"), index.h("span", { key: '833761b7a875c931a8b31913930c3b132f71f618', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), index.h("div", { key: '324801e3e69a8de181755367201d8e8c643e8d72', class: "metric-compact" }, index.h("span", { key: 'f928d9b2dfbf81f5738ab08a42ebb76ff4a483de', class: "metric-label" }, "FRAME"), index.h("span", { key: 'dc9f2e4a1e848be4d240d89c89126203555ae203', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), index.h("div", { key: '7ca0a7b44719b1884334edab586267f021cf86c1', class: "metrics-row" }, index.h("div", { key: '262667c4320d337dae2220b77f142cf293542562', class: "metric-compact" }, index.h("span", { key: '578b53d251e58933f654a5afe558931e4333f022', class: "metric-label" }, "DET"), index.h("span", { key: '4fb36eaebebd73e5cb06d582fa3b4ece61463761', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), index.h("div", { key: 'c17192e3964164ad603427de3befa1615b027022', class: "metric-compact" }, index.h("span", { key: 'd004e74797b9468e94c2af996bb62c1a3ca8d58e', class: "metric-label" }, "RATE"), index.h("span", { key: '0960e8760fd0ac2456a9e76598e5392c24e42d3b', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), index.h("div", { key: '3588fba1fe51af1335d643107ba0355aca6f4f8c', class: "watermark" }, index.h("img", { key: 'd53ec6b0ee9f97f602f4f8857ec605427b2ee478', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
|
|
1927
2123
|
}
|
|
1928
2124
|
// Utility methods
|
|
1929
2125
|
updateDetectionBoxes(boxes) {
|
|
@@ -2004,35 +2200,18 @@ const JaakStamps = class {
|
|
|
2004
2200
|
if (allSidesAligned && bestBox) {
|
|
2005
2201
|
cardOutline?.classList.add('perfect-match');
|
|
2006
2202
|
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
2203
|
if (!this.hasScreenshotTaken) {
|
|
2015
2204
|
const currentTime = Date.now();
|
|
2016
2205
|
// Initialize alignment start time if not set
|
|
2017
2206
|
if (!this.alignmentStartTime) {
|
|
2018
2207
|
this.alignmentStartTime = currentTime;
|
|
2019
|
-
this.logger.state('ALIGNMENT_TIMER_STARTED', { startTime: currentTime });
|
|
2020
2208
|
}
|
|
2021
2209
|
// Check if document has been aligned for the configured delay
|
|
2022
2210
|
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
2211
|
if (alignmentDuration >= this.captureDelay) {
|
|
2029
|
-
this.logger.state('TRIGGERING_CAPTURE', {
|
|
2030
|
-
alignmentDuration,
|
|
2031
|
-
captureDelay: this.captureDelay
|
|
2032
|
-
});
|
|
2033
2212
|
this.lastDetectedBox = bestBox;
|
|
2034
|
-
this.takeScreenshot().catch(
|
|
2035
|
-
|
|
2213
|
+
this.takeScreenshot().catch(() => {
|
|
2214
|
+
// Handle screenshot error silently
|
|
2036
2215
|
});
|
|
2037
2216
|
this.hasScreenshotTaken = true;
|
|
2038
2217
|
this.alignmentStartTime = undefined;
|
|
@@ -2055,24 +2234,150 @@ const JaakStamps = class {
|
|
|
2055
2234
|
}
|
|
2056
2235
|
}
|
|
2057
2236
|
}
|
|
2237
|
+
async takeManualScreenshot() {
|
|
2238
|
+
console.log('🔵 Botón clicked: Capturar (Frente/Reverso)');
|
|
2239
|
+
// Set processing state immediately
|
|
2240
|
+
const captureState = this.stateManager?.getCaptureState();
|
|
2241
|
+
if (captureState?.step === 'front') {
|
|
2242
|
+
this.processingButton = 'capture-front';
|
|
2243
|
+
}
|
|
2244
|
+
else if (captureState?.step === 'back') {
|
|
2245
|
+
this.processingButton = 'capture-back';
|
|
2246
|
+
}
|
|
2247
|
+
console.log('🔵 processingButton set to:', this.processingButton);
|
|
2248
|
+
// Add small delay to show spinner
|
|
2249
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
2250
|
+
if (!this.videoRef) {
|
|
2251
|
+
this.processingButton = null;
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
// When using manual capture, use mask coordinates for cropping
|
|
2255
|
+
// Note: processingButton will be cleared inside takeScreenshotWithMaskCoordinates
|
|
2256
|
+
await this.takeScreenshotWithMaskCoordinates();
|
|
2257
|
+
}
|
|
2258
|
+
async takeScreenshotWithMaskCoordinates() {
|
|
2259
|
+
if (!this.videoRef || !this.detectionContainer)
|
|
2260
|
+
return;
|
|
2261
|
+
this.stateManager.updateCaptureState({ isCapturing: true });
|
|
2262
|
+
this.triggerCaptureAnimation();
|
|
2263
|
+
// Use pooled canvas for optimization
|
|
2264
|
+
const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
|
|
2265
|
+
const captureCtx = captureCanvas.getContext('2d', { alpha: false });
|
|
2266
|
+
captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
|
|
2267
|
+
captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
|
|
2268
|
+
// Calculate mask coordinates for cropping
|
|
2269
|
+
const container = this.detectionContainer.parentElement;
|
|
2270
|
+
const containerRect = container.getBoundingClientRect();
|
|
2271
|
+
// Get mask dimensions from CSS properties
|
|
2272
|
+
const maskWidthPercent = parseFloat(this.el.style.getPropertyValue('--mask-width').replace('%', '')) || 90;
|
|
2273
|
+
const maskHeightPercent = parseFloat(this.el.style.getPropertyValue('--mask-height').replace('%', '')) || 56.25;
|
|
2274
|
+
const maskCenterXPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-x').replace('%', '')) || 50;
|
|
2275
|
+
const maskCenterYPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-y').replace('%', '')) || 50;
|
|
2276
|
+
// Convert percentages to actual video coordinates
|
|
2277
|
+
const videoWidth = this.videoRef.videoWidth;
|
|
2278
|
+
const videoHeight = this.videoRef.videoHeight;
|
|
2279
|
+
const videoAspectRatio = videoWidth / videoHeight;
|
|
2280
|
+
const containerAspectRatio = containerRect.width / containerRect.height;
|
|
2281
|
+
let displayedVideoWidth, displayedVideoHeight;
|
|
2282
|
+
let videoOffsetX = 0, videoOffsetY = 0;
|
|
2283
|
+
if (videoAspectRatio > containerAspectRatio) {
|
|
2284
|
+
displayedVideoWidth = containerRect.width;
|
|
2285
|
+
displayedVideoHeight = containerRect.width / videoAspectRatio;
|
|
2286
|
+
videoOffsetY = (containerRect.height - displayedVideoHeight) / 2;
|
|
2287
|
+
}
|
|
2288
|
+
else {
|
|
2289
|
+
displayedVideoHeight = containerRect.height;
|
|
2290
|
+
displayedVideoWidth = containerRect.height * videoAspectRatio;
|
|
2291
|
+
videoOffsetX = (containerRect.width - displayedVideoWidth) / 2;
|
|
2292
|
+
}
|
|
2293
|
+
// Calculate mask coordinates in video space
|
|
2294
|
+
const maskWidthInContainer = (maskWidthPercent / 100) * containerRect.width;
|
|
2295
|
+
const maskHeightInContainer = (maskHeightPercent / 100) * containerRect.height;
|
|
2296
|
+
const maskCenterXInContainer = (maskCenterXPercent / 100) * containerRect.width;
|
|
2297
|
+
const maskCenterYInContainer = (maskCenterYPercent / 100) * containerRect.height;
|
|
2298
|
+
// Convert to video coordinates
|
|
2299
|
+
const scaleX = videoWidth / displayedVideoWidth;
|
|
2300
|
+
const scaleY = videoHeight / displayedVideoHeight;
|
|
2301
|
+
const maskCenterXInVideo = (maskCenterXInContainer - videoOffsetX) * scaleX;
|
|
2302
|
+
const maskCenterYInVideo = (maskCenterYInContainer - videoOffsetY) * scaleY;
|
|
2303
|
+
const maskWidthInVideo = maskWidthInContainer * scaleX;
|
|
2304
|
+
const maskHeightInVideo = maskHeightInContainer * scaleY;
|
|
2305
|
+
// Calculate crop coordinates
|
|
2306
|
+
const cropX = Math.max(0, maskCenterXInVideo - (maskWidthInVideo / 2) - this.cropMargin);
|
|
2307
|
+
const cropY = Math.max(0, maskCenterYInVideo - (maskHeightInVideo / 2) - this.cropMargin);
|
|
2308
|
+
const cropWidth = Math.min(maskWidthInVideo + (2 * this.cropMargin), videoWidth - cropX);
|
|
2309
|
+
const cropHeight = Math.min(maskHeightInVideo + (2 * this.cropMargin), videoHeight - cropY);
|
|
2310
|
+
// Use pooled canvas for cropped version
|
|
2311
|
+
const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
|
|
2312
|
+
const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
|
|
2313
|
+
croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
|
|
2314
|
+
croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
|
|
2315
|
+
const captureState = this.stateManager.getCaptureState();
|
|
2316
|
+
if (captureState.step === 'front') {
|
|
2317
|
+
this.stateManager.setCapturedImages({
|
|
2318
|
+
front: {
|
|
2319
|
+
fullFrame: captureCanvas.toDataURL('image/png'),
|
|
2320
|
+
cropped: croppedCanvas.toDataURL('image/png')
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
// Check if document classification is enabled (independent of detector)
|
|
2324
|
+
if (this.useDocumentClassification) {
|
|
2325
|
+
try {
|
|
2326
|
+
const classification = await this.detectionService.classifyDocument(croppedCanvas);
|
|
2327
|
+
if (classification && classification.class === 'passport') {
|
|
2328
|
+
this.completeProcess(true);
|
|
2329
|
+
this.returnCanvasToPool(captureCanvas);
|
|
2330
|
+
this.returnCanvasToPool(croppedCanvas);
|
|
2331
|
+
// Clear processing button when passport is detected
|
|
2332
|
+
this.processingButton = null;
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
catch (classifyError) {
|
|
2337
|
+
// No cambiar a manual aquí, solo registrar el error y continuar
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
this.stateManager.updateCaptureState({
|
|
2341
|
+
step: 'back',
|
|
2342
|
+
isDetectionPaused: true,
|
|
2343
|
+
showFlipAnimation: true
|
|
2344
|
+
});
|
|
2345
|
+
this.triggerRerender();
|
|
2346
|
+
setTimeout(() => {
|
|
2347
|
+
this.stateManager.updateCaptureState({
|
|
2348
|
+
showFlipAnimation: false,
|
|
2349
|
+
isDetectionPaused: false
|
|
2350
|
+
});
|
|
2351
|
+
this.triggerRerender();
|
|
2352
|
+
this.startBackDocumentTimer();
|
|
2353
|
+
// Clear processing button after animation completes
|
|
2354
|
+
this.processingButton = null;
|
|
2355
|
+
}, 3000);
|
|
2356
|
+
}
|
|
2357
|
+
else if (captureState.step === 'back') {
|
|
2358
|
+
this.stateManager.setCapturedImages({
|
|
2359
|
+
back: {
|
|
2360
|
+
fullFrame: captureCanvas.toDataURL('image/png'),
|
|
2361
|
+
cropped: croppedCanvas.toDataURL('image/png')
|
|
2362
|
+
}
|
|
2363
|
+
});
|
|
2364
|
+
this.completeProcess(false);
|
|
2365
|
+
// Clear processing button after back capture completes
|
|
2366
|
+
this.processingButton = null;
|
|
2367
|
+
}
|
|
2368
|
+
// Return canvases to pool after use
|
|
2369
|
+
this.returnCanvasToPool(captureCanvas);
|
|
2370
|
+
this.returnCanvasToPool(croppedCanvas);
|
|
2371
|
+
}
|
|
2058
2372
|
async takeScreenshot() {
|
|
2059
2373
|
if (!this.videoRef || !this.lastDetectedBox)
|
|
2060
2374
|
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
2375
|
this.stateManager.updateCaptureState({ isCapturing: true });
|
|
2070
2376
|
this.triggerCaptureAnimation();
|
|
2071
|
-
//
|
|
2072
|
-
const captureCanvas =
|
|
2073
|
-
captureCanvas.width = this.videoRef.videoWidth;
|
|
2074
|
-
captureCanvas.height = this.videoRef.videoHeight;
|
|
2377
|
+
// Use pooled canvas for optimization
|
|
2378
|
+
const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
|
|
2075
2379
|
const captureCtx = captureCanvas.getContext('2d', { alpha: false });
|
|
2380
|
+
captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
|
|
2076
2381
|
captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
|
|
2077
2382
|
// Calculate crop coordinates
|
|
2078
2383
|
const INPUT_SIZE = 320;
|
|
@@ -2082,11 +2387,10 @@ const JaakStamps = class {
|
|
|
2082
2387
|
const cropY = Math.max(0, (this.lastDetectedBox.y * scaleY) - this.cropMargin);
|
|
2083
2388
|
const cropWidth = Math.min((this.lastDetectedBox.w * scaleX) + (2 * this.cropMargin), this.videoRef.videoWidth - cropX);
|
|
2084
2389
|
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;
|
|
2390
|
+
// Use pooled canvas for cropped version
|
|
2391
|
+
const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
|
|
2089
2392
|
const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
|
|
2393
|
+
croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
|
|
2090
2394
|
croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
|
|
2091
2395
|
const captureState = this.stateManager.getCaptureState();
|
|
2092
2396
|
if (captureState.step === 'front') {
|
|
@@ -2098,11 +2402,15 @@ const JaakStamps = class {
|
|
|
2098
2402
|
});
|
|
2099
2403
|
// Check if document classification is enabled
|
|
2100
2404
|
if (this.useDocumentClassification) {
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2405
|
+
try {
|
|
2406
|
+
const classification = await this.detectionService.classifyDocument(croppedCanvas);
|
|
2407
|
+
if (classification && classification.class === 'passport') {
|
|
2408
|
+
this.completeProcess(true);
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
catch (classifyError) {
|
|
2413
|
+
// No cambiar a manual aquí, solo registrar el error y continuar
|
|
2106
2414
|
}
|
|
2107
2415
|
}
|
|
2108
2416
|
this.stateManager.updateCaptureState({
|
|
@@ -2127,6 +2435,9 @@ const JaakStamps = class {
|
|
|
2127
2435
|
});
|
|
2128
2436
|
this.completeProcess(false);
|
|
2129
2437
|
}
|
|
2438
|
+
// Return canvases to pool after use
|
|
2439
|
+
this.returnCanvasToPool(captureCanvas);
|
|
2440
|
+
this.returnCanvasToPool(croppedCanvas);
|
|
2130
2441
|
}
|
|
2131
2442
|
triggerCaptureAnimation() {
|
|
2132
2443
|
const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
|
|
@@ -2136,6 +2447,135 @@ const JaakStamps = class {
|
|
|
2136
2447
|
cardOutline?.classList.remove('capturing');
|
|
2137
2448
|
}, 600);
|
|
2138
2449
|
}
|
|
2450
|
+
// Método simplificado para cambio a modo manual
|
|
2451
|
+
switchToManualMode() {
|
|
2452
|
+
// Only switch if detector is enabled and not already switched
|
|
2453
|
+
if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
// Detener todo el monitoreo de performance
|
|
2457
|
+
this.stopPerformanceMonitoring();
|
|
2458
|
+
// Liberar recursos de ONNX
|
|
2459
|
+
this.releaseOnnxResources();
|
|
2460
|
+
// Resetear máscara a blanco y limpiar detecciones
|
|
2461
|
+
this.resetMaskToWhite();
|
|
2462
|
+
this.detectionBoxes = [];
|
|
2463
|
+
this.hasAutoSwitchedToManual = true;
|
|
2464
|
+
this.performanceDegradedMode = true;
|
|
2465
|
+
this.showManualCaptureButton = true;
|
|
2466
|
+
this.showPerformanceMessage = true;
|
|
2467
|
+
// Hide performance message after 5 seconds
|
|
2468
|
+
setTimeout(() => {
|
|
2469
|
+
this.showPerformanceMessage = false;
|
|
2470
|
+
}, 5000);
|
|
2471
|
+
}
|
|
2472
|
+
// Método para cambiar a modo manual por errores de ONNX
|
|
2473
|
+
switchToManualModeWithWarning(warningMessage) {
|
|
2474
|
+
// Solo cambiar si el detector está habilitado y no se ha cambiado ya
|
|
2475
|
+
if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
// No necesitamos liberar recursos ONNX porque no se han cargado aún
|
|
2479
|
+
// Resetear máscara a blanco y limpiar detecciones
|
|
2480
|
+
this.resetMaskToWhite();
|
|
2481
|
+
this.detectionBoxes = [];
|
|
2482
|
+
// Cambiar a modo manual
|
|
2483
|
+
this.hasAutoSwitchedToManual = true;
|
|
2484
|
+
this.performanceDegradedMode = true;
|
|
2485
|
+
this.showManualCaptureButton = true;
|
|
2486
|
+
this.useDocumentDetector = false; // Desactivar detector automático
|
|
2487
|
+
// Actualizar el estado con mensaje informativo (no error)
|
|
2488
|
+
this.updateStatus('Modo manual activado', warningMessage, 'active');
|
|
2489
|
+
// Continuar con la configuración de cámara
|
|
2490
|
+
this.continueWithCameraSetup();
|
|
2491
|
+
}
|
|
2492
|
+
async continueWithCameraSetup() {
|
|
2493
|
+
try {
|
|
2494
|
+
// Paso 2: Detectar y configurar dispositivos
|
|
2495
|
+
this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
|
|
2496
|
+
try {
|
|
2497
|
+
await this.cameraService.enumerateDevices();
|
|
2498
|
+
}
|
|
2499
|
+
catch (enumerateError) {
|
|
2500
|
+
throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
|
|
2501
|
+
}
|
|
2502
|
+
try {
|
|
2503
|
+
await this.updateCameraInfoWithAutofocus();
|
|
2504
|
+
}
|
|
2505
|
+
catch (cameraInfoError) {
|
|
2506
|
+
throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
|
|
2507
|
+
}
|
|
2508
|
+
// Paso 3: Configurar cámara seleccionada
|
|
2509
|
+
this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
|
|
2510
|
+
let stream;
|
|
2511
|
+
try {
|
|
2512
|
+
stream = await this.cameraService.setupCamera();
|
|
2513
|
+
}
|
|
2514
|
+
catch (setupError) {
|
|
2515
|
+
throw new Error(`Error al configurar cámara: ${setupError.message}`);
|
|
2516
|
+
}
|
|
2517
|
+
// Paso 4: Inicializar video
|
|
2518
|
+
this.updateStatus('Captura manual', 'Posicione su documento y use el botón para capturar', 'active');
|
|
2519
|
+
try {
|
|
2520
|
+
await this.initializeVideoStream(stream);
|
|
2521
|
+
}
|
|
2522
|
+
catch (videoError) {
|
|
2523
|
+
throw new Error(`Error al inicializar video: ${videoError.message}`);
|
|
2524
|
+
}
|
|
2525
|
+
// Paso 5: Calibrar máscara
|
|
2526
|
+
if (this.detectionContainer) {
|
|
2527
|
+
const container = this.detectionContainer.parentElement;
|
|
2528
|
+
const rect = container.getBoundingClientRect();
|
|
2529
|
+
this.updateMaskDimensions(rect);
|
|
2530
|
+
}
|
|
2531
|
+
// Finalizar
|
|
2532
|
+
this.startTime = Date.now();
|
|
2533
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
2534
|
+
// Mostrar botón manual (ya debe estar habilitado)
|
|
2535
|
+
this.showManualCaptureButton = true;
|
|
2536
|
+
}
|
|
2537
|
+
catch (error) {
|
|
2538
|
+
this.updateStatus('Error de cámara', error.message, 'error');
|
|
2539
|
+
throw error;
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
switchToManualModeWithError(errorMessage) {
|
|
2543
|
+
// Solo cambiar si el detector está habilitado y no se ha cambiado ya
|
|
2544
|
+
if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
// Liberar recursos de ONNX inmediatamente
|
|
2548
|
+
this.releaseOnnxResources();
|
|
2549
|
+
// Resetear máscara a blanco y limpiar detecciones
|
|
2550
|
+
this.resetMaskToWhite();
|
|
2551
|
+
this.detectionBoxes = [];
|
|
2552
|
+
// Cambiar a modo manual
|
|
2553
|
+
this.hasAutoSwitchedToManual = true;
|
|
2554
|
+
this.performanceDegradedMode = true;
|
|
2555
|
+
this.showManualCaptureButton = true;
|
|
2556
|
+
this.useDocumentDetector = false; // Desactivar detector automático
|
|
2557
|
+
// Actualizar el estado con mensaje de error
|
|
2558
|
+
this.updateStatus('Modo manual activado', errorMessage, 'error');
|
|
2559
|
+
// Ocultar mensaje después de 5 segundos
|
|
2560
|
+
setTimeout(() => {
|
|
2561
|
+
this.updateStatus('Captura manual', 'Use el botón para capturar', 'ready');
|
|
2562
|
+
}, 5000);
|
|
2563
|
+
}
|
|
2564
|
+
stopPerformanceMonitoring() {
|
|
2565
|
+
// Nota: No necesitamos limpiar más variables porque ya no se usarán
|
|
2566
|
+
// El sistema automáticamente saltará la lógica de performance una vez que hasAutoSwitchedToManual = true
|
|
2567
|
+
}
|
|
2568
|
+
// Método para liberar recursos de ONNX cuando se cambia a modo manual
|
|
2569
|
+
releaseOnnxResources() {
|
|
2570
|
+
try {
|
|
2571
|
+
// Liberar recursos del servicio de detección
|
|
2572
|
+
if (this.detectionService) {
|
|
2573
|
+
this.detectionService.cleanup();
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
catch (error) {
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2139
2579
|
completeProcess(skippedBack = false) {
|
|
2140
2580
|
this.stateManager.updateCaptureState({
|
|
2141
2581
|
step: 'completed',
|
|
@@ -2155,11 +2595,6 @@ const JaakStamps = class {
|
|
|
2155
2595
|
setTimeout(() => {
|
|
2156
2596
|
this.stateManager.updateCaptureState({ showSuccessAnimation: false });
|
|
2157
2597
|
}, 3000);
|
|
2158
|
-
this.logger.state('PROCESO_COMPLETADO', {
|
|
2159
|
-
skippedBack,
|
|
2160
|
-
totalImages: capturedImages.metadata.totalImages,
|
|
2161
|
-
timestamp: new Date().toISOString()
|
|
2162
|
-
});
|
|
2163
2598
|
}
|
|
2164
2599
|
stopDetection() {
|
|
2165
2600
|
if (this.animationId) {
|
|
@@ -2168,7 +2603,6 @@ const JaakStamps = class {
|
|
|
2168
2603
|
}
|
|
2169
2604
|
this.clearBackDocumentTimer();
|
|
2170
2605
|
this.detectionBoxes = [];
|
|
2171
|
-
this.logger.state('DETECTOR_DETENIDO', { timestamp: Date.now() });
|
|
2172
2606
|
}
|
|
2173
2607
|
startBackDocumentTimer() {
|
|
2174
2608
|
if (!this.enableBackDocumentTimer)
|
|
@@ -2197,6 +2631,15 @@ const JaakStamps = class {
|
|
|
2197
2631
|
return; // Don't toggle if switching camera
|
|
2198
2632
|
this.showCameraSelector = !this.showCameraSelector;
|
|
2199
2633
|
}
|
|
2634
|
+
async updateCameraInfoWithAutofocus() {
|
|
2635
|
+
try {
|
|
2636
|
+
const cameraInfo = await this.cameraService.getCameraInfo();
|
|
2637
|
+
this.cameraInfoWithAutofocus = cameraInfo;
|
|
2638
|
+
}
|
|
2639
|
+
catch (error) {
|
|
2640
|
+
// Keep existing state if update fails
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2200
2643
|
async handleCameraSwitch(cameraId) {
|
|
2201
2644
|
if (this.isSwitchingCamera)
|
|
2202
2645
|
return; // Prevent multiple simultaneous switches
|
|
@@ -2204,10 +2647,6 @@ const JaakStamps = class {
|
|
|
2204
2647
|
// Close the selector immediately when user selects a camera
|
|
2205
2648
|
this.showCameraSelector = false;
|
|
2206
2649
|
this.isSwitchingCamera = true;
|
|
2207
|
-
this.logger.state('INICIANDO_CAMBIO_CAMARA', {
|
|
2208
|
-
from: this.cameraService.getSelectedCameraId(),
|
|
2209
|
-
to: cameraId
|
|
2210
|
-
});
|
|
2211
2650
|
// Stop current video stream
|
|
2212
2651
|
if (this.videoStream) {
|
|
2213
2652
|
this.videoStream.getTracks().forEach(track => track.stop());
|
|
@@ -2218,21 +2657,17 @@ const JaakStamps = class {
|
|
|
2218
2657
|
const newStream = await this.cameraService.setupCamera();
|
|
2219
2658
|
// Update video element
|
|
2220
2659
|
await this.initializeVideoStream(newStream);
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
isRearCamera: this.cameraService.isRearCamera(newStream)
|
|
2224
|
-
});
|
|
2660
|
+
// Update camera info with autofocus data
|
|
2661
|
+
await this.updateCameraInfoWithAutofocus();
|
|
2225
2662
|
}
|
|
2226
2663
|
catch (error) {
|
|
2227
|
-
this.logger.error('Error al cambiar cámara:', error);
|
|
2228
2664
|
// Try to restore previous camera if switch failed
|
|
2229
2665
|
try {
|
|
2230
2666
|
const fallbackStream = await this.cameraService.setupCamera();
|
|
2231
2667
|
await this.initializeVideoStream(fallbackStream);
|
|
2232
2668
|
}
|
|
2233
2669
|
catch (fallbackError) {
|
|
2234
|
-
this.
|
|
2235
|
-
this.updateStatus('Error al cambiar cámara', 'No se pudo completar el cambio de dispositivo', 'error');
|
|
2670
|
+
this.updateStatus('Error al cambiar cámara', 'No se pudo cambiar el dispositivo', 'error');
|
|
2236
2671
|
}
|
|
2237
2672
|
}
|
|
2238
2673
|
finally {
|
|
@@ -2254,6 +2689,13 @@ const JaakStamps = class {
|
|
|
2254
2689
|
this.detectionBoxes = [];
|
|
2255
2690
|
this.alignmentStartTime = undefined;
|
|
2256
2691
|
this.hasDocumentDetected = false;
|
|
2692
|
+
// Reset sistema simplificado
|
|
2693
|
+
this.processedFramesCount = 0;
|
|
2694
|
+
this.slowFrameCount = 0;
|
|
2695
|
+
// Reset performance degradation state
|
|
2696
|
+
this.hasAutoSwitchedToManual = false;
|
|
2697
|
+
this.performanceDegradedMode = false;
|
|
2698
|
+
this.showPerformanceMessage = false;
|
|
2257
2699
|
if (this.alignmentTimer) {
|
|
2258
2700
|
clearTimeout(this.alignmentTimer);
|
|
2259
2701
|
this.alignmentTimer = undefined;
|
|
@@ -2273,7 +2715,7 @@ const JaakStamps = class {
|
|
|
2273
2715
|
this.stateManager.updateCaptureState({ isVideoActive: false, isLoading: false });
|
|
2274
2716
|
}
|
|
2275
2717
|
this.isMaskReady = false;
|
|
2276
|
-
this.updateStatus('
|
|
2718
|
+
this.updateStatus('Captura finalizada', '', 'ready');
|
|
2277
2719
|
this.detectionBoxes = [];
|
|
2278
2720
|
this.cleanup();
|
|
2279
2721
|
}
|
|
@@ -2284,7 +2726,6 @@ const JaakStamps = class {
|
|
|
2284
2726
|
this.performanceUpdateInterval = window.setInterval(() => {
|
|
2285
2727
|
this.updatePerformanceMetrics();
|
|
2286
2728
|
}, 500);
|
|
2287
|
-
this.logger.debug('Monitor de performance inicializado');
|
|
2288
2729
|
}
|
|
2289
2730
|
updatePerformanceMetrics() {
|
|
2290
2731
|
const currentTime = performance.now();
|
|
@@ -2327,6 +2768,84 @@ const JaakStamps = class {
|
|
|
2327
2768
|
if (detectionsFound > 0) {
|
|
2328
2769
|
this.performanceMetrics.successfulDetections++;
|
|
2329
2770
|
}
|
|
2771
|
+
// Update performance history for adaptive frame skipping
|
|
2772
|
+
this.performanceHistory.push(processingTime);
|
|
2773
|
+
if (this.performanceHistory.length > this.PERFORMANCE_HISTORY_SIZE) {
|
|
2774
|
+
this.performanceHistory.shift();
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
// Adaptive frame skipping based on performance
|
|
2778
|
+
getAdaptiveFrameSkip() {
|
|
2779
|
+
// Base conditions for high frame skip
|
|
2780
|
+
if (this.consecutiveFailures > 20)
|
|
2781
|
+
return this.MAX_FRAME_SKIP;
|
|
2782
|
+
if (this.performanceMetrics.inferenceTime > 200)
|
|
2783
|
+
return 6;
|
|
2784
|
+
if (this.performanceMetrics.memoryUsage > 200)
|
|
2785
|
+
return 5;
|
|
2786
|
+
// Calculate average processing time
|
|
2787
|
+
if (this.performanceHistory.length > 0) {
|
|
2788
|
+
const avgProcessingTime = this.performanceHistory.reduce((a, b) => a + b, 0) / this.performanceHistory.length;
|
|
2789
|
+
if (avgProcessingTime > 100)
|
|
2790
|
+
return 4;
|
|
2791
|
+
if (avgProcessingTime > 80)
|
|
2792
|
+
return 3;
|
|
2793
|
+
if (avgProcessingTime > 60)
|
|
2794
|
+
return this.BASE_FRAME_SKIP + 1;
|
|
2795
|
+
}
|
|
2796
|
+
// Low memory devices get higher frame skip
|
|
2797
|
+
if (this.performanceMetrics.memoryUsage > 150)
|
|
2798
|
+
return 4;
|
|
2799
|
+
// Performance is good, use base frame skip
|
|
2800
|
+
return this.BASE_FRAME_SKIP;
|
|
2801
|
+
}
|
|
2802
|
+
// Método para resetear la máscara a color blanco
|
|
2803
|
+
resetMaskToWhite() {
|
|
2804
|
+
const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
|
|
2805
|
+
const corners = this.el.shadowRoot?.querySelectorAll('.corner');
|
|
2806
|
+
const topSide = this.el.shadowRoot?.querySelector('.side-top');
|
|
2807
|
+
const rightSide = this.el.shadowRoot?.querySelector('.side-right');
|
|
2808
|
+
const bottomSide = this.el.shadowRoot?.querySelector('.side-bottom');
|
|
2809
|
+
const leftSide = this.el.shadowRoot?.querySelector('.side-left');
|
|
2810
|
+
// Remover todas las clases de alineación y estado perfecto
|
|
2811
|
+
cardOutline?.classList.remove('perfect-match');
|
|
2812
|
+
corners?.forEach(corner => corner.classList.remove('perfect-match'));
|
|
2813
|
+
topSide?.classList.remove('aligned');
|
|
2814
|
+
rightSide?.classList.remove('aligned');
|
|
2815
|
+
bottomSide?.classList.remove('aligned');
|
|
2816
|
+
leftSide?.classList.remove('aligned');
|
|
2817
|
+
// Resetear estado de alineación
|
|
2818
|
+
this.sideAlignment = { top: false, right: false, bottom: false, left: false };
|
|
2819
|
+
}
|
|
2820
|
+
// Canvas pool management for screenshots
|
|
2821
|
+
getPooledCanvas(width, height) {
|
|
2822
|
+
// Try to find a canvas with matching dimensions
|
|
2823
|
+
const matchingIndex = this.canvasPool.findIndex(canvas => canvas.width === width && canvas.height === height);
|
|
2824
|
+
if (matchingIndex !== -1) {
|
|
2825
|
+
return this.canvasPool.splice(matchingIndex, 1)[0];
|
|
2826
|
+
}
|
|
2827
|
+
// If no matching canvas, try to reuse any canvas and resize
|
|
2828
|
+
if (this.canvasPool.length > 0) {
|
|
2829
|
+
const canvas = this.canvasPool.pop();
|
|
2830
|
+
canvas.width = width;
|
|
2831
|
+
canvas.height = height;
|
|
2832
|
+
return canvas;
|
|
2833
|
+
}
|
|
2834
|
+
// Create new canvas if pool is empty
|
|
2835
|
+
const canvas = document.createElement('canvas');
|
|
2836
|
+
canvas.width = width;
|
|
2837
|
+
canvas.height = height;
|
|
2838
|
+
return canvas;
|
|
2839
|
+
}
|
|
2840
|
+
returnCanvasToPool(canvas) {
|
|
2841
|
+
// Only return to pool if not at max capacity
|
|
2842
|
+
if (this.canvasPool.length < this.MAX_CANVAS_POOL_SIZE) {
|
|
2843
|
+
// Clear the canvas before returning to pool
|
|
2844
|
+
const ctx = canvas.getContext('2d');
|
|
2845
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
2846
|
+
this.canvasPool.push(canvas);
|
|
2847
|
+
}
|
|
2848
|
+
// If pool is full, let the canvas be garbage collected
|
|
2330
2849
|
}
|
|
2331
2850
|
};
|
|
2332
2851
|
JaakStamps.style = myComponentCss;
|