@jaak.ai/stamps 2.0.0 → 2.1.0-dev.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 (45) hide show
  1. package/README.md +11 -1
  2. package/dist/cjs/jaak-stamps-webcomponent.cjs.js +1 -1
  3. package/dist/cjs/jaak-stamps.cjs.entry.js +759 -623
  4. package/dist/cjs/jaak-stamps.cjs.entry.js.map +1 -1
  5. package/dist/cjs/jaak-stamps.entry.cjs.js.map +1 -1
  6. package/dist/cjs/loader.cjs.js +1 -1
  7. package/dist/collection/components/my-component/my-component.css +178 -11
  8. package/dist/collection/components/my-component/my-component.js +566 -250
  9. package/dist/collection/components/my-component/my-component.js.map +1 -1
  10. package/dist/collection/services/CameraService.js +156 -344
  11. package/dist/collection/services/CameraService.js.map +1 -1
  12. package/dist/collection/services/DetectionService.js +68 -38
  13. package/dist/collection/services/DetectionService.js.map +1 -1
  14. package/dist/collection/services/EventBusService.js +1 -1
  15. package/dist/collection/services/EventBusService.js.map +1 -1
  16. package/dist/collection/services/ServiceContainer.js +4 -13
  17. package/dist/collection/services/ServiceContainer.js.map +1 -1
  18. package/dist/collection/services/interfaces/ICameraService.js.map +1 -1
  19. package/dist/collection/types/component-types.js.map +1 -1
  20. package/dist/components/jaak-stamps.js +763 -626
  21. package/dist/components/jaak-stamps.js.map +1 -1
  22. package/dist/esm/jaak-stamps-webcomponent.js +1 -1
  23. package/dist/esm/jaak-stamps.entry.js +759 -623
  24. package/dist/esm/jaak-stamps.entry.js.map +1 -1
  25. package/dist/esm/loader.js +1 -1
  26. package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
  27. package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
  28. package/dist/jaak-stamps-webcomponent/p-41e88688.entry.js +2 -0
  29. package/dist/jaak-stamps-webcomponent/p-41e88688.entry.js.map +1 -0
  30. package/dist/types/components/my-component/my-component.d.ts +68 -17
  31. package/dist/types/components.d.ts +14 -8
  32. package/dist/types/services/CameraService.d.ts +12 -14
  33. package/dist/types/services/DetectionService.d.ts +10 -3
  34. package/dist/types/services/ServiceContainer.d.ts +1 -2
  35. package/dist/types/services/interfaces/ICameraService.d.ts +3 -12
  36. package/dist/types/types/component-types.d.ts +3 -0
  37. package/package.json +2 -2
  38. package/dist/collection/services/LoggerService.js +0 -40
  39. package/dist/collection/services/LoggerService.js.map +0 -1
  40. package/dist/collection/services/interfaces/ILogger.js +0 -2
  41. package/dist/collection/services/interfaces/ILogger.js.map +0 -1
  42. package/dist/jaak-stamps-webcomponent/p-2264b5b4.entry.js +0 -2
  43. package/dist/jaak-stamps-webcomponent/p-2264b5b4.entry.js.map +0 -1
  44. package/dist/types/services/LoggerService.d.ts +0 -12
  45. package/dist/types/services/interfaces/ILogger.d.ts +0 -8
@@ -1,45 +1,5 @@
1
1
  import { p as proxyCustomElement, H, c as createEvent, h } from './index.js';
2
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
- }
42
-
43
3
  class EventBusService {
44
4
  events = new Map();
45
5
  on(event, callback) {
@@ -65,7 +25,7 @@ class EventBusService {
65
25
  callback(data);
66
26
  }
67
27
  catch (error) {
68
- console.error(`Error in event callback for ${event}:`, error);
28
+ // Silently handle callback errors
69
29
  }
70
30
  });
71
31
  }
@@ -192,16 +152,20 @@ class StateManagerService {
192
152
  }
193
153
 
