@jaak.ai/stamps 2.0.1 → 2.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +1273 -663
  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 +225 -14
  15. package/dist/collection/components/my-component/my-component.js +964 -286
  16. package/dist/collection/components/my-component/my-component.js.map +1 -1
  17. package/dist/collection/services/CameraService.js +236 -342
  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 +1281 -666
  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 +1273 -663
  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-DQuL1Twl.js +2 -0
  46. package/dist/jaak-stamps-webcomponent/p-DQuL1Twl.js.map +1 -0
  47. package/dist/jaak-stamps-webcomponent/p-Drbzcuq-.js +3 -0
  48. package/dist/jaak-stamps-webcomponent/p-Drbzcuq-.js.map +1 -0
  49. package/dist/jaak-stamps-webcomponent/p-c06e5d7b.entry.js +2 -0
  50. package/dist/jaak-stamps-webcomponent/p-c06e5d7b.entry.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 +13 -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 +6 -4
  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') {
@@ -239,25 +206,31 @@ class CameraService {
239
206
  tempStream.getTracks().forEach(track => track.stop());
240
207
  }
241
208
  const devices = await navigator.mediaDevices.enumerateDevices();
242
- 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
209
+ const allCameras = devices.filter(device => device.kind === 'videoinput');
210
+ // Filter out virtual cameras
211
+ this.availableCameras = allCameras.filter(camera => {
212
+ const isVirtual = this.isVirtualCamera(camera);
213
+ if (isVirtual) {
214
+ console.log(`Virtual camera detected and filtered: ${camera.label}`);
215
+ }
216
+ return !isVirtual;
255
217
  });
218
+ // Check if there are physical cameras available
219
+ if (this.availableCameras.length === 0) {
220
+ console.log('No physical cameras available');
221
+ this.eventBus.emit('error', new Error('Cámaras virtuales no están permitidas. Use una cámara física.'));
222
+ return [];
223
+ }
224
+ // Cache the results for optimization
225
+ CameraService.deviceEnumerationCache = {
226
+ devices: this.availableCameras,
227
+ timestamp: now
228
+ };
229
+ await this.setInitialCameraPreference();
256
230
  this.eventBus.emit('camera-changed', this.selectedCameraId);
257
231
  return this.availableCameras;
258
232
  }
259
233
  catch (error) {
260
- this.logger.error('Error al enumerar cámaras disponibles:', error);
261
234
  this.handleCameraPermissionError(error);
262
235
  return [];
263
236
  }
@@ -278,8 +251,6 @@ class CameraService {
278
251
  }
279
252
  this.selectedCameraId = cameraId;
280
253
  this.updatePreferredFacing(camera);
281
- this.isManuallySelected = true; // Mark as manually selected
282
- this.savePreference();
283
254
  this.eventBus.emit('camera-changed', cameraId);
284
255
  }
285
256
  getPreferredFacing() {
@@ -288,14 +259,35 @@ class CameraService {
288
259
  async setupCamera(constraints) {
289
260
  try {
290
261
  const finalConstraints = constraints || await this.getMaxResolution();
291
- const stream = await navigator.mediaDevices.getUserMedia({
262
+ // Optimization: Check if we can reuse existing stream with same constraints
263
+ if (this.currentStreamPromise && this.constraintsEqual(finalConstraints, this.lastStreamConstraints)) {
264
+ return await this.currentStreamPromise;
265
+ }
266
+ // Create new stream promise for lazy loading
267
+ this.currentStreamPromise = navigator.mediaDevices.getUserMedia({
292
268
  video: finalConstraints,
293
269
  audio: false
294
270
  });
271
+ this.lastStreamConstraints = finalConstraints;
272
+ const stream = await this.currentStreamPromise;
273
+ // Validate that active stream is not from a virtual camera
274
+ const videoTrack = stream.getVideoTracks()[0];
275
+ if (videoTrack) {
276
+ const settings = videoTrack.getSettings();
277
+ const deviceId = settings.deviceId;
278
+ if (deviceId) {
279
+ const devices = await navigator.mediaDevices.enumerateDevices();
280
+ const currentDevice = devices.find(device => device.deviceId === deviceId);
281
+ if (currentDevice && this.isVirtualCamera(currentDevice)) {
282
+ console.log(`Virtual camera detected in active stream: ${currentDevice.label}`);
283
+ stream.getTracks().forEach(track => track.stop());
284
+ throw new Error('Cámaras virtuales no están permitidas. Use una cámara física.');
285
+ }
286
+ }
287
+ }
295
288
  return stream;
296
289
  }
297
290
  catch (error) {
298
- this.logger.error('Error en setupCamera, intentando con restricciones básicas:', error);
299
291
  // Fallback to basic constraints if getMaxResolution fails
300
292
  const basicConstraints = {
301
293
  width: { ideal: 1280 },
@@ -305,65 +297,43 @@ class CameraService {
305
297
  if (this.deviceType !== 'desktop' && this.preferredCameraFacing) {
306
298
  basicConstraints.facingMode = this.preferredCameraFacing;
307
299
  }
308
- this.logger.state('USANDO_RESTRICCIONES_BASICAS', { constraints: basicConstraints });
309
- return await navigator.mediaDevices.getUserMedia({
300
+ this.currentStreamPromise = navigator.mediaDevices.getUserMedia({
310
301
  video: basicConstraints,
311
302
  audio: false
312
303
  });
304
+ this.lastStreamConstraints = basicConstraints;
305
+ const stream = await this.currentStreamPromise;
306
+ // Validate fallback stream as well
307
+ const videoTrack = stream.getVideoTracks()[0];
308
+ if (videoTrack) {
309
+ const settings = videoTrack.getSettings();
310
+ const deviceId = settings.deviceId;
311
+ if (deviceId) {
312
+ const devices = await navigator.mediaDevices.enumerateDevices();
313
+ const currentDevice = devices.find(device => device.deviceId === deviceId);
314
+ if (currentDevice && this.isVirtualCamera(currentDevice)) {
315
+ console.log(`Virtual camera detected in fallback stream: ${currentDevice.label}`);
316
+ stream.getTracks().forEach(track => track.stop());
317
+ throw new Error('Cámaras virtuales no están permitidas. Use una cámara física.');
318
+ }
319
+ }
320
+ }
321
+ return stream;
313
322
  }
314
323
  }
315
324
  async switchCamera(cameraId) {
316
325
  const selectedCamera = this.availableCameras.find(cam => cam.deviceId === cameraId);
317
326
  if (!selectedCamera) {
318
- this.logger.warn('Cámara seleccionada no encontrada, re-enumerando dispositivos...');
319
327
  await this.enumerateDevices();
320
328
  return;
321
329
  }
322
- 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())
330
+ // Validate that the target camera is not virtual
331
+ if (this.isVirtualCamera(selectedCamera)) {
332
+ console.log(`Attempted to switch to virtual camera: ${selectedCamera.label}`);
333
+ this.eventBus.emit('error', new Error('No se puede cambiar a cámara virtual'));
331
334
  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
335
  }
336
+ await this.setSelectedCamera(cameraId);
367
337
  }
368
338
  isRearCamera(stream) {
369
339
  const videoTrack = stream.getVideoTracks()[0];
@@ -372,13 +342,18 @@ class CameraService {
372
342
  const settings = videoTrack.getSettings();
373
343
  return settings.facingMode === 'environment';
374
344
  }
375
- getCameraInfo() {
376
- return {
377
- availableCameras: this.availableCameras.map(camera => ({
345
+ async getCameraInfo() {
346
+ const availableCamerasWithAutofocus = await Promise.all(this.availableCameras.map(async (camera) => {
347
+ const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
348
+ return {
378
349
  id: camera.deviceId,
379
350
  label: camera.label || 'Unknown Camera',
380
- selected: camera.deviceId === this.selectedCameraId
381
- })),
351
+ selected: camera.deviceId === this.selectedCameraId,
352
+ hasAutofocus
353
+ };
354
+ }));
355
+ return {
356
+ availableCameras: availableCamerasWithAutofocus,
382
357
  selectedCameraId: this.selectedCameraId,
383
358
  deviceType: this.deviceType,
384
359
  isMultipleCamerasAvailable: this.isMultipleCamerasAvailable(),
@@ -394,7 +369,6 @@ class CameraService {
394
369
  return permission.state;
395
370
  }
396
371
  catch (error) {
397
- this.logger.warn('No se pudo verificar permisos de cámara:', error);
398
372
  return 'prompt';
399
373
  }
400
374
  }
@@ -402,35 +376,48 @@ class CameraService {
402
376
  this.availableCameras = [];
403
377
  this.eventBus.emit('error', new Error(`Camera permission error: ${error.message}`));
404
378
  }
405
- setInitialCameraPreference() {
379
+ async setInitialCameraPreference() {
406
380
  if (this.availableCameras.length === 0)
407
381
  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
382
  if (this.preferredCamera === 'front') {
416
- this.selectFrontCamera();
383
+ await this.selectFrontCamera();
417
384
  }
418
385
  else if (this.preferredCamera === 'back') {
419
- this.selectBackCamera();
386
+ await this.selectBackCamera();
420
387
  }
421
388
  else {
422
- this.selectAutoCamera();
389
+ await this.selectAutoCamera();
390
+ }
391
+ }
392
+ async checkCameraAutofocus(deviceId) {
393
+ try {
394
+ // Check cache first for optimization
395
+ if (CameraService.cameraCapabilitiesCache.has(deviceId)) {
396
+ const cached = CameraService.cameraCapabilitiesCache.get(deviceId);
397
+ return typeof cached === 'boolean' ? cached : false;
398
+ }
399
+ const stream = await navigator.mediaDevices.getUserMedia({
400
+ video: { deviceId: { exact: deviceId } }
401
+ });
402
+ const videoTrack = stream.getVideoTracks()[0];
403
+ const capabilities = videoTrack.getCapabilities();
404
+ stream.getTracks().forEach(track => track.stop());
405
+ const hasFocusMode = capabilities.focusMode && Array.isArray(capabilities.focusMode) && capabilities.focusMode.length > 0;
406
+ const hasAutofocus = hasFocusMode && (capabilities.focusMode.includes('continuous') ||
407
+ capabilities.focusMode.includes('auto'));
408
+ // Cache the result for future use
409
+ CameraService.cameraCapabilitiesCache.set(deviceId, hasAutofocus);
410
+ return hasAutofocus;
411
+ }
412
+ catch (error) {
413
+ // Cache negative result to avoid repeated failures
414
+ CameraService.cameraCapabilitiesCache.set(deviceId, false);
415
+ return false;
423
416
  }
424
417
  }
425
- selectFrontCamera() {
418
+ async selectFrontCamera() {
426
419
  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 => {
420
+ const frontCameras = this.availableCameras.filter(camera => {
434
421
  const label = camera.label.toLowerCase();
435
422
  return label.includes('front') ||
436
423
  label.includes('user') ||
@@ -438,24 +425,24 @@ class CameraService {
438
425
  label.includes('frontal') ||
439
426
  (!label.includes('back') && !label.includes('rear') && !label.includes('environment'));
440
427
  });
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
- });
428
+ let selectedCamera = null;
429
+ if (frontCameras.length > 0) {
430
+ for (const camera of frontCameras) {
431
+ const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
432
+ if (hasAutofocus) {
433
+ selectedCamera = camera;
434
+ break;
435
+ }
436
+ }
437
+ if (!selectedCamera) {
438
+ selectedCamera = frontCameras[0];
439
+ }
440
+ }
441
+ this.selectedCameraId = selectedCamera ? selectedCamera.deviceId : this.availableCameras[0].deviceId;
448
442
  }
449
- selectBackCamera() {
443
+ async selectBackCamera() {
450
444
  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 => {
445
+ const backCameras = this.availableCameras.filter(camera => {
459
446
  const label = camera.label.toLowerCase();
460
447
  return label.includes('back') ||
461
448
  label.includes('rear') ||
@@ -463,34 +450,33 @@ class CameraService {
463
450
  label.includes('trasera') ||
464
451
  label.includes('posterior');
465
452
  });
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
- });
453
+ let selectedCamera = null;
454
+ if (backCameras.length > 0) {
455
+ for (const camera of backCameras) {
456
+ const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
457
+ if (hasAutofocus) {
458
+ selectedCamera = camera;
459
+ break;
460
+ }
461
+ }
462
+ if (!selectedCamera) {
463
+ selectedCamera = backCameras[0];
464
+ }
473
465
  }
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
- });
466
+ else if (this.availableCameras.length > 1) {
467
+ const lastCamera = this.availableCameras[this.availableCameras.length - 1];
468
+ const hasAutofocus = await this.checkCameraAutofocus(lastCamera.deviceId);
469
+ selectedCamera = hasAutofocus ? lastCamera : this.availableCameras[this.availableCameras.length - 1];
470
+ }
471
+ this.selectedCameraId = selectedCamera ? selectedCamera.deviceId : this.availableCameras[0].deviceId;
480
472
  }
481
- selectAutoCamera() {
473
+ async selectAutoCamera() {
482
474
  if (this.deviceType === 'mobile' || this.deviceType === 'tablet') {
483
- this.selectBackCamera();
484
- this.logger.state('CAMARA_AUTO_SELECCIONADA_MOBILE', { type: 'rear' });
475
+ await this.selectBackCamera();
485
476
  }
486
477
  else {
487
478
  // 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
- });
479
+ await this.selectFrontCamera();
494
480
  }
495
481
  }
496
482
  updatePreferredFacing(camera) {
@@ -506,40 +492,19 @@ class CameraService {
506
492
  await this.detectDeviceType();
507
493
  }
508
494
  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
495
+ // Priority 1: Always use deviceId if available (ensures specific camera with autofocus)
496
+ if (this.selectedCameraId) {
518
497
  videoConstraints.deviceId = { exact: this.selectedCameraId };
519
- this.logger.state('USANDO_CAMARA_MANUAL', {
520
- deviceId: this.selectedCameraId
521
- });
522
498
  }
499
+ // Priority 2: Use facingMode as fallback when no specific camera is selected
523
500
  else if (this.preferredCameraFacing) {
524
- // Use facingMode for initial preference-based selection
525
- // Use 'ideal' instead of 'exact' to avoid OverconstrainedError on desktop
526
501
  const facingConstraint = this.deviceType === 'desktop'
527
502
  ? { ideal: this.preferredCameraFacing }
528
503
  : { exact: this.preferredCameraFacing };
529
504
  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
505
  }
506
+ // Priority 3: Fallback to preferredCamera setting
541
507
  else {
542
- // Use preferredCamera setting to determine default facing mode
543
508
  if (this.preferredCamera === 'front') {
544
509
  videoConstraints.facingMode = "user";
545
510
  }
@@ -547,24 +512,25 @@ class CameraService {
547
512
  videoConstraints.facingMode = "environment";
548
513
  }
549
514
  else {
550
- // Auto mode: use environment for mobile/tablet, user for desktop
551
515
  videoConstraints.facingMode = (this.deviceType === 'mobile' || this.deviceType === 'tablet') ? "environment" : "user";
552
516
  }
553
- this.logger.state('USANDO_PREFERENCIA_CONFIGURADA', {
554
- facingMode: videoConstraints.facingMode,
555
- preferredCamera: this.preferredCamera,
556
- deviceType: this.deviceType
557
- });
558
517
  }
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());
518
+ // Optimization: Use cached capabilities if available
519
+ let capabilities;
520
+ if (this.selectedCameraId && CameraService.cameraCapabilitiesCache.has(this.selectedCameraId + '_caps')) {
521
+ capabilities = JSON.parse(CameraService.cameraCapabilitiesCache.get(this.selectedCameraId + '_caps'));
522
+ }
523
+ else {
524
+ const tempStream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints });
525
+ const videoTrack = tempStream.getVideoTracks()[0];
526
+ capabilities = videoTrack.getCapabilities();
527
+ tempStream.getTracks().forEach(track => track.stop());
528
+ // Cache capabilities for future use
529
+ if (this.selectedCameraId) {
530
+ CameraService.cameraCapabilitiesCache.set(this.selectedCameraId + '_caps', JSON.stringify(capabilities));
531
+ }
532
+ }
565
533
  const constraints = { ...videoConstraints };
566
- // Enhanced focus and image quality settings
567
- this.applyAdvancedCameraSettings(constraints, capabilities);
568
534
  if (capabilities.width && capabilities.height) {
569
535
  const maxWidth = Math.min(capabilities.width.max, 1920);
570
536
  const maxHeight = Math.min(capabilities.height.max, 1080);
@@ -578,15 +544,20 @@ class CameraService {
578
544
  constraints.height = { ideal: maxHeight };
579
545
  }
580
546
  }
581
- this.logger.state('CONFIGURACION_CAMARA_OPTIMIZADA', {
582
- constraints,
583
- capabilities: this.getCapabilitiesSummary(capabilities)
584
- });
547
+ // Configure autofocus if camera has the capability
548
+ const capabilitiesAny = capabilities;
549
+ if (capabilitiesAny.focusMode && Array.isArray(capabilitiesAny.focusMode)) {
550
+ if (capabilitiesAny.focusMode.includes('continuous')) {
551
+ constraints.focusMode = 'continuous';
552
+ }
553
+ else if (capabilitiesAny.focusMode.includes('auto')) {
554
+ constraints.focusMode = 'auto';
555
+ }
556
+ }
585
557
  return constraints;
586
558
  }
587
559
  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
560
+ // Fallback logic with minimal temporary streams
590
561
  if (!this.deviceType || this.deviceType === 'desktop') {
591
562
  await this.detectDeviceType();
592
563
  }
@@ -595,25 +566,16 @@ class CameraService {
595
566
  width: { ideal: isTablet ? 1280 : 1920 },
596
567
  height: { ideal: isTablet ? 720 : 1080 }
597
568
  };
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
569
+ if (this.selectedCameraId) {
602
570
  fallbackConstraints.deviceId = { exact: this.selectedCameraId };
603
571
  }
604
572
  else if (this.preferredCameraFacing) {
605
- // Use facingMode for initial preference-based selection
606
- // Use 'ideal' instead of 'exact' to avoid OverconstrainedError on desktop
607
573
  const facingConstraint = this.deviceType === 'desktop'
608
574
  ? { ideal: this.preferredCameraFacing }
609
575
  : { exact: this.preferredCameraFacing };
610
576
  fallbackConstraints.facingMode = facingConstraint;
611
577
  }
612
- else if (this.selectedCameraId) {
613
- fallbackConstraints.deviceId = { exact: this.selectedCameraId };
614
- }
615
578
  else {
616
- // Use preferredCamera setting to determine default facing mode
617
579
  if (this.preferredCamera === 'front') {
618
580
  fallbackConstraints.facingMode = "user";
619
581
  }
@@ -621,165 +583,57 @@ class CameraService {
621
583
  fallbackConstraints.facingMode = "environment";
622
584
  }
623
585
  else {
624
- // Auto mode: use environment for mobile/tablet, user for desktop
625
586
  fallbackConstraints.facingMode = (this.deviceType === 'mobile' || this.deviceType === 'tablet') ? "environment" : "user";
626
587
  }
627
588
  }
628
589
  return fallbackConstraints;
629
590
  }
630
591
  }
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);
741
- return false;
742
- }
743
- }
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);
592
+ // Optimization helper methods
593
+ constraintsEqual(a, b) {
594
+ if (!a || !b)
781
595
  return false;
782
- }
596
+ return JSON.stringify(a) === JSON.stringify(b);
597
+ }
598
+ // Cleanup method for optimization
599
+ static clearCaches() {
600
+ CameraService.cameraCapabilitiesCache.clear();
601
+ CameraService.deviceEnumerationCache = null;
602
+ }
603
+ // Method to invalidate device cache when needed
604
+ invalidateDeviceCache() {
605
+ CameraService.deviceEnumerationCache = null;
606
+ }
607
+ isVirtualCamera(device) {
608
+ const label = device.label.toLowerCase();
609
+ const virtualCameraIndicators = [
610
+ 'obs',
611
+ 'virtual',
612
+ 'snap camera',
613
+ 'manycam',
614
+ 'xsplit',
615
+ 'camtwist',
616
+ 'ecamm',
617
+ 'nvidia broadcast',
618
+ 'droidcam',
619
+ 'iriun',
620
+ 'elgato',
621
+ 'streamlabs',
622
+ 'wirecast',
623
+ 'zoom virtual',
624
+ 'teams virtual',
625
+ 'mmhmm',
626
+ 'camo',
627
+ 'reincubate camo',
628
+ 'webcamoid',
629
+ 'splitcam',
630
+ 'cheese',
631
+ 'guvcview',
632
+ 'yawcam',
633
+ 'webcam 7',
634
+ 'altserver'
635
+ ];
636
+ return virtualCameraIndicators.some(indicator => label.includes(indicator));
783
637
  }
784
638
  }
