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