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