785
639
 
@@ -858,9 +712,9 @@ class DeviceStrategyFactory {
858
712
  }
859
713
 
860
714
  class DetectionService {
861
- logger;
862
715
  debug;
863
716
  useDocumentClassification;
717
+ useDocumentDetector;
864
718
  session;
865
719
  mobileNetSession;
866
720
  mobileNetClassMap;
@@ -875,30 +729,37 @@ class DetectionService {
875
729
  preprocessCanvas;
876
730
  preprocessCtx;
877
731
  captureCanvas;
878
- constructor(logger, debug = false, useDocumentClassification = false) {
879
- this.logger = logger;
732
+ // Static canvas pool for reuse across instances
733
+ static canvasPool = new Map();
734
+ static MAX_POOL_SIZE = 5;
735
+ constructor(debug = false, useDocumentClassification = false, useDocumentDetector = true) {
880
736
  this.debug = debug;
881
737
  this.useDocumentClassification = useDocumentClassification;
738
+ this.useDocumentDetector = useDocumentDetector;
882
739
  this.deviceStrategy = DeviceStrategyFactory.createStrategy();
883
740
  this.initializeCanvasPool();
884
741
  }
885
742
  async loadModel() {
886
743
  if (this.modelLoaded || this.session) {
887
- this.logger.state('MODELO_YA_PRECARGADO', {
888
- sessionExists: !!this.session,
889
- modelPreloaded: this.modelLoaded
890
- });
744
+ return;
745
+ }
746
+ if (!this.useDocumentDetector) {
747
+ // Skip loading the detection model but mark as "loaded" for compatibility
748
+ this.modelLoaded = true;
891
749
  return;
892
750
  }
893
751
  try {
894
- this.logger.state('PRECARGANDO_MODELO_DETECCION', { modelPath: this.MODEL_PATH });
895
752
  const sessionOptions = this.deviceStrategy.getSessionOptions(this.debug);
896
753
  try {
897
754
  this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, sessionOptions);
898
755
  }
899
756
  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');
757
+ // Múltiples tipos de errores de memoria que pueden ocurrir
758
+ const isMemoryError = error.message.includes('failed to allocate a buffer') ||
759
+ error.message.includes('Out of memory') ||
760
+ error.message.includes('RangeError: Out of memory') ||
761
+ error.message.includes('no available backend found');
762
+ if (isMemoryError) {
902
763
  const fallbackOptions = {
903
764
  executionProviders: ['wasm'],
904
765
  graphOptimizationLevel: 'disabled',
@@ -908,18 +769,25 @@ class DetectionService {
908
769
  executionMode: 'sequential',
909
770
  interOpNumThreads: 1,
910
771
  intraOpNumThreads: 1,
772
+ // Configuraciones adicionales para dispositivos con poca memoria
773
+ enableProfiling: false,
774
+ sessionLogSeverityLevel: 4,
775
+ sessionLogVerbosityLevel: 0,
911
776
  };
912
- this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, fallbackOptions);
777
+ try {
778
+ this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, fallbackOptions);
779
+ }
780
+ catch (fallbackError) {
781
+ throw new Error(`no available backend found. ERR: [wasm] RangeError: Out of memory`);
782
+ }
913
783
  }
914
784
  else {
915
785
  throw error;
916
786
  }
917
787
  }
918
788
  this.modelLoaded = true;
919
- this.logger.state('MODELO_DETECCION_CARGADO_EXITOSAMENTE', { sessionCreated: !!this.session });
920
789
  }
921
790
  catch (error) {
922
- this.logger.error('Error al precargar modelo de detección:', error);
923
791
  throw error;
924
792
  }
925
793
  }
@@ -928,19 +796,14 @@ class DetectionService {
928
796
  return;
929
797
  }
930
798
  try {
931
- this.logger.state('CARGANDO_MODELO_MOBILENET', { path: this.MOBILENET_MODEL_PATH });
932
799
  // Try to load class map (optional - SqueezeNet may not need it for basic classification)
933
800
  try {
934
801
  const classResponse = await fetch(this.MOBILENET_CLASSES_PATH);
935
802
  if (classResponse.ok) {
936
803
  this.mobileNetClassMap = await classResponse.json();
937
- this.logger.state('CLASES_MOBILENET_CARGADAS', {
938
- classCount: Object.keys(this.mobileNetClassMap).length
939
- });
940
804
  }
941
805
  }
942
806
  catch (error) {
943
- this.logger.warn('No se pudo cargar el mapa de clases de MobileNet, usando clasificación básica');
944
807
  // Create a basic class map for document types
945
808
  this.mobileNetClassMap = {
946
809
  "0": "document",
@@ -956,8 +819,12 @@ class DetectionService {
956
819
  this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, sessionOptions);
957
820
  }
958
821
  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');
822
+ // Múltiples tipos de errores de memoria que pueden ocurrir
823
+ const isMemoryError = error.message.includes('failed to allocate a buffer') ||
824
+ error.message.includes('Out of memory') ||
825
+ error.message.includes('RangeError: Out of memory') ||
826
+ error.message.includes('no available backend found');
827
+ if (isMemoryError) {
961
828
  const fallbackOptions = {
962
829
  executionProviders: ['wasm'],
963
830
  graphOptimizationLevel: 'disabled',
@@ -967,23 +834,32 @@ class DetectionService {
967
834
  executionMode: 'sequential',
968
835
  interOpNumThreads: 1,
969
836
  intraOpNumThreads: 1,
837
+ // Configuraciones adicionales para dispositivos con poca memoria
838
+ enableProfiling: false,
839
+ sessionLogSeverityLevel: 4,
840
+ sessionLogVerbosityLevel: 0,
970
841
  };
971
- this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, fallbackOptions);
842
+ try {
843
+ this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, fallbackOptions);
844
+ }
845
+ catch (fallbackError) {
846
+ throw new Error(`no available backend found. ERR: [wasm] RangeError: Out of memory`);
847
+ }
972
848
  }
973
849
  else {
974
850
  throw error;
975
851
  }
976
852
  }
977
- this.logger.state('MODELO_MOBILENET_CARGADO_EXITOSAMENTE', {
978
- sessionCreated: !!this.mobileNetSession
979
- });
980
853
  }
981
854
  catch (error) {
982
- this.logger.error('Error al cargar modelo MobileNet:', error);
983
855
  throw error;
984
856
  }
985
857
  }
986
858
  isModelLoaded() {
859
+ if (!this.useDocumentDetector) {
860
+ // If detector is disabled, consider it "loaded" for compatibility
861
+ return this.modelLoaded;
862
+ }
987
863
  return this.modelLoaded && !!this.session;
988
864
  }
