@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.
Files changed (71) hide show
  1. package/README.md +11 -1
  2. package/dist/cjs/app-globals-V2Kpy_OQ.js +8 -0
  3. package/dist/cjs/app-globals-V2Kpy_OQ.js.map +1 -0
  4. package/dist/cjs/{index-BfhtOB0D.js → index-Ga0t6BMe.js} +176 -119
  5. package/dist/cjs/index-Ga0t6BMe.js.map +1 -0
  6. package/dist/cjs/jaak-stamps-webcomponent.cjs.js +5 -4
  7. package/dist/cjs/jaak-stamps-webcomponent.cjs.js.map +1 -1
  8. package/dist/cjs/jaak-stamps.cjs.entry.js +1174 -655
  9. package/dist/cjs/jaak-stamps.cjs.entry.js.map +1 -1
  10. package/dist/cjs/jaak-stamps.entry.cjs.js.map +1 -1
  11. package/dist/cjs/loader.cjs.js +4 -3
  12. package/dist/cjs/loader.cjs.js.map +1 -1
  13. package/dist/collection/collection-manifest.json +1 -1
  14. package/dist/collection/components/my-component/my-component.css +224 -13
  15. package/dist/collection/components/my-component/my-component.js +950 -281
  16. package/dist/collection/components/my-component/my-component.js.map +1 -1
  17. package/dist/collection/services/CameraService.js +156 -344
  18. package/dist/collection/services/CameraService.js.map +1 -1
  19. package/dist/collection/services/DetectionService.js +100 -42
  20. package/dist/collection/services/DetectionService.js.map +1 -1
  21. package/dist/collection/services/EventBusService.js +1 -1
  22. package/dist/collection/services/EventBusService.js.map +1 -1
  23. package/dist/collection/services/ServiceContainer.js +4 -13
  24. package/dist/collection/services/ServiceContainer.js.map +1 -1
  25. package/dist/collection/services/interfaces/ICameraService.js.map +1 -1
  26. package/dist/collection/types/component-types.js.map +1 -1
  27. package/dist/components/index.js +165 -113
  28. package/dist/components/index.js.map +1 -1
  29. package/dist/components/jaak-stamps.js +1182 -658
  30. package/dist/components/jaak-stamps.js.map +1 -1
  31. package/dist/esm/app-globals-DQuL1Twl.js +6 -0
  32. package/dist/esm/app-globals-DQuL1Twl.js.map +1 -0
  33. package/dist/esm/{index-BP1Q4KOg.js → index-Drbzcuq-.js} +177 -119
  34. package/dist/esm/index-Drbzcuq-.js.map +1 -0
  35. package/dist/esm/jaak-stamps-webcomponent.js +5 -4
  36. package/dist/esm/jaak-stamps-webcomponent.js.map +1 -1
  37. package/dist/esm/jaak-stamps.entry.js +1174 -655
  38. package/dist/esm/jaak-stamps.entry.js.map +1 -1
  39. package/dist/esm/loader.js +4 -3
  40. package/dist/esm/loader.js.map +1 -1
  41. package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
  42. package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js.map +1 -1
  43. package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
  44. package/dist/jaak-stamps-webcomponent/loader.esm.js.map +1 -1
  45. package/dist/jaak-stamps-webcomponent/p-5f7301f1.entry.js +2 -0
  46. package/dist/jaak-stamps-webcomponent/p-5f7301f1.entry.js.map +1 -0
  47. package/dist/jaak-stamps-webcomponent/p-DQuL1Twl.js +2 -0
  48. package/dist/jaak-stamps-webcomponent/p-DQuL1Twl.js.map +1 -0
  49. package/dist/jaak-stamps-webcomponent/p-Drbzcuq-.js +3 -0
  50. package/dist/jaak-stamps-webcomponent/p-Drbzcuq-.js.map +1 -0
  51. package/dist/types/components/my-component/my-component.d.ts +87 -17
  52. package/dist/types/components.d.ts +14 -8
  53. package/dist/types/services/CameraService.d.ts +12 -14
  54. package/dist/types/services/DetectionService.d.ts +10 -3
  55. package/dist/types/services/ServiceContainer.d.ts +1 -2
  56. package/dist/types/services/interfaces/ICameraService.d.ts +3 -12
  57. package/dist/types/stencil-public-runtime.d.ts +12 -5
  58. package/dist/types/types/component-types.d.ts +3 -0
  59. package/package.json +4 -2
  60. package/dist/cjs/index-BfhtOB0D.js.map +0 -1
  61. package/dist/collection/services/LoggerService.js +0 -40
  62. package/dist/collection/services/LoggerService.js.map +0 -1
  63. package/dist/collection/services/interfaces/ILogger.js +0 -2
  64. package/dist/collection/services/interfaces/ILogger.js.map +0 -1
  65. package/dist/esm/index-BP1Q4KOg.js.map +0 -1
  66. package/dist/jaak-stamps-webcomponent/p-2264b5b4.entry.js +0 -2
  67. package/dist/jaak-stamps-webcomponent/p-2264b5b4.entry.js.map +0 -1
  68. package/dist/jaak-stamps-webcomponent/p-BP1Q4KOg.js +0 -3
  69. package/dist/jaak-stamps-webcomponent/p-BP1Q4KOg.js.map +0 -1
  70. package/dist/types/services/LoggerService.d.ts +0 -12
  71. package/dist/types/services/interfaces/ILogger.d.ts +0 -8
@@ -1,44 +1,4 @@
1
- import { r as registerInstance, c as createEvent, a as getElement, h } from './index-BP1Q4KOg.js';
2
-
3
- class LoggerService {
4
- debugMode;
5
- constructor(debug = false) {
6
- this.debugMode = debug;
7
- }
8
- setDebugMode(debug) {
9
- this.debugMode = debug;
10
- }
11
- info(...args) {
12
- if (this.debugMode) {
13
- console.log(`[JAAK-STAMPS] [INFO] [${new Date().toLocaleTimeString()}]`, ...args);
14
- }
15
- }
16
- warn(...args) {
17
- if (this.debugMode) {
18
- console.warn(`[JAAK-STAMPS] [WARN] [${new Date().toLocaleTimeString()}]`, ...args);
19
- }
20
- }
21
- error(...args) {
22
- if (this.debugMode) {
23
- console.error(`[JAAK-STAMPS] [ERROR] [${new Date().toLocaleTimeString()}]`, ...args);
24
- }
25
- }
26
- debug(...args) {
27
- if (this.debugMode) {
28
- console.debug(`[JAAK-STAMPS] [DEBUG] [${new Date().toLocaleTimeString()}]`, ...args);
29
- }
30
- }
31
- state(state, data) {
32
- if (this.debugMode) {
33
- console.log(`[JAAK-STAMPS] [STATE] [${new Date().toLocaleTimeString()}] ${state}`, data || '');
34
- }
35
- }
36
- performance(operation, duration) {
37
- if (this.debugMode) {
38
- console.log(`[JAAK-STAMPS] [PERF] [${new Date().toLocaleTimeString()}] ${operation}: ${duration}ms`);
39
- }
40
- }
41
- }
1
+ import { r as registerInstance, c as createEvent, g as getElement, h } from './index-Drbzcuq-.js';
42
2
 
43
3
  class EventBusService {
44
4
  events = new Map();
@@ -65,7 +25,7 @@ class EventBusService {
65
25
  callback(data);
66
26
  }
67
27
  catch (error) {
68
- console.error(`Error in event callback for ${event}:`, error);
28
+ // Silently handle callback errors
69
29
  }
70
30
  });
71
31
  }
@@ -192,16 +152,20 @@ class StateManagerService {
192
152
  }
193
153
 