194
154
  class CameraService {
195
- logger;
196
155
  eventBus;
197
156
  availableCameras = [];
198
157
  selectedCameraId = null;
199
158
  deviceType = 'desktop';
200
159
  preferredCameraFacing = null;
201
160
  preferredCamera = 'auto';
202
- isManuallySelected = false;
203
- constructor(logger, eventBus, preferredCamera = 'auto') {
204
- this.logger = logger;
161
+ // Optimization: Static caches for performance
162
+ static cameraCapabilitiesCache = new Map();
163
+ static deviceEnumerationCache = null;
164
+ static CACHE_DURATION = 30000; // 30 seconds
165
+ // Stream management for optimization
166
+ currentStreamPromise;
167
+ lastStreamConstraints;
168
+ constructor(eventBus, preferredCamera = 'auto') {
205
169
  this.eventBus = eventBus;
206
170
  this.preferredCamera = preferredCamera;
207
171
  }
@@ -218,18 +182,21 @@ class CameraService {
218
182
  else {
219
183
  this.deviceType = 'desktop';
220
184
  }
221
- this.logger.state('DISPOSITIVO_DETECTADO', {
222
- deviceType: this.deviceType,
223
- userAgent: navigator.userAgent,
224
- screenDimensions: { width: window.innerWidth, height: window.innerHeight }
225
- });
226
185
  return this.deviceType;
227
186
  }
228
187
  async enumerateDevices() {
229
188
  try {
189
+ // Check cache first for optimization
190
+ const now = Date.now();
191
+ if (CameraService.deviceEnumerationCache &&
192
+ (now - CameraService.deviceEnumerationCache.timestamp) < CameraService.CACHE_DURATION) {
193
+ this.availableCameras = CameraService.deviceEnumerationCache.devices;
194
+ await this.setInitialCameraPreference();
195
+ this.eventBus.emit('camera-changed', this.selectedCameraId);
196
+ return this.availableCameras;
197
+ }
230
198
  const permissionStatus = await this.checkCameraPermission();
231
199
  if (permissionStatus === 'denied') {
232
- this.logger.error('Permiso de cámara denegado por el usuario');
233
200
  return [];
234
201
  }
235
202
  if (permissionStatus === 'prompt') {
@@ -238,24 +205,16 @@ class CameraService {
238
205
  }
239
206
  const devices = await navigator.mediaDevices.enumerateDevices();
240
207
  this.availableCameras = devices.filter(device => device.kind === 'videoinput');
241
- this.logger.state('CAMARAS_DETECTADAS', {
242
- count: this.availableCameras.length,
243
- cameras: this.availableCameras.map(cam => ({
244
- id: cam.deviceId,
245
- label: cam.label || 'Unknown Camera'
246
- }))
247
- });
248
- this.setInitialCameraPreference();
249
- this.logger.state('PREFERENCIA_INICIAL_APLICADA', {
250
- selectedCameraId: this.selectedCameraId,
251
- preferredCameraFacing: this.preferredCameraFacing,
252
- preferredCamera: this.preferredCamera
253
- });
208
+ // Cache the results for optimization
209
+ CameraService.deviceEnumerationCache = {
210
+ devices: this.availableCameras,
211
+ timestamp: now
212
+ };
213
+ await this.setInitialCameraPreference();
254
214
  this.eventBus.emit('camera-changed', this.selectedCameraId);
255
215
  return this.availableCameras;
256
216
  }
257
217
  catch (error) {
258
- this.logger.error('Error al enumerar cámaras disponibles:', error);
259
218
  this.handleCameraPermissionError(error);
260
219
  return [];
261
220
  }
@@ -276,8 +235,6 @@ class CameraService {
276
235
  }
277
236
  this.selectedCameraId = cameraId;
278
237
  this.updatePreferredFacing(camera);
279
- this.isManuallySelected = true; // Mark as manually selected
280
- this.savePreference();
281
238
  this.eventBus.emit('camera-changed', cameraId);
282
239
  }
283
240
  getPreferredFacing() {
@@ -286,14 +243,20 @@ class CameraService {
286
243
  async setupCamera(constraints) {
287
244
  try {
288
245
  const finalConstraints = constraints || await this.getMaxResolution();
289
- const stream = await navigator.mediaDevices.getUserMedia({
246
+ // Optimization: Check if we can reuse existing stream with same constraints
247
+ if (this.currentStreamPromise && this.constraintsEqual(finalConstraints, this.lastStreamConstraints)) {
248
+ return await this.currentStreamPromise;
249
+ }
250
+ // Create new stream promise for lazy loading
251
+ this.currentStreamPromise = navigator.mediaDevices.getUserMedia({
290
252
  video: finalConstraints,
291
253
  audio: false
292
254
  });
255
+ this.lastStreamConstraints = finalConstraints;
256
+ const stream = await this.currentStreamPromise;
293
257
  return stream;
294
258
  }
295
259
  catch (error) {
296
- this.logger.error('Error en setupCamera, intentando con restricciones básicas:', error);
297
260
  // Fallback to basic constraints if getMaxResolution fails
298
261
  const basicConstraints = {
299
262
  width: { ideal: 1280 },
@@ -303,65 +266,21 @@ class CameraService {
303
266
  if (this.deviceType !== 'desktop' && this.preferredCameraFacing) {
304
267
  basicConstraints.facingMode = this.preferredCameraFacing;
305
268
  }
306
- this.logger.state('USANDO_RESTRICCIONES_BASICAS', { constraints: basicConstraints });
307
- return await navigator.mediaDevices.getUserMedia({
269
+ this.currentStreamPromise = navigator.mediaDevices.getUserMedia({
308
270
  video: basicConstraints,
309
271
  audio: false
310
272
  });
273
+ this.lastStreamConstraints = basicConstraints;
274
+ return await this.currentStreamPromise;
311
275
  }
312
276
  }
313
277
  async switchCamera(cameraId) {
314
278
  const selectedCamera = this.availableCameras.find(cam => cam.deviceId === cameraId);
315
279
  if (!selectedCamera) {
316
- this.logger.warn('Cámara seleccionada no encontrada, re-enumerando dispositivos...');
317
280
  await this.enumerateDevices();
318
281
  return;
319
282
  }
320
283
  await this.setSelectedCamera(cameraId);
321
- this.logger.state('CAMARA_CAMBIADA', {
322
- label: selectedCamera.label,
323
- deviceId: selectedCamera.deviceId,
324
- isManuallySelected: this.isManuallySelected
325
- });
326
- }
327
- async flipToNextCamera() {
328
- if (!this.isMultipleCamerasAvailable())
329
- return;
330
- const currentIndex = this.availableCameras.findIndex(camera => camera.deviceId === this.selectedCameraId);
331
- const nextIndex = (currentIndex + 1) % this.availableCameras.length;
332
- const nextCamera = this.availableCameras[nextIndex];
333
- await this.switchCamera(nextCamera.deviceId);
334
- }
335
- savePreference() {
336
- try {
337
- const preference = {
338
- cameraId: this.selectedCameraId,
339
- facing: this.preferredCameraFacing,
340
- timestamp: Date.now()
341
- };
342
- localStorage.setItem('jaak-stamps-camera-preference', JSON.stringify(preference));
343
- this.logger.state('PREFERENCIA_CAMARA_GUARDADA', preference);
344
- }
345
- catch (error) {
346
- this.logger.warn('Error al guardar preferencia de cámara:', error);
347
- }
348
- }
349
- loadPreference() {
350
- try {
351
- const saved = localStorage.getItem('jaak-stamps-camera-preference');
352
- if (saved) {
353
- const preference = JSON.parse(saved);
354
- const isStillAvailable = this.availableCameras.some(camera => camera.deviceId === preference.cameraId);
355
- if (isStillAvailable) {
356
- this.selectedCameraId = preference.cameraId;
357
- this.preferredCameraFacing = preference.facing;
358
- this.logger.state('PREFERENCIA_CAMARA_CARGADA', preference);
359
- }
360
- }
361
- }
362
- catch (error) {
363
- this.logger.warn('Error al cargar preferencia de cámara:', error);
364
- }
365
284
  }
366
285
  isRearCamera(stream) {
367
286
  const videoTrack = stream.getVideoTracks()[0];
@@ -370,13 +289,18 @@ class CameraService {
370
289
  const settings = videoTrack.getSettings();
371
290
  return settings.facingMode === 'environment';
372
291
  }
373
- getCameraInfo() {
374
- return {
375
- availableCameras: this.availableCameras.map(camera => ({
292
+ async getCameraInfo() {
293
+ const availableCamerasWithAutofocus = await Promise.all(this.availableCameras.map(async (camera) => {
294
+ const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
295
+ return {
376
296
  id: camera.deviceId,
377
297
  label: camera.label || 'Unknown Camera',
378
- selected: camera.deviceId === this.selectedCameraId
379
- })),
298
+ selected: camera.deviceId === this.selectedCameraId,
299
+ hasAutofocus
300
+ };
301
+ }));
302
+ return {
303
+ availableCameras: availableCamerasWithAutofocus,
380
304
  selectedCameraId: this.selectedCameraId,
381
305
  deviceType: this.deviceType,
382
306
  isMultipleCamerasAvailable: this.isMultipleCamerasAvailable(),
@@ -392,7 +316,6 @@ class CameraService {
392
316
  return permission.state;
393
317
  }
394
318
  catch (error) {
395
- this.logger.warn('No se pudo verificar permisos de cámara:', error);
396
319
  return 'prompt';
397
320
  }
398
321
  }
@@ -400,35 +323,48 @@ class CameraService {
400
323
  this.availableCameras = [];
401
324
  this.eventBus.emit('error', new Error(`Camera permission error: ${error.message}`));
402
325
  }
403
- setInitialCameraPreference() {
326
+ async setInitialCameraPreference() {
404
327
  if (this.availableCameras.length === 0)
405
328
  return;
406
- // Clear any existing localStorage preferences to ensure fresh start
407
- localStorage.removeItem('jaak-stamps-camera-preference');
408
- this.logger.state('APLICANDO_PREFERENCIA_INICIAL', {
409
- preferredCamera: this.preferredCamera,
410
- availableCamerasCount: this.availableCameras.length
411
- });
412
- this.isManuallySelected = false; // Reset manual selection flag
413
329
  if (this.preferredCamera === 'front') {
414
- this.selectFrontCamera();
330
+ await this.selectFrontCamera();
415
331
  }
416
332
  else if (this.preferredCamera === 'back') {
417
- this.selectBackCamera();
333
+ await this.selectBackCamera();
418
334
  }
419
335
  else {
420
- this.selectAutoCamera();
336
+ await this.selectAutoCamera();
337
+ }
338
+ }
339
+ async checkCameraAutofocus(deviceId) {
340
+ try {
341
+ // Check cache first for optimization
342
+ if (CameraService.cameraCapabilitiesCache.has(deviceId)) {
343
+ const cached = CameraService.cameraCapabilitiesCache.get(deviceId);
344
+ return typeof cached === 'boolean' ? cached : false;
345
+ }
346
+ const stream = await navigator.mediaDevices.getUserMedia({
347
+ video: { deviceId: { exact: deviceId } }
348
+ });
349
+ const videoTrack = stream.getVideoTracks()[0];
350
+ const capabilities = videoTrack.getCapabilities();
351
+ stream.getTracks().forEach(track => track.stop());
352
+ const hasFocusMode = capabilities.focusMode && Array.isArray(capabilities.focusMode) && capabilities.focusMode.length > 0;
353
+ const hasAutofocus = hasFocusMode && (capabilities.focusMode.includes('continuous') ||
354
+ capabilities.focusMode.includes('auto'));
355
+ // Cache the result for future use
356
+ CameraService.cameraCapabilitiesCache.set(deviceId, hasAutofocus);
357
+ return hasAutofocus;
358
+ }
359
+ catch (error) {
360
+ // Cache negative result to avoid repeated failures
361
+ CameraService.cameraCapabilitiesCache.set(deviceId, false);
362
+ return false;
421
363
  }
422
364
  }
423
- selectFrontCamera() {
365
+ async selectFrontCamera() {
424
366
  this.preferredCameraFacing = 'user';
425
- this.logger.state('BUSCANDO_CAMARA_FRONTAL', {
426
- availableCameras: this.availableCameras.map(cam => ({
427
- id: cam.deviceId,
428
- label: cam.label
429
- }))
430
- });
431
- const frontCamera = this.availableCameras.find(camera => {
367
+ const frontCameras = this.availableCameras.filter(camera => {
432
368
  const label = camera.label.toLowerCase();
433
369
  return label.includes('front') ||
434
370
  label.includes('user') ||
@@ -436,24 +372,24 @@ class CameraService {
436
372
  label.includes('frontal') ||
437
373
  (!label.includes('back') && !label.includes('rear') && !label.includes('environment'));
438
374
  });
439
- // If not found, use the first camera (usually front on mobile)
440
- this.selectedCameraId = frontCamera ? frontCamera.deviceId : this.availableCameras[0].deviceId;
441
- this.logger.state('CAMARA_FRONTAL_SELECCIONADA', {
442
- found: !!frontCamera,
443
- label: frontCamera?.label || this.availableCameras[0].label,
444
- deviceId: this.selectedCameraId
445
- });
375
+ let selectedCamera = null;
376
+ if (frontCameras.length > 0) {
377
+ for (const camera of frontCameras) {
378
+ const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
379
+ if (hasAutofocus) {
380
+ selectedCamera = camera;
381
+ break;
382
+ }
383
+ }
384
+ if (!selectedCamera) {
385
+ selectedCamera = frontCameras[0];
386
+ }
387
+ }
388
+ this.selectedCameraId = selectedCamera ? selectedCamera.deviceId : this.availableCameras[0].deviceId;
446
389
  }
447
- selectBackCamera() {
390
+ async selectBackCamera() {
448
391
  this.preferredCameraFacing = 'environment';
449
- this.logger.state('BUSCANDO_CAMARA_TRASERA', {
450
- availableCameras: this.availableCameras.map(cam => ({
451
- id: cam.deviceId,
452
- label: cam.label
453
- }))
454
- });
455
- // Try to find rear camera with multiple detection methods
456
- let backCamera = this.availableCameras.find(camera => {
392
+ const backCameras = this.availableCameras.filter(camera => {
457
393
  const label = camera.label.toLowerCase();
458
394
  return label.includes('back') ||
459
395
  label.includes('rear') ||
@@ -461,34 +397,33 @@ class CameraService {
461
397
  label.includes('trasera') ||
462
398
  label.includes('posterior');
463
399
  });
464
- // If not found by label, try to use the last camera (usually rear on mobile)
465
- if (!backCamera && this.availableCameras.length > 1) {
466
- backCamera = this.availableCameras[this.availableCameras.length - 1];
467
- this.logger.state('USANDO_ULTIMA_CAMARA_COMO_TRASERA', {
468
- label: backCamera.label,
469
- deviceId: backCamera.deviceId
470
- });
400
+ let selectedCamera = null;
401
+ if (backCameras.length > 0) {
402
+ for (const camera of backCameras) {
403
+ const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
404
+ if (hasAutofocus) {
405
+ selectedCamera = camera;
406
+ break;
407
+ }
408
+ }
409
+ if (!selectedCamera) {
410
+ selectedCamera = backCameras[0];
411
+ }
471
412
  }
472
- this.selectedCameraId = backCamera ? backCamera.deviceId : this.availableCameras[0].deviceId;
473
- this.logger.state('CAMARA_TRASERA_SELECCIONADA', {
474
- found: !!backCamera,
475
- label: backCamera?.label || this.availableCameras[0].label,
476
- deviceId: this.selectedCameraId
477
- });
413
+ else if (this.availableCameras.length > 1) {
414
+ const lastCamera = this.availableCameras[this.availableCameras.length - 1];
415
+ const hasAutofocus = await this.checkCameraAutofocus(lastCamera.deviceId);
416
+ selectedCamera = hasAutofocus ? lastCamera : this.availableCameras[this.availableCameras.length - 1];
417
+ }
418
+ this.selectedCameraId = selectedCamera ? selectedCamera.deviceId : this.availableCameras[0].deviceId;
478
419
  }
479
- selectAutoCamera() {
420
+ async selectAutoCamera() {
480
421
  if (this.deviceType === 'mobile' || this.deviceType === 'tablet') {
481
- this.selectBackCamera();
482
- this.logger.state('CAMARA_AUTO_SELECCIONADA_MOBILE', { type: 'rear' });
422
+ await this.selectBackCamera();
483
423
  }
484
424
  else {
485
425
  // For desktop, prefer front camera (usually built-in webcam)
486
- this.selectFrontCamera();
487
- this.logger.state('CAMARA_AUTO_SELECCIONADA_DESKTOP', {
488
- type: 'front',
489
- label: this.availableCameras[0].label,
490
- deviceId: this.selectedCameraId
491
- });
426
+ await this.selectFrontCamera();
492
427
  }
493
428
  }
494
429
  updatePreferredFacing(camera) {
@@ -504,40 +439,19 @@ class CameraService {
504
439
  await this.detectDeviceType();
505
440
  }
506
441
  const videoConstraints = {};
507
- this.logger.state('CONFIGURANDO_CONSTRAINS_CAMARA', {
508
- selectedCameraId: this.selectedCameraId,
509
- preferredCameraFacing: this.preferredCameraFacing,
510
- preferredCamera: this.preferredCamera,
511
- deviceType: this.deviceType,
512
- isManuallySelected: this.isManuallySelected
513
- });
514
- if (this.isManuallySelected && this.selectedCameraId) {
515
- // When manually selected, use deviceId for exact camera selection
442
+ // Priority 1: Always use deviceId if available (ensures specific camera with autofocus)
443
+ if (this.selectedCameraId) {
516
444
  videoConstraints.deviceId = { exact: this.selectedCameraId };
517
- this.logger.state('USANDO_CAMARA_MANUAL', {
518
- deviceId: this.selectedCameraId
519
- });
520
445
  }
446
+ // Priority 2: Use facingMode as fallback when no specific camera is selected
521
447
  else if (this.preferredCameraFacing) {
522
- // Use facingMode for initial preference-based selection
523
- // Use 'ideal' instead of 'exact' to avoid OverconstrainedError on desktop
524
448
  const facingConstraint = this.deviceType === 'desktop'
525
449
  ? { ideal: this.preferredCameraFacing }
526
450
  : { exact: this.preferredCameraFacing };
527
451
  videoConstraints.facingMode = facingConstraint;
528
- this.logger.state('USANDO_FACING_CONSTRAINT', {
529
- facingMode: this.preferredCameraFacing,
530
- constraintType: this.deviceType === 'desktop' ? 'ideal' : 'exact'
531
- });
532
- }
533
- else if (this.selectedCameraId) {
534
- videoConstraints.deviceId = { exact: this.selectedCameraId };
535
- this.logger.state('USANDO_CAMARA_SELECCIONADA', {
536
- deviceId: this.selectedCameraId
537
- });
538
452
  }
453
+ // Priority 3: Fallback to preferredCamera setting
539
454
  else {
540
- // Use preferredCamera setting to determine default facing mode
541
455
  if (this.preferredCamera === 'front') {
542
456
  videoConstraints.facingMode = "user";
543
457
  }
@@ -545,24 +459,25 @@ class CameraService {
545
459
  videoConstraints.facingMode = "environment";
546
460
  }
547
461
  else {
548
- // Auto mode: use environment for mobile/tablet, user for desktop
549
462
  videoConstraints.facingMode = (this.deviceType === 'mobile' || this.deviceType === 'tablet') ? "environment" : "user";
550
463
  }
551
- this.logger.state('USANDO_PREFERENCIA_CONFIGURADA', {
552
- facingMode: videoConstraints.facingMode,
553
- preferredCamera: this.preferredCamera,
554
- deviceType: this.deviceType
555
- });
556
464
  }
557
- const tempStream = await navigator.mediaDevices.getUserMedia({
558
- video: videoConstraints
559
- });
560
- const videoTrack = tempStream.getVideoTracks()[0];
561
- const capabilities = videoTrack.getCapabilities();
562
- tempStream.getTracks().forEach(track => track.stop());
465
+ // Optimization: Use cached capabilities if available
466
+ let capabilities;
467
+ if (this.selectedCameraId && CameraService.cameraCapabilitiesCache.has(this.selectedCameraId + '_caps')) {
468
+ capabilities = JSON.parse(CameraService.cameraCapabilitiesCache.get(this.selectedCameraId + '_caps'));
469
+ }
470
+ else {
471
+ const tempStream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints });
472
+ const videoTrack = tempStream.getVideoTracks()[0];
473
+ capabilities = videoTrack.getCapabilities();
474
+ tempStream.getTracks().forEach(track => track.stop());
475
+ // Cache capabilities for future use
476
+ if (this.selectedCameraId) {
477
+ CameraService.cameraCapabilitiesCache.set(this.selectedCameraId + '_caps', JSON.stringify(capabilities));
478
+ }
479
+ }
563
480
  const constraints = { ...videoConstraints };
564
- // Enhanced focus and image quality settings
565
- this.applyAdvancedCameraSettings(constraints, capabilities);
566
481
  if (capabilities.width && capabilities.height) {
567
482
  const maxWidth = Math.min(capabilities.width.max, 1920);
568
483
  const maxHeight = Math.min(capabilities.height.max, 1080);
@@ -576,15 +491,20 @@ class CameraService {
576
491
  constraints.height = { ideal: maxHeight };
577
492
  }
578
493
  }
579
- this.logger.state('CONFIGURACION_CAMARA_OPTIMIZADA', {
580
- constraints,
581
- capabilities: this.getCapabilitiesSummary(capabilities)
582
- });
494
+ // Configure autofocus if camera has the capability
495
+ const capabilitiesAny = capabilities;
496
+ if (capabilitiesAny.focusMode && Array.isArray(capabilitiesAny.focusMode)) {
497
+ if (capabilitiesAny.focusMode.includes('continuous')) {
498
+ constraints.focusMode = 'continuous';
499
+ }
500
+ else if (capabilitiesAny.focusMode.includes('auto')) {
501
+ constraints.focusMode = 'auto';
502
+ }
503
+ }
583
504
  return constraints;
584
505
  }
585
506
  catch (err) {
586
- this.logger.warn('No se pudieron obtener capacidades de cámara, usando configuración de respaldo');
587
- // Ensure device type is detected before determining constraints
507
+ // Fallback logic with minimal temporary streams
588
508
  if (!this.deviceType || this.deviceType === 'desktop') {
589
509
  await this.detectDeviceType();
590
510
  }
@@ -593,25 +513,16 @@ class CameraService {
593
513
  width: { ideal: isTablet ? 1280 : 1920 },
594
514
  height: { ideal: isTablet ? 720 : 1080 }
595
515
  };
596
- // Apply basic focus settings even for fallback
597
- this.applyBasicFocusSettings(fallbackConstraints);
598
- if (this.isManuallySelected && this.selectedCameraId) {
599
- // When manually selected, use deviceId for exact camera selection
516
+ if (this.selectedCameraId) {
600
517
  fallbackConstraints.deviceId = { exact: this.selectedCameraId };
601
518
  }
602
519
  else if (this.preferredCameraFacing) {
603
- // Use facingMode for initial preference-based selection
604
- // Use 'ideal' instead of 'exact' to avoid OverconstrainedError on desktop
605
520
  const facingConstraint = this.deviceType === 'desktop'
606
521
  ? { ideal: this.preferredCameraFacing }
607
522
  : { exact: this.preferredCameraFacing };
608
523
  fallbackConstraints.facingMode = facingConstraint;
609
524
  }
610
- else if (this.selectedCameraId) {
611
- fallbackConstraints.deviceId = { exact: this.selectedCameraId };
612
- }
613
525
  else {
614
- // Use preferredCamera setting to determine default facing mode
615
526
  if (this.preferredCamera === 'front') {
616
527
  fallbackConstraints.facingMode = "user";
617
528
  }
@@ -619,165 +530,26 @@ class CameraService {
619
530
  fallbackConstraints.facingMode = "environment";
620
531
  }
621
532
  else {
622
- // Auto mode: use environment for mobile/tablet, user for desktop
623
533
  fallbackConstraints.facingMode = (this.deviceType === 'mobile' || this.deviceType === 'tablet') ? "environment" : "user";
624
534
  }
625
535
  }
626
536
  return fallbackConstraints;
627
537
  }
628
538
  }
629
- applyAdvancedCameraSettings(constraints, capabilities) {
630
- // Apply standard camera settings that are widely supported
631
- // Frame rate optimization
632
- if (capabilities.frameRate) {
633
- constraints.frameRate = {
634
- ideal: 30,
635
- min: 15,
636
- max: 60
637
- };
638
- }
639
- // Width and height constraints for optimal resolution
640
- if (capabilities.width && capabilities.height) {
641
- constraints.width = {
642
- ideal: Math.min(1920, capabilities.width.max || 1920),
643
- min: 640
644
- };
645
- constraints.height = {
646
- ideal: Math.min(1080, capabilities.height.max || 1080),
647
- min: 480
648
- };
649
- }
650
- // Apply enhanced constraints through advanced array for better browser compatibility
651
- const advancedConstraints = [];
652
- // Try to set optimal settings through advanced constraints
653
- const advanced = {};
654
- // Focus mode - use 'any' type to bypass TypeScript limitations
655
- if (capabilities.focusMode) {
656
- if (capabilities.focusMode.includes('continuous')) {
657
- advanced.focusMode = 'continuous';
658
- }
659
- else if (capabilities.focusMode.includes('single-shot')) {
660
- advanced.focusMode = 'single-shot';
661
- }
662
- }
663
- // Focus distance for close objects
664
- if (capabilities.focusDistance) {
665
- advanced.focusDistance = {
666
- ideal: 0.4, // 40cm - optimal for document capture
667
- min: 0.2,
668
- max: 1.0
669
- };
670
- }
671
- // Zoom control
672
- if (capabilities.zoom) {
673
- advanced.zoom = {
674
- ideal: 1.0,
675
- min: capabilities.zoom.min,
676
- max: Math.min(capabilities.zoom.max, 3.0)
677
- };
678
- }
679
- // Add advanced constraints if any were set
680
- if (Object.keys(advanced).length > 0) {
681
- advancedConstraints.push(advanced);
682
- }
683
- if (advancedConstraints.length > 0) {
684
- constraints.advanced = advancedConstraints;
685
- }
686
- }
687
- applyBasicFocusSettings(constraints) {
688
- // Apply basic focus settings when capabilities are not available
689
- // Use advanced constraints for non-standard properties
690
- const advanced = {
691
- focusMode: 'continuous',
692
- exposureMode: 'continuous',
693
- whiteBalanceMode: 'continuous'
694
- };
695
- if (!constraints.advanced) {
696
- constraints.advanced = [];
697
- }
698
- constraints.advanced.push(advanced);
699
- constraints.frameRate = { ideal: 30 };
700
- }
701
- getCapabilitiesSummary(capabilities) {
702
- return {
703
- hasAutoFocus: !!capabilities.focusMode,
704
- hasFocusDistance: !!capabilities.focusDistance,
705
- hasExposureControl: !!capabilities.exposureMode,
706
- hasWhiteBalance: !!capabilities.whiteBalanceMode,
707
- hasZoom: !!capabilities.zoom,
708
- hasTorch: capabilities.torch !== undefined,
709
- hasImageControls: !!(capabilities.brightness || capabilities.contrast || capabilities.saturation || capabilities.sharpness),
710
- resolutionRange: capabilities.width && capabilities.height ?
711
- `${capabilities.width.min}x${capabilities.height.min} - ${capabilities.width.max}x${capabilities.height.max}` : 'unknown'
712
- };
713
- }
714
- // New method to enable torch/flash for better illumination
715
- async setTorchEnabled(enabled, stream) {
716
- try {
717
- if (!stream) {
718
- this.logger.warn('No hay stream disponible para controlar el flash');
719
- return false;
720
- }
721
- const videoTrack = stream.getVideoTracks()[0];
722
- if (!videoTrack) {
723
- this.logger.warn('No hay track de video disponible');
724
- return false;
725
- }
726
- const capabilities = videoTrack.getCapabilities();
727
- if (capabilities.torch === undefined) {
728
- this.logger.warn('El dispositivo no soporta control de flash');
729
- return false;
730
- }
731
- await videoTrack.applyConstraints({
732
- advanced: [{ torch: enabled }]
733
- });
734
- this.logger.state('FLASH_CONTROLADO', { enabled });
735
- return true;
736
- }
737
- catch (error) {
738
- this.logger.error('Error al controlar el flash:', error);
539
+ // Optimization helper methods
540
+ constraintsEqual(a, b) {
541
+ if (!a || !b)
739
542
  return false;
740
- }
543
+ return JSON.stringify(a) === JSON.stringify(b);
741
544
  }
742
- // Method to manually trigger focus on a specific point
743
- async focusAtPoint(x, y, stream) {
744
- try {
745
- if (!stream) {
746
- this.logger.warn('No hay stream disponible para enfocar');
747
- return false;
748
- }
749
- const videoTrack = stream.getVideoTracks()[0];
750
- if (!videoTrack) {
751
- this.logger.warn('No hay track de video disponible');
752
- return false;
753
- }
754
- const capabilities = videoTrack.getCapabilities();
755
- if (!capabilities.focusMode || !capabilities.focusMode.includes('single-shot')) {
756
- this.logger.warn('El dispositivo no soporta enfoque manual');
757
- return false;
758
- }
759
- // Trigger single-shot focus
760
- await videoTrack.applyConstraints({
761
- advanced: [{ focusMode: 'single-shot' }]
762
- });
763
- // Return to continuous focus after a short delay
764
- setTimeout(async () => {
765
- try {
766
- await videoTrack.applyConstraints({
767
- advanced: [{ focusMode: 'continuous' }]
768
- });
769
- }
770
- catch (error) {
771
- this.logger.warn('Error al volver a enfoque continuo:', error);
772
- }
773
- }, 1000);
774
- this.logger.state('ENFOQUE_MANUAL_ACTIVADO', { x, y });
775
- return true;
776
- }
777
- catch (error) {
778
- this.logger.error('Error al enfocar manualmente:', error);
779
- return false;
780
- }
545
+ // Cleanup method for optimization
546
+ static clearCaches() {
547
+ CameraService.cameraCapabilitiesCache.clear();
548
+ CameraService.deviceEnumerationCache = null;
549
+ }
550
+ // Method to invalidate device cache when needed
551
+ invalidateDeviceCache() {
552
+ CameraService.deviceEnumerationCache = null;
781
553
  }
782
554
  }
783
555
 
@@ -856,9 +628,9 @@ class DeviceStrategyFactory {
856
628
  }
857
629
 
858
630
  class DetectionService {
859
- logger;
860
631
  debug;
861
632
  useDocumentClassification;
633
+ useDocumentDetector;
862
634
  session;
863
635
  mobileNetSession;
864
636
  mobileNetClassMap;
@@ -873,30 +645,32 @@ class DetectionService {
873
645
  preprocessCanvas;
874
646
  preprocessCtx;
875
647
  captureCanvas;
876
- constructor(logger, debug = false, useDocumentClassification = false) {
877
- this.logger = logger;
648
+ // Static canvas pool for reuse across instances
649
+ static canvasPool = new Map();
650
+ static MAX_POOL_SIZE = 5;
651
+ constructor(debug = false, useDocumentClassification = false, useDocumentDetector = true) {
878
652
  this.debug = debug;
879
653
  this.useDocumentClassification = useDocumentClassification;
654
+ this.useDocumentDetector = useDocumentDetector;
880
655
  this.deviceStrategy = DeviceStrategyFactory.createStrategy();
881
656
  this.initializeCanvasPool();
882
657
  }
883
658
  async loadModel() {
884
659
  if (this.modelLoaded || this.session) {
885
- this.logger.state('MODELO_YA_PRECARGADO', {
886
- sessionExists: !!this.session,
887
- modelPreloaded: this.modelLoaded
888
- });
660
+ return;
661
+ }
662
+ if (!this.useDocumentDetector) {
663
+ // Skip loading the detection model but mark as "loaded" for compatibility
664
+ this.modelLoaded = true;
889
665
  return;
890
666
  }
891
667
  try {
892
- this.logger.state('PRECARGANDO_MODELO_DETECCION', { modelPath: this.MODEL_PATH });
893
668
  const sessionOptions = this.deviceStrategy.getSessionOptions(this.debug);
894
669
  try {
895
670
  this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, sessionOptions);
896
671
  }
897
672
  catch (error) {
898
673
  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');
900
674
  const fallbackOptions = {
901
675
  executionProviders: ['wasm'],
902
676
  graphOptimizationLevel: 'disabled',
@@ -914,10 +688,8 @@ class DetectionService {
914
688
  }
915
689
  }
916
690
  this.modelLoaded = true;
917
- this.logger.state('MODELO_DETECCION_CARGADO_EXITOSAMENTE', { sessionCreated: !!this.session });
918
691
  }
919
692
  catch (error) {
920
- this.logger.error('Error al precargar modelo de detección:', error);
921
693
  throw error;
922
694
  }
923
695
  }
@@ -926,19 +698,14 @@ class DetectionService {
926
698
  return;
927
699
  }
928
700
  try {
929
- this.logger.state('CARGANDO_MODELO_MOBILENET', { path: this.MOBILENET_MODEL_PATH });
930
701
  // Try to load class map (optional - SqueezeNet may not need it for basic classification)
931
702
  try {
932
703
  const classResponse = await fetch(this.MOBILENET_CLASSES_PATH);
933
704
  if (classResponse.ok) {
934
705
  this.mobileNetClassMap = await classResponse.json();
935
- this.logger.state('CLASES_MOBILENET_CARGADAS', {
936
- classCount: Object.keys(this.mobileNetClassMap).length
937
- });
938
706
  }
939
707
  }
940
708
  catch (error) {
941
- this.logger.warn('No se pudo cargar el mapa de clases de MobileNet, usando clasificación básica');
942
709
  // Create a basic class map for document types
943
710
  this.mobileNetClassMap = {
944
711
  "0": "document",
@@ -955,7 +722,6 @@ class DetectionService {
955
722
  }
956
723
  catch (error) {
957
724
  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');
959
725
  const fallbackOptions = {
960
726
  executionProviders: ['wasm'],
961
727
  graphOptimizationLevel: 'disabled',
@@ -972,16 +738,16 @@ class DetectionService {
972
738
  throw error;
973
739
  }
974
740
  }
975
- this.logger.state('MODELO_MOBILENET_CARGADO_EXITOSAMENTE', {
976
- sessionCreated: !!this.mobileNetSession
977
- });
978
741
  }
979
742
  catch (error) {
980
- this.logger.error('Error al cargar modelo MobileNet:', error);
981
743
  throw error;
982
744
  }
983
745
  }
984
746
  isModelLoaded() {
747
+ if (!this.useDocumentDetector) {
748
+ // If detector is disabled, consider it "loaded" for compatibility
749
+ return this.modelLoaded;
750
+ }
985
751
  return this.modelLoaded && !!this.session;
986
752
  }
987
753
  preprocess(video) {
@@ -1006,6 +772,10 @@ class DetectionService {
1006
772
  return new window.ort.Tensor("float16", float16Data, [1, 3, this.INPUT_SIZE, this.INPUT_SIZE]);
1007
773
  }
1008
774
  async runInference(inputTensor) {
775
+ if (!this.useDocumentDetector) {
776
+ // Return empty array when document detector is disabled
777
+ return [];
778
+ }
1009
779
  if (!this.session) {
1010
780
  throw new Error('Detection model not loaded');
1011
781
  }
@@ -1030,11 +800,9 @@ class DetectionService {
1030
800
  }
1031
801
  async classifyDocument(canvas) {
1032
802
  if (!this.mobileNetSession || !this.mobileNetClassMap) {
1033
- this.logger.warn('Modelo MobileNet no está cargado, saltando clasificación');
1034
803
  return null;
1035
804
  }
1036
805
  try {
1037
- this.logger.state('CLASIFICANDO_DOCUMENTO', { timestamp: Date.now() });
1038
806
  const inputTensor = this.preprocessMobileNet(canvas);
1039
807
  const feeds = { [this.mobileNetSession.inputNames[0]]: inputTensor };
1040
808
  const results = await this.mobileNetSession.run(feeds);
@@ -1042,13 +810,6 @@ class DetectionService {
1042
810
  const maxIdx = output.reduce((bestIdx, val, idx, arr) => val > arr[bestIdx] ? idx : bestIdx, 0);
1043
811
  const confidence = output[maxIdx];
1044
812
  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
813
  return {
1053
814
  class: className,
1054
815
  confidence: confidence,
@@ -1056,7 +817,6 @@ class DetectionService {
1056
817
  };
1057
818
  }
1058
819
  catch (error) {
1059
- this.logger.error('Error al clasificar documento:', error);
1060
820
  return null;
1061
821
  }
1062
822
  }
@@ -1169,7 +929,6 @@ class DetectionService {
1169
929
  }
1170
930
  this.mobileNetClassMap = undefined;
1171
931
  this.modelLoaded = false;
1172
- this.logger.state('DETECCION_SERVICE_LIMPIADO', { timestamp: Date.now() });
1173
932
  }
1174
933
  initializeCanvasPool() {
1175
934
  this.preprocessCanvas = document.createElement('canvas');
@@ -1180,9 +939,6 @@ class DetectionService {
1180
939
  willReadFrequently: true
1181
940
  });
1182
941
  this.captureCanvas = document.createElement('canvas');
1183
- this.logger.state('CANVAS_POOL_INICIALIZADO', {
1184
- preprocessCanvasSize: this.INPUT_SIZE
1185
- });
1186
942
  }
1187
943
  cleanupCanvasPool() {
1188
944
  if (this.preprocessCanvas) {
@@ -1218,10 +974,11 @@ class DetectionService {
1218
974
  return (sign << 15) | (newExp << 10) | (frac >> 13);
1219
975
  }
1220
976
  preprocessMobileNet(canvas) {
1221
- const tempCanvas = document.createElement('canvas');
1222
- tempCanvas.width = 224;
1223
- tempCanvas.height = 224;
977
+ // Use pooled canvas instead of creating new one
978
+ const tempCanvas = this.getCanvas(224, 224);
1224
979
  const tempCtx = tempCanvas.getContext('2d');
980
+ // Clear canvas before use
981
+ tempCtx.clearRect(0, 0, 224, 224);
1225
982
  // Resize image to 224x224 for SqueezeNet (using MobileNet interface)
1226
983
  tempCtx.drawImage(canvas, 0, 0, 224, 224);
1227
984
  const imageData = tempCtx.getImageData(0, 0, 224, 224);
@@ -1237,8 +994,53 @@ class DetectionService {
1237
994
  // Blue channel
1238
995
  arr[2 * hw + i] = data[i * 4 + 2] / 255.0;
1239
996
  }
997
+ // Return canvas to pool after use
998
+ this.returnCanvas(tempCanvas);
1240
999
  return new window.ort.Tensor('float32', arr, [1, 3, 224, 224]);
1241
1000
  }
1001
+ // Canvas pool management methods for optimization
1002
+ getCanvas(width, height) {
1003
+ const key = `${width}x${height}`;
1004
+ if (!DetectionService.canvasPool.has(key)) {
1005
+ DetectionService.canvasPool.set(key, []);
1006
+ }
1007
+ const pool = DetectionService.canvasPool.get(key);
1008
+ let canvas = pool.pop();
1009
+ if (!canvas) {
1010
+ canvas = document.createElement('canvas');
1011
+ canvas.width = width;
1012
+ canvas.height = height;
1013
+ }
1014
+ else {
1015
+ // Clear the canvas before reuse
1016
+ const ctx = canvas.getContext('2d');
1017
+ ctx.clearRect(0, 0, width, height);
1018
+ }
1019
+ return canvas;
1020
+ }
1021
+ returnCanvas(canvas) {
1022
+ const key = `${canvas.width}x${canvas.height}`;
1023
+ if (!DetectionService.canvasPool.has(key)) {
1024
+ DetectionService.canvasPool.set(key, []);
1025
+ }
1026
+ const pool = DetectionService.canvasPool.get(key);
1027
+ // Only return to pool if not at max capacity
1028
+ if (pool.length < DetectionService.MAX_POOL_SIZE) {
1029
+ pool.push(canvas);
1030
+ }
1031
+ }
1032
+ // Static method to clear all canvas pools (useful for memory management)
1033
+ static clearCanvasPools() {
1034
+ DetectionService.canvasPool.clear();
1035
+ }
1036
+ // Method to get pool statistics for debugging
1037
+ getPoolStats() {
1038
+ const stats = {};
1039
+ DetectionService.canvasPool.forEach((pool, key) => {
1040
+ stats[key] = pool.length;
1041
+ });
1042
+ return stats;
1043
+ }
1242
1044
  }
1243
1045
 
1244
1046
  class ServiceContainer {
@@ -1249,13 +1051,11 @@ class ServiceContainer {
1249
1051
  initializeServices(config) {
1250
1052
  // Initialize services in dependency order
1251
1053
  const eventBus = new EventBusService();
1252
- const logger = new LoggerService(config.debug);
1253
1054
  const stateManager = new StateManagerService(eventBus);
1254
- const cameraService = new CameraService(logger, eventBus, config.preferredCamera);
1255
- const detectionService = new DetectionService(logger, config.debug, config.useDocumentClassification);
1055
+ const cameraService = new CameraService(eventBus, config.preferredCamera);
1056
+ const detectionService = new DetectionService(config.debug, config.useDocumentClassification, config.useDocumentDetector);
1256
1057
  // Register services
1257
1058
  this.services.set('eventBus', eventBus);
1258
- this.services.set('logger', logger);
1259
1059
  this.services.set('stateManager', stateManager);
1260
1060
  this.services.set('cameraService', cameraService);
1261
1061
  this.services.set('detectionService', detectionService);
@@ -1267,9 +1067,6 @@ class ServiceContainer {
1267
1067
  }
1268
1068
  return service;
1269
1069
  }
1270
- getLogger() {
1271
- return this.get('logger');
1272
- }
1273
1070
  getEventBus() {
1274
1071
  return this.get('eventBus');
1275
1072
  }
@@ -1283,10 +1080,6 @@ class ServiceContainer {
1283
1080
  return this.get('detectionService');
1284
1081
  }
1285
1082
  updateConfig(config) {
1286
- if (config.debug !== undefined) {
1287
- const logger = this.services.get('logger');
1288
- logger.setDebugMode(config.debug);
1289
- }
1290
1083
  }
1291
1084
  cleanup() {
1292
1085
  // Cleanup services in reverse order
@@ -1296,7 +1089,7 @@ class ServiceContainer {
1296
1089
  }
1297
1090
  }
1298
1091
 
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}}";
1092
+ const myComponentCss = ":host{display:block;width:100%;height:100%;font-family:system-ui, -apple-system, sans-serif;color:#1a1a1a}.detector-container{display:flex;flex-direction:column;align-items:center;width:100%;height:100%}h1{font-size:24px;font-weight:500;color:#333;margin:0 0 24px 0}.video-container{position:relative;width:100%;height:100%;background:#333;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden}video,.detection-overlay{position:absolute;width:100%;height:100%;border-radius:8px}video.mirror,.detection-overlay.mirror{transform:rotateY(180deg)}.detection-overlay{z-index:1;pointer-events:none}video{z-index:0}.detection-box{transition:opacity 0.2s ease}.status{margin-top:16px;font-size:12px;color:#666}.overlay-mask{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10;display:block;pointer-events:none}.card-outline{position:absolute;top:var(--mask-center-y, 50%);left:var(--mask-center-x, 50%);transform:translate(-50%, -50%);width:var(--mask-width, 88%);height:var(--mask-height, 55%);border:none;border-radius:4px;background:transparent;opacity:0.8}.side{position:absolute;background:#999;transition:background-color 0.3s ease;border-radius:1px}.side-top.aligned{border-left-color:#28a745;border-top-color:#28a745}.side-right.aligned{border-right-color:#28a745;border-top-color:#28a745}.side-bottom.aligned{border-left-color:#28a745;border-bottom-color:#28a745}.side-left.aligned{border-right-color:#28a745;border-bottom-color:#28a745}.side-top{top:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-right{top:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-bottom{bottom:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.side-left{bottom:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.guide-text{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);color:#ffffff;font-size:12px;font-weight:500;text-align:center;white-space:normal;padding:13px;max-width:300px;z-index:20;height:fit-content;width:fit-content;animation:slideInFromTopCentered 0.3s ease-out}.quality-indicator{position:absolute;top:20px;right:20px;display:flex;align-items:center;gap:8px;background:rgba(0, 0, 0, 0.8);padding:8px 12px;border-radius:20px;z-index:25;transition:all 0.3s ease}.quality-indicator.warning{background:rgba(255, 152, 0, 0.9)}.quality-indicator.good{background:rgba(76, 175, 80, 0.9)}.quality-score{color:#fff;font-size:12px;font-weight:600;min-width:30px;text-align:center}.quality-status{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;color:#fff;transition:all 0.3s ease}.quality-status.ready{background:#4CAF50}.quality-status.not-ready{background:#f44336}.card-outline.quality-warning{animation:qualityWarning 1s ease-in-out infinite alternate}@keyframes qualityWarning{0%{box-shadow:0 0 0 3px rgba(255, 152, 0, 0.6)}100%{box-shadow:0 0 0 6px rgba(255, 152, 0, 0.3)}}.capture-animation{position:absolute;top:0;left:0;width:100%;height:100%;background:#fff;opacity:0;z-index:30;pointer-events:none;animation:captureFlash 0.6s ease-out}@keyframes captureFlash{0%{opacity:0}15%{opacity:0.8}30%{opacity:0}45%{opacity:0.4}60%{opacity:0}100%{opacity:0}}.card-outline.capturing{animation:pulseGreen 0.6s ease-out}@keyframes pulseGreen{0%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}50%{border-color:#28a745;box-shadow:0 0 0 8px rgba(40, 167, 69, 0.6)}100%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}}.flip-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showFlipInstruction 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.id-card-icon{width:80px;height:50px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:8px;position:relative;animation:flipCard 2s ease-in-out infinite;box-shadow:0 4px 12px rgba(0, 0, 0, 0.3)}.id-card-icon::before{content:'';position:absolute;top:8px;left:8px;width:16px;height:12px;background:rgba(255, 255, 255, 0.9);border-radius:2px}.id-card-icon::after{content:'';position:absolute;top:25px;left:8px;width:64px;height:3px;background:rgba(255, 255, 255, 0.7);border-radius:1px;box-shadow:0 6px 0 rgba(255, 255, 255, 0.7), 0 12px 0 rgba(255, 255, 255, 0.7)}.flip-text{margin-top:12px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px}@keyframes showFlipInstruction{0%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}}@keyframes flipCard{0%{transform:rotateY(0deg)}50%{transform:rotateY(180deg)}100%{transform:rotateY(360deg)}}.success-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showSuccessMessage 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.check-icon{width:80px;height:80px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:50%;position:relative;animation:bounceSuccess 0.6s ease-out;box-shadow:0 4px 16px rgba(108, 117, 125, 0.4);display:flex;align-items:center;justify-content:center}.check-icon::before{content:'';position:absolute;width:20px;height:35px;border:4px solid #fff;border-top:none;border-left:none;transform:rotate(45deg);animation:drawCheck 0.4s ease-out 0.2s both}.success-text{margin-top:16px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px;animation:fadeInUp 0.5s ease-out 0.4s both}@keyframes showSuccessMessage{0%{opacity:0;transform:translate(-50%, -50%) scale(0.5)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.9)}}@keyframes bounceSuccess{0%{transform:scale(0)}50%{transform:scale(1.1)}100%{transform:scale(1)}}@keyframes drawCheck{0%{width:0;height:0}50%{width:20px;height:0}100%{width:20px;height:35px}}@keyframes fadeInUp{0%{opacity:0;transform:translateY(20px)}100%{opacity:1;transform:translateY(0)}}.skip-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:300px}.back-capture-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:320px}.back-capture-buttons{display:flex;gap:12px;align-items:center;justify-content:center;flex-wrap:wrap}.capture-button{pointer-events:auto;background:#fff;color:#333;border:none;border-radius:25px;padding:12px 24px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.3s ease}.capture-button:hover{background:#f8f9fa}.capture-button:active{background:#e9ecef;transform:translateY(1px)}.capture-button:disabled{background:#e9ecef;color:#6c757d;cursor:not-allowed;transform:none}@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}}.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;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(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)}}@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{background:#e9ecef;color:#6c757d;cursor:not-allowed;transform:none}@media (max-width: 480px){.manual-capture-section{bottom:16px;max-width:280px}.manual-capture-button{padding:10px 20px;font-size:13px}}";
1300
1093
 
1301
1094
  const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H {
1302
1095
  constructor() {
@@ -1312,6 +1105,7 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1312
1105
  maskSize = 90;
1313
1106
  cropMargin = 20;
1314
1107
  useDocumentClassification = false;
1108
+ useDocumentDetector = true;
1315
1109
  preferredCamera = 'auto';
1316
1110
  captureDelay = 1500;
1317
1111
  enableBackDocumentTimer = false;
@@ -1328,9 +1122,16 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1328
1122
  showCameraSelector = false;
1329
1123
  isSwitchingCamera = false;
1330
1124
  hasDocumentDetected = false;
1125
+ cameraInfoWithAutofocus = {
1126
+ availableCameras: [],
1127
+ selectedCameraId: null,
1128
+ deviceType: 'desktop',
1129
+ isMultipleCamerasAvailable: false,
1130
+ preferredFacing: null
1131
+ };
1331
1132
  currentStatus = {
1332
- message: 'Inicializando componente...',
1333
- description: 'Configurando servicios y cargando recursos',
1133
+ message: 'Preparando capturador...',
1134
+ description: 'Configurando herramientas de captura',
1334
1135
  type: 'initializing',
1335
1136
  isInitialized: false
1336
1137
  };
@@ -1345,9 +1146,9 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1345
1146
  detectionRate: 0
1346
1147
  };
1347
1148
  backDocumentTimerRemaining = 0;
1149
+ showManualCaptureButton = false;
1348
1150
  // Services
1349
1151
  serviceContainer;
1350
- logger;
1351
1152
  eventBus;
1352
1153
  stateManager;
1353
1154
  cameraService;
@@ -1378,19 +1179,25 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1378
1179
  lastUpdateTime: 0
1379
1180
  };
1380
1181
  performanceUpdateInterval;
1381
- // Performance optimization
1182
+ // Performance optimization with adaptive frame skipping
1382
1183
  frameSkipCounter = 0;
1383
- FRAME_SKIP = 2;
1184
+ BASE_FRAME_SKIP = 2;
1185
+ MAX_FRAME_SKIP = 8;
1384
1186
  consecutiveFailures = 0;
1385
1187
  MAX_FAILURES = 30;
1386
1188
  lastInferenceTime = 0;
1387
1189
  MIN_INFERENCE_INTERVAL = 50;
1190
+ performanceHistory = [];
1191
+ PERFORMANCE_HISTORY_SIZE = 10;
1192
+ // Canvas pool for optimized screenshot capture
1193
+ canvasPool = [];
1194
+ MAX_CANVAS_POOL_SIZE = 3;
1388
1195
  async componentDidLoad() {
1389
- this.updateStatus('Iniciando servicios...', 'Configurando módulos internos', 'initializing');
1196
+ this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
1390
1197
  await this.initializeServices();
1391
- this.updateStatus('Configurando eventos...', 'Preparando comunicación entre servicios', 'initializing');
1198
+ this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
1392
1199
  await this.setupEventListeners();
1393
- this.updateStatus('Inicializando cámara...', 'Detectando dispositivos disponibles', 'initializing');
1200
+ this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
1394
1201
  await this.initializeComponent();
1395
1202
  if (this.debug) {
1396
1203
  this.initializePerformanceMonitor();
@@ -1403,37 +1210,28 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1403
1210
  maskSize: this.maskSize,
1404
1211
  cropMargin: this.cropMargin,
1405
1212
  useDocumentClassification: this.useDocumentClassification,
1213
+ useDocumentDetector: this.useDocumentDetector,
1406
1214
  preferredCamera: this.preferredCamera,
1407
1215
  captureDelay: this.captureDelay
1408
1216
  };
1409
1217
  this.serviceContainer = new ServiceContainer(config);
1410
- this.logger = this.serviceContainer.getLogger();
1411
1218
  this.eventBus = this.serviceContainer.getEventBus();
1412
1219
  this.stateManager = this.serviceContainer.getStateManager();
1413
1220
  this.cameraService = this.serviceContainer.getCameraService();
1414
1221
  this.detectionService = this.serviceContainer.getDetectionService();
1415
- this.logger.state('SERVICIOS_INICIALIZADOS', { timestamp: Date.now() });
1416
1222
  }
1417
1223
  async setupEventListeners() {
1418
1224
  this.eventBus.on('state-changed', (data) => {
1419
1225
  this.handleStateChange(data);
1420
1226
  });
1421
- this.eventBus.on('camera-changed', (cameraId) => {
1422
- this.logger.state('CAMARA_CAMBIADA_EVENT', { cameraId });
1227
+ this.eventBus.on('camera-changed', () => {
1228
+ // Camera changed event
1423
1229
  });
1424
- this.eventBus.on('error', (error) => {
1425
- this.logger.error('Error en servicio:', error);
1230
+ this.eventBus.on('error', () => {
1231
+ // Handle service errors
1426
1232
  });
1427
1233
  }
1428
1234
  async initializeComponent() {
1429
- this.logger.state('COMPONENTE_INICIALIZANDO', {
1430
- debug: this.debug,
1431
- maskSize: this.maskSize,
1432
- cropMargin: this.cropMargin,
1433
- useDocumentClassification: this.useDocumentClassification,
1434
- preferredCamera: this.preferredCamera,
1435
- captureDelay: this.captureDelay
1436
- });
1437
1235
  this.validateProps();
1438
1236
  if (this.debug) {
1439
1237
  this.stateManager.updateCaptureState({ isLoading: true });
@@ -1442,58 +1240,67 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1442
1240
  this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura', 'initializing');
1443
1241
  await this.cameraService.detectDeviceType();
1444
1242
  await this.cameraService.enumerateDevices();
1445
- this.updateStatus('Cargando modelo IA...', 'Preparando reconocimiento de documentos', 'initializing');
1243
+ await this.updateCameraInfoWithAutofocus();
1244
+ this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'initializing');
1446
1245
  await this.loadOnnxRuntime();
1447
1246
  this.initializeResizeObserver();
1448
1247
  }
1449
1248
  validateProps() {
1450
1249
  if (this.maskSize < 50 || this.maskSize > 100) {
1451
- this.logger.warn(`Propiedad maskSize inválida. Valor: ${this.maskSize}, esperado: 50-100. Usando valor por defecto: 90`);
1452
1250
  this.maskSize = 90;
1453
1251
  }
1454
1252
  if (this.cropMargin < 0 || this.cropMargin > 100) {
1455
- this.logger.warn(`Propiedad cropMargin inválida. Valor: ${this.cropMargin}, esperado: 0-100. Usando valor por defecto: 0`);
1456
1253
  this.cropMargin = 0;
1457
1254
  }
1458
1255
  const validOptions = ['auto', 'front', 'back'];
1459
1256
  if (!validOptions.includes(this.preferredCamera)) {
1460
- this.logger.warn(`Propiedad preferredCamera inválida. Valor: ${this.preferredCamera}, esperado: ${validOptions.join(', ')}. Usando valor por defecto: 'auto'`);
1461
1257
  this.preferredCamera = 'auto';
1462
1258
  }
1463
1259
  if (this.captureDelay < 0 || this.captureDelay > 10000) {
1464
- this.logger.warn(`Propiedad captureDelay inválida. Valor: ${this.captureDelay}, esperado: 0-10000. Usando valor por defecto: 1500`);
1465
1260
  this.captureDelay = 1500;
1466
1261
  }
1467
1262
  }
1468
1263
  async loadOnnxRuntime() {
1469
1264
  if (!window.ort) {
1470
- this.updateStatus('Descargando librerías...', 'Obteniendo recursos de reconocimiento', 'initializing');
1265
+ this.updateStatus('Preparando herramientas...', 'Configurando funciones de captura', 'initializing');
1471
1266
  const script = document.createElement('script');
1472
1267
  script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
1473
1268
  document.head.appendChild(script);
1474
1269
  await new Promise((resolve) => {
1475
1270
  script.onload = () => {
1476
- setTimeout(() => {
1477
- this.finalizeInitialization();
1271
+ setTimeout(async () => {
1272
+ await this.finalizeInitialization();
1478
1273
  resolve(undefined);
1479
1274
  }, 300);
1480
1275
  };
1481
1276
  });
1482
1277
  }
1483
1278
  else {
1484
- setTimeout(() => {
1485
- this.finalizeInitialization();
1279
+ setTimeout(async () => {
1280
+ await this.finalizeInitialization();
1486
1281
  }, 300);
1487
1282
  }
1488
1283
  }
1489
- finalizeInitialization() {
1284
+ async finalizeInitialization() {
1490
1285
  this.stateManager.updateCaptureState({ isLoading: false });
1491
- this.currentStatus = {
1492
- message: 'Listo para capturar',
1493
- description: '',
1494
- type: 'ready',
1495
- isInitialized: true
1496
- };
1286
+ // Check camera permissions before showing "ready" status
1287
+ const hasPermissions = await this.checkCameraPermissions();
1288
+ if (hasPermissions) {
1289
+ this.currentStatus = {
1290
+ message: 'Listo para capturar',
1291
+ description: '',
1292
+ type: 'ready',
1293
+ isInitialized: true
1294
+ };
1295
+ }
1296
+ else {
1297
+ this.currentStatus = {
1298
+ message: 'Permisos de cámara requeridos',
1299
+ description: 'Por favor, otorgue permisos de cámara para continuar',
1300
+ type: 'error',
1301
+ isInitialized: true
1302
+ };
1303
+ }
1497
1304
  this.emitReadyEvent();
1498
1305
  }
1499
1306
  updateStatus(message, description, type = 'loading') {
@@ -1507,11 +1314,26 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1507
1314
  emitReadyEvent() {
1508
1315
  const isDocumentReady = !!window.ort && this.detectionService.isModelLoaded();
1509
1316
  this.isReady.emit(isDocumentReady);
1510
- this.logger.state('COMPONENTE_LISTO', {
1511
- ortLibraryLoaded: !!window.ort,
1512
- modelPreloaded: this.detectionService.isModelLoaded(),
1513
- isReady: isDocumentReady
1514
- });
1317
+ }
1318
+ async checkCameraPermissions() {
1319
+ try {
1320
+ if (!navigator.permissions) {
1321
+ // Fallback: try to access camera directly
1322
+ try {
1323
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
1324
+ stream.getTracks().forEach(track => track.stop());
1325
+ return true;
1326
+ }
1327
+ catch {
1328
+ return false;
1329
+ }
1330
+ }
1331
+ const permission = await navigator.permissions.query({ name: 'camera' });
1332
+ return permission.state === 'granted';
1333
+ }
1334
+ catch (error) {
1335
+ return false;
1336
+ }
1515
1337
  }
1516
1338
  handleStateChange(data) {
1517
1339
  }
@@ -1528,9 +1350,31 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1528
1350
  const container = this.detectionContainer.parentElement;
1529
1351
  const rect = container.getBoundingClientRect();
1530
1352
  this.updateMaskDimensions(rect);
1531
- this.logger.debug('Container redimensionado:', { width: rect.width, height: rect.height });
1532
1353
  }
1533
1354
  }
1355
+ getGuideText(step) {
1356
+ if (step === 'completed') {
1357
+ return 'Proceso completado';
1358
+ }
1359
+ if (step === 'front') {
1360
+ // Para el frente, si el detector manual está activo, mostrar instrucciones de captura manual
1361
+ if (this.showManualCaptureButton) {
1362
+ return 'Posicione el frente de su documento en el marco y presione el botón para capturar';
1363
+ }
1364
+ // Instrucción básica de alineación
1365
+ return 'Alinee su identificación con el marco';
1366
+ }
1367
+ else if (step === 'back') {
1368
+ // Para el reverso, mostrar instrucciones específicas según el modo
1369
+ if (this.useDocumentDetector) {
1370
+ return 'Si tu documento no tiene lado trasero, da clic en el botón para continuar con el proceso';
1371
+ }
1372
+ else {
1373
+ return 'Posicione el reverso de su documento en el marco y capture, o salte este paso';
1374
+ }
1375
+ }
1376
+ return 'Alinee su identificación con el marco';
1377
+ }
1534
1378
  updateMaskDimensions(containerRect) {
1535
1379
  if (!this.videoRef)
1536
1380
  return;
@@ -1582,16 +1426,38 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1582
1426
  this.el.style.setProperty('--mask-center-x', `${videoCenterXPercent}%`);
1583
1427
  this.el.style.setProperty('--mask-center-y', `${videoCenterYPercent}%`);
1584
1428
  this.isMaskReady = true;
1585
- this.logger.state('DIMENSIONES_MASCARA_ACTUALIZADAS', {
1586
- video: { width: videoWidth, height: videoHeight },
1587
- displayed: { width: displayedVideoWidth, height: displayedVideoHeight },
1588
- mask: { widthPercent: maskWidthPercent, heightPercent: maskHeightPercent },
1589
- center: { x: videoCenterXPercent, y: videoCenterYPercent },
1590
- offset: { x: videoOffsetX, y: videoOffsetY }
1591
- });
1592
1429
  }
1593
1430
  // PUBLIC METHODS
1431
+ // Helper method to check if component is ready for operations
1432
+ isComponentReady() {
1433
+ if (!this.currentStatus.isInitialized) {
1434
+ return {
1435
+ ready: false,
1436
+ message: 'El componente aún se está inicializando',
1437
+ status: 'initializing'
1438
+ };
1439
+ }
1440
+ if (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') {
1441
+ return {
1442
+ ready: false,
1443
+ message: 'El componente está cargando recursos',
1444
+ status: 'loading'
1445
+ };
1446
+ }
1447
+ if (!this.serviceContainer || !this.stateManager || !this.cameraService || !this.detectionService) {
1448
+ return {
1449
+ ready: false,
1450
+ message: 'Los servicios del componente no están disponibles',
1451
+ status: 'error'
1452
+ };
1453
+ }
1454
+ return { ready: true };
1455
+ }
1594
1456
  async getCapturedImages() {
1457
+ const readyCheck = this.isComponentReady();
1458
+ if (!readyCheck.ready) {
1459
+ throw new Error(readyCheck.message);
1460
+ }
1595
1461
  const state = this.stateManager.getCaptureState();
1596
1462
  if (state.step !== 'completed') {
1597
1463
  throw new Error('El proceso de captura no ha sido completado');
@@ -1599,47 +1465,128 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1599
1465
  return this.stateManager.getCapturedImages();
1600
1466
  }
1601
1467
  async isProcessCompleted() {
1468
+ const readyCheck = this.isComponentReady();
1469
+ if (!readyCheck.ready) {
1470
+ return false; // Return false instead of error for this method
1471
+ }
1602
1472
  return this.stateManager.isProcessCompleted();
1603
1473
  }
1604
1474
  async startCapture() {
1605
- await this.startDetection();
1475
+ const readyCheck = this.isComponentReady();
1476
+ if (!readyCheck.ready) {
1477
+ this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
1478
+ return { success: false, error: readyCheck.message };
1479
+ }
1480
+ try {
1481
+ await this.startDetection();
1482
+ return { success: true };
1483
+ }
1484
+ catch (error) {
1485
+ this.updateStatus('Error al iniciar captura', error.message, 'error');
1486
+ return { success: false, error: error.message };
1487
+ }
1606
1488
  }
1607
1489
  async stopCapture() {
1608
- this.exitSession();
1490
+ const readyCheck = this.isComponentReady();
1491
+ if (!readyCheck.ready) {
1492
+ return { success: false, error: readyCheck.message };
1493
+ }
1494
+ try {
1495
+ this.exitSession();
1496
+ return { success: true };
1497
+ }
1498
+ catch (error) {
1499
+ return { success: false, error: error.message };
1500
+ }
1609
1501
  }
1610
1502
  async resetCapture() {
1611
- this.resetDetection();
1503
+ const readyCheck = this.isComponentReady();
1504
+ if (!readyCheck.ready) {
1505
+ return { success: false, error: readyCheck.message };
1506
+ }
1507
+ try {
1508
+ this.resetDetection();
1509
+ return { success: true };
1510
+ }
1511
+ catch (error) {
1512
+ return { success: false, error: error.message };
1513
+ }
1612
1514
  }
1613
1515
  async skipBackCapture() {
1614
- const captureState = this.stateManager.getCaptureState();
1615
- const capturedImages = this.stateManager.getCapturedImages();
1616
- if (captureState.step === 'back' && capturedImages.front.fullFrame) {
1617
- this.completeProcess(true);
1516
+ const readyCheck = this.isComponentReady();
1517
+ if (!readyCheck.ready) {
1518
+ return { success: false, error: readyCheck.message };
1519
+ }
1520
+ try {
1521
+ const captureState = this.stateManager.getCaptureState();
1522
+ const capturedImages = this.stateManager.getCapturedImages();
1523
+ if (captureState.step === 'back' && capturedImages.front.fullFrame) {
1524
+ this.completeProcess(true);
1525
+ return { success: true };
1526
+ }
1527
+ else {
1528
+ return { success: false, error: 'No se puede saltar el reverso en el estado actual' };
1529
+ }
1530
+ }
1531
+ catch (error) {
1532
+ return { success: false, error: error.message };
1618
1533
  }
1619
1534
  }
1620
1535
  async getStatus() {
1621
- const captureState = this.stateManager.getCaptureState();
1622
- const capturedImages = this.stateManager.getCapturedImages();
1623
- return {
1624
- isVideoActive: captureState.isVideoActive,
1625
- captureStep: captureState.step,
1626
- hasImages: !!(capturedImages.front.fullFrame || capturedImages.back.fullFrame),
1627
- isProcessCompleted: this.stateManager.isProcessCompleted(),
1628
- isModelPreloaded: this.detectionService.isModelLoaded()
1629
- };
1536
+ const readyCheck = this.isComponentReady();
1537
+ if (!readyCheck.ready) {
1538
+ return {
1539
+ isVideoActive: false,
1540
+ captureStep: 'front',
1541
+ hasImages: false,
1542
+ isProcessCompleted: false,
1543
+ isModelPreloaded: false,
1544
+ componentStatus: readyCheck.status,
1545
+ componentMessage: readyCheck.message
1546
+ };
1547
+ }
1548
+ try {
1549
+ const captureState = this.stateManager.getCaptureState();
1550
+ const capturedImages = this.stateManager.getCapturedImages();
1551
+ return {
1552
+ isVideoActive: captureState.isVideoActive,
1553
+ captureStep: captureState.step,
1554
+ hasImages: !!(capturedImages.front.fullFrame || capturedImages.back.fullFrame),
1555
+ isProcessCompleted: this.stateManager.isProcessCompleted(),
1556
+ isModelPreloaded: this.detectionService.isModelLoaded(),
1557
+ componentStatus: 'ready'
1558
+ };
1559
+ }
1560
+ catch (error) {
1561
+ return {
1562
+ isVideoActive: false,
1563
+ captureStep: 'front',
1564
+ hasImages: false,
1565
+ isProcessCompleted: false,
1566
+ isModelPreloaded: false,
1567
+ componentStatus: 'error',
1568
+ componentMessage: error.message
1569
+ };
1570
+ }
1630
1571
  }
1631
1572
  async preloadModel() {
1573
+ const readyCheck = this.isComponentReady();
1574
+ if (!readyCheck.ready) {
1575
+ this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
1576
+ return { success: false, error: readyCheck.message };
1577
+ }
1632
1578
  if (this.detectionService.isModelLoaded()) {
1633
- this.logger.state('MODELO_YA_PRECARGADO');
1634
- this.updateStatus('Modelos ya cargados', '', 'ready');
1579
+ this.updateStatus('Reconocimiento listo', '', 'ready');
1635
1580
  return { success: true, message: 'Model already loaded' };
1636
1581
  }
1637
1582
  try {
1638
1583
  const loadStartTime = performance.now();
1639
- this.updateStatus('Descargando modelo de detección...', 'Obteniendo red neuronal para reconocimiento de documentos', 'loading');
1640
- this.stateManager.updateCaptureState({ isLoading: true });
1584
+ this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
1585
+ if (this.stateManager) {
1586
+ this.stateManager.updateCaptureState({ isLoading: true });
1587
+ }
1641
1588
  await this.detectionService.loadModel();
1642
- this.updateStatus('Cargando clasificador...', 'Preparando modelo de clasificación de tipos de documento', 'loading');
1589
+ this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
1643
1590
  await this.detectionService.loadClassificationModel();
1644
1591
  const loadEndTime = performance.now();
1645
1592
  const loadTime = loadEndTime - loadStartTime;
@@ -1647,80 +1594,118 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1647
1594
  if (this.debug) {
1648
1595
  this.recordOnnxPerformance(loadTime, 0);
1649
1596
  }
1650
- this.updateStatus('Optimizando modelos...', 'Configurando parámetros de rendimiento', 'loading');
1597
+ this.updateStatus('Finalizando configuración...', 'Preparando herramientas para captura', 'loading');
1651
1598
  await new Promise(resolve => setTimeout(resolve, 300));
1652
- this.updateStatus('Modelos precargados', '', 'ready');
1653
- this.stateManager.updateCaptureState({ isLoading: false });
1599
+ this.updateStatus('Reconocimiento preparado', '', 'ready');
1600
+ if (this.stateManager) {
1601
+ this.stateManager.updateCaptureState({ isLoading: false });
1602
+ }
1654
1603
  this.emitReadyEvent();
1655
- this.logger.state('MODELOS_PRECARGADOS_EXITOSAMENTE', { loadTime: Math.round(loadTime) });
1656
1604
  return { success: true, message: 'Models preloaded successfully' };
1657
1605
  }
1658
1606
  catch (error) {
1659
- this.logger.error('Error al precargar modelos:', error);
1660
- this.updateStatus('Error al cargar modelos', 'No se pudieron descargar los recursos necesarios', 'error');
1661
- this.stateManager.updateCaptureState({ isLoading: false });
1607
+ this.updateStatus('Error de configuración', 'No se pudieron preparar las herramientas necesarias', 'error');
1608
+ if (this.stateManager) {
1609
+ this.stateManager.updateCaptureState({ isLoading: false });
1610
+ }
1662
1611
  return { success: false, error: error.message };
1663
1612
  }
1664
1613
  }
1665
1614
  async getCameraInfo() {
1666
- return this.cameraService.getCameraInfo();
1615
+ const readyCheck = this.isComponentReady();
1616
+ if (!readyCheck.ready) {
1617
+ return {
1618
+ availableCameras: [],
1619
+ selectedCameraId: null,
1620
+ deviceType: 'desktop',
1621
+ isMultipleCamerasAvailable: false,
1622
+ preferredFacing: null,
1623
+ error: readyCheck.message
1624
+ };
1625
+ }
1626
+ try {
1627
+ return await this.cameraService.getCameraInfo();
1628
+ }
1629
+ catch (error) {
1630
+ return {
1631
+ availableCameras: [],
1632
+ selectedCameraId: null,
1633
+ deviceType: 'desktop',
1634
+ isMultipleCamerasAvailable: false,
1635
+ preferredFacing: null,
1636
+ error: error.message
1637
+ };
1638
+ }
1667
1639
  }
1668
1640
  async setPreferredCamera(camera) {
1669
- this.preferredCamera = camera;
1670
- this.serviceContainer.updateConfig({ preferredCamera: camera });
1671
- await this.cameraService.enumerateDevices();
1672
- const captureState = this.stateManager.getCaptureState();
1673
- if (captureState.isVideoActive) {
1674
- const selectedCameraId = this.cameraService.getSelectedCameraId();
1675
- if (selectedCameraId) {
1676
- await this.cameraService.switchCamera(selectedCameraId);
1641
+ const readyCheck = this.isComponentReady();
1642
+ if (!readyCheck.ready) {
1643
+ return {
1644
+ success: false,
1645
+ error: readyCheck.message,
1646
+ selectedCamera: null,
1647
+ availableCameras: 0
1648
+ };
1649
+ }
1650
+ try {
1651
+ this.preferredCamera = camera;
1652
+ this.serviceContainer.updateConfig({ preferredCamera: camera });
1653
+ await this.cameraService.enumerateDevices();
1654
+ await this.updateCameraInfoWithAutofocus();
1655
+ const captureState = this.stateManager.getCaptureState();
1656
+ if (captureState.isVideoActive) {
1657
+ const selectedCameraId = this.cameraService.getSelectedCameraId();
1658
+ if (selectedCameraId) {
1659
+ await this.cameraService.switchCamera(selectedCameraId);
1660
+ }
1677
1661
  }
1662
+ return {
1663
+ success: true,
1664
+ selectedCamera: this.cameraService.getSelectedCameraId(),
1665
+ availableCameras: this.cameraService.getAvailableCameras().length
1666
+ };
1667
+ }
1668
+ catch (error) {
1669
+ return {
1670
+ success: false,
1671
+ error: error.message,
1672
+ selectedCamera: null,
1673
+ availableCameras: 0
1674
+ };
1678
1675
  }
1679
- return {
1680
- success: true,
1681
- selectedCamera: this.cameraService.getSelectedCameraId(),
1682
- availableCameras: this.cameraService.getAvailableCameras().length
1683
- };
1684
1676
  }
1685
1677
  async setCaptureDelay(delay) {
1686
1678
  if (delay < 0 || delay > 10000) {
1687
- throw new Error('Capture delay must be between 0 and 10000 milliseconds');
1679
+ return {
1680
+ success: false,
1681
+ error: 'Capture delay must be between 0 and 10000 milliseconds',
1682
+ captureDelay: this.captureDelay
1683
+ };
1688
1684
  }
1685
+ // Allow setting capture delay even during initialization
1689
1686
  this.captureDelay = delay;
1690
- this.serviceContainer.updateConfig({ captureDelay: delay });
1687
+ if (this.serviceContainer) {
1688
+ this.serviceContainer.updateConfig({ captureDelay: delay });
1689
+ }
1691
1690
  return {
1692
1691
  success: true,
1693
1692
  captureDelay: this.captureDelay
1694
1693
  };
1695
1694
  }
1696
1695
  async getCaptureDelay() {
1696
+ // Always allow getting capture delay, even during initialization
1697
1697
  return this.captureDelay;
1698
1698
  }
1699
- async setTorchEnabled(enabled) {
1700
- const torchEnabled = await this.cameraService.setTorchEnabled(enabled, this.videoStream);
1701
- return {
1702
- success: torchEnabled,
1703
- enabled: torchEnabled ? enabled : false
1704
- };
1705
- }
1706
- async focusAtPoint(x, y) {
1707
- const focused = await this.cameraService.focusAtPoint(x, y, this.videoStream);
1708
- return {
1709
- success: focused,
1710
- coordinates: { x, y }
1711
- };
1712
- }
1713
1699
  // DETECTION METHODS
1714
1700
  async startDetection() {
1715
- this.logger.state('INICIANDO_DETECCION');
1716
1701
  try {
1717
1702
  // Paso 1: Verificar y cargar modelos si es necesario
1718
1703
  if (!this.detectionService.isModelLoaded()) {
1719
1704
  const loadStartTime = performance.now();
1720
- this.updateStatus('Cargando modelo de detección...', 'Descargando red neuronal para reconocimiento', 'loading');
1705
+ this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
1721
1706
  this.stateManager.updateCaptureState({ isLoading: true });
1722
1707
  await this.detectionService.loadModel();
1723
- this.updateStatus('Cargando clasificador...', 'Preparando modelo de clasificación de documentos', 'loading');
1708
+ this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
1724
1709
  await this.detectionService.loadClassificationModel();
1725
1710
  const loadEndTime = performance.now();
1726
1711
  const loadTime = loadEndTime - loadStartTime;
@@ -1728,16 +1713,21 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1728
1713
  if (this.debug) {
1729
1714
  this.recordOnnxPerformance(loadTime, 0);
1730
1715
  }
1731
- this.logger.state('MODELOS_CARGADOS_EN_DETECCION', { loadTime: Math.round(loadTime) });
1732
1716
  }
1733
1717
  // Paso 2: Detectar y configurar dispositivos
1734
- this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura disponibles', 'loading');
1718
+ this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
1735
1719
  await this.cameraService.enumerateDevices();
1720
+ await this.updateCameraInfoWithAutofocus();
1736
1721
  // Paso 3: Configurar cámara seleccionada
1737
- this.updateStatus('Configurando cámara...', 'Estableciendo resolución y parámetros óptimos', 'loading');
1722
+ this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
1738
1723
  const stream = await this.cameraService.setupCamera();
1739
1724
  // Paso 4: Inicializar video y ocultar estado inmediatamente
1740
- this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
1725
+ if (this.useDocumentDetector) {
1726
+ this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
1727
+ }
1728
+ else {
1729
+ this.updateStatus('Captura activa', 'Posicione su documento y use el botón para capturar', 'active');
1730
+ }
1741
1731
  await this.initializeVideoStream(stream);
1742
1732
  // Paso 5: Calibrar máscara
1743
1733
  if (this.detectionContainer) {
@@ -1748,11 +1738,14 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1748
1738
  // Finalizar e iniciar captura
1749
1739
  this.startTime = Date.now();
1750
1740
  this.stateManager.updateCaptureState({ isLoading: false });
1741
+ // Mostrar botón manual si el detector está deshabilitado
1742
+ if (!this.useDocumentDetector) {
1743
+ this.showManualCaptureButton = true;
1744
+ }
1751
1745
  this.detectFrame();
1752
1746
  }
1753
1747
  catch (err) {
1754
- this.logger.error('Error al inicializar detección:', err);
1755
- this.updateStatus('Error al iniciar captura', 'No se pudo completar la inicialización', 'error');
1748
+ this.updateStatus('Error al iniciar', 'No se pudo preparar la captura', 'error');
1756
1749
  this.stateManager.updateCaptureState({ isLoading: false });
1757
1750
  }
1758
1751
  }
@@ -1762,10 +1755,6 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1762
1755
  this.videoStream = stream;
1763
1756
  const isRear = this.cameraService.isRearCamera(stream);
1764
1757
  this.shouldMirrorVideo = !isRear;
1765
- this.logger.state('CAMARA_CONFIGURADA', {
1766
- isRearCamera: isRear,
1767
- shouldMirrorVideo: this.shouldMirrorVideo
1768
- });
1769
1758
  return new Promise((resolve) => {
1770
1759
  this.videoRef.onloadedmetadata = async () => {
1771
1760
  await this.videoRef.play();
@@ -1784,15 +1773,28 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1784
1773
  const captureState = this.stateManager.getCaptureState();
1785
1774
  if (!this.videoRef || !this.detectionContainer || !this.detectionService.isModelLoaded())
1786
1775
  return;
1776
+ // Show manual capture button when document detector is disabled
1777
+ if (!this.useDocumentDetector) {
1778
+ this.showManualCaptureButton = true;
1779
+ // Continue the loop for UI updates but skip detection logic
1780
+ if (captureState.step !== 'completed') {
1781
+ this.animationId = requestAnimationFrame(() => this.detectFrame());
1782
+ }
1783
+ return;
1784
+ }
1785
+ else {
1786
+ this.showManualCaptureButton = false;
1787
+ }
1787
1788
  if (captureState.isDetectionPaused) {
1788
1789
  if (captureState.step !== 'completed') {
1789
1790
  this.animationId = requestAnimationFrame(() => this.detectFrame());
1790
1791
  }
1791
1792
  return;
1792
1793
  }
1793
- // Frame skipping for performance
1794
+ // Adaptive frame skipping for performance
1794
1795
  this.frameSkipCounter++;
1795
- if (this.frameSkipCounter <= this.FRAME_SKIP) {
1796
+ const currentFrameSkip = this.getAdaptiveFrameSkip();
1797
+ if (this.frameSkipCounter <= currentFrameSkip) {
1796
1798
  if (captureState.step !== 'completed') {
1797
1799
  this.animationId = requestAnimationFrame(() => this.detectFrame());
1798
1800
  }
@@ -1868,7 +1870,6 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1868
1870
  }
1869
1871
  }
1870
1872
  catch (e) {
1871
- this.logger.error('Error en inferencia de modelo:', e);
1872
1873
  const captureState = this.stateManager.getCaptureState();
1873
1874
  if (captureState.step !== 'completed') {
1874
1875
  setTimeout(() => this.detectFrame(), 100);
@@ -1895,6 +1896,8 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1895
1896
  clearInterval(this.performanceUpdateInterval);
1896
1897
  this.performanceUpdateInterval = undefined;
1897
1898
  }
1899
+ // Clear canvas pool
1900
+ this.canvasPool = [];
1898
1901
  this.detectionBoxes = [];
1899
1902
  this.alignmentStartTime = undefined;
1900
1903
  this.hasDocumentDetected = false;
@@ -1908,11 +1911,8 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1908
1911
  step: 'front',
1909
1912
  isCapturing: false
1910
1913
  };
1911
- const cameraInfo = this.cameraService?.getCameraInfo() || {
1912
- availableCameras: [],
1913
- selectedCameraId: null,
1914
- deviceType: 'desktop'};
1915
- 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: {
1914
+ const cameraInfo = this.cameraInfoWithAutofocus;
1915
+ return (h("div", { key: 'e0b32b1b41b58e3a58b552199384080e8f2c069b', class: "detector-container" }, h("div", { key: '03c77af311ff12d936994d629d58550cbaae190c', class: "video-container" }, h("video", { key: '94f15b34f34367a10d17816a31cd147adb57bc90', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: '982be192792dfaf644eb173bfee9b26ebd229288', 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: {
1916
1916
  position: 'absolute',
1917
1917
  left: `${box.x}px`,
1918
1918
  top: `${box.y}px`,
@@ -1921,9 +1921,9 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
1921
1921
  border: '2px solid #32406C',
1922
1922
  pointerEvents: 'none',
1923
1923
  boxSizing: 'border-box'
1924
- } })))), 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
1924
+ } })))), this.isMaskReady && (h("div", { key: 'f5212ff70db65d2b836a3ac456a1701701630660', class: "overlay-mask" }, h("div", { key: '5080217f4962c2c7152def39cc978ba1c40a3a1c', class: "card-outline" }, h("div", { key: 'd37f1ca8723d79b10d1c6a3afaf54e2f2657ae79', class: "side side-top" }), h("div", { key: 'c5bb4f7d646a991f43598688ffa909c04697bb06', class: "side side-right" }), h("div", { key: '4a5040b4a236367dcdf72f4e72d497b8b6dee6c0', class: "side side-bottom" }), h("div", { key: 'c74ecef56eee8e92cd9491d693fa89a6c01bdbbd', class: "side side-left" })), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '2a2932b0c03835193ebf86ddb5f13173b1dba367', class: "guide-text" }, this.getGuideText(captureState.step))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '41fcf2d79106d3ed928fc2842cb6a6725ba4d4f6', class: "back-capture-section" }, h("div", { key: '98dbe1bbe454bad42c3e155247f25a9151aaf1f9', class: "back-capture-buttons" }, !this.useDocumentDetector && (h("button", { key: '01693bc0d6915a4d02f318268020dfbcf3a70b94', class: "capture-button primary-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: captureState.isCapturing }, captureState.isCapturing ? 'Capturando...' : 'Capturar Reverso')), h("button", { key: '0a4ba55cbbea867b936a83a16a622a3c36a225f9', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
1925
1925
  ? `Saltar reverso (${this.backDocumentTimerRemaining}s)`
1926
- : '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" })))));
1926
+ : 'Saltar reverso')))), captureState.isVideoActive && (h("div", { key: '1db208a53dea37211bdd88254e575142b7abf91f', class: "camera-controls" }, h("button", { key: '90755839358f997c3ce2f4c36a8d17d39ea5f8ad', 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: 'ef7889ab82f9eaabcabec3e3dc3ebb7850818dd4', class: "camera-selector-dropdown" }, h("div", { key: 'e28fb005316a742318be3cf8582f520392b71bba', class: "camera-selector-header" }, h("span", { key: '97338030b1aedb50c5f5a588ea03c0f19bcb0ddd' }, "Seleccionar C\u00E1mara"), h("button", { key: '1537c907dfab17c232a256f9fc9210cbd0125cc6', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: '509178119e6eed5fe0a6ea287904a53bd9fbecbc', 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: '485f7e1a97c43ed280a106bde3c6ee910625025c', class: "device-info" }, h("small", { key: 'd76edba943f4e06c14024923496555fa1fe6d845' }, "Dispositivo: ", cameraInfo.deviceType)))), this.showManualCaptureButton && captureState.step === 'front' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: 'c5184dd93aa41cff61b88ea2046b481efc87c1f2', class: "manual-capture-section" }, h("button", { key: '6e45d9a0fec60469eb926ff353cb1a440d59f5fe', class: "manual-capture-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: captureState.isCapturing }, captureState.isCapturing ? 'Capturando...' : 'Capturar Frente'))))), captureState.isCapturing && (h("div", { key: '9e296a942ba51cc1fd152e76ae406620c84ed721', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: '517db50f7273788d94d5e6db6c40f65e00e18784', class: "flip-animation" }, h("div", { key: 'fbf6a26c4cc0539bd16b45899d19984af3449610', class: "id-card-icon" }), h("div", { key: 'fb3a201864d04531a078074458aa6e7f6580d565', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: '10c694e233d26523d538ff0d2edbf6dcfb8e37c8', class: "success-animation" }, h("div", { key: '87e545650825ab805e887667281ecdf8bdb34c45', class: "check-icon" }), h("div", { key: '359da71f391d68e9437b749b949ee90685b96b3f', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '7c5533bb875eb3d9cdcc6cfdfc20e4d5bd98d103', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: 'f32df46ab83279387de606eadf53cd4a4009f449', class: "status-spinner" })), h("div", { key: 'dd640de5a3b64f5f99363318e2fc70e32ccbc7db', class: "status-content" }, h("div", { key: '618c0ab9cb0a24b0af3f6946052beab0d660fe6d', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: 'e52c2a4945c7f5a190a446ac588aa1668c21f26d', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: '61cea10bc3438fb74aa176d71c4238b070cb0f09', class: "performance-monitor" }, h("div", { key: '44f6b1accb56d99a236b22c0769f870638d9c768', class: "performance-expanded" }, h("div", { key: '6febc85e319752b0aebc0a4e4050ea6d597375a7', class: "metrics-row" }, h("div", { key: '96b81895ad3cd742c96308ed53a2126af76eb619', class: "metric-compact" }, h("span", { key: '774a11c6fed6a87db7673b018b3cdc378c5252ef', class: "metric-label" }, "FPS"), h("span", { key: 'cb866dcbea661e5f715b82c4dd760f786fd5bb2a', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: 'a935aaadb83ea400bfaa984d1d845bdfd87becc8', class: "metric-compact" }, h("span", { key: 'ffacaa2cb7838403a297145b70586e255cc845c7', class: "metric-label" }, "MEM"), h("span", { key: 'a1d7a7abec97dc0544d50b11b372f699f750b76f', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: 'bb3d156a46569a8c290bc518c42ec7957bc5b94e', class: "metrics-row" }, h("div", { key: '4cadd8f845e4c12120ed549e4a73e02c6cbd9257', class: "metric-compact" }, h("span", { key: '1fc5661222f495b79fd7eb126e04f52eecab6fb3', class: "metric-label" }, "INF"), h("span", { key: 'eca1f59d1c41ab99158eec81592bc7150bfeabe1', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: 'f708e2757adcb03e633111d96bd6786fb6b80ea6', class: "metric-compact" }, h("span", { key: 'a13099ded64180996d6f65bf215483ba83daccbc', class: "metric-label" }, "FRAME"), h("span", { key: '2765cd1f7c1a49df9de64639cc1b777c79f89f4c', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: '13aaf85a1b4b66294c4204f02d956b64665aa766', class: "metrics-row" }, h("div", { key: '40012f3f646d70c535a606ebbe581a2fe6fd2e50', class: "metric-compact" }, h("span", { key: '03f8b17746fe404485820d06a50173678db2fd25', class: "metric-label" }, "DET"), h("span", { key: 'c4f87430f142355054d9361330d6a5d508974b70', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: 'cf366a41d7f7cda6046c32cc7c514eb7b46b05d1', class: "metric-compact" }, h("span", { key: '925e7de15b96ba4dbbcdbf8a9f0e3173bae49573', class: "metric-label" }, "RATE"), h("span", { key: '9701e60c7793f2c205f3c9ffd254b6a115b8a68c', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: '636aeb9b2b8dd992d8a28a8db44b213e12514096', class: "watermark" }, h("img", { key: '0407b550d962a6113e05ad75e25a1a1bbd76d9ce', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
1927
1927
  }
1928
1928
  // Utility methods
1929
1929
  updateDetectionBoxes(boxes) {
@@ -2004,35 +2004,18 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
2004
2004
  if (allSidesAligned && bestBox) {
2005
2005
  cardOutline?.classList.add('perfect-match');
2006
2006
  corners?.forEach(corner => corner.classList.add('perfect-match'));
2007
- // Debug logging
2008
- this.logger.state('CAPTURE_EVALUATION', {
2009
- allSidesAligned: true,
2010
- hasScreenshotTaken: this.hasScreenshotTaken,
2011
- captureDelay: this.captureDelay,
2012
- alignmentStartTime: this.alignmentStartTime
2013
- });
2014
2007
  if (!this.hasScreenshotTaken) {
2015
2008
  const currentTime = Date.now();
2016
2009
  // Initialize alignment start time if not set
2017
2010
  if (!this.alignmentStartTime) {
2018
2011
  this.alignmentStartTime = currentTime;
2019
- this.logger.state('ALIGNMENT_TIMER_STARTED', { startTime: currentTime });
2020
2012
  }
2021
2013
  // Check if document has been aligned for the configured delay
2022
2014
  const alignmentDuration = currentTime - this.alignmentStartTime;
2023
- this.logger.state('ALIGNMENT_DURATION_CHECK', {
2024
- alignmentDuration,
2025
- captureDelay: this.captureDelay,
2026
- readyToCapture: alignmentDuration >= this.captureDelay
2027
- });
2028
2015
  if (alignmentDuration >= this.captureDelay) {
2029
- this.logger.state('TRIGGERING_CAPTURE', {
2030
- alignmentDuration,
2031
- captureDelay: this.captureDelay
2032
- });
2033
2016
  this.lastDetectedBox = bestBox;
2034
- this.takeScreenshot().catch(error => {
2035
- this.logger.error('Error al tomar captura de pantalla:', error);
2017
+ this.takeScreenshot().catch(() => {
2018
+ // Handle screenshot error silently
2036
2019
  });
2037
2020
  this.hasScreenshotTaken = true;
2038
2021
  this.alignmentStartTime = undefined;
@@ -2055,24 +2038,122 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
2055
2038
  }
2056
2039
  }
2057
2040
  }
2041
+ async takeManualScreenshot() {
2042
+ if (!this.videoRef)
2043
+ return;
2044
+ // When using manual capture, use mask coordinates for cropping
2045
+ await this.takeScreenshotWithMaskCoordinates();
2046
+ }
2047
+ async takeScreenshotWithMaskCoordinates() {
2048
+ if (!this.videoRef || !this.detectionContainer)
2049
+ return;
2050
+ this.stateManager.updateCaptureState({ isCapturing: true });
2051
+ this.triggerCaptureAnimation();
2052
+ // Use pooled canvas for optimization
2053
+ const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
2054
+ const captureCtx = captureCanvas.getContext('2d', { alpha: false });
2055
+ captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
2056
+ captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
2057
+ // Calculate mask coordinates for cropping
2058
+ const container = this.detectionContainer.parentElement;
2059
+ const containerRect = container.getBoundingClientRect();
2060
+ // Get mask dimensions from CSS properties
2061
+ const maskWidthPercent = parseFloat(this.el.style.getPropertyValue('--mask-width').replace('%', '')) || 90;
2062
+ const maskHeightPercent = parseFloat(this.el.style.getPropertyValue('--mask-height').replace('%', '')) || 56.25;
2063
+ const maskCenterXPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-x').replace('%', '')) || 50;
2064
+ const maskCenterYPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-y').replace('%', '')) || 50;
2065
+ // Convert percentages to actual video coordinates
2066
+ const videoWidth = this.videoRef.videoWidth;
2067
+ const videoHeight = this.videoRef.videoHeight;
2068
+ const videoAspectRatio = videoWidth / videoHeight;
2069
+ const containerAspectRatio = containerRect.width / containerRect.height;
2070
+ let displayedVideoWidth, displayedVideoHeight;
2071
+ let videoOffsetX = 0, videoOffsetY = 0;
2072
+ if (videoAspectRatio > containerAspectRatio) {
2073
+ displayedVideoWidth = containerRect.width;
2074
+ displayedVideoHeight = containerRect.width / videoAspectRatio;
2075
+ videoOffsetY = (containerRect.height - displayedVideoHeight) / 2;
2076
+ }
2077
+ else {
2078
+ displayedVideoHeight = containerRect.height;
2079
+ displayedVideoWidth = containerRect.height * videoAspectRatio;
2080
+ videoOffsetX = (containerRect.width - displayedVideoWidth) / 2;
2081
+ }
2082
+ // Calculate mask coordinates in video space
2083
+ const maskWidthInContainer = (maskWidthPercent / 100) * containerRect.width;
2084
+ const maskHeightInContainer = (maskHeightPercent / 100) * containerRect.height;
2085
+ const maskCenterXInContainer = (maskCenterXPercent / 100) * containerRect.width;
2086
+ const maskCenterYInContainer = (maskCenterYPercent / 100) * containerRect.height;
2087
+ // Convert to video coordinates
2088
+ const scaleX = videoWidth / displayedVideoWidth;
2089
+ const scaleY = videoHeight / displayedVideoHeight;
2090
+ const maskCenterXInVideo = (maskCenterXInContainer - videoOffsetX) * scaleX;
2091
+ const maskCenterYInVideo = (maskCenterYInContainer - videoOffsetY) * scaleY;
2092
+ const maskWidthInVideo = maskWidthInContainer * scaleX;
2093
+ const maskHeightInVideo = maskHeightInContainer * scaleY;
2094
+ // Calculate crop coordinates
2095
+ const cropX = Math.max(0, maskCenterXInVideo - (maskWidthInVideo / 2) - this.cropMargin);
2096
+ const cropY = Math.max(0, maskCenterYInVideo - (maskHeightInVideo / 2) - this.cropMargin);
2097
+ const cropWidth = Math.min(maskWidthInVideo + (2 * this.cropMargin), videoWidth - cropX);
2098
+ const cropHeight = Math.min(maskHeightInVideo + (2 * this.cropMargin), videoHeight - cropY);
2099
+ // Use pooled canvas for cropped version
2100
+ const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
2101
+ const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
2102
+ croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
2103
+ croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
2104
+ const captureState = this.stateManager.getCaptureState();
2105
+ if (captureState.step === 'front') {
2106
+ this.stateManager.setCapturedImages({
2107
+ front: {
2108
+ fullFrame: captureCanvas.toDataURL('image/png'),
2109
+ cropped: croppedCanvas.toDataURL('image/png')
2110
+ }
2111
+ });
2112
+ // Check if document classification is enabled (independent of detector)
2113
+ if (this.useDocumentClassification) {
2114
+ const classification = await this.detectionService.classifyDocument(croppedCanvas);
2115
+ if (classification && classification.class === 'passport') {
2116
+ this.completeProcess(true);
2117
+ this.returnCanvasToPool(captureCanvas);
2118
+ this.returnCanvasToPool(croppedCanvas);
2119
+ return;
2120
+ }
2121
+ }
2122
+ this.stateManager.updateCaptureState({
2123
+ step: 'back',
2124
+ isDetectionPaused: true,
2125
+ showFlipAnimation: true
2126
+ });
2127
+ setTimeout(() => {
2128
+ this.stateManager.updateCaptureState({
2129
+ showFlipAnimation: false,
2130
+ isDetectionPaused: false
2131
+ });
2132
+ this.startBackDocumentTimer();
2133
+ }, 3000);
2134
+ }
2135
+ else if (captureState.step === 'back') {
2136
+ this.stateManager.setCapturedImages({
2137
+ back: {
2138
+ fullFrame: captureCanvas.toDataURL('image/png'),
2139
+ cropped: croppedCanvas.toDataURL('image/png')
2140
+ }
2141
+ });
2142
+ this.completeProcess(false);
2143
+ }
2144
+ // Return canvases to pool after use
2145
+ this.returnCanvasToPool(captureCanvas);
2146
+ this.returnCanvasToPool(croppedCanvas);
2147
+ }
2058
2148
  async takeScreenshot() {
2059
2149
  if (!this.videoRef || !this.lastDetectedBox)
2060
2150
  return;
2061
- this.logger.state('INICIANDO_CAPTURA', {
2062
- captureStep: this.stateManager.getCaptureState().step,
2063
- detectedBox: this.lastDetectedBox,
2064
- videoResolution: {
2065
- width: this.videoRef.videoWidth,
2066
- height: this.videoRef.videoHeight
2067
- }
2068
- });
2069
2151
  this.stateManager.updateCaptureState({ isCapturing: true });
2070
2152
  this.triggerCaptureAnimation();
2071
- // Create capture canvas
2072
- const captureCanvas = document.createElement('canvas');
2073
- captureCanvas.width = this.videoRef.videoWidth;
2074
- captureCanvas.height = this.videoRef.videoHeight;
2153
+ // Use pooled canvas for optimization
2154
+ const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
2075
2155
  const captureCtx = captureCanvas.getContext('2d', { alpha: false });
2156
+ captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
2076
2157
  captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
2077
2158
  // Calculate crop coordinates
2078
2159
  const INPUT_SIZE = 320;
@@ -2082,11 +2163,10 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
2082
2163
  const cropY = Math.max(0, (this.lastDetectedBox.y * scaleY) - this.cropMargin);
2083
2164
  const cropWidth = Math.min((this.lastDetectedBox.w * scaleX) + (2 * this.cropMargin), this.videoRef.videoWidth - cropX);
2084
2165
  const cropHeight = Math.min((this.lastDetectedBox.h * scaleY) + (2 * this.cropMargin), this.videoRef.videoHeight - cropY);
2085
- // Create cropped version
2086
- const croppedCanvas = document.createElement('canvas');
2087
- croppedCanvas.width = cropWidth;
2088
- croppedCanvas.height = cropHeight;
2166
+ // Use pooled canvas for cropped version
2167
+ const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
2089
2168
  const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
2169
+ croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
2090
2170
  croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
2091
2171
  const captureState = this.stateManager.getCaptureState();
2092
2172
  if (captureState.step === 'front') {
@@ -2100,7 +2180,6 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
2100
2180
  if (this.useDocumentClassification) {
2101
2181
  const classification = await this.detectionService.classifyDocument(croppedCanvas);
2102
2182
  if (classification && classification.class === 'passport') {
2103
- this.logger.state('PASAPORTE_DETECTADO_SALTANDO_REVERSO', { classification: classification?.class });
2104
2183
  this.completeProcess(true);
2105
2184
  return;
2106
2185
  }
@@ -2127,6 +2206,9 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
2127
2206
  });
2128
2207
  this.completeProcess(false);
2129
2208
  }
2209
+ // Return canvases to pool after use
2210
+ this.returnCanvasToPool(captureCanvas);
2211
+ this.returnCanvasToPool(croppedCanvas);
2130
2212
  }
2131
2213
  triggerCaptureAnimation() {
2132
2214
  const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
@@ -2155,11 +2237,6 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
2155
2237
  setTimeout(() => {
2156
2238
  this.stateManager.updateCaptureState({ showSuccessAnimation: false });
2157
2239
  }, 3000);
2158
- this.logger.state('PROCESO_COMPLETADO', {
2159
- skippedBack,
2160
- totalImages: capturedImages.metadata.totalImages,
2161
- timestamp: new Date().toISOString()
2162
- });
2163
2240
  }
2164
2241
  stopDetection() {
2165
2242
  if (this.animationId) {
@@ -2168,7 +2245,6 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
2168
2245
  }
2169
2246
  this.clearBackDocumentTimer();
2170
2247
  this.detectionBoxes = [];
2171
- this.logger.state('DETECTOR_DETENIDO', { timestamp: Date.now() });
2172
2248
  }
2173
2249
  startBackDocumentTimer() {
2174
2250
  if (!this.enableBackDocumentTimer)
@@ -2197,6 +2273,15 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
2197
2273
  return; // Don't toggle if switching camera
2198
2274
  this.showCameraSelector = !this.showCameraSelector;
2199
2275
  }
2276
+ async updateCameraInfoWithAutofocus() {
2277
+ try {
2278
+ const cameraInfo = await this.cameraService.getCameraInfo();
2279
+ this.cameraInfoWithAutofocus = cameraInfo;
2280
+ }
2281
+ catch (error) {
2282
+ // Keep existing state if update fails
2283
+ }
2284
+ }
2200
2285
  async handleCameraSwitch(cameraId) {
2201
2286
  if (this.isSwitchingCamera)
2202
2287
  return; // Prevent multiple simultaneous switches
@@ -2204,10 +2289,6 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
2204
2289
  // Close the selector immediately when user selects a camera
2205
2290
  this.showCameraSelector = false;
2206
2291
  this.isSwitchingCamera = true;
2207
- this.logger.state('INICIANDO_CAMBIO_CAMARA', {
2208
- from: this.cameraService.getSelectedCameraId(),
2209
- to: cameraId
2210
- });
2211
2292
  // Stop current video stream
2212
2293
  if (this.videoStream) {
2213
2294
  this.videoStream.getTracks().forEach(track => track.stop());
@@ -2218,21 +2299,17 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
2218
2299
  const newStream = await this.cameraService.setupCamera();
2219
2300
  // Update video element
2220
2301
  await this.initializeVideoStream(newStream);
2221
- this.logger.state('CAMBIO_CAMARA_EXITOSO', {
2222
- newCameraId: cameraId,
2223
- isRearCamera: this.cameraService.isRearCamera(newStream)
2224
- });
2302
+ // Update camera info with autofocus data
2303
+ await this.updateCameraInfoWithAutofocus();
2225
2304
  }
2226
2305
  catch (error) {
2227
- this.logger.error('Error al cambiar cámara:', error);
2228
2306
  // Try to restore previous camera if switch failed
2229
2307
  try {
2230
2308
  const fallbackStream = await this.cameraService.setupCamera();
2231
2309
  await this.initializeVideoStream(fallbackStream);
2232
2310
  }
2233
2311
  catch (fallbackError) {
2234
- this.logger.error('Error al restaurar cámara anterior:', fallbackError);
2235
- this.updateStatus('Error al cambiar cámara', 'No se pudo completar el cambio de dispositivo', 'error');
2312
+ this.updateStatus('Error al cambiar cámara', 'No se pudo cambiar el dispositivo', 'error');
2236
2313
  }
2237
2314
  }
2238
2315
  finally {
@@ -2273,7 +2350,7 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
2273
2350
  this.stateManager.updateCaptureState({ isVideoActive: false, isLoading: false });
2274
2351
  }
2275
2352
  this.isMaskReady = false;
2276
- this.updateStatus('Sesión finalizada', '', 'ready');
2353
+ this.updateStatus('Captura finalizada', '', 'ready');
2277
2354
  this.detectionBoxes = [];
2278
2355
  this.cleanup();
2279
2356
  }
@@ -2284,7 +2361,6 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
2284
2361
  this.performanceUpdateInterval = window.setInterval(() => {
2285
2362
  this.updatePerformanceMetrics();
2286
2363
  }, 500);
2287
- this.logger.debug('Monitor de performance inicializado');
2288
2364
  }
2289
2365
  updatePerformanceMetrics() {
2290
2366
  const currentTime = performance.now();
@@ -2327,6 +2403,66 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
2327
2403
  if (detectionsFound > 0) {
2328
2404
  this.performanceMetrics.successfulDetections++;
2329
2405
  }
2406
+ // Update performance history for adaptive frame skipping
2407
+ this.performanceHistory.push(processingTime);
2408
+ if (this.performanceHistory.length > this.PERFORMANCE_HISTORY_SIZE) {
2409
+ this.performanceHistory.shift();
2410
+ }
2411
+ }
2412
+ // Adaptive frame skipping based on performance
2413
+ getAdaptiveFrameSkip() {
2414
+ // Base conditions for high frame skip
2415
+ if (this.consecutiveFailures > 20)
2416
+ return this.MAX_FRAME_SKIP;
2417
+ if (this.performanceMetrics.inferenceTime > 200)
2418
+ return 6;
2419
+ if (this.performanceMetrics.memoryUsage > 200)
2420
+ return 5;
2421
+ // Calculate average processing time
2422
+ if (this.performanceHistory.length > 0) {
2423
+ const avgProcessingTime = this.performanceHistory.reduce((a, b) => a + b, 0) / this.performanceHistory.length;
2424
+ if (avgProcessingTime > 100)
2425
+ return 4;
2426
+ if (avgProcessingTime > 80)
2427
+ return 3;
2428
+ if (avgProcessingTime > 60)
2429
+ return this.BASE_FRAME_SKIP + 1;
2430
+ }
2431
+ // Low memory devices get higher frame skip
2432
+ if (this.performanceMetrics.memoryUsage > 150)
2433
+ return 4;
2434
+ // Performance is good, use base frame skip
2435
+ return this.BASE_FRAME_SKIP;
2436
+ }
2437
+ // Canvas pool management for screenshots
2438
+ getPooledCanvas(width, height) {
2439
+ // Try to find a canvas with matching dimensions
2440
+ const matchingIndex = this.canvasPool.findIndex(canvas => canvas.width === width && canvas.height === height);
2441
+ if (matchingIndex !== -1) {
2442
+ return this.canvasPool.splice(matchingIndex, 1)[0];
2443
+ }
2444
+ // If no matching canvas, try to reuse any canvas and resize
2445
+ if (this.canvasPool.length > 0) {
2446
+ const canvas = this.canvasPool.pop();
2447
+ canvas.width = width;
2448
+ canvas.height = height;
2449
+ return canvas;
2450
+ }
2451
+ // Create new canvas if pool is empty
2452
+ const canvas = document.createElement('canvas');
2453
+ canvas.width = width;
2454
+ canvas.height = height;
2455
+ return canvas;
2456
+ }
2457
+ returnCanvasToPool(canvas) {
2458
+ // Only return to pool if not at max capacity
2459
+ if (this.canvasPool.length < this.MAX_CANVAS_POOL_SIZE) {
2460
+ // Clear the canvas before returning to pool
2461
+ const ctx = canvas.getContext('2d');
2462
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
2463
+ this.canvasPool.push(canvas);
2464
+ }
2465
+ // If pool is full, let the canvas be garbage collected
2330
2466
  }
2331
2467
  static get style() { return myComponentCss; }
2332
2468
  }, [1, "jaak-stamps", {
@@ -2335,6 +2471,7 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
2335
2471
  "maskSize": [2, "mask-size"],
2336
2472
  "cropMargin": [2, "crop-margin"],
2337
2473
  "useDocumentClassification": [4, "use-document-classification"],
2474
+ "useDocumentDetector": [4, "use-document-detector"],
2338
2475
  "preferredCamera": [1, "preferred-camera"],
2339
2476
  "captureDelay": [2, "capture-delay"],
2340
2477
  "enableBackDocumentTimer": [4, "enable-back-document-timer"],
@@ -2346,9 +2483,11 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
2346
2483
  "showCameraSelector": [32],
2347
2484
  "isSwitchingCamera": [32],
2348
2485
  "hasDocumentDetected": [32],
2486
+ "cameraInfoWithAutofocus": [32],
2349
2487
  "currentStatus": [32],
2350
2488
  "performanceData": [32],
2351
2489
  "backDocumentTimerRemaining": [32],
2490
+ "showManualCaptureButton": [32],
2352
2491
  "getCapturedImages": [64],
2353
2492
  "isProcessCompleted": [64],
2354
2493
  "startCapture": [64],
@@ -2360,9 +2499,7 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
2360
2499
  "getCameraInfo": [64],
2361
2500
  "setPreferredCamera": [64],
2362
2501
  "setCaptureDelay": [64],
2363
- "getCaptureDelay": [64],
2364
- "setTorchEnabled": [64],
2365
- "focusAtPoint": [64]
2502
+ "getCaptureDelay": [64]
2366
2503
  }]);
2367
2504
  function defineCustomElement$1() {
2368
2505
  if (typeof customElements === "undefined") {