989
865
  preprocess(video) {
@@ -1008,6 +884,10 @@ class DetectionService {
1008
884
  return new window.ort.Tensor("float16", float16Data, [1, 3, this.INPUT_SIZE, this.INPUT_SIZE]);
1009
885
  }
1010
886
  async runInference(inputTensor) {
887
+ if (!this.useDocumentDetector) {
888
+ // Return empty array when document detector is disabled
889
+ return [];
890
+ }
1011
891
  if (!this.session) {
1012
892
  throw new Error('Detection model not loaded');
1013
893
  }
@@ -1032,11 +912,9 @@ class DetectionService {
1032
912
  }
1033
913
  async classifyDocument(canvas) {
1034
914
  if (!this.mobileNetSession || !this.mobileNetClassMap) {
1035
- this.logger.warn('Modelo MobileNet no está cargado, saltando clasificación');
1036
915
  return null;
1037
916
  }
1038
917
  try {
1039
- this.logger.state('CLASIFICANDO_DOCUMENTO', { timestamp: Date.now() });
1040
918
  const inputTensor = this.preprocessMobileNet(canvas);
1041
919
  const feeds = { [this.mobileNetSession.inputNames[0]]: inputTensor };
1042
920
  const results = await this.mobileNetSession.run(feeds);
@@ -1044,13 +922,6 @@ class DetectionService {
1044
922
  const maxIdx = output.reduce((bestIdx, val, idx, arr) => val > arr[bestIdx] ? idx : bestIdx, 0);
1045
923
  const confidence = output[maxIdx];
1046
924
  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
925
  return {
1055
926
  class: className,
1056
927
  confidence: confidence,
@@ -1058,7 +929,6 @@ class DetectionService {
1058
929
  };
1059
930
  }
1060
931
  catch (error) {
1061
- this.logger.error('Error al clasificar documento:', error);
1062
932
  return null;
1063
933
  }
1064
934
  }
@@ -1171,7 +1041,6 @@ class DetectionService {
1171
1041
  }
1172
1042
  this.mobileNetClassMap = undefined;
1173
1043
  this.modelLoaded = false;
1174
- this.logger.state('DETECCION_SERVICE_LIMPIADO', { timestamp: Date.now() });
1175
1044
  }
1176
1045
  initializeCanvasPool() {
1177
1046
  this.preprocessCanvas = document.createElement('canvas');
@@ -1182,9 +1051,6 @@ class DetectionService {
1182
1051
  willReadFrequently: true
1183
1052
  });
1184
1053
  this.captureCanvas = document.createElement('canvas');
1185
- this.logger.state('CANVAS_POOL_INICIALIZADO', {
1186
- preprocessCanvasSize: this.INPUT_SIZE
1187
- });
1188
1054
  }
1189
1055
  cleanupCanvasPool() {
1190
1056
  if (this.preprocessCanvas) {
@@ -1220,10 +1086,11 @@ class DetectionService {
1220
1086
  return (sign << 15) | (newExp << 10) | (frac >> 13);
1221
1087
  }
1222
1088
  preprocessMobileNet(canvas) {
1223
- const tempCanvas = document.createElement('canvas');
1224
- tempCanvas.width = 224;
1225
- tempCanvas.height = 224;
1089
+ // Use pooled canvas instead of creating new one
1090
+ const tempCanvas = this.getCanvas(224, 224);
1226
1091
  const tempCtx = tempCanvas.getContext('2d');
1092
+ // Clear canvas before use
1093
+ tempCtx.clearRect(0, 0, 224, 224);
1227
1094
  // Resize image to 224x224 for SqueezeNet (using MobileNet interface)
1228
1095
  tempCtx.drawImage(canvas, 0, 0, 224, 224);
1229
1096
  const imageData = tempCtx.getImageData(0, 0, 224, 224);
@@ -1239,8 +1106,53 @@ class DetectionService {
1239
1106
  // Blue channel
1240
1107
  arr[2 * hw + i] = data[i * 4 + 2] / 255.0;
1241
1108
  }
1109
+ // Return canvas to pool after use
1110
+ this.returnCanvas(tempCanvas);
1242
1111
  return new window.ort.Tensor('float32', arr, [1, 3, 224, 224]);
1243
1112
  }
1113
+ // Canvas pool management methods for optimization
1114
+ getCanvas(width, height) {
1115
+ const key = `${width}x${height}`;
1116
+ if (!DetectionService.canvasPool.has(key)) {
1117
+ DetectionService.canvasPool.set(key, []);
1118
+ }
1119
+ const pool = DetectionService.canvasPool.get(key);
1120
+ let canvas = pool.pop();
1121
+ if (!canvas) {
1122
+ canvas = document.createElement('canvas');
1123
+ canvas.width = width;
1124
+ canvas.height = height;
1125
+ }
1126
+ else {
1127
+ // Clear the canvas before reuse
1128
+ const ctx = canvas.getContext('2d');
1129
+ ctx.clearRect(0, 0, width, height);
1130
+ }
1131
+ return canvas;
1132
+ }
1133
+ returnCanvas(canvas) {
1134
+ const key = `${canvas.width}x${canvas.height}`;
1135
+ if (!DetectionService.canvasPool.has(key)) {
1136
+ DetectionService.canvasPool.set(key, []);
1137
+ }
1138
+ const pool = DetectionService.canvasPool.get(key);
1139
+ // Only return to pool if not at max capacity
1140
+ if (pool.length < DetectionService.MAX_POOL_SIZE) {
1141
+ pool.push(canvas);
1142
+ }
1143
+ }
1144
+ // Static method to clear all canvas pools (useful for memory management)
1145
+ static clearCanvasPools() {
1146
+ DetectionService.canvasPool.clear();
1147
+ }
1148
+ // Method to get pool statistics for debugging
1149
+ getPoolStats() {
1150
+ const stats = {};
1151
+ DetectionService.canvasPool.forEach((pool, key) => {
1152
+ stats[key] = pool.length;
1153
+ });
1154
+ return stats;
1155
+ }
1244
1156
  }
1245
1157
 
1246
1158
  class ServiceContainer {
@@ -1251,13 +1163,11 @@ class ServiceContainer {
1251
1163
  initializeServices(config) {
1252
1164
  // Initialize services in dependency order
1253
1165
  const eventBus = new EventBusService();
1254
- const logger = new LoggerService(config.debug);
1255
1166
  const stateManager = new StateManagerService(eventBus);
1256
- const cameraService = new CameraService(logger, eventBus, config.preferredCamera);
1257
- const detectionService = new DetectionService(logger, config.debug, config.useDocumentClassification);
1167
+ const cameraService = new CameraService(eventBus, config.preferredCamera);
1168
+ const detectionService = new DetectionService(config.debug, config.useDocumentClassification, config.useDocumentDetector);
1258
1169
  // Register services
1259
1170
  this.services.set('eventBus', eventBus);
1260
- this.services.set('logger', logger);
1261
1171
  this.services.set('stateManager', stateManager);
1262
1172
  this.services.set('cameraService', cameraService);
1263
1173
  this.services.set('detectionService', detectionService);
@@ -1269,9 +1179,6 @@ class ServiceContainer {
1269
1179
  }
1270
1180
  return service;
1271
1181
  }
1272
- getLogger() {
1273
- return this.get('logger');
1274
- }
1275
1182
  getEventBus() {
1276
1183
  return this.get('eventBus');
1277
1184
  }
@@ -1285,10 +1192,6 @@ class ServiceContainer {
1285
1192
  return this.get('detectionService');
1286
1193
  }
1287
1194
  updateConfig(config) {
1288
- if (config.debug !== undefined) {
1289
- const logger = this.services.get('logger');
1290
- logger.setDebugMode(config.debug);
1291
- }
1292
1195
  }
1293
1196
  cleanup() {
1294
1197
  // Cleanup services in reverse order
@@ -1298,7 +1201,7 @@ class ServiceContainer {
1298
1201
  }
1299
1202
  }
1300
1203
 
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}}";
1204
+ 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);}.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
1205
 
1303
1206
  const JaakStamps = class {
1304
1207
  constructor(hostRef) {
@@ -1312,6 +1215,7 @@ const JaakStamps = class {
1312
1215
  maskSize = 90;
1313
1216
  cropMargin = 20;
1314
1217
  useDocumentClassification = false;
1218
+ useDocumentDetector = true;
1315
1219
  preferredCamera = 'auto';
1316
1220
  captureDelay = 1500;
1317
1221
  enableBackDocumentTimer = false;
@@ -1328,9 +1232,16 @@ const JaakStamps = class {
1328
1232
  showCameraSelector = false;
1329
1233
  isSwitchingCamera = false;
1330
1234
  hasDocumentDetected = false;
1235
+ cameraInfoWithAutofocus = {
1236
+ availableCameras: [],
1237
+ selectedCameraId: null,
1238
+ deviceType: 'desktop',
1239
+ isMultipleCamerasAvailable: false,
1240
+ preferredFacing: null
1241
+ };
1331
1242
  currentStatus = {
1332
- message: 'Inicializando componente...',
1333
- description: 'Configurando servicios y cargando recursos',
1243
+ message: 'Preparando capturador...',
1244
+ description: 'Configurando herramientas de captura',
1334
1245
  type: 'initializing',
1335
1246
  isInitialized: false
1336
1247
  };
@@ -1345,9 +1256,13 @@ const JaakStamps = class {
1345
1256
  detectionRate: 0
1346
1257
  };
1347
1258
  backDocumentTimerRemaining = 0;
1259
+ showManualCaptureButton = false;
1260
+ performanceDegradedMode = false; // Auto-switched to manual mode due to performance
1261
+ showPerformanceMessage = false; // Show performance message temporarily
1262
+ captureStateVersion = 0; // Triggers re-render when StateManager changes
1263
+ processingButton = null; // Track which button is processing
1348
1264
  // Services
1349
1265
  serviceContainer;
1350
- logger;
1351
1266
  eventBus;
1352
1267
  stateManager;
1353
1268
  cameraService;
@@ -1364,12 +1279,12 @@ const JaakStamps = class {
1364
1279
  alignmentStartTime;
1365
1280
  alignmentTimer;
1366
1281
  backDocumentTimer;
1282
+ orientationTimer;
1367
1283
  // Performance monitoring
1368
1284
  performanceMetrics = {
1369
1285
  fps: 0,
1370
1286
  inferenceTime: 0,
1371
1287
  memoryUsage: 0,
1372
- cpuUsage: 0,
1373
1288
  onnxLoadTime: 0,
1374
1289
  frameProcessingTime: 0,
1375
1290
  totalDetections: 0,
@@ -1378,22 +1293,39 @@ const JaakStamps = class {
1378
1293
  lastUpdateTime: 0
1379
1294
  };
1380
1295
  performanceUpdateInterval;
1381
- // Performance optimization
1296
+ // Performance optimization with adaptive frame skipping
1382
1297
  frameSkipCounter = 0;
1383
- FRAME_SKIP = 2;
1298
+ BASE_FRAME_SKIP = 2;
1299
+ MAX_FRAME_SKIP = 8;
1384
1300
  consecutiveFailures = 0;
1385
1301
  MAX_FAILURES = 30;
1386
1302
  lastInferenceTime = 0;
1387
1303
  MIN_INFERENCE_INTERVAL = 50;
1304
+ performanceHistory = [];
1305
+ PERFORMANCE_HISTORY_SIZE = 10;
1306
+ // Sistema simplificado de monitoreo de rendimiento
1307
+ PERFORMANCE_THRESHOLD_MS = 2000; // Umbral único para todos los frames
1308
+ SLOW_FRAMES_TO_TRIGGER = 2; // Cuántos frames lentos consecutivos antes de cambiar
1309
+ slowFrameCount = 0; // Contador de frames consecutivos lentos
1310
+ processedFramesCount = 0; // Contador total de frames procesados
1311
+ hasAutoSwitchedToManual = false;
1312
+ // Canvas pool for optimized screenshot capture
1313
+ canvasPool = [];
1314
+ MAX_CANVAS_POOL_SIZE = 3;
1388
1315
  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();
1316
+ try {
1317
+ this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
1318
+ await this.initializeServices();
1319
+ this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
1320
+ await this.setupEventListeners();
1321
+ this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
1322
+ await this.initializeComponent();
1323
+ if (this.debug) {
1324
+ this.initializePerformanceMonitor();
1325
+ }
1326
+ }
1327
+ catch (error) {
1328
+ this.updateStatus('Error de inicialización', error.message, 'error');
1397
1329
  }
1398
1330
  }
1399
1331
  async initializeServices() {
@@ -1403,37 +1335,22 @@ const JaakStamps = class {
1403
1335
  maskSize: this.maskSize,
1404
1336
  cropMargin: this.cropMargin,
1405
1337
  useDocumentClassification: this.useDocumentClassification,
1338
+ useDocumentDetector: this.useDocumentDetector,
1406
1339
  preferredCamera: this.preferredCamera,
1407
1340
  captureDelay: this.captureDelay
1408
1341
  };
1409
1342
  this.serviceContainer = new ServiceContainer(config);
1410
- this.logger = this.serviceContainer.getLogger();
1411
1343
  this.eventBus = this.serviceContainer.getEventBus();
1412
1344
  this.stateManager = this.serviceContainer.getStateManager();
1413
1345
  this.cameraService = this.serviceContainer.getCameraService();
1414
1346
  this.detectionService = this.serviceContainer.getDetectionService();
1415
- this.logger.state('SERVICIOS_INICIALIZADOS', { timestamp: Date.now() });
1416
1347
  }
1417
1348
  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);
1349
+ this.eventBus.on('state-changed', () => {
1350
+ this.handleStateChange();
1426
1351
  });
1427
1352
  }
1428
1353
  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
1354
  this.validateProps();
1438
1355
  if (this.debug) {
1439
1356
  this.stateManager.updateCaptureState({ isLoading: true });
@@ -1442,58 +1359,67 @@ const JaakStamps = class {
1442
1359
  this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura', 'initializing');
1443
1360
  await this.cameraService.detectDeviceType();
1444
1361
  await this.cameraService.enumerateDevices();
1445
- this.updateStatus('Cargando modelo IA...', 'Preparando reconocimiento de documentos', 'initializing');
1362
+ await this.updateCameraInfoWithAutofocus();
1363
+ this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'initializing');
1446
1364
  await this.loadOnnxRuntime();
1447
1365
  this.initializeResizeObserver();
1448
1366
  }
1449
1367
  validateProps() {
1450
1368
  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
1369
  this.maskSize = 90;
1453
1370
  }
1454
1371
  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
1372
  this.cropMargin = 0;
1457
1373
  }
1458
1374
  const validOptions = ['auto', 'front', 'back'];
1459
1375
  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
1376
  this.preferredCamera = 'auto';
1462
1377
  }
1463
1378
  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
1379
  this.captureDelay = 1500;
1466
1380
  }
1467
1381
  }