194
154
  class CameraService {
195
- logger;
196
155
  eventBus;
197
156
  availableCameras = [];
198
157
  selectedCameraId = null;
199
158
  deviceType = 'desktop';
200
159
  preferredCameraFacing = null;
201
160
  preferredCamera = 'auto';
202
- isManuallySelected = false;
203
- constructor(logger, eventBus, preferredCamera = 'auto') {
204
- this.logger = logger;
161
+ // Optimization: Static caches for performance
162
+ static cameraCapabilitiesCache = new Map();
163
+ static deviceEnumerationCache = null;
164
+ static CACHE_DURATION = 30000; // 30 seconds
165
+ // Stream management for optimization
166
+ currentStreamPromise;
167
+ lastStreamConstraints;
168
+ constructor(eventBus, preferredCamera = 'auto') {
205
169
  this.eventBus = eventBus;
206
170
  this.preferredCamera = preferredCamera;
207
171
  }
@@ -218,18 +182,21 @@ class CameraService {
218
182
  else {
219
183
  this.deviceType = 'desktop';
220
184
  }
221
- this.logger.state('DISPOSITIVO_DETECTADO', {
222
- deviceType: this.deviceType,
223
- userAgent: navigator.userAgent,
224
- screenDimensions: { width: window.innerWidth, height: window.innerHeight }
225
- });
226
185
  return this.deviceType;
227
186
  }
228
187
  async enumerateDevices() {
229
188
  try {
189
+ // Check cache first for optimization
190
+ const now = Date.now();
191
+ if (CameraService.deviceEnumerationCache &&
192
+ (now - CameraService.deviceEnumerationCache.timestamp) < CameraService.CACHE_DURATION) {
193
+ this.availableCameras = CameraService.deviceEnumerationCache.devices;
194
+ await this.setInitialCameraPreference();
195
+ this.eventBus.emit('camera-changed', this.selectedCameraId);
196
+ return this.availableCameras;
197
+ }
230
198
  const permissionStatus = await this.checkCameraPermission();
231
199
  if (permissionStatus === 'denied') {
232
- this.logger.error('Permiso de cámara denegado por el usuario');
233
200
  return [];
234
201
  }
235
202
  if (permissionStatus === 'prompt') {
@@ -238,24 +205,16 @@ class CameraService {
238
205
  }
239
206
  const devices = await navigator.mediaDevices.enumerateDevices();
240
207
  this.availableCameras = devices.filter(device => device.kind === 'videoinput');
241
- this.logger.state('CAMARAS_DETECTADAS', {
242
- count: this.availableCameras.length,
243
- cameras: this.availableCameras.map(cam => ({
244
- id: cam.deviceId,
245
- label: cam.label || 'Unknown Camera'
246
- }))
247
- });
248
- this.setInitialCameraPreference();
249
- this.logger.state('PREFERENCIA_INICIAL_APLICADA', {
250
- selectedCameraId: this.selectedCameraId,
251
- preferredCameraFacing: this.preferredCameraFacing,
252
- preferredCamera: this.preferredCamera
253
- });
208
+ // Cache the results for optimization
209
+ CameraService.deviceEnumerationCache = {
210
+ devices: this.availableCameras,
211
+ timestamp: now
212
+ };
213
+ await this.setInitialCameraPreference();
254
214
  this.eventBus.emit('camera-changed', this.selectedCameraId);
255
215
  return this.availableCameras;
256
216
  }
257
217
  catch (error) {
258
- this.logger.error('Error al enumerar cámaras disponibles:', error);
259
218
  this.handleCameraPermissionError(error);
260
219
  return [];
261
220
  }
@@ -276,8 +235,6 @@ class CameraService {
276
235
  }
277
236
  this.selectedCameraId = cameraId;
278
237
  this.updatePreferredFacing(camera);
279
- this.isManuallySelected = true; // Mark as manually selected
280
- this.savePreference();
281
238
  this.eventBus.emit('camera-changed', cameraId);
282
239
  }
283
240
  getPreferredFacing() {
@@ -286,14 +243,20 @@ class CameraService {
286
243
  async setupCamera(constraints) {
287
244
  try {
288
245
  const finalConstraints = constraints || await this.getMaxResolution();
289
- const stream = await navigator.mediaDevices.getUserMedia({
246
+ // Optimization: Check if we can reuse existing stream with same constraints
247
+ if (this.currentStreamPromise && this.constraintsEqual(finalConstraints, this.lastStreamConstraints)) {
248
+ return await this.currentStreamPromise;
249
+ }
250
+ // Create new stream promise for lazy loading
251
+ this.currentStreamPromise = navigator.mediaDevices.getUserMedia({
290
252
  video: finalConstraints,
291
253
  audio: false
292
254
  });
255
+ this.lastStreamConstraints = finalConstraints;
256
+ const stream = await this.currentStreamPromise;
293
257
  return stream;
294
258
  }
295
259
  catch (error) {
296
- this.logger.error('Error en setupCamera, intentando con restricciones básicas:', error);
297
260
  // Fallback to basic constraints if getMaxResolution fails
298
261
  const basicConstraints = {
299
262
  width: { ideal: 1280 },
@@ -303,65 +266,21 @@ class CameraService {
303
266
  if (this.deviceType !== 'desktop' && this.preferredCameraFacing) {
304
267
  basicConstraints.facingMode = this.preferredCameraFacing;
305
268
  }
306
- this.logger.state('USANDO_RESTRICCIONES_BASICAS', { constraints: basicConstraints });
307
- return await navigator.mediaDevices.getUserMedia({
269
+ this.currentStreamPromise = navigator.mediaDevices.getUserMedia({
308
270
  video: basicConstraints,
309
271
  audio: false
310
272
  });
273
+ this.lastStreamConstraints = basicConstraints;
274
+ return await this.currentStreamPromise;
311
275
  }
312
276
  }
313
277
  async switchCamera(cameraId) {
314
278
  const selectedCamera = this.availableCameras.find(cam => cam.deviceId === cameraId);
315
279
  if (!selectedCamera) {
316
- this.logger.warn('Cámara seleccionada no encontrada, re-enumerando dispositivos...');
317
280
  await this.enumerateDevices();
318
281
  return;
319
282
  }
320
283
  await this.setSelectedCamera(cameraId);
321
- this.logger.state('CAMARA_CAMBIADA', {
322
- label: selectedCamera.label,
323
- deviceId: selectedCamera.deviceId,
324
- isManuallySelected: this.isManuallySelected
325
- });
326
- }
327
- async flipToNextCamera() {
328
- if (!this.isMultipleCamerasAvailable())
329
- return;
330
- const currentIndex = this.availableCameras.findIndex(camera => camera.deviceId === this.selectedCameraId);
331
- const nextIndex = (currentIndex + 1) % this.availableCameras.length;
332
- const nextCamera = this.availableCameras[nextIndex];
333
- await this.switchCamera(nextCamera.deviceId);
334
- }
335
- savePreference() {
336
- try {
337
- const preference = {
338
- cameraId: this.selectedCameraId,
339
- facing: this.preferredCameraFacing,
340
- timestamp: Date.now()
341
- };
342
- localStorage.setItem('jaak-stamps-camera-preference', JSON.stringify(preference));
343
- this.logger.state('PREFERENCIA_CAMARA_GUARDADA', preference);
344
- }
345
- catch (error) {
346
- this.logger.warn('Error al guardar preferencia de cámara:', error);
347
- }
348
- }
349
- loadPreference() {
350
- try {
351
- const saved = localStorage.getItem('jaak-stamps-camera-preference');
352
- if (saved) {
353
- const preference = JSON.parse(saved);
354
- const isStillAvailable = this.availableCameras.some(camera => camera.deviceId === preference.cameraId);
355
- if (isStillAvailable) {
356
- this.selectedCameraId = preference.cameraId;
357
- this.preferredCameraFacing = preference.facing;
358
- this.logger.state('PREFERENCIA_CAMARA_CARGADA', preference);
359
- }
360
- }
361
- }
362
- catch (error) {
363
- this.logger.warn('Error al cargar preferencia de cámara:', error);
364
- }
365
284
  }
366
285
  isRearCamera(stream) {
367
286
  const videoTrack = stream.getVideoTracks()[0];
@@ -370,13 +289,18 @@ class CameraService {
370
289
  const settings = videoTrack.getSettings();
371
290
  return settings.facingMode === 'environment';
372
291
  }
373
- getCameraInfo() {
374
- return {
375
- availableCameras: this.availableCameras.map(camera => ({
292
+ async getCameraInfo() {
293
+ const availableCamerasWithAutofocus = await Promise.all(this.availableCameras.map(async (camera) => {
294
+ const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
295
+ return {
376
296
  id: camera.deviceId,
377
297
  label: camera.label || 'Unknown Camera',
378
- selected: camera.deviceId === this.selectedCameraId
379
- })),
298
+ selected: camera.deviceId === this.selectedCameraId,
299
+ hasAutofocus
300
+ };
301
+ }));
302
+ return {
303
+ availableCameras: availableCamerasWithAutofocus,
380
304
  selectedCameraId: this.selectedCameraId,
381
305
  deviceType: this.deviceType,
382
306
  isMultipleCamerasAvailable: this.isMultipleCamerasAvailable(),
@@ -392,7 +316,6 @@ class CameraService {
392
316
  return permission.state;
393
317
  }
394
318
  catch (error) {
395
- this.logger.warn('No se pudo verificar permisos de cámara:', error);
396
319
  return 'prompt';
397
320
  }
398
321
  }
@@ -400,35 +323,48 @@ class CameraService {
400
323
  this.availableCameras = [];
401
324
  this.eventBus.emit('error', new Error(`Camera permission error: ${error.message}`));
402
325
  }
403
- setInitialCameraPreference() {
326
+ async setInitialCameraPreference() {
404
327
  if (this.availableCameras.length === 0)
405
328
  return;
406
- // Clear any existing localStorage preferences to ensure fresh start
407
- localStorage.removeItem('jaak-stamps-camera-preference');
408
- this.logger.state('APLICANDO_PREFERENCIA_INICIAL', {
409
- preferredCamera: this.preferredCamera,
410
- availableCamerasCount: this.availableCameras.length
411
- });
412
- this.isManuallySelected = false; // Reset manual selection flag
413
329
  if (this.preferredCamera === 'front') {
414
- this.selectFrontCamera();
330
+ await this.selectFrontCamera();
415
331
  }
416
332
  else if (this.preferredCamera === 'back') {
417
- this.selectBackCamera();
333
+ await this.selectBackCamera();
418
334
  }
419
335
  else {
420
- this.selectAutoCamera();
336
+ await this.selectAutoCamera();
421
337
  }
422
338
  }
423
- selectFrontCamera() {
339
+ async checkCameraAutofocus(deviceId) {
340
+ try {
341
+ // Check cache first for optimization
342
+ if (CameraService.cameraCapabilitiesCache.has(deviceId)) {
343
+ const cached = CameraService.cameraCapabilitiesCache.get(deviceId);
344
+ return typeof cached === 'boolean' ? cached : false;
345
+ }
346
+ const stream = await navigator.mediaDevices.getUserMedia({
347
+ video: { deviceId: { exact: deviceId } }
348
+ });
349
+ const videoTrack = stream.getVideoTracks()[0];
350
+ const capabilities = videoTrack.getCapabilities();
351
+ stream.getTracks().forEach(track => track.stop());
352
+ const hasFocusMode = capabilities.focusMode && Array.isArray(capabilities.focusMode) && capabilities.focusMode.length > 0;
353
+ const hasAutofocus = hasFocusMode && (capabilities.focusMode.includes('continuous') ||
354
+ capabilities.focusMode.includes('auto'));
355
+ // Cache the result for future use
356
+ CameraService.cameraCapabilitiesCache.set(deviceId, hasAutofocus);
357
+ return hasAutofocus;
358
+ }
359
+ catch (error) {
360
+ // Cache negative result to avoid repeated failures
361
+ CameraService.cameraCapabilitiesCache.set(deviceId, false);
362
+ return false;
363
+ }
364
+ }
365
+ async selectFrontCamera() {
424
366
  this.preferredCameraFacing = 'user';
425
- this.logger.state('BUSCANDO_CAMARA_FRONTAL', {
426
- availableCameras: this.availableCameras.map(cam => ({
427
- id: cam.deviceId,
428
- label: cam.label
429
- }))
430
- });
431
- const frontCamera = this.availableCameras.find(camera => {
367
+ const frontCameras = this.availableCameras.filter(camera => {
432
368
  const label = camera.label.toLowerCase();
433
369
  return label.includes('front') ||
434
370
  label.includes('user') ||
@@ -436,24 +372,24 @@ class CameraService {
436
372
  label.includes('frontal') ||
437
373
  (!label.includes('back') && !label.includes('rear') && !label.includes('environment'));
438
374
  });
439
- // If not found, use the first camera (usually front on mobile)
440
- this.selectedCameraId = frontCamera ? frontCamera.deviceId : this.availableCameras[0].deviceId;
441
- this.logger.state('CAMARA_FRONTAL_SELECCIONADA', {
442
- found: !!frontCamera,
443
- label: frontCamera?.label || this.availableCameras[0].label,
444
- deviceId: this.selectedCameraId
445
- });
375
+ let selectedCamera = null;
376
+ if (frontCameras.length > 0) {
377
+ for (const camera of frontCameras) {
378
+ const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
379
+ if (hasAutofocus) {
380
+ selectedCamera = camera;
381
+ break;
382
+ }
383
+ }
384
+ if (!selectedCamera) {
385
+ selectedCamera = frontCameras[0];
386
+ }
387
+ }
388
+ this.selectedCameraId = selectedCamera ? selectedCamera.deviceId : this.availableCameras[0].deviceId;
446
389
  }
447
- selectBackCamera() {
390
+ async selectBackCamera() {
448
391
  this.preferredCameraFacing = 'environment';
449
- this.logger.state('BUSCANDO_CAMARA_TRASERA', {
450
- availableCameras: this.availableCameras.map(cam => ({
451
- id: cam.deviceId,
452
- label: cam.label
453
- }))
454
- });
455
- // Try to find rear camera with multiple detection methods
456
- let backCamera = this.availableCameras.find(camera => {
392
+ const backCameras = this.availableCameras.filter(camera => {
457
393
  const label = camera.label.toLowerCase();
458
394
  return label.includes('back') ||
459
395
  label.includes('rear') ||
@@ -461,34 +397,33 @@ class CameraService {
461
397
  label.includes('trasera') ||
462
398
  label.includes('posterior');
463
399
  });
464
- // If not found by label, try to use the last camera (usually rear on mobile)
465
- if (!backCamera && this.availableCameras.length > 1) {
466
- backCamera = this.availableCameras[this.availableCameras.length - 1];
467
- this.logger.state('USANDO_ULTIMA_CAMARA_COMO_TRASERA', {
468
- label: backCamera.label,
469
- deviceId: backCamera.deviceId
470
- });
400
+ let selectedCamera = null;
401
+ if (backCameras.length > 0) {
402
+ for (const camera of backCameras) {
403
+ const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
404
+ if (hasAutofocus) {
405
+ selectedCamera = camera;
406
+ break;
407
+ }
408
+ }
409
+ if (!selectedCamera) {
410
+ selectedCamera = backCameras[0];
411
+ }
471
412
  }
472
- this.selectedCameraId = backCamera ? backCamera.deviceId : this.availableCameras[0].deviceId;
473
- this.logger.state('CAMARA_TRASERA_SELECCIONADA', {
474
- found: !!backCamera,
475
- label: backCamera?.label || this.availableCameras[0].label,
476
- deviceId: this.selectedCameraId
477
- });
413
+ else if (this.availableCameras.length > 1) {
414
+ const lastCamera = this.availableCameras[this.availableCameras.length - 1];
415
+ const hasAutofocus = await this.checkCameraAutofocus(lastCamera.deviceId);
416
+ selectedCamera = hasAutofocus ? lastCamera : this.availableCameras[this.availableCameras.length - 1];
417
+ }
418
+ this.selectedCameraId = selectedCamera ? selectedCamera.deviceId : this.availableCameras[0].deviceId;
478
419
  }
479
- selectAutoCamera() {
420
+ async selectAutoCamera() {
480
421
  if (this.deviceType === 'mobile' || this.deviceType === 'tablet') {
481
- this.selectBackCamera();
482
- this.logger.state('CAMARA_AUTO_SELECCIONADA_MOBILE', { type: 'rear' });
422
+ await this.selectBackCamera();
483
423
  }
484
424
  else {
485
425
  // For desktop, prefer front camera (usually built-in webcam)
486
- this.selectFrontCamera();
487
- this.logger.state('CAMARA_AUTO_SELECCIONADA_DESKTOP', {
488
- type: 'front',
489
- label: this.availableCameras[0].label,
490
- deviceId: this.selectedCameraId
491
- });
426
+ await this.selectFrontCamera();
492
427
  }
493
428
  }
494
429
  updatePreferredFacing(camera) {
@@ -504,40 +439,19 @@ class CameraService {
504
439
  await this.detectDeviceType();
505
440
  }
506
441
  const videoConstraints = {};
507
- this.logger.state('CONFIGURANDO_CONSTRAINS_CAMARA', {
508
- selectedCameraId: this.selectedCameraId,
509
- preferredCameraFacing: this.preferredCameraFacing,
510
- preferredCamera: this.preferredCamera,
511
- deviceType: this.deviceType,
512
- isManuallySelected: this.isManuallySelected
513
- });
514
- if (this.isManuallySelected && this.selectedCameraId) {
515
- // When manually selected, use deviceId for exact camera selection
442
+ // Priority 1: Always use deviceId if available (ensures specific camera with autofocus)
443
+ if (this.selectedCameraId) {
516
444
  videoConstraints.deviceId = { exact: this.selectedCameraId };
517
- this.logger.state('USANDO_CAMARA_MANUAL', {
518
- deviceId: this.selectedCameraId
519
- });
520
445
  }
446
+ // Priority 2: Use facingMode as fallback when no specific camera is selected
521
447
  else if (this.preferredCameraFacing) {
522
- // Use facingMode for initial preference-based selection
523
- // Use 'ideal' instead of 'exact' to avoid OverconstrainedError on desktop
524
448
  const facingConstraint = this.deviceType === 'desktop'
525
449
  ? { ideal: this.preferredCameraFacing }
526
450
  : { exact: this.preferredCameraFacing };
527
451
  videoConstraints.facingMode = facingConstraint;
528
- this.logger.state('USANDO_FACING_CONSTRAINT', {
529
- facingMode: this.preferredCameraFacing,
530
- constraintType: this.deviceType === 'desktop' ? 'ideal' : 'exact'
531
- });
532
- }
533
- else if (this.selectedCameraId) {
534
- videoConstraints.deviceId = { exact: this.selectedCameraId };
535
- this.logger.state('USANDO_CAMARA_SELECCIONADA', {
536
- deviceId: this.selectedCameraId
537
- });
538
452
  }
453
+ // Priority 3: Fallback to preferredCamera setting
539
454
  else {
540
- // Use preferredCamera setting to determine default facing mode
541
455
  if (this.preferredCamera === 'front') {
542
456
  videoConstraints.facingMode = "user";
543
457
  }
@@ -545,24 +459,25 @@ class CameraService {
545
459
  videoConstraints.facingMode = "environment";
546
460
  }
547
461
  else {
548
- // Auto mode: use environment for mobile/tablet, user for desktop
549
462
  videoConstraints.facingMode = (this.deviceType === 'mobile' || this.deviceType === 'tablet') ? "environment" : "user";
550
463
  }
551
- this.logger.state('USANDO_PREFERENCIA_CONFIGURADA', {
552
- facingMode: videoConstraints.facingMode,
553
- preferredCamera: this.preferredCamera,
554
- deviceType: this.deviceType
555
- });
556
464
  }
557
- const tempStream = await navigator.mediaDevices.getUserMedia({
558
- video: videoConstraints
559
- });
560
- const videoTrack = tempStream.getVideoTracks()[0];
561
- const capabilities = videoTrack.getCapabilities();
562
- tempStream.getTracks().forEach(track => track.stop());
465
+ // Optimization: Use cached capabilities if available
466
+ let capabilities;
467
+ if (this.selectedCameraId && CameraService.cameraCapabilitiesCache.has(this.selectedCameraId + '_caps')) {
468
+ capabilities = JSON.parse(CameraService.cameraCapabilitiesCache.get(this.selectedCameraId + '_caps'));
469
+ }
470
+ else {
471
+ const tempStream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints });
472
+ const videoTrack = tempStream.getVideoTracks()[0];
473
+ capabilities = videoTrack.getCapabilities();
474
+ tempStream.getTracks().forEach(track => track.stop());
475
+ // Cache capabilities for future use
476
+ if (this.selectedCameraId) {
477
+ CameraService.cameraCapabilitiesCache.set(this.selectedCameraId + '_caps', JSON.stringify(capabilities));
478
+ }
479
+ }
563
480
  const constraints = { ...videoConstraints };
564
- // Enhanced focus and image quality settings
565
- this.applyAdvancedCameraSettings(constraints, capabilities);
566
481
  if (capabilities.width && capabilities.height) {
567
482
  const maxWidth = Math.min(capabilities.width.max, 1920);
568
483
  const maxHeight = Math.min(capabilities.height.max, 1080);
@@ -576,15 +491,20 @@ class CameraService {
576
491
  constraints.height = { ideal: maxHeight };
577
492
  }
578
493
  }
579
- this.logger.state('CONFIGURACION_CAMARA_OPTIMIZADA', {
580
- constraints,
581
- capabilities: this.getCapabilitiesSummary(capabilities)
582
- });
494
+ // Configure autofocus if camera has the capability
495
+ const capabilitiesAny = capabilities;
496
+ if (capabilitiesAny.focusMode && Array.isArray(capabilitiesAny.focusMode)) {
497
+ if (capabilitiesAny.focusMode.includes('continuous')) {
498
+ constraints.focusMode = 'continuous';
499
+ }
500
+ else if (capabilitiesAny.focusMode.includes('auto')) {
501
+ constraints.focusMode = 'auto';
502
+ }
503
+ }
583
504
  return constraints;
584
505
  }
585
506
  catch (err) {
586
- this.logger.warn('No se pudieron obtener capacidades de cámara, usando configuración de respaldo');
587
- // Ensure device type is detected before determining constraints
507
+ // Fallback logic with minimal temporary streams
588
508
  if (!this.deviceType || this.deviceType === 'desktop') {
589
509
  await this.detectDeviceType();
590
510
  }
@@ -593,25 +513,16 @@ class CameraService {
593
513
  width: { ideal: isTablet ? 1280 : 1920 },
594
514
  height: { ideal: isTablet ? 720 : 1080 }
595
515
  };
596
- // Apply basic focus settings even for fallback
597
- this.applyBasicFocusSettings(fallbackConstraints);
598
- if (this.isManuallySelected && this.selectedCameraId) {
599
- // When manually selected, use deviceId for exact camera selection
516
+ if (this.selectedCameraId) {
600
517
  fallbackConstraints.deviceId = { exact: this.selectedCameraId };
601
518
  }
602
519
  else if (this.preferredCameraFacing) {
603
- // Use facingMode for initial preference-based selection
604
- // Use 'ideal' instead of 'exact' to avoid OverconstrainedError on desktop
605
520
  const facingConstraint = this.deviceType === 'desktop'
606
521
  ? { ideal: this.preferredCameraFacing }
607
522
  : { exact: this.preferredCameraFacing };
608
523
  fallbackConstraints.facingMode = facingConstraint;
609
524
  }
610
- else if (this.selectedCameraId) {
611
- fallbackConstraints.deviceId = { exact: this.selectedCameraId };
612
- }
613
525
  else {
614
- // Use preferredCamera setting to determine default facing mode
615
526
  if (this.preferredCamera === 'front') {
616
527
  fallbackConstraints.facingMode = "user";
617
528
  }
@@ -619,165 +530,26 @@ class CameraService {
619
530
  fallbackConstraints.facingMode = "environment";
620
531
  }
621
532
  else {
622
- // Auto mode: use environment for mobile/tablet, user for desktop
623
533
  fallbackConstraints.facingMode = (this.deviceType === 'mobile' || this.deviceType === 'tablet') ? "environment" : "user";
624
534
  }
625
535
  }
626
536
  return fallbackConstraints;
627
537
  }
628
538
  }
629
- applyAdvancedCameraSettings(constraints, capabilities) {
630
- // Apply standard camera settings that are widely supported
631
- // Frame rate optimization
632
- if (capabilities.frameRate) {
633
- constraints.frameRate = {
634
- ideal: 30,
635
- min: 15,
636
- max: 60
637
- };
638
- }
639
- // Width and height constraints for optimal resolution
640
- if (capabilities.width && capabilities.height) {
641
- constraints.width = {
642
- ideal: Math.min(1920, capabilities.width.max || 1920),
643
- min: 640
644
- };
645
- constraints.height = {
646
- ideal: Math.min(1080, capabilities.height.max || 1080),
647
- min: 480
648
- };
649
- }
650
- // Apply enhanced constraints through advanced array for better browser compatibility
651
- const advancedConstraints = [];
652
- // Try to set optimal settings through advanced constraints
653
- const advanced = {};
654
- // Focus mode - use 'any' type to bypass TypeScript limitations
655
- if (capabilities.focusMode) {
656
- if (capabilities.focusMode.includes('continuous')) {
657
- advanced.focusMode = 'continuous';
658
- }
659
- else if (capabilities.focusMode.includes('single-shot')) {
660
- advanced.focusMode = 'single-shot';
661
- }
662
- }
663
- // Focus distance for close objects
664
- if (capabilities.focusDistance) {
665
- advanced.focusDistance = {
666
- ideal: 0.4, // 40cm - optimal for document capture
667
- min: 0.2,
668
- max: 1.0
669
- };
670
- }
671
- // Zoom control
672
- if (capabilities.zoom) {
673
- advanced.zoom = {
674
- ideal: 1.0,
675
- min: capabilities.zoom.min,
676
- max: Math.min(capabilities.zoom.max, 3.0)
677
- };
678
- }
679
- // Add advanced constraints if any were set
680
- if (Object.keys(advanced).length > 0) {
681
- advancedConstraints.push(advanced);
682
- }
683
- if (advancedConstraints.length > 0) {
684
- constraints.advanced = advancedConstraints;
685
- }
686
- }
687
- applyBasicFocusSettings(constraints) {
688
- // Apply basic focus settings when capabilities are not available
689
- // Use advanced constraints for non-standard properties
690
- const advanced = {
691
- focusMode: 'continuous',
692
- exposureMode: 'continuous',
693
- whiteBalanceMode: 'continuous'
694
- };
695
- if (!constraints.advanced) {
696
- constraints.advanced = [];
697
- }
698
- constraints.advanced.push(advanced);
699
- constraints.frameRate = { ideal: 30 };
700
- }
701
- getCapabilitiesSummary(capabilities) {
702
- return {
703
- hasAutoFocus: !!capabilities.focusMode,
704
- hasFocusDistance: !!capabilities.focusDistance,
705
- hasExposureControl: !!capabilities.exposureMode,
706
- hasWhiteBalance: !!capabilities.whiteBalanceMode,
707
- hasZoom: !!capabilities.zoom,
708
- hasTorch: capabilities.torch !== undefined,
709
- hasImageControls: !!(capabilities.brightness || capabilities.contrast || capabilities.saturation || capabilities.sharpness),
710
- resolutionRange: capabilities.width && capabilities.height ?
711
- `${capabilities.width.min}x${capabilities.height.min} - ${capabilities.width.max}x${capabilities.height.max}` : 'unknown'
712
- };
713
- }
714
- // New method to enable torch/flash for better illumination
715
- async setTorchEnabled(enabled, stream) {
716
- try {
717
- if (!stream) {
718
- this.logger.warn('No hay stream disponible para controlar el flash');
719
- return false;
720
- }
721
- const videoTrack = stream.getVideoTracks()[0];
722
- if (!videoTrack) {
723
- this.logger.warn('No hay track de video disponible');
724
- return false;
725
- }
726
- const capabilities = videoTrack.getCapabilities();
727
- if (capabilities.torch === undefined) {
728
- this.logger.warn('El dispositivo no soporta control de flash');
729
- return false;
730
- }
731
- await videoTrack.applyConstraints({
732
- advanced: [{ torch: enabled }]
733
- });
734
- this.logger.state('FLASH_CONTROLADO', { enabled });
735
- return true;
736
- }
737
- catch (error) {
738
- this.logger.error('Error al controlar el flash:', error);
539
+ // Optimization helper methods
540
+ constraintsEqual(a, b) {
541
+ if (!a || !b)
739
542
  return false;
740
- }
543
+ return JSON.stringify(a) === JSON.stringify(b);
741
544
  }
742
- // Method to manually trigger focus on a specific point
743
- async focusAtPoint(x, y, stream) {
744
- try {
745
- if (!stream) {
746
- this.logger.warn('No hay stream disponible para enfocar');
747
- return false;
748
- }
749
- const videoTrack = stream.getVideoTracks()[0];
750
- if (!videoTrack) {
751
- this.logger.warn('No hay track de video disponible');
752
- return false;
753
- }
754
- const capabilities = videoTrack.getCapabilities();
755
- if (!capabilities.focusMode || !capabilities.focusMode.includes('single-shot')) {
756
- this.logger.warn('El dispositivo no soporta enfoque manual');
757
- return false;
758
- }
759
- // Trigger single-shot focus
760
- await videoTrack.applyConstraints({
761
- advanced: [{ focusMode: 'single-shot' }]
762
- });
763
- // Return to continuous focus after a short delay
764
- setTimeout(async () => {
765
- try {
766
- await videoTrack.applyConstraints({
767
- advanced: [{ focusMode: 'continuous' }]
768
- });
769
- }
770
- catch (error) {
771
- this.logger.warn('Error al volver a enfoque continuo:', error);
772
- }
773
- }, 1000);
774
- this.logger.state('ENFOQUE_MANUAL_ACTIVADO', { x, y });
775
- return true;
776
- }
777
- catch (error) {
778
- this.logger.error('Error al enfocar manualmente:', error);
779
- return false;
780
- }
545
+ // Cleanup method for optimization
546
+ static clearCaches() {
547
+ CameraService.cameraCapabilitiesCache.clear();
548
+ CameraService.deviceEnumerationCache = null;
549
+ }
550
+ // Method to invalidate device cache when needed
551
+ invalidateDeviceCache() {
552
+ CameraService.deviceEnumerationCache = null;
781
553
  }
782
554
  }
783
555
 
@@ -856,9 +628,9 @@ class DeviceStrategyFactory {
856
628
  }
857
629
 
858
630
  class DetectionService {
859
- logger;
860
631
  debug;
861
632
  useDocumentClassification;
633
+ useDocumentDetector;
862
634
  session;
863
635
  mobileNetSession;
864
636
  mobileNetClassMap;
@@ -873,30 +645,37 @@ class DetectionService {
873
645
  preprocessCanvas;
874
646
  preprocessCtx;
875
647
  captureCanvas;
876
- constructor(logger, debug = false, useDocumentClassification = false) {
877
- this.logger = logger;
648
+ // Static canvas pool for reuse across instances
649
+ static canvasPool = new Map();
650
+ static MAX_POOL_SIZE = 5;
651
+ constructor(debug = false, useDocumentClassification = false, useDocumentDetector = true) {
878
652
  this.debug = debug;
879
653
  this.useDocumentClassification = useDocumentClassification;
654
+ this.useDocumentDetector = useDocumentDetector;
880
655
  this.deviceStrategy = DeviceStrategyFactory.createStrategy();
881
656
  this.initializeCanvasPool();
882
657
  }
883
658
  async loadModel() {
884
659
  if (this.modelLoaded || this.session) {
885
- this.logger.state('MODELO_YA_PRECARGADO', {
886
- sessionExists: !!this.session,
887
- modelPreloaded: this.modelLoaded
888
- });
660
+ return;
661
+ }
662
+ if (!this.useDocumentDetector) {
663
+ // Skip loading the detection model but mark as "loaded" for compatibility
664
+ this.modelLoaded = true;
889
665
  return;
890
666
  }
891
667
  try {
892
- this.logger.state('PRECARGANDO_MODELO_DETECCION', { modelPath: this.MODEL_PATH });
893
668
  const sessionOptions = this.deviceStrategy.getSessionOptions(this.debug);
894
669
  try {
895
670
  this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, sessionOptions);
896
671
  }
897
672
  catch (error) {
898
- if (error.message.includes('failed to allocate a buffer')) {
899
- this.logger.warn('Fallo en asignación de buffer durante precarga, intentando con configuración mínima');
673
+ // Múltiples tipos de errores de memoria que pueden ocurrir
674
+ const isMemoryError = error.message.includes('failed to allocate a buffer') ||
675
+ error.message.includes('Out of memory') ||
676
+ error.message.includes('RangeError: Out of memory') ||
677
+ error.message.includes('no available backend found');
678
+ if (isMemoryError) {
900
679
  const fallbackOptions = {
901
680
  executionProviders: ['wasm'],
902
681
  graphOptimizationLevel: 'disabled',
@@ -906,18 +685,25 @@ class DetectionService {
906
685
  executionMode: 'sequential',
907
686
  interOpNumThreads: 1,
908
687
  intraOpNumThreads: 1,
688
+ // Configuraciones adicionales para dispositivos con poca memoria
689
+ enableProfiling: false,
690
+ sessionLogSeverityLevel: 4,
691
+ sessionLogVerbosityLevel: 0,
909
692
  };
910
- this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, fallbackOptions);
693
+ try {
694
+ this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, fallbackOptions);
695
+ }
696
+ catch (fallbackError) {
697
+ throw new Error(`no available backend found. ERR: [wasm] RangeError: Out of memory`);
698
+ }
911
699
  }
912
700
  else {
913
701
  throw error;
914
702
  }
915
703
  }
916
704
  this.modelLoaded = true;
917
- this.logger.state('MODELO_DETECCION_CARGADO_EXITOSAMENTE', { sessionCreated: !!this.session });
918
705
  }
919
706
  catch (error) {
920
- this.logger.error('Error al precargar modelo de detección:', error);
921
707
  throw error;
922
708
  }
923
709
  }
@@ -926,19 +712,14 @@ class DetectionService {
926
712
  return;
927
713
  }
928
714
  try {
929
- this.logger.state('CARGANDO_MODELO_MOBILENET', { path: this.MOBILENET_MODEL_PATH });
930
715
  // Try to load class map (optional - SqueezeNet may not need it for basic classification)
931
716
  try {
932
717
  const classResponse = await fetch(this.MOBILENET_CLASSES_PATH);
933
718
  if (classResponse.ok) {
934
719
  this.mobileNetClassMap = await classResponse.json();
935
- this.logger.state('CLASES_MOBILENET_CARGADAS', {
936
- classCount: Object.keys(this.mobileNetClassMap).length
937
- });
938
720
  }
939
721
  }
940
722
  catch (error) {
941
- this.logger.warn('No se pudo cargar el mapa de clases de MobileNet, usando clasificación básica');
942
723
  // Create a basic class map for document types
943
724
  this.mobileNetClassMap = {
944
725
  "0": "document",
@@ -954,8 +735,12 @@ class DetectionService {
954
735
  this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, sessionOptions);
955
736
  }
956
737
  catch (error) {
957
- if (error.message.includes('failed to allocate a buffer')) {
958
- this.logger.warn('Fallo en asignación de buffer de MobileNet, intentando con configuración mínima');
738
+ // Múltiples tipos de errores de memoria que pueden ocurrir
739
+ const isMemoryError = error.message.includes('failed to allocate a buffer') ||
740
+ error.message.includes('Out of memory') ||
741
+ error.message.includes('RangeError: Out of memory') ||
742
+ error.message.includes('no available backend found');
743
+ if (isMemoryError) {
959
744
  const fallbackOptions = {
960
745
  executionProviders: ['wasm'],
961
746
  graphOptimizationLevel: 'disabled',
@@ -965,23 +750,32 @@ class DetectionService {
965
750
  executionMode: 'sequential',
966
751
  interOpNumThreads: 1,
967
752
  intraOpNumThreads: 1,
753
+ // Configuraciones adicionales para dispositivos con poca memoria
754
+ enableProfiling: false,
755
+ sessionLogSeverityLevel: 4,
756
+ sessionLogVerbosityLevel: 0,
968
757
  };
969
- this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, fallbackOptions);
758
+ try {
759
+ this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, fallbackOptions);
760
+ }
761
+ catch (fallbackError) {
762
+ throw new Error(`no available backend found. ERR: [wasm] RangeError: Out of memory`);
763
+ }
970
764
  }
971
765
  else {
972
766
  throw error;
973
767
  }
974
768
  }
975
- this.logger.state('MODELO_MOBILENET_CARGADO_EXITOSAMENTE', {
976
- sessionCreated: !!this.mobileNetSession
977
- });
978
769
  }
979
770
  catch (error) {
980
- this.logger.error('Error al cargar modelo MobileNet:', error);
981
771
  throw error;
982
772
  }
983
773
  }
984
774
  isModelLoaded() {
775
+ if (!this.useDocumentDetector) {
776
+ // If detector is disabled, consider it "loaded" for compatibility
777
+ return this.modelLoaded;
778
+ }
985
779
  return this.modelLoaded && !!this.session;
986
780
  }
987
781
  preprocess(video) {
@@ -1006,6 +800,10 @@ class DetectionService {
1006
800
  return new window.ort.Tensor("float16", float16Data, [1, 3, this.INPUT_SIZE, this.INPUT_SIZE]);
1007
801
  }
1008
802
  async runInference(inputTensor) {
803
+ if (!this.useDocumentDetector) {
804
+ // Return empty array when document detector is disabled
805
+ return [];
806
+ }
1009
807
  if (!this.session) {
1010
808
  throw new Error('Detection model not loaded');
1011
809
  }
@@ -1030,11 +828,9 @@ class DetectionService {
1030
828
  }
1031
829
  async classifyDocument(canvas) {
1032
830
  if (!this.mobileNetSession || !this.mobileNetClassMap) {
1033
- this.logger.warn('Modelo MobileNet no está cargado, saltando clasificación');
1034
831
  return null;
1035
832
  }
1036
833
  try {
1037
- this.logger.state('CLASIFICANDO_DOCUMENTO', { timestamp: Date.now() });
1038
834
  const inputTensor = this.preprocessMobileNet(canvas);
1039
835
  const feeds = { [this.mobileNetSession.inputNames[0]]: inputTensor };
1040
836
  const results = await this.mobileNetSession.run(feeds);
@@ -1042,13 +838,6 @@ class DetectionService {
1042
838
  const maxIdx = output.reduce((bestIdx, val, idx, arr) => val > arr[bestIdx] ? idx : bestIdx, 0);
1043
839
  const confidence = output[maxIdx];
1044
840
  const className = this.mobileNetClassMap[maxIdx.toString()] || "unknown";
1045
- this.logger.state('DOCUMENTO_CLASIFICADO', {
1046
- class: className,
1047
- confidence: confidence.toFixed(3),
1048
- classIndex: maxIdx,
1049
- timestamp: Date.now(),
1050
- results
1051
- });
1052
841
  return {
1053
842
  class: className,
1054
843
  confidence: confidence,
@@ -1056,7 +845,6 @@ class DetectionService {
1056
845
  };
1057
846
  }
1058
847
  catch (error) {
1059
- this.logger.error('Error al clasificar documento:', error);
1060
848
  return null;
1061
849
  }
1062
850
  }
@@ -1169,7 +957,6 @@ class DetectionService {
1169
957
  }
1170
958
  this.mobileNetClassMap = undefined;
1171
959
  this.modelLoaded = false;
1172
- this.logger.state('DETECCION_SERVICE_LIMPIADO', { timestamp: Date.now() });
1173
960
  }
1174
961
  initializeCanvasPool() {
1175
962
  this.preprocessCanvas = document.createElement('canvas');
@@ -1180,9 +967,6 @@ class DetectionService {
1180
967
  willReadFrequently: true
1181
968
  });
1182
969
  this.captureCanvas = document.createElement('canvas');
1183
- this.logger.state('CANVAS_POOL_INICIALIZADO', {
1184
- preprocessCanvasSize: this.INPUT_SIZE
1185
- });
1186
970
  }
1187
971
  cleanupCanvasPool() {
1188
972
  if (this.preprocessCanvas) {
@@ -1218,10 +1002,11 @@ class DetectionService {
1218
1002
  return (sign << 15) | (newExp << 10) | (frac >> 13);
1219
1003
  }
1220
1004
  preprocessMobileNet(canvas) {
1221
- const tempCanvas = document.createElement('canvas');
1222
- tempCanvas.width = 224;
1223
- tempCanvas.height = 224;
1005
+ // Use pooled canvas instead of creating new one
1006
+ const tempCanvas = this.getCanvas(224, 224);
1224
1007
  const tempCtx = tempCanvas.getContext('2d');
1008
+ // Clear canvas before use
1009
+ tempCtx.clearRect(0, 0, 224, 224);
1225
1010
  // Resize image to 224x224 for SqueezeNet (using MobileNet interface)
1226
1011
  tempCtx.drawImage(canvas, 0, 0, 224, 224);
1227
1012
  const imageData = tempCtx.getImageData(0, 0, 224, 224);
@@ -1237,8 +1022,53 @@ class DetectionService {
1237
1022
  // Blue channel
1238
1023
  arr[2 * hw + i] = data[i * 4 + 2] / 255.0;
1239
1024
  }
1025
+ // Return canvas to pool after use
1026
+ this.returnCanvas(tempCanvas);
1240
1027
  return new window.ort.Tensor('float32', arr, [1, 3, 224, 224]);
1241
1028
  }
1029
+ // Canvas pool management methods for optimization
1030
+ getCanvas(width, height) {
1031
+ const key = `${width}x${height}`;
1032
+ if (!DetectionService.canvasPool.has(key)) {
1033
+ DetectionService.canvasPool.set(key, []);
1034
+ }
1035
+ const pool = DetectionService.canvasPool.get(key);
1036
+ let canvas = pool.pop();
1037
+ if (!canvas) {
1038
+ canvas = document.createElement('canvas');
1039
+ canvas.width = width;
1040
+ canvas.height = height;
1041
+ }
1042
+ else {
1043
+ // Clear the canvas before reuse
1044
+ const ctx = canvas.getContext('2d');
1045
+ ctx.clearRect(0, 0, width, height);
1046
+ }
1047
+ return canvas;
1048
+ }
1049
+ returnCanvas(canvas) {
1050
+ const key = `${canvas.width}x${canvas.height}`;
1051
+ if (!DetectionService.canvasPool.has(key)) {
1052
+ DetectionService.canvasPool.set(key, []);
1053
+ }
1054
+ const pool = DetectionService.canvasPool.get(key);
1055
+ // Only return to pool if not at max capacity
1056
+ if (pool.length < DetectionService.MAX_POOL_SIZE) {
1057
+ pool.push(canvas);
1058
+ }
1059
+ }
1060
+ // Static method to clear all canvas pools (useful for memory management)
1061
+ static clearCanvasPools() {
1062
+ DetectionService.canvasPool.clear();
1063
+ }
1064
+ // Method to get pool statistics for debugging
1065
+ getPoolStats() {
1066
+ const stats = {};
1067
+ DetectionService.canvasPool.forEach((pool, key) => {
1068
+ stats[key] = pool.length;
1069
+ });
1070
+ return stats;
1071
+ }
1242
1072
  }
1243
1073
 
1244
1074
  class ServiceContainer {
@@ -1249,13 +1079,11 @@ class ServiceContainer {
1249
1079
  initializeServices(config) {
1250
1080
  // Initialize services in dependency order
1251
1081
  const eventBus = new EventBusService();
1252
- const logger = new LoggerService(config.debug);
1253
1082
  const stateManager = new StateManagerService(eventBus);
1254
- const cameraService = new CameraService(logger, eventBus, config.preferredCamera);
1255
- const detectionService = new DetectionService(logger, config.debug, config.useDocumentClassification);
1083
+ const cameraService = new CameraService(eventBus, config.preferredCamera);
1084
+ const detectionService = new DetectionService(config.debug, config.useDocumentClassification, config.useDocumentDetector);
1256
1085
  // Register services
1257
1086
  this.services.set('eventBus', eventBus);
1258
- this.services.set('logger', logger);
1259
1087
  this.services.set('stateManager', stateManager);
1260
1088
  this.services.set('cameraService', cameraService);
1261
1089
  this.services.set('detectionService', detectionService);
@@ -1267,9 +1095,6 @@ class ServiceContainer {
1267
1095
  }
1268
1096
  return service;
1269
1097
  }
1270
- getLogger() {
1271
- return this.get('logger');
1272
- }
1273
1098
  getEventBus() {
1274
1099
  return this.get('eventBus');
1275
1100
  }
@@ -1283,10 +1108,6 @@ class ServiceContainer {
1283
1108
  return this.get('detectionService');
1284
1109
  }
1285
1110
  updateConfig(config) {
1286
- if (config.debug !== undefined) {
1287
- const logger = this.services.get('logger');
1288
- logger.setDebugMode(config.debug);
1289
- }
1290
1111
  }
1291
1112
  cleanup() {
1292
1113
  // Cleanup services in reverse order
@@ -1296,7 +1117,7 @@ class ServiceContainer {
1296
1117
  }
1297
1118
  }
1298
1119
 
1299
- 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}}";
1120
+ 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}}";
1300
1121
 
1301
1122
  const JaakStamps = class {
1302
1123
  constructor(hostRef) {
@@ -1310,6 +1131,7 @@ const JaakStamps = class {
1310
1131
  maskSize = 90;
1311
1132
  cropMargin = 20;
1312
1133
  useDocumentClassification = false;
1134
+ useDocumentDetector = true;
1313
1135
  preferredCamera = 'auto';
1314
1136
  captureDelay = 1500;
1315
1137
  enableBackDocumentTimer = false;
@@ -1326,9 +1148,16 @@ const JaakStamps = class {
1326
1148
  showCameraSelector = false;
1327
1149
  isSwitchingCamera = false;
1328
1150
  hasDocumentDetected = false;
1151
+ cameraInfoWithAutofocus = {
1152
+ availableCameras: [],
1153
+ selectedCameraId: null,
1154
+ deviceType: 'desktop',
1155
+ isMultipleCamerasAvailable: false,
1156
+ preferredFacing: null
1157
+ };
1329
1158
  currentStatus = {
1330
- message: 'Inicializando componente...',
1331
- description: 'Configurando servicios y cargando recursos',
1159
+ message: 'Preparando capturador...',
1160
+ description: 'Configurando herramientas de captura',
1332
1161
  type: 'initializing',
1333
1162
  isInitialized: false
1334
1163
  };
@@ -1343,9 +1172,13 @@ const JaakStamps = class {
1343
1172
  detectionRate: 0
1344
1173
  };
1345
1174
  backDocumentTimerRemaining = 0;
1175
+ showManualCaptureButton = false;
1176
+ performanceDegradedMode = false; // Auto-switched to manual mode due to performance
1177
+ showPerformanceMessage = false; // Show performance message temporarily
1178
+ captureStateVersion = 0; // Triggers re-render when StateManager changes
1179
+ processingButton = null; // Track which button is processing
1346
1180
  // Services
1347
1181
  serviceContainer;
1348
- logger;
1349
1182
  eventBus;
1350
1183
  stateManager;
1351
1184
  cameraService;
@@ -1362,12 +1195,12 @@ const JaakStamps = class {
1362
1195
  alignmentStartTime;
1363
1196
  alignmentTimer;
1364
1197
  backDocumentTimer;
1198
+ orientationTimer;
1365
1199
  // Performance monitoring
1366
1200
  performanceMetrics = {
1367
1201
  fps: 0,
1368
1202
  inferenceTime: 0,
1369
1203
  memoryUsage: 0,
1370
- cpuUsage: 0,
1371
1204
  onnxLoadTime: 0,
1372
1205
  frameProcessingTime: 0,
1373
1206
  totalDetections: 0,
@@ -1376,22 +1209,39 @@ const JaakStamps = class {
1376
1209
  lastUpdateTime: 0
1377
1210
  };
1378
1211
  performanceUpdateInterval;
1379
- // Performance optimization
1212
+ // Performance optimization with adaptive frame skipping
1380
1213
  frameSkipCounter = 0;
1381
- FRAME_SKIP = 2;
1214
+ BASE_FRAME_SKIP = 2;
1215
+ MAX_FRAME_SKIP = 8;
1382
1216
  consecutiveFailures = 0;
1383
1217
  MAX_FAILURES = 30;
1384
1218
  lastInferenceTime = 0;
1385
1219
  MIN_INFERENCE_INTERVAL = 50;
1220
+ performanceHistory = [];
1221
+ PERFORMANCE_HISTORY_SIZE = 10;
1222
+ // Sistema simplificado de monitoreo de rendimiento
1223
+ PERFORMANCE_THRESHOLD_MS = 2000; // Umbral único para todos los frames
1224
+ SLOW_FRAMES_TO_TRIGGER = 2; // Cuántos frames lentos consecutivos antes de cambiar
1225
+ slowFrameCount = 0; // Contador de frames consecutivos lentos
1226
+ processedFramesCount = 0; // Contador total de frames procesados
1227
+ hasAutoSwitchedToManual = false;
1228
+ // Canvas pool for optimized screenshot capture
1229
+ canvasPool = [];
1230
+ MAX_CANVAS_POOL_SIZE = 3;
1386
1231
  async componentDidLoad() {
1387
- this.updateStatus('Iniciando servicios...', 'Configurando módulos internos', 'initializing');
1388
- await this.initializeServices();
1389
- this.updateStatus('Configurando eventos...', 'Preparando comunicación entre servicios', 'initializing');
1390
- await this.setupEventListeners();
1391
- this.updateStatus('Inicializando cámara...', 'Detectando dispositivos disponibles', 'initializing');
1392
- await this.initializeComponent();
1393
- if (this.debug) {
1394
- this.initializePerformanceMonitor();
1232
+ try {
1233
+ this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
1234
+ await this.initializeServices();
1235
+ this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
1236
+ await this.setupEventListeners();
1237
+ this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
1238
+ await this.initializeComponent();
1239
+ if (this.debug) {
1240
+ this.initializePerformanceMonitor();
1241
+ }
1242
+ }
1243
+ catch (error) {
1244
+ this.updateStatus('Error de inicialización', error.message, 'error');
1395
1245
  }
1396
1246
  }
1397
1247
  async initializeServices() {
@@ -1401,37 +1251,22 @@ const JaakStamps = class {
1401
1251
  maskSize: this.maskSize,
1402
1252
  cropMargin: this.cropMargin,
1403
1253
  useDocumentClassification: this.useDocumentClassification,
1254
+ useDocumentDetector: this.useDocumentDetector,
1404
1255
  preferredCamera: this.preferredCamera,
1405
1256
  captureDelay: this.captureDelay
1406
1257
  };
1407
1258
  this.serviceContainer = new ServiceContainer(config);
1408
- this.logger = this.serviceContainer.getLogger();
1409
1259
  this.eventBus = this.serviceContainer.getEventBus();
1410
1260
  this.stateManager = this.serviceContainer.getStateManager();
1411
1261
  this.cameraService = this.serviceContainer.getCameraService();
1412
1262
  this.detectionService = this.serviceContainer.getDetectionService();
1413
- this.logger.state('SERVICIOS_INICIALIZADOS', { timestamp: Date.now() });
1414
1263
  }
1415
1264
  async setupEventListeners() {
1416
- this.eventBus.on('state-changed', (data) => {
1417
- this.handleStateChange(data);
1418
- });
1419
- this.eventBus.on('camera-changed', (cameraId) => {
1420
- this.logger.state('CAMARA_CAMBIADA_EVENT', { cameraId });
1421
- });
1422
- this.eventBus.on('error', (error) => {
1423
- this.logger.error('Error en servicio:', error);
1265
+ this.eventBus.on('state-changed', () => {
1266
+ this.handleStateChange();
1424
1267
  });
1425
1268
  }
1426
1269
  async initializeComponent() {
1427
- this.logger.state('COMPONENTE_INICIALIZANDO', {
1428
- debug: this.debug,
1429
- maskSize: this.maskSize,
1430
- cropMargin: this.cropMargin,
1431
- useDocumentClassification: this.useDocumentClassification,
1432
- preferredCamera: this.preferredCamera,
1433
- captureDelay: this.captureDelay
1434
- });
1435
1270
  this.validateProps();
1436
1271
  if (this.debug) {
1437
1272
  this.stateManager.updateCaptureState({ isLoading: true });
@@ -1440,58 +1275,67 @@ const JaakStamps = class {
1440
1275
  this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura', 'initializing');
1441
1276
  await this.cameraService.detectDeviceType();
1442
1277
  await this.cameraService.enumerateDevices();
1443
- this.updateStatus('Cargando modelo IA...', 'Preparando reconocimiento de documentos', 'initializing');
1278
+ await this.updateCameraInfoWithAutofocus();
1279
+ this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'initializing');
1444
1280
  await this.loadOnnxRuntime();
1445
1281
  this.initializeResizeObserver();
1446
1282
  }
1447
1283
  validateProps() {
1448
1284
  if (this.maskSize < 50 || this.maskSize > 100) {
1449
- this.logger.warn(`Propiedad maskSize inválida. Valor: ${this.maskSize}, esperado: 50-100. Usando valor por defecto: 90`);
1450
1285
  this.maskSize = 90;
1451
1286
  }
1452
1287
  if (this.cropMargin < 0 || this.cropMargin > 100) {
1453
- this.logger.warn(`Propiedad cropMargin inválida. Valor: ${this.cropMargin}, esperado: 0-100. Usando valor por defecto: 0`);
1454
1288
  this.cropMargin = 0;
1455
1289
  }
1456
1290
  const validOptions = ['auto', 'front', 'back'];
1457
1291
  if (!validOptions.includes(this.preferredCamera)) {
1458
- this.logger.warn(`Propiedad preferredCamera inválida. Valor: ${this.preferredCamera}, esperado: ${validOptions.join(', ')}. Usando valor por defecto: 'auto'`);
1459
1292
  this.preferredCamera = 'auto';
1460
1293
  }
1461
1294
  if (this.captureDelay < 0 || this.captureDelay > 10000) {
1462
- this.logger.warn(`Propiedad captureDelay inválida. Valor: ${this.captureDelay}, esperado: 0-10000. Usando valor por defecto: 1500`);
1463
1295
  this.captureDelay = 1500;
1464
1296
  }
1465
1297
  }
1466
1298
  async loadOnnxRuntime() {
1467
1299
  if (!window.ort) {
1468
- this.updateStatus('Descargando librerías...', 'Obteniendo recursos de reconocimiento', 'initializing');
1300
+ this.updateStatus('Preparando herramientas...', 'Configurando funciones de captura', 'initializing');
1469
1301
  const script = document.createElement('script');
1470
1302
  script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
1471
1303
  document.head.appendChild(script);
1472
1304
  await new Promise((resolve) => {
1473
1305
  script.onload = () => {
1474
- setTimeout(() => {
1475
- this.finalizeInitialization();
1306
+ setTimeout(async () => {
1307
+ await this.finalizeInitialization();
1476
1308
  resolve(undefined);
1477
1309
  }, 300);
1478
1310
  };
1479
1311
  });
1480
1312
  }
1481
1313
  else {
1482
- setTimeout(() => {
1483
- this.finalizeInitialization();
1314
+ setTimeout(async () => {
1315
+ await this.finalizeInitialization();
1484
1316
  }, 300);
1485
1317
  }
1486
1318
  }
1487
- finalizeInitialization() {
1319
+ async finalizeInitialization() {
1488
1320
  this.stateManager.updateCaptureState({ isLoading: false });
1489
- this.currentStatus = {
1490
- message: 'Listo para capturar',
1491
- description: '',
1492
- type: 'ready',
1493
- isInitialized: true
1494
- };
1321
+ // Check camera permissions before showing "ready" status
1322
+ const hasPermissions = await this.checkCameraPermissions();
1323
+ if (hasPermissions) {
1324
+ this.currentStatus = {
1325
+ message: 'Listo para capturar',
1326
+ description: '',
1327
+ type: 'ready',
1328
+ isInitialized: true
1329
+ };
1330
+ }
1331
+ else {
1332
+ this.currentStatus = {
1333
+ message: 'Permisos de cámara requeridos',
1334
+ description: 'Por favor, otorgue permisos de cámara para continuar',
1335
+ type: 'error',
1336
+ isInitialized: true
1337
+ };
1338
+ }
1495
1339
  this.emitReadyEvent();
1496
1340
  }
1497
1341
  updateStatus(message, description, type = 'loading') {
@@ -1505,13 +1349,29 @@ const JaakStamps = class {
1505
1349
  emitReadyEvent() {
1506
1350
  const isDocumentReady = !!window.ort && this.detectionService.isModelLoaded();
1507
1351
  this.isReady.emit(isDocumentReady);
1508
- this.logger.state('COMPONENTE_LISTO', {
1509
- ortLibraryLoaded: !!window.ort,
1510
- modelPreloaded: this.detectionService.isModelLoaded(),
1511
- isReady: isDocumentReady
1512
- });
1513
1352
  }
1514
- handleStateChange(data) {
1353
+ async checkCameraPermissions() {
1354
+ try {
1355
+ if (!navigator.permissions) {
1356
+ // Fallback: try to access camera directly
1357
+ try {
1358
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
1359
+ stream.getTracks().forEach(track => track.stop());
1360
+ return true;
1361
+ }
1362
+ catch {
1363
+ return false;
1364
+ }
1365
+ }
1366
+ const permission = await navigator.permissions.query({ name: 'camera' });
1367
+ return permission.state === 'granted';
1368
+ }
1369
+ catch (error) {
1370
+ return false;
1371
+ }
1372
+ }
1373
+ handleStateChange() {
1374
+ // Handle state changes from StateManager if needed
1515
1375
  }
1516
1376
  initializeResizeObserver() {
1517
1377
  if ('ResizeObserver' in window && this.detectionContainer) {
@@ -1520,15 +1380,56 @@ const JaakStamps = class {
1520
1380
  });
1521
1381
  resizeObserver.observe(this.detectionContainer.parentElement);
1522
1382
  }
1383
+ // Handle mobile device orientation changes
1384
+ window.addEventListener('orientationchange', () => {
1385
+ // Clear any existing orientation timer
1386
+ if (this.orientationTimer) {
1387
+ clearTimeout(this.orientationTimer);
1388
+ }
1389
+ // Add delay to ensure video dimensions are updated after rotation
1390
+ this.orientationTimer = window.setTimeout(() => {
1391
+ this.handleResize();
1392
+ this.orientationTimer = undefined;
1393
+ }, 300);
1394
+ });
1523
1395
  }
1524
1396
  handleResize() {
1525
1397
  if (this.detectionContainer) {
1526
1398
  const container = this.detectionContainer.parentElement;
1527
1399
  const rect = container.getBoundingClientRect();
1528
1400
  this.updateMaskDimensions(rect);
1529
- this.logger.debug('Container redimensionado:', { width: rect.width, height: rect.height });
1530
1401
  }
1531
1402
  }
1403
+ triggerRerender() {
1404
+ this.captureStateVersion = this.captureStateVersion + 1;
1405
+ }
1406
+ getGuideText(step) {
1407
+ if (step === 'completed') {
1408
+ return 'Proceso completado';
1409
+ }
1410
+ // Check if we should show performance message
1411
+ if (this.showPerformanceMessage && step === 'front') {
1412
+ return 'Modo manual activado por rendimiento. Use el botón para capturar.';
1413
+ }
1414
+ if (step === 'front') {
1415
+ // Para el frente, si el detector manual está activo, mostrar instrucciones de captura manual
1416
+ if (this.showManualCaptureButton) {
1417
+ return 'Posicione el frente de su documento en el marco y presione el botón para capturar';
1418
+ }
1419
+ // Instrucción básica de alineación
1420
+ return 'Alinee su identificación con el marco';
1421
+ }
1422
+ else if (step === 'back') {
1423
+ // Para el reverso, mostrar instrucciones específicas según el modo
1424
+ if (this.useDocumentDetector) {
1425
+ return 'Si tu documento no tiene lado trasero, da clic en el botón para continuar con el proceso';
1426
+ }
1427
+ else {
1428
+ return 'Posicione el reverso de su documento en el marco y capture, o salte este paso';
1429
+ }
1430
+ }
1431
+ return 'Alinee su identificación con el marco';
1432
+ }
1532
1433
  updateMaskDimensions(containerRect) {
1533
1434
  if (!this.videoRef)
1534
1435
  return;
@@ -1580,16 +1481,38 @@ const JaakStamps = class {
1580
1481
  this.el.style.setProperty('--mask-center-x', `${videoCenterXPercent}%`);
1581
1482
  this.el.style.setProperty('--mask-center-y', `${videoCenterYPercent}%`);
1582
1483
  this.isMaskReady = true;
1583
- this.logger.state('DIMENSIONES_MASCARA_ACTUALIZADAS', {
1584
- video: { width: videoWidth, height: videoHeight },
1585
- displayed: { width: displayedVideoWidth, height: displayedVideoHeight },
1586
- mask: { widthPercent: maskWidthPercent, heightPercent: maskHeightPercent },
1587
- center: { x: videoCenterXPercent, y: videoCenterYPercent },
1588
- offset: { x: videoOffsetX, y: videoOffsetY }
1589
- });
1590
1484
  }
1591
1485
  // PUBLIC METHODS
1486
+ // Helper method to check if component is ready for operations
1487
+ isComponentReady() {
1488
+ if (!this.currentStatus.isInitialized) {
1489
+ return {
1490
+ ready: false,
1491
+ message: 'El componente aún se está inicializando',
1492
+ status: 'initializing'
1493
+ };
1494
+ }
1495
+ if (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') {
1496
+ return {
1497
+ ready: false,
1498
+ message: 'El componente está cargando recursos',
1499
+ status: 'loading'
1500
+ };
1501
+ }
1502
+ if (!this.serviceContainer || !this.stateManager || !this.cameraService || !this.detectionService) {
1503
+ return {
1504
+ ready: false,
1505
+ message: 'Los servicios del componente no están disponibles',
1506
+ status: 'error'
1507
+ };
1508
+ }
1509
+ return { ready: true };
1510
+ }
1592
1511
  async getCapturedImages() {
1512
+ const readyCheck = this.isComponentReady();
1513
+ if (!readyCheck.ready) {
1514
+ throw new Error(readyCheck.message);
1515
+ }
1593
1516
  const state = this.stateManager.getCaptureState();
1594
1517
  if (state.step !== 'completed') {
1595
1518
  throw new Error('El proceso de captura no ha sido completado');
@@ -1597,146 +1520,359 @@ const JaakStamps = class {
1597
1520
  return this.stateManager.getCapturedImages();
1598
1521
  }
1599
1522
  async isProcessCompleted() {
1523
+ const readyCheck = this.isComponentReady();
1524
+ if (!readyCheck.ready) {
1525
+ return false; // Return false instead of error for this method
1526
+ }
1600
1527
  return this.stateManager.isProcessCompleted();
1601
1528
  }
1602
1529
  async startCapture() {
1603
- await this.startDetection();
1530
+ const readyCheck = this.isComponentReady();
1531
+ if (!readyCheck.ready) {
1532
+ this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
1533
+ return { success: false, error: readyCheck.message };
1534
+ }
1535
+ try {
1536
+ await this.startDetection();
1537
+ return { success: true };
1538
+ }
1539
+ catch (error) {
1540
+ this.updateStatus('Error al iniciar captura', error.message, 'error');
1541
+ return { success: false, error: error.message };
1542
+ }
1604
1543
  }
1605
1544
  async stopCapture() {
1606
- this.exitSession();
1545
+ const readyCheck = this.isComponentReady();
1546
+ if (!readyCheck.ready) {
1547
+ return { success: false, error: readyCheck.message };
1548
+ }
1549
+ try {
1550
+ this.exitSession();
1551
+ return { success: true };
1552
+ }
1553
+ catch (error) {
1554
+ return { success: false, error: error.message };
1555
+ }
1607
1556
  }
1608
1557
  async resetCapture() {
1609
- this.resetDetection();
1558
+ const readyCheck = this.isComponentReady();
1559
+ if (!readyCheck.ready) {
1560
+ return { success: false, error: readyCheck.message };
1561
+ }
1562
+ try {
1563
+ this.resetDetection();
1564
+ return { success: true };
1565
+ }
1566
+ catch (error) {
1567
+ return { success: false, error: error.message };
1568
+ }
1610
1569
  }
1611
1570
  async skipBackCapture() {
1612
- const captureState = this.stateManager.getCaptureState();
1613
- const capturedImages = this.stateManager.getCapturedImages();
1614
- if (captureState.step === 'back' && capturedImages.front.fullFrame) {
1615
- this.completeProcess(true);
1571
+ console.log('🟡 Botón clicked: Saltar reverso');
1572
+ // Set processing state immediately
1573
+ this.processingButton = 'skip-back';
1574
+ // Add small delay to show spinner
1575
+ await new Promise(resolve => setTimeout(resolve, 100));
1576
+ const readyCheck = this.isComponentReady();
1577
+ if (!readyCheck.ready) {
1578
+ this.processingButton = null;
1579
+ return { success: false, error: readyCheck.message };
1616
1580
  }
1617
- }
1581
+ try {
1582
+ const captureState = this.stateManager.getCaptureState();
1583
+ const capturedImages = this.stateManager.getCapturedImages();
1584
+ if (captureState.step === 'back' && capturedImages.front.fullFrame) {
1585
+ this.completeProcess(true);
1586
+ return { success: true };
1587
+ }
1588
+ else {
1589
+ return { success: false, error: 'No se puede saltar el reverso en el estado actual' };
1590
+ }
1591
+ }
1592
+ catch (error) {
1593
+ return { success: false, error: error.message };
1594
+ }
1595
+ finally {
1596
+ // Always clear processing state when done
1597
+ this.processingButton = null;
1598
+ }
1599
+ }
1618
1600
  async getStatus() {
1619
- const captureState = this.stateManager.getCaptureState();
1620
- const capturedImages = this.stateManager.getCapturedImages();
1621
- return {
1622
- isVideoActive: captureState.isVideoActive,
1623
- captureStep: captureState.step,
1624
- hasImages: !!(capturedImages.front.fullFrame || capturedImages.back.fullFrame),
1625
- isProcessCompleted: this.stateManager.isProcessCompleted(),
1626
- isModelPreloaded: this.detectionService.isModelLoaded()
1627
- };
1601
+ const readyCheck = this.isComponentReady();
1602
+ if (!readyCheck.ready) {
1603
+ return {
1604
+ isVideoActive: false,
1605
+ captureStep: 'front',
1606
+ hasImages: false,
1607
+ isProcessCompleted: false,
1608
+ isModelPreloaded: false,
1609
+ componentStatus: readyCheck.status,
1610
+ componentMessage: readyCheck.message
1611
+ };
1612
+ }
1613
+ try {
1614
+ const captureState = this.stateManager.getCaptureState();
1615
+ const capturedImages = this.stateManager.getCapturedImages();
1616
+ return {
1617
+ isVideoActive: captureState.isVideoActive,
1618
+ captureStep: captureState.step,
1619
+ hasImages: !!(capturedImages.front.fullFrame || capturedImages.back.fullFrame),
1620
+ isProcessCompleted: this.stateManager.isProcessCompleted(),
1621
+ isModelPreloaded: this.detectionService.isModelLoaded(),
1622
+ componentStatus: 'ready'
1623
+ };
1624
+ }
1625
+ catch (error) {
1626
+ return {
1627
+ isVideoActive: false,
1628
+ captureStep: 'front',
1629
+ hasImages: false,
1630
+ isProcessCompleted: false,
1631
+ isModelPreloaded: false,
1632
+ componentStatus: 'error',
1633
+ componentMessage: error.message
1634
+ };
1635
+ }
1628
1636
  }
1629
1637
  async preloadModel() {
1638
+ const readyCheck = this.isComponentReady();
1639
+ if (!readyCheck.ready) {
1640
+ this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
1641
+ return { success: false, error: readyCheck.message };
1642
+ }
1630
1643
  if (this.detectionService.isModelLoaded()) {
1631
- this.logger.state('MODELO_YA_PRECARGADO');
1632
- this.updateStatus('Modelos ya cargados', '', 'ready');
1644
+ this.updateStatus('Reconocimiento listo', '', 'ready');
1633
1645
  return { success: true, message: 'Model already loaded' };
1634
1646
  }
1635
1647
  try {
1636
1648
  const loadStartTime = performance.now();
1637
- this.updateStatus('Descargando modelo de detección...', 'Obteniendo red neuronal para reconocimiento de documentos', 'loading');
1638
- this.stateManager.updateCaptureState({ isLoading: true });
1639
- await this.detectionService.loadModel();
1640
- this.updateStatus('Cargando clasificador...', 'Preparando modelo de clasificación de tipos de documento', 'loading');
1641
- await this.detectionService.loadClassificationModel();
1649
+ this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
1650
+ if (this.stateManager) {
1651
+ this.stateManager.updateCaptureState({ isLoading: true });
1652
+ }
1653
+ try {
1654
+ await this.detectionService.loadModel();
1655
+ }
1656
+ catch (modelError) {
1657
+ this.switchToManualModeWithError('Error al cargar modelo de detección');
1658
+ throw modelError;
1659
+ }
1660
+ this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
1661
+ try {
1662
+ await this.detectionService.loadClassificationModel();
1663
+ }
1664
+ catch (classificationError) {
1665
+ this.switchToManualModeWithError('Error al cargar modelo de clasificación');
1666
+ throw classificationError;
1667
+ }
1642
1668
  const loadEndTime = performance.now();
1643
1669
  const loadTime = loadEndTime - loadStartTime;
1644
1670
  // Record ONNX load time
1645
1671
  if (this.debug) {
1646
1672
  this.recordOnnxPerformance(loadTime, 0);
1647
1673
  }
1648
- this.updateStatus('Optimizando modelos...', 'Configurando parámetros de rendimiento', 'loading');
1674
+ this.updateStatus('Finalizando configuración...', 'Preparando herramientas para captura', 'loading');
1649
1675
  await new Promise(resolve => setTimeout(resolve, 300));
1650
- this.updateStatus('Modelos precargados', '', 'ready');
1651
- this.stateManager.updateCaptureState({ isLoading: false });
1676
+ this.updateStatus('Reconocimiento preparado', '', 'ready');
1677
+ if (this.stateManager) {
1678
+ this.stateManager.updateCaptureState({ isLoading: false });
1679
+ }
1652
1680
  this.emitReadyEvent();
1653
- this.logger.state('MODELOS_PRECARGADOS_EXITOSAMENTE', { loadTime: Math.round(loadTime) });
1654
1681
  return { success: true, message: 'Models preloaded successfully' };
1655
1682
  }
1656
1683
  catch (error) {
1657
- this.logger.error('Error al precargar modelos:', error);
1658
- this.updateStatus('Error al cargar modelos', 'No se pudieron descargar los recursos necesarios', 'error');
1659
- this.stateManager.updateCaptureState({ isLoading: false });
1684
+ this.updateStatus('Error de configuración', 'No se pudieron preparar las herramientas necesarias', 'error');
1685
+ if (this.stateManager) {
1686
+ this.stateManager.updateCaptureState({ isLoading: false });
1687
+ }
1660
1688
  return { success: false, error: error.message };
1661
1689
  }
1662
1690
  }
1663
1691
  async getCameraInfo() {
1664
- return this.cameraService.getCameraInfo();
1692
+ const readyCheck = this.isComponentReady();
1693
+ if (!readyCheck.ready) {
1694
+ return {
1695
+ availableCameras: [],
1696
+ selectedCameraId: null,
1697
+ deviceType: 'desktop',
1698
+ isMultipleCamerasAvailable: false,
1699
+ preferredFacing: null,
1700
+ error: readyCheck.message
1701
+ };
1702
+ }
1703
+ try {
1704
+ return await this.cameraService.getCameraInfo();
1705
+ }
1706
+ catch (error) {
1707
+ return {
1708
+ availableCameras: [],
1709
+ selectedCameraId: null,
1710
+ deviceType: 'desktop',
1711
+ isMultipleCamerasAvailable: false,
1712
+ preferredFacing: null,
1713
+ error: error.message
1714
+ };
1715
+ }
1665
1716
  }
1666
1717
  async setPreferredCamera(camera) {
1667
- this.preferredCamera = camera;
1668
- this.serviceContainer.updateConfig({ preferredCamera: camera });
1669
- await this.cameraService.enumerateDevices();
1670
- const captureState = this.stateManager.getCaptureState();
1671
- if (captureState.isVideoActive) {
1672
- const selectedCameraId = this.cameraService.getSelectedCameraId();
1673
- if (selectedCameraId) {
1674
- await this.cameraService.switchCamera(selectedCameraId);
1718
+ const readyCheck = this.isComponentReady();
1719
+ if (!readyCheck.ready) {
1720
+ return {
1721
+ success: false,
1722
+ error: readyCheck.message,
1723
+ selectedCamera: null,
1724
+ availableCameras: 0
1725
+ };
1726
+ }
1727
+ try {
1728
+ this.preferredCamera = camera;
1729
+ this.serviceContainer.updateConfig({ preferredCamera: camera });
1730
+ await this.cameraService.enumerateDevices();
1731
+ await this.updateCameraInfoWithAutofocus();
1732
+ const captureState = this.stateManager.getCaptureState();
1733
+ if (captureState.isVideoActive) {
1734
+ const selectedCameraId = this.cameraService.getSelectedCameraId();
1735
+ if (selectedCameraId) {
1736
+ await this.cameraService.switchCamera(selectedCameraId);
1737
+ }
1675
1738
  }
1739
+ return {
1740
+ success: true,
1741
+ selectedCamera: this.cameraService.getSelectedCameraId(),
1742
+ availableCameras: this.cameraService.getAvailableCameras().length
1743
+ };
1744
+ }
1745
+ catch (error) {
1746
+ return {
1747
+ success: false,
1748
+ error: error.message,
1749
+ selectedCamera: null,
1750
+ availableCameras: 0
1751
+ };
1676
1752
  }
1677
- return {
1678
- success: true,
1679
- selectedCamera: this.cameraService.getSelectedCameraId(),
1680
- availableCameras: this.cameraService.getAvailableCameras().length
1681
- };
1682
1753
  }
1683
1754
  async setCaptureDelay(delay) {
1684
1755
  if (delay < 0 || delay > 10000) {
1685
- throw new Error('Capture delay must be between 0 and 10000 milliseconds');
1756
+ return {
1757
+ success: false,
1758
+ error: 'Capture delay must be between 0 and 10000 milliseconds',
1759
+ captureDelay: this.captureDelay
1760
+ };
1686
1761
  }
1762
+ // Allow setting capture delay even during initialization
1687
1763
  this.captureDelay = delay;
1688
- this.serviceContainer.updateConfig({ captureDelay: delay });
1764
+ if (this.serviceContainer) {
1765
+ this.serviceContainer.updateConfig({ captureDelay: delay });
1766
+ }
1689
1767
  return {
1690
1768
  success: true,
1691
1769
  captureDelay: this.captureDelay
1692
1770
  };
1693
1771
  }
1694
1772
  async getCaptureDelay() {
1773
+ // Always allow getting capture delay, even during initialization
1695
1774
  return this.captureDelay;
1696
1775
  }
1697
- async setTorchEnabled(enabled) {
1698
- const torchEnabled = await this.cameraService.setTorchEnabled(enabled, this.videoStream);
1699
- return {
1700
- success: torchEnabled,
1701
- enabled: torchEnabled ? enabled : false
1702
- };
1703
- }
1704
- async focusAtPoint(x, y) {
1705
- const focused = await this.cameraService.focusAtPoint(x, y, this.videoStream);
1776
+ // Memory and device capability detection
1777
+ checkDeviceCapabilities() {
1778
+ const deviceMemory = navigator.deviceMemory;
1779
+ // Siempre intentar usar ML, independientemente del dispositivo
1780
+ // Las restricciones anteriores no eran confiables
1706
1781
  return {
1707
- success: focused,
1708
- coordinates: { x, y }
1782
+ canUseML: true,
1783
+ reason: 'Dispositivo compatible con detección automática.',
1784
+ memoryMB: deviceMemory ? deviceMemory * 1024 : null
1709
1785
  };
1710
1786
  }
1711
1787
  // DETECTION METHODS
1712
1788
  async startDetection() {
1713
- this.logger.state('INICIANDO_DETECCION');
1714
1789
  try {
1790
+ // Verificar capacidades del dispositivo antes de cargar modelos
1791
+ const capabilities = this.checkDeviceCapabilities();
1792
+ // Si el dispositivo no puede usar ML o tenemos problemas de memoria, usar modo manual
1793
+ if (!capabilities.canUseML && this.useDocumentDetector) {
1794
+ this.switchToManualModeWithWarning(capabilities.reason);
1795
+ return;
1796
+ }
1715
1797
  // Paso 1: Verificar y cargar modelos si es necesario
1716
1798
  if (!this.detectionService.isModelLoaded()) {
1717
1799
  const loadStartTime = performance.now();
1718
- this.updateStatus('Cargando modelo de detección...', 'Descargando red neuronal para reconocimiento', 'loading');
1800
+ this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
1719
1801
  this.stateManager.updateCaptureState({ isLoading: true });
1720
- await this.detectionService.loadModel();
1721
- this.updateStatus('Cargando clasificador...', 'Preparando modelo de clasificación de documentos', 'loading');
1722
- await this.detectionService.loadClassificationModel();
1802
+ try {
1803
+ await this.detectionService.loadModel();
1804
+ }
1805
+ catch (modelError) {
1806
+ // Si es un error de memoria, cambiar a modo manual con mensaje específico
1807
+ if (modelError.message.includes('Out of memory') || modelError.message.includes('no available backend')) {
1808
+ const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
1809
+ this.switchToManualModeWithWarning(`Memoria insuficiente para detección automática ${memoryInfo}. Modo manual activado - funciona igual de bien!`);
1810
+ return;
1811
+ }
1812
+ else {
1813
+ this.switchToManualModeWithError('Error al cargar modelo de detección');
1814
+ throw modelError;
1815
+ }
1816
+ }
1817
+ this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
1818
+ try {
1819
+ await this.detectionService.loadClassificationModel();
1820
+ }
1821
+ catch (classificationError) {
1822
+ // Si es un error de memoria, cambiar a modo manual con mensaje específico
1823
+ if (classificationError.message.includes('Out of memory') || classificationError.message.includes('no available backend')) {
1824
+ const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
1825
+ this.switchToManualModeWithWarning(`Memoria insuficiente para clasificación automática ${memoryInfo}. Modo manual activado para optimizar rendimiento.`);
1826
+ return;
1827
+ }
1828
+ else {
1829
+ this.switchToManualModeWithError('Error al cargar modelo de clasificación');
1830
+ throw classificationError;
1831
+ }
1832
+ }
1723
1833
  const loadEndTime = performance.now();
1724
1834
  const loadTime = loadEndTime - loadStartTime;
1725
1835
  // Record ONNX load time
1726
1836
  if (this.debug) {
1727
1837
  this.recordOnnxPerformance(loadTime, 0);
1728
1838
  }
1729
- this.logger.state('MODELOS_CARGADOS_EN_DETECCION', { loadTime: Math.round(loadTime) });
1730
1839
  }
1731
1840
  // Paso 2: Detectar y configurar dispositivos
1732
- this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura disponibles', 'loading');
1733
- await this.cameraService.enumerateDevices();
1841
+ this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
1842
+ try {
1843
+ await this.cameraService.enumerateDevices();
1844
+ }
1845
+ catch (enumerateError) {
1846
+ throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
1847
+ }
1848
+ try {
1849
+ await this.updateCameraInfoWithAutofocus();
1850
+ }
1851
+ catch (cameraInfoError) {
1852
+ throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
1853
+ }
1734
1854
  // Paso 3: Configurar cámara seleccionada
1735
- this.updateStatus('Configurando cámara...', 'Estableciendo resolución y parámetros óptimos', 'loading');
1736
- const stream = await this.cameraService.setupCamera();
1737
- // Paso 4: Inicializar video y ocultar estado inmediatamente
1738
- this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
1739
- await this.initializeVideoStream(stream);
1855
+ this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
1856
+ let stream;
1857
+ try {
1858
+ stream = await this.cameraService.setupCamera();
1859
+ }
1860
+ catch (setupError) {
1861
+ throw new Error(`Error al configurar cámara: ${setupError.message}`);
1862
+ }
1863
+ // Paso 4: Inicializar video y mostrar estado de análisis inicial
1864
+ if (this.useDocumentDetector) {
1865
+ this.updateStatus('Analizando estabilidad...', 'Evaluando rendimiento del sistema', 'loading');
1866
+ }
1867
+ else {
1868
+ this.updateStatus('Captura activa', 'Posicione su documento y use el botón para capturar', 'active');
1869
+ }
1870
+ try {
1871
+ await this.initializeVideoStream(stream);
1872
+ }
1873
+ catch (videoError) {
1874
+ throw new Error(`Error al inicializar video: ${videoError.message}`);
1875
+ }
1740
1876
  // Paso 5: Calibrar máscara
1741
1877
  if (this.detectionContainer) {
1742
1878
  const container = this.detectionContainer.parentElement;
@@ -1746,11 +1882,14 @@ const JaakStamps = class {
1746
1882
  // Finalizar e iniciar captura
1747
1883
  this.startTime = Date.now();
1748
1884
  this.stateManager.updateCaptureState({ isLoading: false });
1885
+ // Mostrar botón manual si el detector está deshabilitado
1886
+ if (!this.useDocumentDetector) {
1887
+ this.showManualCaptureButton = true;
1888
+ }
1749
1889
  this.detectFrame();
1750
1890
  }
1751
1891
  catch (err) {
1752
- this.logger.error('Error al inicializar detección:', err);
1753
- this.updateStatus('Error al iniciar captura', 'No se pudo completar la inicialización', 'error');
1892
+ this.updateStatus('Error al iniciar', 'No se pudo preparar la captura', 'error');
1754
1893
  this.stateManager.updateCaptureState({ isLoading: false });
1755
1894
  }
1756
1895
  }
@@ -1760,14 +1899,16 @@ const JaakStamps = class {
1760
1899
  this.videoStream = stream;
1761
1900
  const isRear = this.cameraService.isRearCamera(stream);
1762
1901
  this.shouldMirrorVideo = !isRear;
1763
- this.logger.state('CAMARA_CONFIGURADA', {
1764
- isRearCamera: isRear,
1765
- shouldMirrorVideo: this.shouldMirrorVideo
1766
- });
1767
1902
  return new Promise((resolve) => {
1768
1903
  this.videoRef.onloadedmetadata = async () => {
1769
1904
  await this.videoRef.play();
1770
1905
  this.stateManager.updateCaptureState({ isVideoActive: true });
1906
+ // Recalculate mask dimensions when new camera stream loads
1907
+ if (this.detectionContainer) {
1908
+ const container = this.detectionContainer.parentElement;
1909
+ const rect = container.getBoundingClientRect();
1910
+ this.updateMaskDimensions(rect);
1911
+ }
1771
1912
  resolve();
1772
1913
  };
1773
1914
  });
@@ -1782,15 +1923,28 @@ const JaakStamps = class {
1782
1923
  const captureState = this.stateManager.getCaptureState();
1783
1924
  if (!this.videoRef || !this.detectionContainer || !this.detectionService.isModelLoaded())
1784
1925
  return;
1926
+ // Show manual capture button when document detector is disabled or performance is degraded
1927
+ if (!this.useDocumentDetector || this.performanceDegradedMode) {
1928
+ this.showManualCaptureButton = true;
1929
+ // Continue the loop for UI updates but skip detection logic
1930
+ if (captureState.step !== 'completed') {
1931
+ this.animationId = requestAnimationFrame(() => this.detectFrame());
1932
+ }
1933
+ return;
1934
+ }
1935
+ else if (!this.performanceDegradedMode) {
1936
+ this.showManualCaptureButton = false;
1937
+ }
1785
1938
  if (captureState.isDetectionPaused) {
1786
1939
  if (captureState.step !== 'completed') {
1787
1940
  this.animationId = requestAnimationFrame(() => this.detectFrame());
1788
1941
  }
1789
1942
  return;
1790
1943
  }
1791
- // Frame skipping for performance
1944
+ // Adaptive frame skipping for performance
1792
1945
  this.frameSkipCounter++;
1793
- if (this.frameSkipCounter <= this.FRAME_SKIP) {
1946
+ const currentFrameSkip = this.getAdaptiveFrameSkip();
1947
+ if (this.frameSkipCounter <= currentFrameSkip) {
1794
1948
  if (captureState.step !== 'completed') {
1795
1949
  this.animationId = requestAnimationFrame(() => this.detectFrame());
1796
1950
  }
@@ -1808,10 +1962,27 @@ const JaakStamps = class {
1808
1962
  this.lastInferenceTime = currentTime;
1809
1963
  // Measure preprocessing and inference time
1810
1964
  const inferenceStartTime = performance.now();
1811
- const inputTensor = this.detectionService.preprocess(this.videoRef);
1812
- // Standard detection without quality validation
1813
- const detections = await this.detectionService.runInference(inputTensor);
1814
- const inferenceTime = performance.now() - inferenceStartTime;
1965
+ let inputTensor;
1966
+ let detections;
1967
+ let inferenceTime;
1968
+ try {
1969
+ inputTensor = this.detectionService.preprocess(this.videoRef);
1970
+ }
1971
+ catch (preprocessError) {
1972
+ this.switchToManualModeWithError('Error al procesar imagen');
1973
+ return;
1974
+ }
1975
+ try {
1976
+ // Standard detection without timeout (timeout is handled by frame counting)
1977
+ detections = await this.detectionService.runInference(inputTensor);
1978
+ inferenceTime = performance.now() - inferenceStartTime;
1979
+ }
1980
+ catch (inferenceError) {
1981
+ this.switchToManualModeWithError('Error en detección automática');
1982
+ return;
1983
+ }
1984
+ // Increment frame count
1985
+ this.processedFramesCount++;
1815
1986
  // Record ONNX performance metrics
1816
1987
  if (this.debug) {
1817
1988
  this.recordOnnxPerformance(0, inferenceTime);
@@ -1850,9 +2021,30 @@ const JaakStamps = class {
1850
2021
  }
1851
2022
  this.updateMaskColor(detections);
1852
2023
  // Record frame processing metrics
2024
+ const frameEndTime = performance.now();
2025
+ const totalFrameTime = frameEndTime - frameStartTime;
2026
+ // Sistema simplificado: Verificar rendimiento de cada frame (solo si no se ha cambiado a manual)
2027
+ if (this.useDocumentDetector && !this.hasAutoSwitchedToManual) {
2028
+ // Verificar si el frame es lento
2029
+ if (totalFrameTime >= this.PERFORMANCE_THRESHOLD_MS) {
2030
+ this.slowFrameCount++;
2031
+ // Si tenemos suficientes frames lentos consecutivos, cambiar a manual
2032
+ if (this.slowFrameCount >= this.SLOW_FRAMES_TO_TRIGGER) {
2033
+ this.switchToManualMode();
2034
+ }
2035
+ }
2036
+ else {
2037
+ // Frame rápido, reiniciar contador
2038
+ if (this.slowFrameCount > 0) {
2039
+ this.slowFrameCount = 0;
2040
+ }
2041
+ }
2042
+ // Cambiar mensaje de estado después de analizar los primeros 2 frames
2043
+ if (this.processedFramesCount === 2) {
2044
+ this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
2045
+ }
2046
+ }
1853
2047
  if (this.debug) {
1854
- const frameEndTime = performance.now();
1855
- const totalFrameTime = frameEndTime - frameStartTime;
1856
2048
  this.recordFrameProcessing(totalFrameTime, detections.length);
1857
2049
  }
1858
2050
  // Continue detection loop
@@ -1866,7 +2058,6 @@ const JaakStamps = class {
1866
2058
  }
1867
2059
  }
1868
2060
  catch (e) {
1869
- this.logger.error('Error en inferencia de modelo:', e);
1870
2061
  const captureState = this.stateManager.getCaptureState();
1871
2062
  if (captureState.step !== 'completed') {
1872
2063
  setTimeout(() => this.detectFrame(), 100);
@@ -1889,10 +2080,18 @@ const JaakStamps = class {
1889
2080
  clearTimeout(this.alignmentTimer);
1890
2081
  this.alignmentTimer = undefined;
1891
2082
  }
2083
+ if (this.orientationTimer) {
2084
+ clearTimeout(this.orientationTimer);
2085
+ this.orientationTimer = undefined;
2086
+ }
2087
+ // Liberar recursos de ONNX al limpiar el componente
2088
+ this.releaseOnnxResources();
1892
2089
  if (this.performanceUpdateInterval) {
1893
2090
  clearInterval(this.performanceUpdateInterval);
1894
2091
  this.performanceUpdateInterval = undefined;
1895
2092
  }
2093
+ // Clear canvas pool
2094
+ this.canvasPool = [];
1896
2095
  this.detectionBoxes = [];
1897
2096
  this.alignmentStartTime = undefined;
1898
2097
  this.hasDocumentDetected = false;
@@ -1906,11 +2105,8 @@ const JaakStamps = class {
1906
2105
  step: 'front',
1907
2106
  isCapturing: false
1908
2107
  };
1909
- const cameraInfo = this.cameraService?.getCameraInfo() || {
1910
- availableCameras: [],
1911
- selectedCameraId: null,
1912
- deviceType: 'desktop'};
1913
- return (h("div", { key: '9d4d77042218ab3d0bc4fb1ce879db09dc3e78b4', class: "detector-container" }, h("div", { key: '51ac8ccb1275637282b038ad8364d19f14ef61b1', class: "video-container" }, 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' } }), h("div", { key: '43b71dfe3eaf20ffa66a4139e24b1af6a2074631', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index) => (h("div", { key: index, class: "detection-box", style: {
2108
+ const cameraInfo = this.cameraInfoWithAutofocus;
2109
+ return (h("div", { key: 'dda3e3054a18dac59fd6dab4bf722f405d1c1b0f', class: "detector-container" }, h("div", { key: 'c260bf52307e072a634cddcc273c1f55037d01ef', class: "video-container" }, 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' } }), h("div", { key: '6afd8a46c16592c67e6bb93f1655de1660338f30', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index) => (h("div", { key: index, class: "detection-box", style: {
1914
2110
  position: 'absolute',
1915
2111
  left: `${box.x}px`,
1916
2112
  top: `${box.y}px`,
@@ -1919,9 +2115,9 @@ const JaakStamps = class {
1919
2115
  border: '2px solid #32406C',
1920
2116
  pointerEvents: 'none',
1921
2117
  boxSizing: 'border-box'
1922
- } })))), this.isMaskReady && (h("div", { key: 'c2596f8cabb227e3ea3b082495f52b141cb1690f', class: "overlay-mask" }, h("div", { key: 'ba1ea39b5df1cd312a9bd9410b7225a29cfb8a29', class: "card-outline" }, h("div", { key: '6ce4d4d969d5f3fbcf75a4eb285f61d434c5633b', class: "side side-top" }), h("div", { key: '36653ecc4fcda25dfc84bc4d1f4c4c5dfb71ced7', class: "side side-right" }), h("div", { key: '8b108fa8fb370b229a0d1a77cb1ff044319a8f75', class: "side side-bottom" }), h("div", { key: 'a7aa3b45c065a50445aafd5c3ccefb57b0c9c769', class: "side side-left" }), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: 'b94b92273e12a019238e11ae57f21e140f13c3f1', class: "guide-text" }, "Alinee su identificaci\u00F3n con el marco"))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '144c3741003ec0cc148bb6ce92012d7b3cd110c8', class: "skip-section" }, h("div", { key: 'f0a984a6b75afa374c5d47da9d53e39f98071da0', class: "skip-explanation" }, "Si tu documento no tiene lado trasero, da clic en el bot\u00F3n para continuar con el proceso"), h("button", { key: '57ac30d776e30709aab6e0a621cef889da3cc677', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
2118
+ } })))), this.isMaskReady && (h("div", { key: '178f2022bed334f8c9c8c866a92b44c8cb0369b1', class: "overlay-mask" }, h("div", { key: 'acb1bc996409095afd29ffa5a038c3eeae6c7ed9', class: "card-outline" }, h("div", { key: 'dcc952d9011c5fd13b229f3a759cc74721a1ee11', class: "side side-top" }), h("div", { key: 'addb25f6c41ad10a09b6918e6357052294d14adf', class: "side side-right" }), h("div", { key: 'a1ec08f3bf1b16731a224c034c76dd8c6412597b', class: "side side-bottom" }), h("div", { key: '22f3c158f2c2bb1dc582f796c39ea60bc11505dd', class: "side side-left" })), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '5b150f7df5b0c5402e42bae4601936d4e5d1f5d4', class: `guide-text ${this.showPerformanceMessage ? 'performance-warning-text' : ''}` }, this.getGuideText(captureState.step))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '17b0a81437eb8691cb4dc5042c72bda01ac92be0', class: "back-capture-section" }, h("div", { key: 'a65ccbd5f53b06ec340b36a0a2348cf04920b3df', class: "back-capture-buttons" }, (!this.useDocumentDetector || this.performanceDegradedMode) && (h("button", { key: '383d7398079dbb8b1adfe695407cc598059b2a94', class: "capture-button primary-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'capture-back' && (h("span", { key: '86bddcfeb6f90da9b18926f4c6e1208296502dcf', class: "button-spinner" })), "Capturar Reverso")), h("button", { key: '5c9d116cbb9b5bc16481b2b90c8451182ad436c5', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'skip-back' && (h("span", { key: '881f00cfc3a24167bc6b8da5849cb404101b692d', class: "button-spinner" })), this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
1923
2119
  ? `Saltar reverso (${this.backDocumentTimerRemaining}s)`
1924
- : 'Saltar reverso'))), captureState.isVideoActive && (h("div", { key: 'bd45628707e0169317ed3dd4247e1056d7046104', class: "camera-controls" }, h("button", { key: 'e750fbf4d5d76d9e9596823bb2b86801ef19e485', class: `camera-selector-button ${this.isSwitchingCamera ? 'loading' : ''}`, onClick: () => this.toggleCameraSelector(), type: "button", title: "Seleccionar c\u00E1mara", disabled: this.isSwitchingCamera }, this.isSwitchingCamera ? (h("div", { class: "button-spinner" })) : ('Cámaras')))), this.showCameraSelector && cameraInfo.availableCameras.length > 0 && (h("div", { key: '40ada780a8ea13aeeaee9cb39bc6496f69f2679d', class: "camera-selector-dropdown" }, h("div", { key: '3a920bb02ba024f0359513f10544bf0528ecee6d', class: "camera-selector-header" }, h("span", { key: 'e5efd478a602b13821ba53cb9a7a59d1b4a71178' }, "Seleccionar C\u00E1mara"), h("button", { key: '2df6cc11e162be74862029e33815927e3fa8f2f8', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: '06934642dff134c9e1a6872f935a5892a9c7b835', class: "camera-list" }, cameraInfo.availableCameras.map((camera) => (h("button", { key: camera.id, class: `camera-option ${cameraInfo.selectedCameraId === camera.id ? 'selected' : ''}`, onClick: () => this.handleCameraSwitch(camera.id), type: "button" }, h("span", { class: "camera-label" }, camera.label || `Cámara ${cameraInfo.availableCameras.indexOf(camera) + 1}`), cameraInfo.selectedCameraId === camera.id && (h("span", { class: "selected-indicator" }, "\u2713")))))), h("div", { key: '8d5ede287c2efb0ebe418bee8dc7fb310443c991', class: "device-info" }, h("small", { key: 'b7dd10565113694b09f01ab28ffa29262ae1d2bc' }, "Dispositivo: ", cameraInfo.deviceType)))))), captureState.isCapturing && (h("div", { key: '2f9e549e6c0c56d4db5835e58361fb17bba6cf90', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: 'a6ef0ed127f23e9d764896ed743ca8d8454851a6', class: "flip-animation" }, h("div", { key: '0b04411f88a634bb3243b00ca2d5a45fd6962dea', class: "id-card-icon" }), h("div", { key: '9327be58879b407cea87f455989406e58302e9d1', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: 'bd268578b5003e99256e3340b4a9a6fb7656be64', class: "success-animation" }, h("div", { key: '064e0a5b9851de14bc6829f028e7ea7161fd1633', class: "check-icon" }), h("div", { key: 'bdcd94a8806399e396e218a359c10b11d05f38d6', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '3d9af617376c16e44e065873919c3d90ff46110a', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: 'be7b56decfd6f12dcca46b2a9456480e3d5ac00e', class: "status-spinner" })), h("div", { key: '5fe9850883cb749cebaa18c3e9c13ca21325e373', class: "status-content" }, h("div", { key: '43fdb50691e4b98e56126af1a129f625061128bf', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: 'b76cbfa4a0349421fcd535d3fd8bf40c89ca3aef', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: 'eddefb464af0487ab9ab2a0f76dccbb6a2092e51', class: "performance-monitor" }, h("div", { key: '7b7d93b56cb4cd4fbb1bdde89c6150b282fa805e', class: "performance-expanded" }, h("div", { key: 'f302e8b9d985fb8cad53c4c4e0d37a3bf5a63261', class: "metrics-row" }, h("div", { key: 'e8d8f2420ee8643a7f454d38065c75b5be598f77', class: "metric-compact" }, h("span", { key: '6e52eb5bb06e0df57e61774de38c31349f45e22b', class: "metric-label" }, "FPS"), h("span", { key: '6636f9cea20c49b49c17db86188e2f6b214c45c3', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: 'afa1d9dec05b14256a1b1fcec8638b8c21a56e37', class: "metric-compact" }, h("span", { key: '9c55e30f5d01582648bacc1fb38ca1990485a591', class: "metric-label" }, "MEM"), h("span", { key: 'ca263601e8715027f95ffb2d5413a643dc570cf2', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: '03caf19961fd01b7781484d49d8121b1e78c161f', class: "metrics-row" }, h("div", { key: 'bffb08f01f703411f7c8605e4f8d95b1c37b5c0f', class: "metric-compact" }, h("span", { key: '9b92279b3253cc669412733facb3751f4344d69f', class: "metric-label" }, "INF"), h("span", { key: '3234d4150e2e1479e3b552767af8123e69911d6c', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: '77082d2669db53f19fb50b3bbfa1ebefb0b86ff4', class: "metric-compact" }, h("span", { key: 'cc6ef96c72c800ba36dab2a388f1de863b2dacd5', class: "metric-label" }, "FRAME"), h("span", { key: 'e2bb4b7351869a76e810f8bbf20d805154495e35', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: '5700d7ef839fa77686299d79445203090dc8079f', class: "metrics-row" }, h("div", { key: 'a2999a035fccb7f250c01b084075490678513c87', class: "metric-compact" }, h("span", { key: '3677d58665996fa5969507dbaff9e956b2ea54ee', class: "metric-label" }, "DET"), h("span", { key: 'c584312438b339c82a3bdc33f00fb985ff2a6f55', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: '58866219f213bf8607083f30be2d5ca5d0515cea', class: "metric-compact" }, h("span", { key: 'd90b450051331347f219b6c0f197536d0dcee9b7', class: "metric-label" }, "RATE"), h("span", { key: 'f6e6cd7948e06014e5a0416e4e81394fa36ec61d', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: 'c1d0194f181a0082cc1aa5452de382908da61b65', class: "watermark" }, h("img", { key: 'e04d35f3c79a2b4a3922ab5b0f102220171fa7c0', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
2120
+ : 'Saltar reverso')))), captureState.isVideoActive && (h("div", { key: 'd929fc0eeeeee944f3d722fde98c52642443e9a5', class: "camera-controls" }, 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 ? (h("div", { class: "button-spinner" })) : ('Cámaras')))), this.showCameraSelector && cameraInfo.availableCameras.length > 0 && (h("div", { key: 'f37feefd9def935417467dffb52b9fe8d8acea79', class: "camera-selector-dropdown" }, h("div", { key: 'a8479ec6a6c4076b5dc40677e1988295100e4d0d', class: "camera-selector-header" }, h("span", { key: '074f02967354293a1e5df4e3ac8a126faee33d04' }, "Seleccionar C\u00E1mara"), h("button", { key: 'd6ba0252fb269c3a04967fb0e5c075166f202d71', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: '63f5f9be2b05da71f4fe5a4e38b11ab3e39fb1e0', class: "camera-list" }, cameraInfo.availableCameras.map((camera) => (h("button", { key: camera.id, class: `camera-option ${cameraInfo.selectedCameraId === camera.id ? 'selected' : ''}`, onClick: () => this.handleCameraSwitch(camera.id), type: "button" }, h("span", { class: "camera-label" }, camera.label || `Cámara ${cameraInfo.availableCameras.indexOf(camera) + 1}`, camera.hasAutofocus && (h("span", { class: "autofocus-icon", title: "Autofoco disponible" }, "\u29BF"))), cameraInfo.selectedCameraId === camera.id && (h("span", { class: "selected-indicator" }, "\u2713")))))), h("div", { key: '4ca46fdebe0eaa078f1dae4d2f1af03321577c7f', class: "device-info" }, h("small", { key: '10c54ddc5a196b11a69077b045119187ed09f4be' }, "Dispositivo: ", cameraInfo.deviceType)))), this.showManualCaptureButton && captureState.step === 'front' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '25393018948798f4d9d5f44738e61c7a0a6be990', class: "manual-capture-section" }, h("button", { key: '7a6c8686c0db3e352f9689bc41f7fd3f612c800c', class: "manual-capture-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'capture-front' && (h("span", { key: '1916b97da3f8dce7d06b86b21f0361dd7ce10b95', class: "button-spinner" })), "Capturar Frente"))))), captureState.isCapturing && (h("div", { key: 'aed05a3280dc96121564ffea87945f441d4286f6', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: '63d262e5e10325d145fada8e91a878c47305f2b9', class: "flip-animation" }, h("div", { key: '2a3699f47e595c5e4ab00dfd11e1e66ab2e13039', class: "id-card-icon" }), h("div", { key: 'df195fe9a6db4ec3e14b04b4e932d55adb7c3f3c', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: '670341c302a31c8017a9083ebf35e2988122ac3f', class: "success-animation" }, h("div", { key: '27b0b0d91de6a882182b6ccdebecc7c440997e36', class: "check-icon" }), h("div", { key: '71ee5327faf48ea26c2ae36a8a6906c448c3a389', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '39c92aa0b82f6169058b1092894845fcba3f27b4', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: '74b02ba81756f0e8bfb6e2feafdb2c44fb80afad', class: "status-spinner" })), h("div", { key: 'c7419efb2c6e2760019ecce619534e24ae3d7351', class: "status-content" }, h("div", { key: '57f315384630f17f6332eddfea628b333299e831', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: 'ac8632fdd68c24ed00192cb04a13026ada1f953d', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: 'e51286a839795b18cfac2874e7e1f6e9f8e3bb4b', class: "performance-monitor" }, h("div", { key: 'f8cd289d260af9df2daf23bdfb8db70fe8bf6f45', class: "performance-expanded" }, h("div", { key: 'bf2becb2ee3ad3bffb82b0e90246a4385a19f567', class: "metrics-row" }, h("div", { key: 'f334c2b5de45df73aec6e06cd371151110789a45', class: "metric-compact" }, h("span", { key: 'cbf32f72eb3aaf1bf770d651c32123a43b81035c', class: "metric-label" }, "FPS"), h("span", { key: '9d2d2f46ba3fdb23c010f781044f00b1dd1250e6', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: 'f29406e8a5b00f5838a144de9fda77ce53177d43', class: "metric-compact" }, h("span", { key: '8f44e29f6134c340f0a58f8aa321612c8a9bbfb8', class: "metric-label" }, "MEM"), h("span", { key: '7a58e164d292329d95054b2679a90e031f6fe4c2', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: 'd0284a0e65fc2879557d73fc2e03295c29f512ae', class: "metrics-row" }, h("div", { key: '78dcbce552e65c95fcd01158f3aa8916ef0e7175', class: "metric-compact" }, h("span", { key: '673cfbc8c9dabb020a3d3c18ee07558a61da9b51', class: "metric-label" }, "INF"), h("span", { key: '833761b7a875c931a8b31913930c3b132f71f618', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: '324801e3e69a8de181755367201d8e8c643e8d72', class: "metric-compact" }, h("span", { key: 'f928d9b2dfbf81f5738ab08a42ebb76ff4a483de', class: "metric-label" }, "FRAME"), h("span", { key: 'dc9f2e4a1e848be4d240d89c89126203555ae203', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: '7ca0a7b44719b1884334edab586267f021cf86c1', class: "metrics-row" }, h("div", { key: '262667c4320d337dae2220b77f142cf293542562', class: "metric-compact" }, h("span", { key: '578b53d251e58933f654a5afe558931e4333f022', class: "metric-label" }, "DET"), h("span", { key: '4fb36eaebebd73e5cb06d582fa3b4ece61463761', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: 'c17192e3964164ad603427de3befa1615b027022', class: "metric-compact" }, h("span", { key: 'd004e74797b9468e94c2af996bb62c1a3ca8d58e', class: "metric-label" }, "RATE"), h("span", { key: '0960e8760fd0ac2456a9e76598e5392c24e42d3b', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: '3588fba1fe51af1335d643107ba0355aca6f4f8c', class: "watermark" }, h("img", { key: 'd53ec6b0ee9f97f602f4f8857ec605427b2ee478', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
1925
2121
  }
1926
2122
  // Utility methods
1927
2123
  updateDetectionBoxes(boxes) {
@@ -2002,35 +2198,18 @@ const JaakStamps = class {
2002
2198
  if (allSidesAligned && bestBox) {
2003
2199
  cardOutline?.classList.add('perfect-match');
2004
2200
  corners?.forEach(corner => corner.classList.add('perfect-match'));
2005
- // Debug logging
2006
- this.logger.state('CAPTURE_EVALUATION', {
2007
- allSidesAligned: true,
2008
- hasScreenshotTaken: this.hasScreenshotTaken,
2009
- captureDelay: this.captureDelay,
2010
- alignmentStartTime: this.alignmentStartTime
2011
- });
2012
2201
  if (!this.hasScreenshotTaken) {
2013
2202
  const currentTime = Date.now();
2014
2203
  // Initialize alignment start time if not set
2015
2204
  if (!this.alignmentStartTime) {
2016
2205
  this.alignmentStartTime = currentTime;
2017
- this.logger.state('ALIGNMENT_TIMER_STARTED', { startTime: currentTime });
2018
2206
  }
2019
2207
  // Check if document has been aligned for the configured delay
2020
2208
  const alignmentDuration = currentTime - this.alignmentStartTime;
2021
- this.logger.state('ALIGNMENT_DURATION_CHECK', {
2022
- alignmentDuration,
2023
- captureDelay: this.captureDelay,
2024
- readyToCapture: alignmentDuration >= this.captureDelay
2025
- });
2026
2209
  if (alignmentDuration >= this.captureDelay) {
2027
- this.logger.state('TRIGGERING_CAPTURE', {
2028
- alignmentDuration,
2029
- captureDelay: this.captureDelay
2030
- });
2031
2210
  this.lastDetectedBox = bestBox;
2032
- this.takeScreenshot().catch(error => {
2033
- this.logger.error('Error al tomar captura de pantalla:', error);
2211
+ this.takeScreenshot().catch(() => {
2212
+ // Handle screenshot error silently
2034
2213
  });
2035
2214
  this.hasScreenshotTaken = true;
2036
2215
  this.alignmentStartTime = undefined;
@@ -2053,24 +2232,150 @@ const JaakStamps = class {
2053
2232
  }
2054
2233
  }
2055
2234
  }
2235
+ async takeManualScreenshot() {
2236
+ console.log('🔵 Botón clicked: Capturar (Frente/Reverso)');
2237
+ // Set processing state immediately
2238
+ const captureState = this.stateManager?.getCaptureState();
2239
+ if (captureState?.step === 'front') {
2240
+ this.processingButton = 'capture-front';
2241
+ }
2242
+ else if (captureState?.step === 'back') {
2243
+ this.processingButton = 'capture-back';
2244
+ }
2245
+ console.log('🔵 processingButton set to:', this.processingButton);
2246
+ // Add small delay to show spinner
2247
+ await new Promise(resolve => setTimeout(resolve, 100));
2248
+ if (!this.videoRef) {
2249
+ this.processingButton = null;
2250
+ return;
2251
+ }
2252
+ // When using manual capture, use mask coordinates for cropping
2253
+ // Note: processingButton will be cleared inside takeScreenshotWithMaskCoordinates
2254
+ await this.takeScreenshotWithMaskCoordinates();
2255
+ }
2256
+ async takeScreenshotWithMaskCoordinates() {
2257
+ if (!this.videoRef || !this.detectionContainer)
2258
+ return;
2259
+ this.stateManager.updateCaptureState({ isCapturing: true });
2260
+ this.triggerCaptureAnimation();
2261
+ // Use pooled canvas for optimization
2262
+ const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
2263
+ const captureCtx = captureCanvas.getContext('2d', { alpha: false });
2264
+ captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
2265
+ captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
2266
+ // Calculate mask coordinates for cropping
2267
+ const container = this.detectionContainer.parentElement;
2268
+ const containerRect = container.getBoundingClientRect();
2269
+ // Get mask dimensions from CSS properties
2270
+ const maskWidthPercent = parseFloat(this.el.style.getPropertyValue('--mask-width').replace('%', '')) || 90;
2271
+ const maskHeightPercent = parseFloat(this.el.style.getPropertyValue('--mask-height').replace('%', '')) || 56.25;
2272
+ const maskCenterXPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-x').replace('%', '')) || 50;
2273
+ const maskCenterYPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-y').replace('%', '')) || 50;
2274
+ // Convert percentages to actual video coordinates
2275
+ const videoWidth = this.videoRef.videoWidth;
2276
+ const videoHeight = this.videoRef.videoHeight;
2277
+ const videoAspectRatio = videoWidth / videoHeight;
2278
+ const containerAspectRatio = containerRect.width / containerRect.height;
2279
+ let displayedVideoWidth, displayedVideoHeight;
2280
+ let videoOffsetX = 0, videoOffsetY = 0;
2281
+ if (videoAspectRatio > containerAspectRatio) {
2282
+ displayedVideoWidth = containerRect.width;
2283
+ displayedVideoHeight = containerRect.width / videoAspectRatio;
2284
+ videoOffsetY = (containerRect.height - displayedVideoHeight) / 2;
2285
+ }
2286
+ else {
2287
+ displayedVideoHeight = containerRect.height;
2288
+ displayedVideoWidth = containerRect.height * videoAspectRatio;
2289
+ videoOffsetX = (containerRect.width - displayedVideoWidth) / 2;
2290
+ }
2291
+ // Calculate mask coordinates in video space
2292
+ const maskWidthInContainer = (maskWidthPercent / 100) * containerRect.width;
2293
+ const maskHeightInContainer = (maskHeightPercent / 100) * containerRect.height;
2294
+ const maskCenterXInContainer = (maskCenterXPercent / 100) * containerRect.width;
2295
+ const maskCenterYInContainer = (maskCenterYPercent / 100) * containerRect.height;
2296
+ // Convert to video coordinates
2297
+ const scaleX = videoWidth / displayedVideoWidth;
2298
+ const scaleY = videoHeight / displayedVideoHeight;
2299
+ const maskCenterXInVideo = (maskCenterXInContainer - videoOffsetX) * scaleX;
2300
+ const maskCenterYInVideo = (maskCenterYInContainer - videoOffsetY) * scaleY;
2301
+ const maskWidthInVideo = maskWidthInContainer * scaleX;
2302
+ const maskHeightInVideo = maskHeightInContainer * scaleY;
2303
+ // Calculate crop coordinates
2304
+ const cropX = Math.max(0, maskCenterXInVideo - (maskWidthInVideo / 2) - this.cropMargin);
2305
+ const cropY = Math.max(0, maskCenterYInVideo - (maskHeightInVideo / 2) - this.cropMargin);
2306
+ const cropWidth = Math.min(maskWidthInVideo + (2 * this.cropMargin), videoWidth - cropX);
2307
+ const cropHeight = Math.min(maskHeightInVideo + (2 * this.cropMargin), videoHeight - cropY);
2308
+ // Use pooled canvas for cropped version
2309
+ const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
2310
+ const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
2311
+ croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
2312
+ croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
2313
+ const captureState = this.stateManager.getCaptureState();
2314
+ if (captureState.step === 'front') {
2315
+ this.stateManager.setCapturedImages({
2316
+ front: {
2317
+ fullFrame: captureCanvas.toDataURL('image/png'),
2318
+ cropped: croppedCanvas.toDataURL('image/png')
2319
+ }
2320
+ });
2321
+ // Check if document classification is enabled (independent of detector)
2322
+ if (this.useDocumentClassification) {
2323
+ try {
2324
+ const classification = await this.detectionService.classifyDocument(croppedCanvas);
2325
+ if (classification && classification.class === 'passport') {
2326
+ this.completeProcess(true);
2327
+ this.returnCanvasToPool(captureCanvas);
2328
+ this.returnCanvasToPool(croppedCanvas);
2329
+ // Clear processing button when passport is detected
2330
+ this.processingButton = null;
2331
+ return;
2332
+ }
2333
+ }
2334
+ catch (classifyError) {
2335
+ // No cambiar a manual aquí, solo registrar el error y continuar
2336
+ }
2337
+ }
2338
+ this.stateManager.updateCaptureState({
2339
+ step: 'back',
2340
+ isDetectionPaused: true,
2341
+ showFlipAnimation: true
2342
+ });
2343
+ this.triggerRerender();
2344
+ setTimeout(() => {
2345
+ this.stateManager.updateCaptureState({
2346
+ showFlipAnimation: false,
2347
+ isDetectionPaused: false
2348
+ });
2349
+ this.triggerRerender();
2350
+ this.startBackDocumentTimer();
2351
+ // Clear processing button after animation completes
2352
+ this.processingButton = null;
2353
+ }, 3000);
2354
+ }
2355
+ else if (captureState.step === 'back') {
2356
+ this.stateManager.setCapturedImages({
2357
+ back: {
2358
+ fullFrame: captureCanvas.toDataURL('image/png'),
2359
+ cropped: croppedCanvas.toDataURL('image/png')
2360
+ }
2361
+ });
2362
+ this.completeProcess(false);
2363
+ // Clear processing button after back capture completes
2364
+ this.processingButton = null;
2365
+ }
2366
+ // Return canvases to pool after use
2367
+ this.returnCanvasToPool(captureCanvas);
2368
+ this.returnCanvasToPool(croppedCanvas);
2369
+ }
2056
2370
  async takeScreenshot() {
2057
2371
  if (!this.videoRef || !this.lastDetectedBox)
2058
2372
  return;
2059
- this.logger.state('INICIANDO_CAPTURA', {
2060
- captureStep: this.stateManager.getCaptureState().step,
2061
- detectedBox: this.lastDetectedBox,
2062
- videoResolution: {
2063
- width: this.videoRef.videoWidth,
2064
- height: this.videoRef.videoHeight
2065
- }
2066
- });
2067
2373
  this.stateManager.updateCaptureState({ isCapturing: true });
2068
2374
  this.triggerCaptureAnimation();
2069
- // Create capture canvas
2070
- const captureCanvas = document.createElement('canvas');
2071
- captureCanvas.width = this.videoRef.videoWidth;
2072
- captureCanvas.height = this.videoRef.videoHeight;
2375
+ // Use pooled canvas for optimization
2376
+ const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
2073
2377
  const captureCtx = captureCanvas.getContext('2d', { alpha: false });
2378
+ captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
2074
2379
  captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
2075
2380
  // Calculate crop coordinates
2076
2381
  const INPUT_SIZE = 320;
@@ -2080,11 +2385,10 @@ const JaakStamps = class {
2080
2385
  const cropY = Math.max(0, (this.lastDetectedBox.y * scaleY) - this.cropMargin);
2081
2386
  const cropWidth = Math.min((this.lastDetectedBox.w * scaleX) + (2 * this.cropMargin), this.videoRef.videoWidth - cropX);
2082
2387
  const cropHeight = Math.min((this.lastDetectedBox.h * scaleY) + (2 * this.cropMargin), this.videoRef.videoHeight - cropY);
2083
- // Create cropped version
2084
- const croppedCanvas = document.createElement('canvas');
2085
- croppedCanvas.width = cropWidth;
2086
- croppedCanvas.height = cropHeight;
2388
+ // Use pooled canvas for cropped version
2389
+ const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
2087
2390
  const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
2391
+ croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
2088
2392
  croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
2089
2393
  const captureState = this.stateManager.getCaptureState();
2090
2394
  if (captureState.step === 'front') {
@@ -2096,11 +2400,15 @@ const JaakStamps = class {
2096
2400
  });
2097
2401
  // Check if document classification is enabled
2098
2402
  if (this.useDocumentClassification) {
2099
- const classification = await this.detectionService.classifyDocument(croppedCanvas);
2100
- if (classification && classification.class === 'passport') {
2101
- this.logger.state('PASAPORTE_DETECTADO_SALTANDO_REVERSO', { classification: classification?.class });
2102
- this.completeProcess(true);
2103
- return;
2403
+ try {
2404
+ const classification = await this.detectionService.classifyDocument(croppedCanvas);
2405
+ if (classification && classification.class === 'passport') {
2406
+ this.completeProcess(true);
2407
+ return;
2408
+ }
2409
+ }
2410
+ catch (classifyError) {
2411
+ // No cambiar a manual aquí, solo registrar el error y continuar
2104
2412
  }
2105
2413
  }
2106
2414
  this.stateManager.updateCaptureState({
@@ -2125,6 +2433,9 @@ const JaakStamps = class {
2125
2433
  });
2126
2434
  this.completeProcess(false);
2127
2435
  }
2436
+ // Return canvases to pool after use
2437
+ this.returnCanvasToPool(captureCanvas);
2438
+ this.returnCanvasToPool(croppedCanvas);
2128
2439
  }
2129
2440
  triggerCaptureAnimation() {
2130
2441
  const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
@@ -2134,6 +2445,135 @@ const JaakStamps = class {
2134
2445
  cardOutline?.classList.remove('capturing');
2135
2446
  }, 600);
2136
2447
  }
2448
+ // Método simplificado para cambio a modo manual
2449
+ switchToManualMode() {
2450
+ // Only switch if detector is enabled and not already switched
2451
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
2452
+ return;
2453
+ }
2454
+ // Detener todo el monitoreo de performance
2455
+ this.stopPerformanceMonitoring();
2456
+ // Liberar recursos de ONNX
2457
+ this.releaseOnnxResources();
2458
+ // Resetear máscara a blanco y limpiar detecciones
2459
+ this.resetMaskToWhite();
2460
+ this.detectionBoxes = [];
2461
+ this.hasAutoSwitchedToManual = true;
2462
+ this.performanceDegradedMode = true;
2463
+ this.showManualCaptureButton = true;
2464
+ this.showPerformanceMessage = true;
2465
+ // Hide performance message after 5 seconds
2466
+ setTimeout(() => {
2467
+ this.showPerformanceMessage = false;
2468
+ }, 5000);
2469
+ }
2470
+ // Método para cambiar a modo manual por errores de ONNX
2471
+ switchToManualModeWithWarning(warningMessage) {
2472
+ // Solo cambiar si el detector está habilitado y no se ha cambiado ya
2473
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
2474
+ return;
2475
+ }
2476
+ // No necesitamos liberar recursos ONNX porque no se han cargado aún
2477
+ // Resetear máscara a blanco y limpiar detecciones
2478
+ this.resetMaskToWhite();
2479
+ this.detectionBoxes = [];
2480
+ // Cambiar a modo manual
2481
+ this.hasAutoSwitchedToManual = true;
2482
+ this.performanceDegradedMode = true;
2483
+ this.showManualCaptureButton = true;
2484
+ this.useDocumentDetector = false; // Desactivar detector automático
2485
+ // Actualizar el estado con mensaje informativo (no error)
2486
+ this.updateStatus('Modo manual activado', warningMessage, 'active');
2487
+ // Continuar con la configuración de cámara
2488
+ this.continueWithCameraSetup();
2489
+ }
2490
+ async continueWithCameraSetup() {
2491
+ try {
2492
+ // Paso 2: Detectar y configurar dispositivos
2493
+ this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
2494
+ try {
2495
+ await this.cameraService.enumerateDevices();
2496
+ }
2497
+ catch (enumerateError) {
2498
+ throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
2499
+ }
2500
+ try {
2501
+ await this.updateCameraInfoWithAutofocus();
2502
+ }
2503
+ catch (cameraInfoError) {
2504
+ throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
2505
+ }
2506
+ // Paso 3: Configurar cámara seleccionada
2507
+ this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
2508
+ let stream;
2509
+ try {
2510
+ stream = await this.cameraService.setupCamera();
2511
+ }
2512
+ catch (setupError) {
2513
+ throw new Error(`Error al configurar cámara: ${setupError.message}`);
2514
+ }
2515
+ // Paso 4: Inicializar video
2516
+ this.updateStatus('Captura manual', 'Posicione su documento y use el botón para capturar', 'active');
2517
+ try {
2518
+ await this.initializeVideoStream(stream);
2519
+ }
2520
+ catch (videoError) {
2521
+ throw new Error(`Error al inicializar video: ${videoError.message}`);
2522
+ }
2523
+ // Paso 5: Calibrar máscara
2524
+ if (this.detectionContainer) {
2525
+ const container = this.detectionContainer.parentElement;
2526
+ const rect = container.getBoundingClientRect();
2527
+ this.updateMaskDimensions(rect);
2528
+ }
2529
+ // Finalizar
2530
+ this.startTime = Date.now();
2531
+ this.stateManager.updateCaptureState({ isLoading: false });
2532
+ // Mostrar botón manual (ya debe estar habilitado)
2533
+ this.showManualCaptureButton = true;
2534
+ }
2535
+ catch (error) {
2536
+ this.updateStatus('Error de cámara', error.message, 'error');
2537
+ throw error;
2538
+ }
2539
+ }
2540
+ switchToManualModeWithError(errorMessage) {
2541
+ // Solo cambiar si el detector está habilitado y no se ha cambiado ya
2542
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
2543
+ return;
2544
+ }
2545
+ // Liberar recursos de ONNX inmediatamente
2546
+ this.releaseOnnxResources();
2547
+ // Resetear máscara a blanco y limpiar detecciones
2548
+ this.resetMaskToWhite();
2549
+ this.detectionBoxes = [];
2550
+ // Cambiar a modo manual
2551
+ this.hasAutoSwitchedToManual = true;
2552
+ this.performanceDegradedMode = true;
2553
+ this.showManualCaptureButton = true;
2554
+ this.useDocumentDetector = false; // Desactivar detector automático
2555
+ // Actualizar el estado con mensaje de error
2556
+ this.updateStatus('Modo manual activado', errorMessage, 'error');
2557
+ // Ocultar mensaje después de 5 segundos
2558
+ setTimeout(() => {
2559
+ this.updateStatus('Captura manual', 'Use el botón para capturar', 'ready');
2560
+ }, 5000);
2561
+ }
2562
+ stopPerformanceMonitoring() {
2563
+ // Nota: No necesitamos limpiar más variables porque ya no se usarán
2564
+ // El sistema automáticamente saltará la lógica de performance una vez que hasAutoSwitchedToManual = true
2565
+ }
2566
+ // Método para liberar recursos de ONNX cuando se cambia a modo manual
2567
+ releaseOnnxResources() {
2568
+ try {
2569
+ // Liberar recursos del servicio de detección
2570
+ if (this.detectionService) {
2571
+ this.detectionService.cleanup();
2572
+ }
2573
+ }
2574
+ catch (error) {
2575
+ }
2576
+ }
2137
2577
  completeProcess(skippedBack = false) {
2138
2578
  this.stateManager.updateCaptureState({
2139
2579
  step: 'completed',
@@ -2153,11 +2593,6 @@ const JaakStamps = class {
2153
2593
  setTimeout(() => {
2154
2594
  this.stateManager.updateCaptureState({ showSuccessAnimation: false });
2155
2595
  }, 3000);
2156
- this.logger.state('PROCESO_COMPLETADO', {
2157
- skippedBack,
2158
- totalImages: capturedImages.metadata.totalImages,
2159
- timestamp: new Date().toISOString()
2160
- });
2161
2596
  }
2162
2597
  stopDetection() {
2163
2598
  if (this.animationId) {
@@ -2166,7 +2601,6 @@ const JaakStamps = class {
2166
2601
  }
2167
2602
  this.clearBackDocumentTimer();
2168
2603
  this.detectionBoxes = [];
2169
- this.logger.state('DETECTOR_DETENIDO', { timestamp: Date.now() });
2170
2604
  }
2171
2605
  startBackDocumentTimer() {
2172
2606
  if (!this.enableBackDocumentTimer)
@@ -2195,6 +2629,15 @@ const JaakStamps = class {
2195
2629
  return; // Don't toggle if switching camera
2196
2630
  this.showCameraSelector = !this.showCameraSelector;
2197
2631
  }
2632
+ async updateCameraInfoWithAutofocus() {
2633
+ try {
2634
+ const cameraInfo = await this.cameraService.getCameraInfo();
2635
+ this.cameraInfoWithAutofocus = cameraInfo;
2636
+ }
2637
+ catch (error) {
2638
+ // Keep existing state if update fails
2639
+ }
2640
+ }
2198
2641
  async handleCameraSwitch(cameraId) {
2199
2642
  if (this.isSwitchingCamera)
2200
2643
  return; // Prevent multiple simultaneous switches
@@ -2202,10 +2645,6 @@ const JaakStamps = class {
2202
2645
  // Close the selector immediately when user selects a camera
2203
2646
  this.showCameraSelector = false;
2204
2647
  this.isSwitchingCamera = true;
2205
- this.logger.state('INICIANDO_CAMBIO_CAMARA', {
2206
- from: this.cameraService.getSelectedCameraId(),
2207
- to: cameraId
2208
- });
2209
2648
  // Stop current video stream
2210
2649
  if (this.videoStream) {
2211
2650
  this.videoStream.getTracks().forEach(track => track.stop());
@@ -2216,21 +2655,17 @@ const JaakStamps = class {
2216
2655
  const newStream = await this.cameraService.setupCamera();
2217
2656
  // Update video element
2218
2657
  await this.initializeVideoStream(newStream);
2219
- this.logger.state('CAMBIO_CAMARA_EXITOSO', {
2220
- newCameraId: cameraId,
2221
- isRearCamera: this.cameraService.isRearCamera(newStream)
2222
- });
2658
+ // Update camera info with autofocus data
2659
+ await this.updateCameraInfoWithAutofocus();
2223
2660
  }
2224
2661
  catch (error) {
2225
- this.logger.error('Error al cambiar cámara:', error);
2226
2662
  // Try to restore previous camera if switch failed
2227
2663
  try {
2228
2664
  const fallbackStream = await this.cameraService.setupCamera();
2229
2665
  await this.initializeVideoStream(fallbackStream);
2230
2666
  }
2231
2667
  catch (fallbackError) {
2232
- this.logger.error('Error al restaurar cámara anterior:', fallbackError);
2233
- this.updateStatus('Error al cambiar cámara', 'No se pudo completar el cambio de dispositivo', 'error');
2668
+ this.updateStatus('Error al cambiar cámara', 'No se pudo cambiar el dispositivo', 'error');
2234
2669
  }
2235
2670
  }
2236
2671
  finally {
@@ -2252,6 +2687,13 @@ const JaakStamps = class {
2252
2687
  this.detectionBoxes = [];
2253
2688
  this.alignmentStartTime = undefined;
2254
2689
  this.hasDocumentDetected = false;
2690
+ // Reset sistema simplificado
2691
+ this.processedFramesCount = 0;
2692
+ this.slowFrameCount = 0;
2693
+ // Reset performance degradation state
2694
+ this.hasAutoSwitchedToManual = false;
2695
+ this.performanceDegradedMode = false;
2696
+ this.showPerformanceMessage = false;
2255
2697
  if (this.alignmentTimer) {
2256
2698
  clearTimeout(this.alignmentTimer);
2257
2699
  this.alignmentTimer = undefined;
@@ -2271,7 +2713,7 @@ const JaakStamps = class {
2271
2713
  this.stateManager.updateCaptureState({ isVideoActive: false, isLoading: false });
2272
2714
  }
2273
2715
  this.isMaskReady = false;
2274
- this.updateStatus('Sesión finalizada', '', 'ready');
2716
+ this.updateStatus('Captura finalizada', '', 'ready');
2275
2717
  this.detectionBoxes = [];
2276
2718
  this.cleanup();
2277
2719
  }
@@ -2282,7 +2724,6 @@ const JaakStamps = class {
2282
2724
  this.performanceUpdateInterval = window.setInterval(() => {
2283
2725
  this.updatePerformanceMetrics();
2284
2726
  }, 500);
2285
- this.logger.debug('Monitor de performance inicializado');
2286
2727
  }
2287
2728
  updatePerformanceMetrics() {
2288
2729
  const currentTime = performance.now();
@@ -2325,6 +2766,84 @@ const JaakStamps = class {
2325
2766
  if (detectionsFound > 0) {
2326
2767
  this.performanceMetrics.successfulDetections++;
2327
2768
  }
2769
+ // Update performance history for adaptive frame skipping
2770
+ this.performanceHistory.push(processingTime);
2771
+ if (this.performanceHistory.length > this.PERFORMANCE_HISTORY_SIZE) {
2772
+ this.performanceHistory.shift();
2773
+ }
2774
+ }
2775
+ // Adaptive frame skipping based on performance
2776
+ getAdaptiveFrameSkip() {
2777
+ // Base conditions for high frame skip
2778
+ if (this.consecutiveFailures > 20)
2779
+ return this.MAX_FRAME_SKIP;
2780
+ if (this.performanceMetrics.inferenceTime > 200)
2781
+ return 6;
2782
+ if (this.performanceMetrics.memoryUsage > 200)
2783
+ return 5;
2784
+ // Calculate average processing time
2785
+ if (this.performanceHistory.length > 0) {
2786
+ const avgProcessingTime = this.performanceHistory.reduce((a, b) => a + b, 0) / this.performanceHistory.length;
2787
+ if (avgProcessingTime > 100)
2788
+ return 4;
2789
+ if (avgProcessingTime > 80)
2790
+ return 3;
2791
+ if (avgProcessingTime > 60)
2792
+ return this.BASE_FRAME_SKIP + 1;
2793
+ }
2794
+ // Low memory devices get higher frame skip
2795
+ if (this.performanceMetrics.memoryUsage > 150)
2796
+ return 4;
2797
+ // Performance is good, use base frame skip
2798
+ return this.BASE_FRAME_SKIP;
2799
+ }
2800
+ // Método para resetear la máscara a color blanco
2801
+ resetMaskToWhite() {
2802
+ const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
2803
+ const corners = this.el.shadowRoot?.querySelectorAll('.corner');
2804
+ const topSide = this.el.shadowRoot?.querySelector('.side-top');
2805
+ const rightSide = this.el.shadowRoot?.querySelector('.side-right');
2806
+ const bottomSide = this.el.shadowRoot?.querySelector('.side-bottom');
2807
+ const leftSide = this.el.shadowRoot?.querySelector('.side-left');
2808
+ // Remover todas las clases de alineación y estado perfecto
2809
+ cardOutline?.classList.remove('perfect-match');
2810
+ corners?.forEach(corner => corner.classList.remove('perfect-match'));
2811
+ topSide?.classList.remove('aligned');
2812
+ rightSide?.classList.remove('aligned');
2813
+ bottomSide?.classList.remove('aligned');
2814
+ leftSide?.classList.remove('aligned');
2815
+ // Resetear estado de alineación
2816
+ this.sideAlignment = { top: false, right: false, bottom: false, left: false };
2817
+ }
2818
+ // Canvas pool management for screenshots
2819
+ getPooledCanvas(width, height) {
2820
+ // Try to find a canvas with matching dimensions
2821
+ const matchingIndex = this.canvasPool.findIndex(canvas => canvas.width === width && canvas.height === height);
2822
+ if (matchingIndex !== -1) {
2823
+ return this.canvasPool.splice(matchingIndex, 1)[0];
2824
+ }
2825
+ // If no matching canvas, try to reuse any canvas and resize
2826
+ if (this.canvasPool.length > 0) {
2827
+ const canvas = this.canvasPool.pop();
2828
+ canvas.width = width;
2829
+ canvas.height = height;
2830
+ return canvas;
2831
+ }
2832
+ // Create new canvas if pool is empty
2833
+ const canvas = document.createElement('canvas');
2834
+ canvas.width = width;
2835
+ canvas.height = height;
2836
+ return canvas;
2837
+ }
2838
+ returnCanvasToPool(canvas) {
2839
+ // Only return to pool if not at max capacity
2840
+ if (this.canvasPool.length < this.MAX_CANVAS_POOL_SIZE) {
2841
+ // Clear the canvas before returning to pool
2842
+ const ctx = canvas.getContext('2d');
2843
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
2844
+ this.canvasPool.push(canvas);
2845
+ }
2846
+ // If pool is full, let the canvas be garbage collected
2328
2847
  }
2329
2848
  };
2330
2849
  JaakStamps.style = myComponentCss;