1468
1382
  async loadOnnxRuntime() {
1469
1383
  if (!window.ort) {
1470
- this.updateStatus('Descargando librerías...', 'Obteniendo recursos de reconocimiento', 'initializing');
1384
+ this.updateStatus('Preparando herramientas...', 'Configurando funciones de captura', 'initializing');
1471
1385
  const script = document.createElement('script');
1472
- script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
1386
+ script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.23.0/dist/ort.min.js';
1473
1387
  document.head.appendChild(script);
1474
1388
  await new Promise((resolve) => {
1475
1389
  script.onload = () => {
1476
- setTimeout(() => {
1477
- this.finalizeInitialization();
1390
+ setTimeout(async () => {
1391
+ await this.finalizeInitialization();
1478
1392
  resolve(undefined);
1479
1393
  }, 300);
1480
1394
  };
1481
1395
  });
1482
1396
  }
1483
1397
  else {
1484
- setTimeout(() => {
1485
- this.finalizeInitialization();
1398
+ setTimeout(async () => {
1399
+ await this.finalizeInitialization();
1486
1400
  }, 300);
1487
1401
  }
1488
1402
  }
1489
- finalizeInitialization() {
1403
+ async finalizeInitialization() {
1490
1404
  this.stateManager.updateCaptureState({ isLoading: false });
1491
- this.currentStatus = {
1492
- message: 'Listo para capturar',
1493
- description: '',
1494
- type: 'ready',
1495
- isInitialized: true
1496
- };
1405
+ // Check camera permissions before showing "ready" status
1406
+ const hasPermissions = await this.checkCameraPermissions();
1407
+ if (hasPermissions) {
1408
+ this.currentStatus = {
1409
+ message: 'Listo para capturar',
1410
+ description: '',
1411
+ type: 'ready',
1412
+ isInitialized: true
1413
+ };
1414
+ }
1415
+ else {
1416
+ this.currentStatus = {
1417
+ message: 'Permisos de cámara requeridos',
1418
+ description: 'Por favor, otorgue permisos de cámara para continuar',
1419
+ type: 'error',
1420
+ isInitialized: true
1421
+ };
1422
+ }
1497
1423
  this.emitReadyEvent();
1498
1424
  }
1499
1425
  updateStatus(message, description, type = 'loading') {
@@ -1507,13 +1433,29 @@ const JaakStamps = class {
1507
1433
  emitReadyEvent() {
1508
1434
  const isDocumentReady = !!window.ort && this.detectionService.isModelLoaded();
1509
1435
  this.isReady.emit(isDocumentReady);
1510
- this.logger.state('COMPONENTE_LISTO', {
1511
- ortLibraryLoaded: !!window.ort,
1512
- modelPreloaded: this.detectionService.isModelLoaded(),
1513
- isReady: isDocumentReady
1514
- });
1515
1436
  }
1516
- handleStateChange(data) {
1437
+ async checkCameraPermissions() {
1438
+ try {
1439
+ if (!navigator.permissions) {
1440
+ // Fallback: try to access camera directly
1441
+ try {
1442
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
1443
+ stream.getTracks().forEach(track => track.stop());
1444
+ return true;
1445
+ }
1446
+ catch {
1447
+ return false;
1448
+ }
1449
+ }
1450
+ const permission = await navigator.permissions.query({ name: 'camera' });
1451
+ return permission.state === 'granted';
1452
+ }
1453
+ catch (error) {
1454
+ return false;
1455
+ }
1456
+ }
1457
+ handleStateChange() {
1458
+ // Handle state changes from StateManager if needed
1517
1459
  }
1518
1460
  initializeResizeObserver() {
1519
1461
  if ('ResizeObserver' in window && this.detectionContainer) {
@@ -1522,15 +1464,56 @@ const JaakStamps = class {
1522
1464
  });
1523
1465
  resizeObserver.observe(this.detectionContainer.parentElement);
1524
1466
  }
1467
+ // Handle mobile device orientation changes
1468
+ window.addEventListener('orientationchange', () => {
1469
+ // Clear any existing orientation timer
1470
+ if (this.orientationTimer) {
1471
+ clearTimeout(this.orientationTimer);
1472
+ }
1473
+ // Add delay to ensure video dimensions are updated after rotation
1474
+ this.orientationTimer = window.setTimeout(() => {
1475
+ this.handleResize();
1476
+ this.orientationTimer = undefined;
1477
+ }, 300);
1478
+ });
1525
1479
  }
1526
1480
  handleResize() {
1527
1481
  if (this.detectionContainer) {
1528
1482
  const container = this.detectionContainer.parentElement;
1529
1483
  const rect = container.getBoundingClientRect();
1530
1484
  this.updateMaskDimensions(rect);
1531
- this.logger.debug('Container redimensionado:', { width: rect.width, height: rect.height });
1532
1485
  }
1533
1486
  }
1487
+ triggerRerender() {
1488
+ this.captureStateVersion = this.captureStateVersion + 1;
1489
+ }
1490
+ getGuideText(step) {
1491
+ if (step === 'completed') {
1492
+ return 'Proceso completado';
1493
+ }
1494
+ // Check if we should show performance message
1495
+ if (this.showPerformanceMessage && step === 'front') {
1496
+ return 'Modo manual activado por rendimiento. Use el botón para capturar.';
1497
+ }
1498
+ if (step === 'front') {
1499
+ // Para el frente, si el detector manual está activo, mostrar instrucciones de captura manual
1500
+ if (this.showManualCaptureButton) {
1501
+ return 'Posicione el frente de su documento en el marco y presione el botón para capturar';
1502
+ }
1503
+ // Instrucción básica de alineación
1504
+ return 'Alinee su identificación con el marco';
1505
+ }
1506
+ else if (step === 'back') {
1507
+ // Para el reverso, mostrar instrucciones específicas según el modo
1508
+ if (this.useDocumentDetector) {
1509
+ return 'Si tu documento no tiene lado trasero, da clic en el botón para continuar con el proceso';
1510
+ }
1511
+ else {
1512
+ return 'Posicione el reverso de su documento en el marco y capture, o salte este paso';
1513
+ }
1514
+ }
1515
+ return 'Alinee su identificación con el marco';
1516
+ }
1534
1517
  updateMaskDimensions(containerRect) {
1535
1518
  if (!this.videoRef)
1536
1519
  return;
@@ -1582,16 +1565,38 @@ const JaakStamps = class {
1582
1565
  this.el.style.setProperty('--mask-center-x', `${videoCenterXPercent}%`);
1583
1566
  this.el.style.setProperty('--mask-center-y', `${videoCenterYPercent}%`);
1584
1567
  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
1568
  }
1593
1569
  // PUBLIC METHODS
1570
+ // Helper method to check if component is ready for operations
1571
+ isComponentReady() {
1572
+ if (!this.currentStatus.isInitialized) {
1573
+ return {
1574
+ ready: false,
1575
+ message: 'El componente aún se está inicializando',
1576
+ status: 'initializing'
1577
+ };
1578
+ }
1579
+ if (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') {
1580
+ return {
1581
+ ready: false,
1582
+ message: 'El componente está cargando recursos',
1583
+ status: 'loading'
1584
+ };
1585
+ }
1586
+ if (!this.serviceContainer || !this.stateManager || !this.cameraService || !this.detectionService) {
1587
+ return {
1588
+ ready: false,
1589
+ message: 'Los servicios del componente no están disponibles',
1590
+ status: 'error'
1591
+ };
1592
+ }
1593
+ return { ready: true };
1594
+ }
1594
1595
  async getCapturedImages() {
1596
+ const readyCheck = this.isComponentReady();
1597
+ if (!readyCheck.ready) {
1598
+ throw new Error(readyCheck.message);
1599
+ }
1595
1600
  const state = this.stateManager.getCaptureState();
1596
1601
  if (state.step !== 'completed') {
1597
1602
  throw new Error('El proceso de captura no ha sido completado');
@@ -1599,146 +1604,359 @@ const JaakStamps = class {
1599
1604
  return this.stateManager.getCapturedImages();
1600
1605
  }
1601
1606
  async isProcessCompleted() {
1607
+ const readyCheck = this.isComponentReady();
1608
+ if (!readyCheck.ready) {
1609
+ return false; // Return false instead of error for this method
1610
+ }
1602
1611
  return this.stateManager.isProcessCompleted();
1603
1612
  }
1604
1613
  async startCapture() {
1605
- await this.startDetection();
1614
+ const readyCheck = this.isComponentReady();
1615
+ if (!readyCheck.ready) {
1616
+ this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
1617
+ return { success: false, error: readyCheck.message };
1618
+ }
1619
+ try {
1620
+ await this.startDetection();
1621
+ return { success: true };
1622
+ }
1623
+ catch (error) {
1624
+ this.updateStatus('Error al iniciar captura', error.message, 'error');
1625
+ return { success: false, error: error.message };
1626
+ }
1606
1627
  }
1607
1628
  async stopCapture() {
1608
- this.exitSession();
1609
- }
1610
- async resetCapture() {
1611
- this.resetDetection();
1612
- }
1613
- 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);
1629
+ const readyCheck = this.isComponentReady();
1630
+ if (!readyCheck.ready) {
1631
+ return { success: false, error: readyCheck.message };
1632
+ }
1633
+ try {
1634
+ this.exitSession();
1635
+ return { success: true };
1636
+ }
1637
+ catch (error) {
1638
+ return { success: false, error: error.message };
1639
+ }
1640
+ }
1641
+ async resetCapture() {
1642
+ const readyCheck = this.isComponentReady();
1643
+ if (!readyCheck.ready) {
1644
+ return { success: false, error: readyCheck.message };
1645
+ }
1646
+ try {
1647
+ this.resetDetection();
1648
+ return { success: true };
1649
+ }
1650
+ catch (error) {
1651
+ return { success: false, error: error.message };
1652
+ }
1653
+ }
1654
+ async skipBackCapture() {
1655
+ console.log('🟡 Botón clicked: Saltar reverso');
1656
+ // Set processing state immediately
1657
+ this.processingButton = 'skip-back';
1658
+ // Add small delay to show spinner
1659
+ await new Promise(resolve => setTimeout(resolve, 100));
1660
+ const readyCheck = this.isComponentReady();
1661
+ if (!readyCheck.ready) {
1662
+ this.processingButton = null;
1663
+ return { success: false, error: readyCheck.message };
1664
+ }
1665
+ try {
1666
+ const captureState = this.stateManager.getCaptureState();
1667
+ const capturedImages = this.stateManager.getCapturedImages();
1668
+ if (captureState.step === 'back' && capturedImages.front.fullFrame) {
1669
+ this.completeProcess(true);
1670
+ return { success: true };
1671
+ }
1672
+ else {
1673
+ return { success: false, error: 'No se puede saltar el reverso en el estado actual' };
1674
+ }
1675
+ }
1676
+ catch (error) {
1677
+ return { success: false, error: error.message };
1678
+ }
1679
+ finally {
1680
+ // Always clear processing state when done
1681
+ this.processingButton = null;
1618
1682
  }
1619
1683
  }
1620
1684
  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
- };
1685
+ const readyCheck = this.isComponentReady();
1686
+ if (!readyCheck.ready) {
1687
+ return {
1688
+ isVideoActive: false,
1689
+ captureStep: 'front',
1690
+ hasImages: false,
1691
+ isProcessCompleted: false,
1692
+ isModelPreloaded: false,
1693
+ componentStatus: readyCheck.status,
1694
+ componentMessage: readyCheck.message
1695
+ };
1696
+ }
1697
+ try {
1698
+ const captureState = this.stateManager.getCaptureState();
1699
+ const capturedImages = this.stateManager.getCapturedImages();
1700
+ return {
1701
+ isVideoActive: captureState.isVideoActive,
1702
+ captureStep: captureState.step,
1703
+ hasImages: !!(capturedImages.front.fullFrame || capturedImages.back.fullFrame),
1704
+ isProcessCompleted: this.stateManager.isProcessCompleted(),
1705
+ isModelPreloaded: this.detectionService.isModelLoaded(),
1706
+ componentStatus: 'ready'
1707
+ };
1708
+ }
1709
+ catch (error) {
1710
+ return {
1711
+ isVideoActive: false,
1712
+ captureStep: 'front',
1713
+ hasImages: false,
1714
+ isProcessCompleted: false,
1715
+ isModelPreloaded: false,
1716
+ componentStatus: 'error',
1717
+ componentMessage: error.message
1718
+ };
1719
+ }
1630
1720
  }
1631
1721
  async preloadModel() {
1722
+ const readyCheck = this.isComponentReady();
1723
+ if (!readyCheck.ready) {
1724
+ this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
1725
+ return { success: false, error: readyCheck.message };
1726
+ }
1632
1727
  if (this.detectionService.isModelLoaded()) {
1633
- this.logger.state('MODELO_YA_PRECARGADO');
1634
- this.updateStatus('Modelos ya cargados', '', 'ready');
1728
+ this.updateStatus('Reconocimiento listo', '', 'ready');
1635
1729
  return { success: true, message: 'Model already loaded' };
1636
1730
  }
1637
1731
  try {
1638
1732
  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();
1733
+ this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
1734
+ if (this.stateManager) {
1735
+ this.stateManager.updateCaptureState({ isLoading: true });
1736
+ }
1737
+ try {
1738
+ await this.detectionService.loadModel();
1739
+ }
1740
+ catch (modelError) {
1741
+ this.switchToManualModeWithError('Error al cargar modelo de detección');
1742
+ throw modelError;
1743
+ }
1744
+ this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
1745
+ try {
1746
+ await this.detectionService.loadClassificationModel();
1747
+ }
1748
+ catch (classificationError) {
1749
+ this.switchToManualModeWithError('Error al cargar modelo de clasificación');
1750
+ throw classificationError;
1751
+ }
1644
1752
  const loadEndTime = performance.now();
1645
1753
  const loadTime = loadEndTime - loadStartTime;
1646
1754
  // Record ONNX load time
1647
1755
  if (this.debug) {
1648
1756
  this.recordOnnxPerformance(loadTime, 0);
1649
1757
  }
1650
- this.updateStatus('Optimizando modelos...', 'Configurando parámetros de rendimiento', 'loading');
1758
+ this.updateStatus('Finalizando configuración...', 'Preparando herramientas para captura', 'loading');
1651
1759
  await new Promise(resolve => setTimeout(resolve, 300));
1652
- this.updateStatus('Modelos precargados', '', 'ready');
1653
- this.stateManager.updateCaptureState({ isLoading: false });
1760
+ this.updateStatus('Reconocimiento preparado', '', 'ready');
1761
+ if (this.stateManager) {
1762
+ this.stateManager.updateCaptureState({ isLoading: false });
1763
+ }
1654
1764
  this.emitReadyEvent();
1655
- this.logger.state('MODELOS_PRECARGADOS_EXITOSAMENTE', { loadTime: Math.round(loadTime) });
1656
1765
  return { success: true, message: 'Models preloaded successfully' };
1657
1766
  }
1658
1767
  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 });
1768
+ this.updateStatus('Error de configuración', 'No se pudieron preparar las herramientas necesarias', 'error');
1769
+ if (this.stateManager) {
1770
+ this.stateManager.updateCaptureState({ isLoading: false });
1771
+ }
1662
1772
  return { success: false, error: error.message };
1663
1773
  }
1664
1774
  }
1665
1775
  async getCameraInfo() {
1666
- return this.cameraService.getCameraInfo();
1776
+ const readyCheck = this.isComponentReady();
1777
+ if (!readyCheck.ready) {
1778
+ return {
1779
+ availableCameras: [],
1780
+ selectedCameraId: null,
1781
+ deviceType: 'desktop',
1782
+ isMultipleCamerasAvailable: false,
1783
+ preferredFacing: null,
1784
+ error: readyCheck.message
1785
+ };
1786
+ }
1787
+ try {
1788
+ return await this.cameraService.getCameraInfo();
1789
+ }
1790
+ catch (error) {
1791
+ return {
1792
+ availableCameras: [],
1793
+ selectedCameraId: null,
1794
+ deviceType: 'desktop',
1795
+ isMultipleCamerasAvailable: false,
1796
+ preferredFacing: null,
1797
+ error: error.message
1798
+ };
1799
+ }
1667
1800
  }
1668
1801
  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);
1802
+ const readyCheck = this.isComponentReady();
1803
+ if (!readyCheck.ready) {
1804
+ return {
1805
+ success: false,
1806
+ error: readyCheck.message,
1807
+ selectedCamera: null,
1808
+ availableCameras: 0
1809
+ };
1810
+ }
1811
+ try {
1812
+ this.preferredCamera = camera;
1813
+ this.serviceContainer.updateConfig({ preferredCamera: camera });
1814
+ await this.cameraService.enumerateDevices();
1815
+ await this.updateCameraInfoWithAutofocus();
1816
+ const captureState = this.stateManager.getCaptureState();
1817
+ if (captureState.isVideoActive) {
1818
+ const selectedCameraId = this.cameraService.getSelectedCameraId();
1819
+ if (selectedCameraId) {
1820
+ await this.cameraService.switchCamera(selectedCameraId);
1821
+ }
1677
1822
  }
1823
+ return {
1824
+ success: true,
1825
+ selectedCamera: this.cameraService.getSelectedCameraId(),
1826
+ availableCameras: this.cameraService.getAvailableCameras().length
1827
+ };
1828
+ }
1829
+ catch (error) {
1830
+ return {
1831
+ success: false,
1832
+ error: error.message,
1833
+ selectedCamera: null,
1834
+ availableCameras: 0
1835
+ };
1678
1836
  }
1679
- return {
1680
- success: true,
1681
- selectedCamera: this.cameraService.getSelectedCameraId(),
1682
- availableCameras: this.cameraService.getAvailableCameras().length
1683
- };
1684
1837
  }
1685
1838
  async setCaptureDelay(delay) {
1686
1839
  if (delay < 0 || delay > 10000) {
1687
- throw new Error('Capture delay must be between 0 and 10000 milliseconds');
1840
+ return {
1841
+ success: false,
1842
+ error: 'Capture delay must be between 0 and 10000 milliseconds',
1843
+ captureDelay: this.captureDelay
1844
+ };
1688
1845
  }
1846
+ // Allow setting capture delay even during initialization
1689
1847
  this.captureDelay = delay;
1690
- this.serviceContainer.updateConfig({ captureDelay: delay });
1848
+ if (this.serviceContainer) {
1849
+ this.serviceContainer.updateConfig({ captureDelay: delay });
1850
+ }
1691
1851
  return {
1692
1852
  success: true,
1693
1853
  captureDelay: this.captureDelay
1694
1854
  };
1695
1855
  }
1696
1856
  async getCaptureDelay() {
1857
+ // Always allow getting capture delay, even during initialization
1697
1858
  return this.captureDelay;
1698
1859
  }
1699
- async setTorchEnabled(enabled) {
1700
- const torchEnabled = await this.cameraService.setTorchEnabled(enabled, this.videoStream);
1860
+ // Memory and device capability detection
1861
+ checkDeviceCapabilities() {
1862
+ const deviceMemory = navigator.deviceMemory;
1863
+ // Siempre intentar usar ML, independientemente del dispositivo
1864
+ // Las restricciones anteriores no eran confiables
1701
1865
  return {
1702
- success: torchEnabled,
1703
- enabled: torchEnabled ? enabled : false
1704
- };
1705
- }
1706
- async focusAtPoint(x, y) {
1707
- const focused = await this.cameraService.focusAtPoint(x, y, this.videoStream);
1708
- return {
1709
- success: focused,
1710
- coordinates: { x, y }
1866
+ canUseML: true,
1867
+ reason: 'Dispositivo compatible con detección automática.',
1868
+ memoryMB: deviceMemory ? deviceMemory * 1024 : null
1711
1869
  };
1712
1870
  }
1713
1871
  // DETECTION METHODS
1714
1872
  async startDetection() {
1715
- this.logger.state('INICIANDO_DETECCION');
1716
1873
  try {
1874
+ // Verificar capacidades del dispositivo antes de cargar modelos
1875
+ const capabilities = this.checkDeviceCapabilities();
1876
+ // Si el dispositivo no puede usar ML o tenemos problemas de memoria, usar modo manual
1877
+ if (!capabilities.canUseML && this.useDocumentDetector) {
1878
+ this.switchToManualModeWithWarning(capabilities.reason);
1879
+ return;
1880
+ }
1717
1881
  // Paso 1: Verificar y cargar modelos si es necesario
1718
1882
  if (!this.detectionService.isModelLoaded()) {
1719
1883
  const loadStartTime = performance.now();
1720
- this.updateStatus('Cargando modelo de detección...', 'Descargando red neuronal para reconocimiento', 'loading');
1884
+ this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
1721
1885
  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();
1886
+ try {
1887
+ await this.detectionService.loadModel();
1888
+ }
1889
+ catch (modelError) {
1890
+ // Si es un error de memoria, cambiar a modo manual con mensaje específico
1891
+ if (modelError.message.includes('Out of memory') || modelError.message.includes('no available backend')) {
1892
+ const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
1893
+ this.switchToManualModeWithWarning(`Memoria insuficiente para detección automática ${memoryInfo}. Modo manual activado - funciona igual de bien!`);
1894
+ return;
1895
+ }
1896
+ else {
1897
+ this.switchToManualModeWithError('Error al cargar modelo de detección');
1898
+ throw modelError;
1899
+ }
1900
+ }
1901
+ this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
1902
+ try {
1903
+ await this.detectionService.loadClassificationModel();
1904
+ }
1905
+ catch (classificationError) {
1906
+ // Si es un error de memoria, cambiar a modo manual con mensaje específico
1907
+ if (classificationError.message.includes('Out of memory') || classificationError.message.includes('no available backend')) {
1908
+ const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
1909
+ this.switchToManualModeWithWarning(`Memoria insuficiente para clasificación automática ${memoryInfo}. Modo manual activado para optimizar rendimiento.`);
1910
+ return;
1911
+ }
1912
+ else {
1913
+ this.switchToManualModeWithError('Error al cargar modelo de clasificación');
1914
+ throw classificationError;
1915
+ }
1916
+ }
1725
1917
  const loadEndTime = performance.now();
1726
1918
  const loadTime = loadEndTime - loadStartTime;
1727
1919
  // Record ONNX load time
1728
1920
  if (this.debug) {
1729
1921
  this.recordOnnxPerformance(loadTime, 0);
1730
1922
  }
1731
- this.logger.state('MODELOS_CARGADOS_EN_DETECCION', { loadTime: Math.round(loadTime) });
1732
1923
  }
1733
1924
  // Paso 2: Detectar y configurar dispositivos
1734
- this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura disponibles', 'loading');
1735
- await this.cameraService.enumerateDevices();
1925
+ this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
1926
+ try {
1927
+ await this.cameraService.enumerateDevices();
1928
+ }
1929
+ catch (enumerateError) {
1930
+ throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
1931
+ }
1932
+ try {
1933
+ await this.updateCameraInfoWithAutofocus();
1934
+ }
1935
+ catch (cameraInfoError) {
1936
+ throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
1937
+ }
1736
1938
  // 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);
1939
+ this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
1940
+ let stream;
1941
+ try {
1942
+ stream = await this.cameraService.setupCamera();
1943
+ }
1944
+ catch (setupError) {
1945
+ throw new Error(`Error al configurar cámara: ${setupError.message}`);
1946
+ }
1947
+ // Paso 4: Inicializar video y mostrar estado de análisis inicial
1948
+ if (this.useDocumentDetector) {
1949
+ this.updateStatus('Analizando estabilidad...', 'Evaluando rendimiento del sistema', 'loading');
1950
+ }
1951
+ else {
1952
+ this.updateStatus('Captura activa', 'Posicione su documento y use el botón para capturar', 'active');
1953
+ }
1954
+ try {
1955
+ await this.initializeVideoStream(stream);
1956
+ }
1957
+ catch (videoError) {
1958
+ throw new Error(`Error al inicializar video: ${videoError.message}`);
1959
+ }
1742
1960
  // Paso 5: Calibrar máscara
1743
1961
  if (this.detectionContainer) {
1744
1962
  const container = this.detectionContainer.parentElement;
@@ -1748,11 +1966,14 @@ const JaakStamps = class {
1748
1966
  // Finalizar e iniciar captura
1749
1967
  this.startTime = Date.now();
1750
1968
  this.stateManager.updateCaptureState({ isLoading: false });
1969
+ // Mostrar botón manual si el detector está deshabilitado
1970
+ if (!this.useDocumentDetector) {
1971
+ this.showManualCaptureButton = true;
1972
+ }
1751
1973
  this.detectFrame();
1752
1974
  }
1753
1975
  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');
1976
+ this.updateStatus('Error al iniciar', 'No se pudo preparar la captura', 'error');
1756
1977
  this.stateManager.updateCaptureState({ isLoading: false });
1757
1978
  }
1758
1979
  }
@@ -1762,14 +1983,16 @@ const JaakStamps = class {
1762
1983
  this.videoStream = stream;
1763
1984
  const isRear = this.cameraService.isRearCamera(stream);
1764
1985
  this.shouldMirrorVideo = !isRear;
1765
- this.logger.state('CAMARA_CONFIGURADA', {
1766
- isRearCamera: isRear,
1767
- shouldMirrorVideo: this.shouldMirrorVideo
1768
- });
1769
1986
  return new Promise((resolve) => {
1770
1987
  this.videoRef.onloadedmetadata = async () => {
1771
1988
  await this.videoRef.play();
1772
1989
  this.stateManager.updateCaptureState({ isVideoActive: true });
1990
+ // Recalculate mask dimensions when new camera stream loads
1991
+ if (this.detectionContainer) {
1992
+ const container = this.detectionContainer.parentElement;
1993
+ const rect = container.getBoundingClientRect();
1994
+ this.updateMaskDimensions(rect);
1995
+ }
1773
1996
  resolve();
1774
1997
  };
1775
1998
  });
@@ -1784,15 +2007,28 @@ const JaakStamps = class {
1784
2007
  const captureState = this.stateManager.getCaptureState();
1785
2008
  if (!this.videoRef || !this.detectionContainer || !this.detectionService.isModelLoaded())
1786
2009
  return;
2010
+ // Show manual capture button when document detector is disabled or performance is degraded
2011
+ if (!this.useDocumentDetector || this.performanceDegradedMode) {
2012
+ this.showManualCaptureButton = true;
2013
+ // Continue the loop for UI updates but skip detection logic
2014
+ if (captureState.step !== 'completed') {
2015
+ this.animationId = requestAnimationFrame(() => this.detectFrame());
2016
+ }
2017
+ return;
2018
+ }
2019
+ else if (!this.performanceDegradedMode) {
2020
+ this.showManualCaptureButton = false;
2021
+ }
1787
2022
  if (captureState.isDetectionPaused) {
1788
2023
  if (captureState.step !== 'completed') {
1789
2024
  this.animationId = requestAnimationFrame(() => this.detectFrame());
1790
2025
  }
1791
2026
  return;
1792
2027
  }
1793
- // Frame skipping for performance
2028
+ // Adaptive frame skipping for performance
1794
2029
  this.frameSkipCounter++;
1795
- if (this.frameSkipCounter <= this.FRAME_SKIP) {
2030
+ const currentFrameSkip = this.getAdaptiveFrameSkip();
2031
+ if (this.frameSkipCounter <= currentFrameSkip) {
1796
2032
  if (captureState.step !== 'completed') {
1797
2033
  this.animationId = requestAnimationFrame(() => this.detectFrame());
1798
2034
  }
@@ -1810,10 +2046,27 @@ const JaakStamps = class {
1810
2046
  this.lastInferenceTime = currentTime;
1811
2047
  // Measure preprocessing and inference time
1812
2048
  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;
2049
+ let inputTensor;
2050
+ let detections;
2051
+ let inferenceTime;
2052
+ try {
2053
+ inputTensor = this.detectionService.preprocess(this.videoRef);
2054
+ }
2055
+ catch (preprocessError) {
2056
+ this.switchToManualModeWithError('Error al procesar imagen');
2057
+ return;
2058
+ }
2059
+ try {
2060
+ // Standard detection without timeout (timeout is handled by frame counting)
2061
+ detections = await this.detectionService.runInference(inputTensor);
2062
+ inferenceTime = performance.now() - inferenceStartTime;
2063
+ }
2064
+ catch (inferenceError) {
2065
+ this.switchToManualModeWithError('Error en detección automática');
2066
+ return;
2067
+ }
2068
+ // Increment frame count
2069
+ this.processedFramesCount++;
1817
2070
  // Record ONNX performance metrics
1818
2071
  if (this.debug) {
1819
2072
  this.recordOnnxPerformance(0, inferenceTime);
@@ -1852,9 +2105,30 @@ const JaakStamps = class {
1852
2105
  }
1853
2106
  this.updateMaskColor(detections);
1854
2107
  // Record frame processing metrics
2108
+ const frameEndTime = performance.now();
2109
+ const totalFrameTime = frameEndTime - frameStartTime;
2110
+ // Sistema simplificado: Verificar rendimiento de cada frame (solo si no se ha cambiado a manual)
2111
+ if (this.useDocumentDetector && !this.hasAutoSwitchedToManual) {
2112
+ // Verificar si el frame es lento
2113
+ if (totalFrameTime >= this.PERFORMANCE_THRESHOLD_MS) {
2114
+ this.slowFrameCount++;
2115
+ // Si tenemos suficientes frames lentos consecutivos, cambiar a manual
2116
+ if (this.slowFrameCount >= this.SLOW_FRAMES_TO_TRIGGER) {
2117
+ this.switchToManualMode();
2118
+ }
2119
+ }
2120
+ else {
2121
+ // Frame rápido, reiniciar contador
2122
+ if (this.slowFrameCount > 0) {
2123
+ this.slowFrameCount = 0;
2124
+ }
2125
+ }
2126
+ // Cambiar mensaje de estado después de analizar los primeros 2 frames
2127
+ if (this.processedFramesCount === 2) {
2128
+ this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
2129
+ }
2130
+ }
1855
2131
  if (this.debug) {
1856
- const frameEndTime = performance.now();
1857
- const totalFrameTime = frameEndTime - frameStartTime;
1858
2132
  this.recordFrameProcessing(totalFrameTime, detections.length);
1859
2133
  }
1860
2134
  // Continue detection loop
@@ -1868,7 +2142,6 @@ const JaakStamps = class {
1868
2142
  }
1869
2143
  }
1870
2144
  catch (e) {
1871
- this.logger.error('Error en inferencia de modelo:', e);
1872
2145
  const captureState = this.stateManager.getCaptureState();
1873
2146
  if (captureState.step !== 'completed') {
1874
2147
  setTimeout(() => this.detectFrame(), 100);
@@ -1891,10 +2164,18 @@ const JaakStamps = class {
1891
2164
  clearTimeout(this.alignmentTimer);
1892
2165
  this.alignmentTimer = undefined;
1893
2166
  }
2167
+ if (this.orientationTimer) {
2168
+ clearTimeout(this.orientationTimer);
2169
+ this.orientationTimer = undefined;
2170
+ }
2171
+ // Liberar recursos de ONNX al limpiar el componente
2172
+ this.releaseOnnxResources();
1894
2173
  if (this.performanceUpdateInterval) {
1895
2174
  clearInterval(this.performanceUpdateInterval);
1896
2175
  this.performanceUpdateInterval = undefined;
1897
2176
  }
2177
+ // Clear canvas pool
2178
+ this.canvasPool = [];
1898
2179
  this.detectionBoxes = [];
1899
2180
  this.alignmentStartTime = undefined;
1900
2181
  this.hasDocumentDetected = false;
@@ -1908,11 +2189,8 @@ const JaakStamps = class {
1908
2189
  step: 'front',
1909
2190
  isCapturing: false
1910
2191
  };
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: {
2192
+ const cameraInfo = this.cameraInfoWithAutofocus;
2193
+ return (index.h("div", { key: 'f9cbca2fd6b8548a29ace7fcb972b53f402f61af', class: "detector-container" }, index.h("div", { key: '4f3cac734eb6bfd44a8de644eadca17e7893e11a', class: "video-container" }, index.h("video", { key: '5e15a38c989a72d5274281272d41ba064447ca6f', 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: '38c8ad8b691db60e9b5a2a8b641fe6febbeacfca', 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
2194
  position: 'absolute',
1917
2195
  left: `${box.x}px`,
1918
2196
  top: `${box.y}px`,
@@ -1921,9 +2199,9 @@ const JaakStamps = class {
1921
2199
  border: '2px solid #32406C',
1922
2200
  pointerEvents: 'none',
1923
2201
  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
2202
+ } })))), this.isMaskReady && (index.h("div", { key: 'c56fde56b7a86a4e8a4cf558745291ea5cde12fd', class: "overlay-mask" }, index.h("div", { key: '04ed3f01f271dc84eed594cb4b60c2453645bd41', class: "card-outline" }, index.h("div", { key: '648bd423a5e886d4768115237de5896c7223b30b', class: "side side-top" }), index.h("div", { key: 'b8878059493ca61961b9b3c82ecc18b50b79ac85', class: "side side-right" }), index.h("div", { key: '09e5ad64ff33dbd5c112bf52b71e14409ab6aa65', class: "side side-bottom" }), index.h("div", { key: 'b8ba3d83c9bc1b4a0da859d7ed4b9a15eaddcc2a', class: "side side-left" })), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("div", { key: '3279701a240f77ac96e68a34ad95c099340c9a6a', class: `guide-text ${this.showPerformanceMessage ? 'performance-warning-text' : ''}` }, this.getGuideText(captureState.step))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("div", { key: '2adf96d0a57e482372fd9586229dac2301b64fd4', class: "back-capture-section" }, index.h("div", { key: '305a147c9cfee854c60ac946a48a1d21b2792725', class: "back-capture-buttons" }, (!this.useDocumentDetector || this.performanceDegradedMode || this.showManualCaptureButton) && (index.h("button", { key: '43270a0647f39307229c540dd5130fb58b1ff4fb', class: "capture-button primary-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'capture-back' && (index.h("span", { key: 'eb4666603774564eef95a120ac13faeb9e10c8b4', class: "button-spinner" })), "Capturar Reverso")), index.h("button", { key: 'cac667d9548ae2314ad5a677c5ed90f25184f728', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'skip-back' && (index.h("span", { key: '0af7aa4106ce189ae2d79bec78ede01abe25b1e7', class: "button-spinner" })), this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
1925
2203
  ? `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" })))));
2204
+ : 'Saltar reverso')))), captureState.isVideoActive && (index.h("div", { key: 'de6e5c4fad4d4d996b894457c9af52bc71e533fb', class: "camera-controls" }, index.h("button", { key: 'cfddcf2420d5f5923a6b02ca446bbe1b8e9fc5e4', 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: '5fb73f958b9a5a6f9242032c4530b0bdab84f6c5', class: "camera-selector-dropdown" }, index.h("div", { key: '8ee2d26b1001e571e7b6c326e927926fa08287fd', class: "camera-selector-header" }, index.h("span", { key: '78350b0fb1bd933f7c8c59d8dab0b7122ffbe363' }, "Seleccionar C\u00E1mara"), index.h("button", { key: 'bb750e0ddd5a1ee5ff96bd57805a7f90b8821e9f', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), index.h("div", { key: 'd0f6343cf1b7367f3da33ec8cf64891109d923b8', 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: 'c9642fbfb0acea766d3f0542f4cc9f17f4af4d7b', class: "device-info" }, index.h("small", { key: 'b6e9507ff7addf011f339f6c22a88a96b40db68a' }, "Dispositivo: ", cameraInfo.deviceType)))), this.showManualCaptureButton && captureState.step === 'front' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (index.h("div", { key: '04ae9262409284782850b90945eb7038673e2097', class: "manual-capture-section" }, index.h("button", { key: '27c315e503ccba468b89ebd58d6b70f40ddd06ba', class: "manual-capture-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'capture-front' && (index.h("span", { key: '8f2dcd2df9a218b37eb0b8204a8724362cfeaa53', class: "button-spinner" })), "Capturar Frente"))))), captureState.isCapturing && (index.h("div", { key: '6fd6fe1d0213b01c8119a67fb8108f73227feee1', class: "capture-animation" })), captureState.showFlipAnimation && (index.h("div", { key: 'fba3816b48b99ca64510ecbefc0d41431569c9ae', class: "flip-animation" }, index.h("div", { key: 'beea1c8245b5d81f1008efc6d1b079a61c821e7f', class: "id-card-icon" }), index.h("div", { key: 'f0fbc34f619a653e2c0ade764ca99e7aa582d157', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (index.h("div", { key: 'e4a0e6d31112e3a50024d1d0430b4ad997049fe7', class: "success-animation" }, index.h("div", { key: '5dd3f781f55630bc904c81207548ae35bc79b452', class: "check-icon" }), index.h("div", { key: 'a10ebc557582325091efb0abd12d62b27a43b98c', class: "success-text" }, "\u00A1Proceso completado!"))), index.h("div", { key: '975865955dee1803b5215ecf049701c98b324855', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (index.h("div", { key: '84f6f67202e26ce80f7fc5d5f321e23b5bc55c57', class: "status-spinner" })), index.h("div", { key: 'a1ae56ed156e8feb279a1bf160f749c5d0b5ce45', class: "status-content" }, index.h("div", { key: 'd62932554cea257ce19763df26e0ba702e50eeca', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (index.h("div", { key: 'c5506fa12dd0d37d26dc26c9f2381c843330451f', class: "status-description" }, this.currentStatus.description)))), this.debug && (index.h("div", { key: '2097eda63185f336f7319014fae2ef6db1e41761', class: "performance-monitor" }, index.h("div", { key: 'd611c494c6b1a5930ef64aebdcba41e5d94c9ee6', class: "performance-expanded" }, index.h("div", { key: '800d46831e7b9bec260d9c5e67ea1a3eb9c2933e', class: "metrics-row" }, index.h("div", { key: 'dd0db1faa9050d98e71da4b5cd26a4a484c37c64', class: "metric-compact" }, index.h("span", { key: 'f0e6ea61a35ddb2ae5c2a6aff8c255a02c5c1219', class: "metric-label" }, "FPS"), index.h("span", { key: 'cbf4df4d142f6f01e45f566a8ede8fbbf29ad673', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), index.h("div", { key: '83861ad31fa46c8199a5583e4b3f8eefd44bacd6', class: "metric-compact" }, index.h("span", { key: '41b8de4cda47959dfc9df393e95fdadde534dd21', class: "metric-label" }, "MEM"), index.h("span", { key: 'b268f13cc78066e240ed158f1443113a94827aff', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), index.h("div", { key: '9f9a9d0690906b186492af23b900c5fa0f5cff89', class: "metrics-row" }, index.h("div", { key: '84fa6e453dc36e4a74e8b573447b17230766229e', class: "metric-compact" }, index.h("span", { key: 'f17d52899090c89f532d74c552dd8c5338fc2d83', class: "metric-label" }, "INF"), index.h("span", { key: '2beca0d7ce55ed574f6b01efc086c5846983e024', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), index.h("div", { key: '981e300cf076e81bfe946974f6cffd63619eacde', class: "metric-compact" }, index.h("span", { key: '3dfb0c996a69eb87b0f711322675cad69c471146', class: "metric-label" }, "FRAME"), index.h("span", { key: '3ae2ac6e90cda77f23e559f3197c3b761f9bdc40', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), index.h("div", { key: '2c51216838d87728d9d7764e5ba8a50fa02eb493', class: "metrics-row" }, index.h("div", { key: 'eda810ffb80ec38ab84583f59b26706f90a50fc9', class: "metric-compact" }, index.h("span", { key: '978ba9c0c67d6da6b29b3535af33a314bab9276a', class: "metric-label" }, "DET"), index.h("span", { key: '3e5e169fd4a3906283d8f918b5bbc7c5df6ccd0f', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), index.h("div", { key: 'b4b915c745e35c09470a4dc5edfc112d32cfb8b6', class: "metric-compact" }, index.h("span", { key: '3e2a7b88a4446813c955efb0dfa9e2958e8e1448', class: "metric-label" }, "RATE"), index.h("span", { key: 'f4f073f7ee622167a241c6a1c535ef580c13d9ab', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), index.h("div", { key: '76c556e92f6d7980539f1a30efe8751f665164fb', class: "watermark" }, index.h("img", { key: 'af23e115d6b4e92a72e55d6d98fde0e4b1d09e9b', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
1927
2205
  }
1928
2206
  // Utility methods
1929
2207
  updateDetectionBoxes(boxes) {
@@ -2004,35 +2282,18 @@ const JaakStamps = class {
2004
2282
  if (allSidesAligned && bestBox) {
2005
2283
  cardOutline?.classList.add('perfect-match');
2006
2284
  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
2285
  if (!this.hasScreenshotTaken) {
2015
2286
  const currentTime = Date.now();
2016
2287
  // Initialize alignment start time if not set
2017
2288
  if (!this.alignmentStartTime) {
2018
2289
  this.alignmentStartTime = currentTime;
2019
- this.logger.state('ALIGNMENT_TIMER_STARTED', { startTime: currentTime });
2020
2290
  }
2021
2291
  // Check if document has been aligned for the configured delay
2022
2292
  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
2293
  if (alignmentDuration >= this.captureDelay) {
2029
- this.logger.state('TRIGGERING_CAPTURE', {
2030
- alignmentDuration,
2031
- captureDelay: this.captureDelay
2032
- });
2033
2294
  this.lastDetectedBox = bestBox;
2034
- this.takeScreenshot().catch(error => {
2035
- this.logger.error('Error al tomar captura de pantalla:', error);
2295
+ this.takeScreenshot().catch(() => {
2296
+ // Handle screenshot error silently
2036
2297
  });
2037
2298
  this.hasScreenshotTaken = true;
2038
2299
  this.alignmentStartTime = undefined;
@@ -2055,24 +2316,150 @@ const JaakStamps = class {
2055
2316
  }
2056
2317
  }
2057
2318
  }
2319
+ async takeManualScreenshot() {
2320
+ console.log('🔵 Botón clicked: Capturar (Frente/Reverso)');
2321
+ // Set processing state immediately
2322
+ const captureState = this.stateManager?.getCaptureState();
2323
+ if (captureState?.step === 'front') {
2324
+ this.processingButton = 'capture-front';
2325
+ }
2326
+ else if (captureState?.step === 'back') {
2327
+ this.processingButton = 'capture-back';
2328
+ }
2329
+ console.log('🔵 processingButton set to:', this.processingButton);
2330
+ // Add small delay to show spinner
2331
+ await new Promise(resolve => setTimeout(resolve, 100));
2332
+ if (!this.videoRef) {
2333
+ this.processingButton = null;
2334
+ return;
2335
+ }
2336
+ // When using manual capture, use mask coordinates for cropping
2337
+ // Note: processingButton will be cleared inside takeScreenshotWithMaskCoordinates
2338
+ await this.takeScreenshotWithMaskCoordinates();
2339
+ }
2340
+ async takeScreenshotWithMaskCoordinates() {
2341
+ if (!this.videoRef || !this.detectionContainer)
2342
+ return;
2343
+ this.stateManager.updateCaptureState({ isCapturing: true });
2344
+ this.triggerCaptureAnimation();
2345
+ // Use pooled canvas for optimization
2346
+ const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
2347
+ const captureCtx = captureCanvas.getContext('2d', { alpha: false });
2348
+ captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
2349
+ captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
2350
+ // Calculate mask coordinates for cropping
2351
+ const container = this.detectionContainer.parentElement;
2352
+ const containerRect = container.getBoundingClientRect();
2353
+ // Get mask dimensions from CSS properties
2354
+ const maskWidthPercent = parseFloat(this.el.style.getPropertyValue('--mask-width').replace('%', '')) || 90;
2355
+ const maskHeightPercent = parseFloat(this.el.style.getPropertyValue('--mask-height').replace('%', '')) || 56.25;
2356
+ const maskCenterXPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-x').replace('%', '')) || 50;
2357
+ const maskCenterYPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-y').replace('%', '')) || 50;
2358
+ // Convert percentages to actual video coordinates
2359
+ const videoWidth = this.videoRef.videoWidth;
2360
+ const videoHeight = this.videoRef.videoHeight;
2361
+ const videoAspectRatio = videoWidth / videoHeight;
2362
+ const containerAspectRatio = containerRect.width / containerRect.height;
2363
+ let displayedVideoWidth, displayedVideoHeight;
2364
+ let videoOffsetX = 0, videoOffsetY = 0;
2365
+ if (videoAspectRatio > containerAspectRatio) {
2366
+ displayedVideoWidth = containerRect.width;
2367
+ displayedVideoHeight = containerRect.width / videoAspectRatio;
2368
+ videoOffsetY = (containerRect.height - displayedVideoHeight) / 2;
2369
+ }
2370
+ else {
2371
+ displayedVideoHeight = containerRect.height;
2372
+ displayedVideoWidth = containerRect.height * videoAspectRatio;
2373
+ videoOffsetX = (containerRect.width - displayedVideoWidth) / 2;
2374
+ }
2375
+ // Calculate mask coordinates in video space
2376
+ const maskWidthInContainer = (maskWidthPercent / 100) * containerRect.width;
2377
+ const maskHeightInContainer = (maskHeightPercent / 100) * containerRect.height;
2378
+ const maskCenterXInContainer = (maskCenterXPercent / 100) * containerRect.width;
2379
+ const maskCenterYInContainer = (maskCenterYPercent / 100) * containerRect.height;
2380
+ // Convert to video coordinates
2381
+ const scaleX = videoWidth / displayedVideoWidth;
2382
+ const scaleY = videoHeight / displayedVideoHeight;
2383
+ const maskCenterXInVideo = (maskCenterXInContainer - videoOffsetX) * scaleX;
2384
+ const maskCenterYInVideo = (maskCenterYInContainer - videoOffsetY) * scaleY;
2385
+ const maskWidthInVideo = maskWidthInContainer * scaleX;
2386
+ const maskHeightInVideo = maskHeightInContainer * scaleY;
2387
+ // Calculate crop coordinates
2388
+ const cropX = Math.max(0, maskCenterXInVideo - (maskWidthInVideo / 2) - this.cropMargin);
2389
+ const cropY = Math.max(0, maskCenterYInVideo - (maskHeightInVideo / 2) - this.cropMargin);
2390
+ const cropWidth = Math.min(maskWidthInVideo + (2 * this.cropMargin), videoWidth - cropX);
2391
+ const cropHeight = Math.min(maskHeightInVideo + (2 * this.cropMargin), videoHeight - cropY);
2392
+ // Use pooled canvas for cropped version
2393
+ const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
2394
+ const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
2395
+ croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
2396
+ croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
2397
+ const captureState = this.stateManager.getCaptureState();
2398
+ if (captureState.step === 'front') {
2399
+ this.stateManager.setCapturedImages({
2400
+ front: {
2401
+ fullFrame: captureCanvas.toDataURL('image/jpeg'),
2402
+ cropped: croppedCanvas.toDataURL('image/jpeg')
2403
+ }
2404
+ });
2405
+ // Check if document classification is enabled (independent of detector)
2406
+ if (this.useDocumentClassification) {
2407
+ try {
2408
+ const classification = await this.detectionService.classifyDocument(croppedCanvas);
2409
+ if (classification && classification.class === 'passport') {
2410
+ this.completeProcess(true);
2411
+ this.returnCanvasToPool(captureCanvas);
2412
+ this.returnCanvasToPool(croppedCanvas);
2413
+ // Clear processing button when passport is detected
2414
+ this.processingButton = null;
2415
+ return;
2416
+ }
2417
+ }
2418
+ catch (classifyError) {
2419
+ // No cambiar a manual aquí, solo registrar el error y continuar
2420
+ }
2421
+ }
2422
+ this.stateManager.updateCaptureState({
2423
+ step: 'back',
2424
+ isDetectionPaused: true,
2425
+ showFlipAnimation: true
2426
+ });
2427
+ this.triggerRerender();
2428
+ setTimeout(() => {
2429
+ this.stateManager.updateCaptureState({
2430
+ showFlipAnimation: false,
2431
+ isDetectionPaused: false
2432
+ });
2433
+ this.triggerRerender();
2434
+ this.startBackDocumentTimer();
2435
+ // Clear processing button after animation completes
2436
+ this.processingButton = null;
2437
+ }, 3000);
2438
+ }
2439
+ else if (captureState.step === 'back') {
2440
+ this.stateManager.setCapturedImages({
2441
+ back: {
2442
+ fullFrame: captureCanvas.toDataURL('image/jpeg'),
2443
+ cropped: croppedCanvas.toDataURL('image/jpeg')
2444
+ }
2445
+ });
2446
+ this.completeProcess(false);
2447
+ // Clear processing button after back capture completes
2448
+ this.processingButton = null;
2449
+ }
2450
+ // Return canvases to pool after use
2451
+ this.returnCanvasToPool(captureCanvas);
2452
+ this.returnCanvasToPool(croppedCanvas);
2453
+ }
2058
2454
  async takeScreenshot() {
2059
2455
  if (!this.videoRef || !this.lastDetectedBox)
2060
2456
  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
2457
  this.stateManager.updateCaptureState({ isCapturing: true });
2070
2458
  this.triggerCaptureAnimation();
2071
- // Create capture canvas
2072
- const captureCanvas = document.createElement('canvas');
2073
- captureCanvas.width = this.videoRef.videoWidth;
2074
- captureCanvas.height = this.videoRef.videoHeight;
2459
+ // Use pooled canvas for optimization
2460
+ const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
2075
2461
  const captureCtx = captureCanvas.getContext('2d', { alpha: false });
2462
+ captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
2076
2463
  captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
2077
2464
  // Calculate crop coordinates
2078
2465
  const INPUT_SIZE = 320;
@@ -2082,27 +2469,30 @@ const JaakStamps = class {
2082
2469
  const cropY = Math.max(0, (this.lastDetectedBox.y * scaleY) - this.cropMargin);
2083
2470
  const cropWidth = Math.min((this.lastDetectedBox.w * scaleX) + (2 * this.cropMargin), this.videoRef.videoWidth - cropX);
2084
2471
  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;
2472
+ // Use pooled canvas for cropped version
2473
+ const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
2089
2474
  const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
2475
+ croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
2090
2476
  croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
2091
2477
  const captureState = this.stateManager.getCaptureState();
2092
2478
  if (captureState.step === 'front') {
2093
2479
  this.stateManager.setCapturedImages({
2094
2480
  front: {
2095
- fullFrame: captureCanvas.toDataURL('image/png'),
2096
- cropped: croppedCanvas.toDataURL('image/png')
2481
+ fullFrame: captureCanvas.toDataURL('image/jpeg'),
2482
+ cropped: croppedCanvas.toDataURL('image/jpeg')
2097
2483
  }
2098
2484
  });
2099
2485
  // Check if document classification is enabled
2100
2486
  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;
2487
+ try {
2488
+ const classification = await this.detectionService.classifyDocument(croppedCanvas);
2489
+ if (classification && classification.class === 'passport') {
2490
+ this.completeProcess(true);
2491
+ return;
2492
+ }
2493
+ }
2494
+ catch (classifyError) {
2495
+ // No cambiar a manual aquí, solo registrar el error y continuar
2106
2496
  }
2107
2497
  }
2108
2498
  this.stateManager.updateCaptureState({
@@ -2121,12 +2511,15 @@ const JaakStamps = class {
2121
2511
  else if (captureState.step === 'back') {
2122
2512
  this.stateManager.setCapturedImages({
2123
2513
  back: {
2124
- fullFrame: captureCanvas.toDataURL('image/png'),
2125
- cropped: croppedCanvas.toDataURL('image/png')
2514
+ fullFrame: captureCanvas.toDataURL('image/jpeg'),
2515
+ cropped: croppedCanvas.toDataURL('image/jpeg')
2126
2516
  }
2127
2517
  });
2128
2518
  this.completeProcess(false);
2129
2519
  }
2520
+ // Return canvases to pool after use
2521
+ this.returnCanvasToPool(captureCanvas);
2522
+ this.returnCanvasToPool(croppedCanvas);
2130
2523
  }
2131
2524
  triggerCaptureAnimation() {
2132
2525
  const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
@@ -2136,6 +2529,138 @@ const JaakStamps = class {
2136
2529
  cardOutline?.classList.remove('capturing');
2137
2530
  }, 600);
2138
2531
  }
2532
+ // Método simplificado para cambio a modo manual
2533
+ switchToManualMode() {
2534
+ // Only switch if detector is enabled and not already switched
2535
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
2536
+ return;
2537
+ }
2538
+ // Detener todo el monitoreo de performance
2539
+ this.stopPerformanceMonitoring();
2540
+ // Liberar recursos de ONNX
2541
+ this.releaseOnnxResources();
2542
+ // Resetear máscara a blanco y limpiar detecciones
2543
+ this.resetMaskToWhite();
2544
+ this.detectionBoxes = [];
2545
+ this.hasAutoSwitchedToManual = true;
2546
+ this.performanceDegradedMode = true;
2547
+ this.showManualCaptureButton = true;
2548
+ this.showPerformanceMessage = true;
2549
+ // Hide performance message after 5 seconds
2550
+ setTimeout(() => {
2551
+ this.showPerformanceMessage = false;
2552
+ }, 5000);
2553
+ }
2554
+ // Método para cambiar a modo manual por errores de ONNX
2555
+ switchToManualModeWithWarning(warningMessage) {
2556
+ // Solo cambiar si el detector está habilitado y no se ha cambiado ya
2557
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
2558
+ return;
2559
+ }
2560
+ // No necesitamos liberar recursos ONNX porque no se han cargado aún
2561
+ // Resetear máscara a blanco y limpiar detecciones
2562
+ this.resetMaskToWhite();
2563
+ this.detectionBoxes = [];
2564
+ // Cambiar a modo manual
2565
+ this.hasAutoSwitchedToManual = true;
2566
+ this.performanceDegradedMode = true;
2567
+ this.showManualCaptureButton = true;
2568
+ this.useDocumentDetector = false; // Desactivar detector automático
2569
+ // Actualizar el estado con mensaje informativo (no error)
2570
+ this.updateStatus('Modo manual activado', warningMessage, 'active');
2571
+ // Continuar con la configuración de cámara
2572
+ this.continueWithCameraSetup();
2573
+ }
2574
+ async continueWithCameraSetup() {
2575
+ try {
2576
+ // Paso 2: Detectar y configurar dispositivos
2577
+ this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
2578
+ try {
2579
+ await this.cameraService.enumerateDevices();
2580
+ }
2581
+ catch (enumerateError) {
2582
+ throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
2583
+ }
2584
+ try {
2585
+ await this.updateCameraInfoWithAutofocus();
2586
+ }
2587
+ catch (cameraInfoError) {
2588
+ throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
2589
+ }
2590
+ // Paso 3: Configurar cámara seleccionada
2591
+ this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
2592
+ let stream;
2593
+ try {
2594
+ stream = await this.cameraService.setupCamera();
2595
+ }
2596
+ catch (setupError) {
2597
+ throw new Error(`Error al configurar cámara: ${setupError.message}`);
2598
+ }
2599
+ // Paso 4: Inicializar video
2600
+ this.updateStatus('Captura manual', 'Posicione su documento y use el botón para capturar', 'active');
2601
+ try {
2602
+ await this.initializeVideoStream(stream);
2603
+ }
2604
+ catch (videoError) {
2605
+ throw new Error(`Error al inicializar video: ${videoError.message}`);
2606
+ }
2607
+ // Paso 5: Calibrar máscara
2608
+ if (this.detectionContainer) {
2609
+ const container = this.detectionContainer.parentElement;
2610
+ const rect = container.getBoundingClientRect();
2611
+ this.updateMaskDimensions(rect);
2612
+ }
2613
+ // Finalizar
2614
+ this.startTime = Date.now();
2615
+ this.stateManager.updateCaptureState({ isLoading: false });
2616
+ // Mostrar botón manual (ya debe estar habilitado)
2617
+ this.showManualCaptureButton = true;
2618
+ }
2619
+ catch (error) {
2620
+ this.updateStatus('Error de cámara', error.message, 'error');
2621
+ throw error;
2622
+ }
2623
+ }
2624
+ switchToManualModeWithError(errorMessage) {
2625
+ // Solo cambiar si el detector está habilitado y no se ha cambiado ya
2626
+ if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
2627
+ return;
2628
+ }
2629
+ // Liberar recursos de ONNX inmediatamente
2630
+ this.releaseOnnxResources();
2631
+ // Resetear máscara a blanco y limpiar detecciones
2632
+ this.resetMaskToWhite();
2633
+ this.detectionBoxes = [];
2634
+ // Cambiar a modo manual
2635
+ this.hasAutoSwitchedToManual = true;
2636
+ this.performanceDegradedMode = true;
2637
+ this.showManualCaptureButton = true;
2638
+ this.useDocumentDetector = false; // Desactivar detector automático
2639
+ // Actualizar el estado con mensaje de error
2640
+ this.updateStatus('Modo manual activado', errorMessage, 'error');
2641
+ // Continuar con la configuración de cámara para mostrar el video
2642
+ this.continueWithCameraSetup().catch(() => {
2643
+ // Si falla la configuración de cámara, mantener el mensaje de error pero cambiar el estado
2644
+ setTimeout(() => {
2645
+ this.updateStatus('Captura manual', 'Use el botón para capturar', 'ready');
2646
+ }, 5000);
2647
+ });
2648
+ }
2649
+ stopPerformanceMonitoring() {
2650
+ // Nota: No necesitamos limpiar más variables porque ya no se usarán
2651
+ // El sistema automáticamente saltará la lógica de performance una vez que hasAutoSwitchedToManual = true
2652
+ }
2653
+ // Método para liberar recursos de ONNX cuando se cambia a modo manual
2654
+ releaseOnnxResources() {
2655
+ try {
2656
+ // Liberar recursos del servicio de detección
2657
+ if (this.detectionService) {
2658
+ this.detectionService.cleanup();
2659
+ }
2660
+ }
2661
+ catch (error) {
2662
+ }
2663
+ }
2139
2664
  completeProcess(skippedBack = false) {
2140
2665
  this.stateManager.updateCaptureState({
2141
2666
  step: 'completed',
@@ -2155,11 +2680,6 @@ const JaakStamps = class {
2155
2680
  setTimeout(() => {
2156
2681
  this.stateManager.updateCaptureState({ showSuccessAnimation: false });
2157
2682
  }, 3000);
2158
- this.logger.state('PROCESO_COMPLETADO', {
2159
- skippedBack,
2160
- totalImages: capturedImages.metadata.totalImages,
2161
- timestamp: new Date().toISOString()
2162
- });
2163
2683
  }
2164
2684
  stopDetection() {
2165
2685
  if (this.animationId) {
@@ -2168,7 +2688,6 @@ const JaakStamps = class {
2168
2688
  }
2169
2689
  this.clearBackDocumentTimer();
2170
2690
  this.detectionBoxes = [];
2171
- this.logger.state('DETECTOR_DETENIDO', { timestamp: Date.now() });
2172
2691
  }
2173
2692
  startBackDocumentTimer() {
2174
2693
  if (!this.enableBackDocumentTimer)
@@ -2197,17 +2716,28 @@ const JaakStamps = class {
2197
2716
  return; // Don't toggle if switching camera
2198
2717
  this.showCameraSelector = !this.showCameraSelector;
2199
2718
  }
2719
+ async updateCameraInfoWithAutofocus() {
2720
+ try {
2721
+ const cameraInfo = await this.cameraService.getCameraInfo();
2722
+ this.cameraInfoWithAutofocus = cameraInfo;
2723
+ }
2724
+ catch (error) {
2725
+ // Keep existing state if update fails
2726
+ }
2727
+ }
2200
2728
  async handleCameraSwitch(cameraId) {
2201
2729
  if (this.isSwitchingCamera)
2202
2730
  return; // Prevent multiple simultaneous switches
2203
2731
  try {
2204
2732
  // Close the selector immediately when user selects a camera
2205
2733
  this.showCameraSelector = false;
2734
+ // Check if user selected the same camera that's already active
2735
+ const currentCameraId = this.cameraService.getSelectedCameraId();
2736
+ if (currentCameraId === cameraId) {
2737
+ // Same camera selected, just close the selector without switching
2738
+ return;
2739
+ }
2206
2740
  this.isSwitchingCamera = true;
2207
- this.logger.state('INICIANDO_CAMBIO_CAMARA', {
2208
- from: this.cameraService.getSelectedCameraId(),
2209
- to: cameraId
2210
- });
2211
2741
  // Stop current video stream
2212
2742
  if (this.videoStream) {
2213
2743
  this.videoStream.getTracks().forEach(track => track.stop());
@@ -2218,21 +2748,17 @@ const JaakStamps = class {
2218
2748
  const newStream = await this.cameraService.setupCamera();
2219
2749
  // Update video element
2220
2750
  await this.initializeVideoStream(newStream);
2221
- this.logger.state('CAMBIO_CAMARA_EXITOSO', {
2222
- newCameraId: cameraId,
2223
- isRearCamera: this.cameraService.isRearCamera(newStream)
2224
- });
2751
+ // Update camera info with autofocus data
2752
+ await this.updateCameraInfoWithAutofocus();
2225
2753
  }
2226
2754
  catch (error) {
2227
- this.logger.error('Error al cambiar cámara:', error);
2228
2755
  // Try to restore previous camera if switch failed
2229
2756
  try {
2230
2757
  const fallbackStream = await this.cameraService.setupCamera();
2231
2758
  await this.initializeVideoStream(fallbackStream);
2232
2759
  }
2233
2760
  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');
2761
+ this.updateStatus('Error al cambiar cámara', 'No se pudo cambiar el dispositivo', 'error');
2236
2762
  }
2237
2763
  }
2238
2764
  finally {
@@ -2254,6 +2780,13 @@ const JaakStamps = class {
2254
2780
  this.detectionBoxes = [];
2255
2781
  this.alignmentStartTime = undefined;
2256
2782
  this.hasDocumentDetected = false;
2783
+ // Reset sistema simplificado
2784
+ this.processedFramesCount = 0;
2785
+ this.slowFrameCount = 0;
2786
+ // Reset performance degradation state
2787
+ this.hasAutoSwitchedToManual = false;
2788
+ this.performanceDegradedMode = false;
2789
+ this.showPerformanceMessage = false;
2257
2790
  if (this.alignmentTimer) {
2258
2791
  clearTimeout(this.alignmentTimer);
2259
2792
  this.alignmentTimer = undefined;
@@ -2273,7 +2806,7 @@ const JaakStamps = class {
2273
2806
  this.stateManager.updateCaptureState({ isVideoActive: false, isLoading: false });
2274
2807
  }
2275
2808
  this.isMaskReady = false;
2276
- this.updateStatus('Sesión finalizada', '', 'ready');
2809
+ this.updateStatus('Captura finalizada', '', 'ready');
2277
2810
  this.detectionBoxes = [];
2278
2811
  this.cleanup();
2279
2812
  }
@@ -2284,7 +2817,6 @@ const JaakStamps = class {
2284
2817
  this.performanceUpdateInterval = window.setInterval(() => {
2285
2818
  this.updatePerformanceMetrics();
2286
2819
  }, 500);
2287
- this.logger.debug('Monitor de performance inicializado');
2288
2820
  }
2289
2821
  updatePerformanceMetrics() {
2290
2822
  const currentTime = performance.now();
@@ -2327,6 +2859,84 @@ const JaakStamps = class {
2327
2859
  if (detectionsFound > 0) {
2328
2860
  this.performanceMetrics.successfulDetections++;
2329
2861
  }
2862
+ // Update performance history for adaptive frame skipping
2863
+ this.performanceHistory.push(processingTime);
2864
+ if (this.performanceHistory.length > this.PERFORMANCE_HISTORY_SIZE) {
2865
+ this.performanceHistory.shift();
2866
+ }
2867
+ }
2868
+ // Adaptive frame skipping based on performance
2869
+ getAdaptiveFrameSkip() {
2870
+ // Base conditions for high frame skip
2871
+ if (this.consecutiveFailures > 20)
2872
+ return this.MAX_FRAME_SKIP;
2873
+ if (this.performanceMetrics.inferenceTime > 200)
2874
+ return 6;
2875
+ if (this.performanceMetrics.memoryUsage > 200)
2876
+ return 5;
2877
+ // Calculate average processing time
2878
+ if (this.performanceHistory.length > 0) {
2879
+ const avgProcessingTime = this.performanceHistory.reduce((a, b) => a + b, 0) / this.performanceHistory.length;
2880
+ if (avgProcessingTime > 100)
2881
+ return 4;
2882
+ if (avgProcessingTime > 80)
2883
+ return 3;
2884
+ if (avgProcessingTime > 60)
2885
+ return this.BASE_FRAME_SKIP + 1;
2886
+ }
2887
+ // Low memory devices get higher frame skip
2888
+ if (this.performanceMetrics.memoryUsage > 150)
2889
+ return 4;
2890
+ // Performance is good, use base frame skip
2891
+ return this.BASE_FRAME_SKIP;
2892
+ }
2893
+ // Método para resetear la máscara a color blanco
2894
+ resetMaskToWhite() {
2895
+ const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
2896
+ const corners = this.el.shadowRoot?.querySelectorAll('.corner');
2897
+ const topSide = this.el.shadowRoot?.querySelector('.side-top');
2898
+ const rightSide = this.el.shadowRoot?.querySelector('.side-right');
2899
+ const bottomSide = this.el.shadowRoot?.querySelector('.side-bottom');
2900
+ const leftSide = this.el.shadowRoot?.querySelector('.side-left');
2901
+ // Remover todas las clases de alineación y estado perfecto
2902
+ cardOutline?.classList.remove('perfect-match');
2903
+ corners?.forEach(corner => corner.classList.remove('perfect-match'));
2904
+ topSide?.classList.remove('aligned');
2905
+ rightSide?.classList.remove('aligned');
2906
+ bottomSide?.classList.remove('aligned');
2907
+ leftSide?.classList.remove('aligned');
2908
+ // Resetear estado de alineación
2909
+ this.sideAlignment = { top: false, right: false, bottom: false, left: false };
2910
+ }
2911
+ // Canvas pool management for screenshots
2912
+ getPooledCanvas(width, height) {
2913
+ // Try to find a canvas with matching dimensions
2914
+ const matchingIndex = this.canvasPool.findIndex(canvas => canvas.width === width && canvas.height === height);
2915
+ if (matchingIndex !== -1) {
2916
+ return this.canvasPool.splice(matchingIndex, 1)[0];
2917
+ }
2918
+ // If no matching canvas, try to reuse any canvas and resize
2919
+ if (this.canvasPool.length > 0) {
2920
+ const canvas = this.canvasPool.pop();
2921
+ canvas.width = width;
2922
+ canvas.height = height;
2923
+ return canvas;
2924
+ }
2925
+ // Create new canvas if pool is empty
2926
+ const canvas = document.createElement('canvas');
2927
+ canvas.width = width;
2928
+ canvas.height = height;
2929
+ return canvas;
2930
+ }
2931
+ returnCanvasToPool(canvas) {
2932
+ // Only return to pool if not at max capacity
2933
+ if (this.canvasPool.length < this.MAX_CANVAS_POOL_SIZE) {
2934
+ // Clear the canvas before returning to pool
2935
+ const ctx = canvas.getContext('2d');
2936
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
2937
+ this.canvasPool.push(canvas);
2938
+ }
2939
+ // If pool is full, let the canvas be garbage collected
2330
2940
  }
2331
2941
  };
2332
2942
  JaakStamps.style = myComponentCss;