@jaak.ai/stamps 2.0.0-dev.57 → 2.0.0-dev.58

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 (44) hide show
  1. package/dist/cjs/jaak-stamps-webcomponent.cjs.js +1 -1
  2. package/dist/cjs/jaak-stamps.cjs.entry.js +546 -611
  3. package/dist/cjs/jaak-stamps.cjs.entry.js.map +1 -1
  4. package/dist/cjs/jaak-stamps.entry.cjs.js.map +1 -1
  5. package/dist/cjs/loader.cjs.js +1 -1
  6. package/dist/collection/components/my-component/my-component.css +14 -2
  7. package/dist/collection/components/my-component/my-component.js +346 -237
  8. package/dist/collection/components/my-component/my-component.js.map +1 -1
  9. package/dist/collection/services/CameraService.js +156 -344
  10. package/dist/collection/services/CameraService.js.map +1 -1
  11. package/dist/collection/services/DetectionService.js +53 -38
  12. package/dist/collection/services/DetectionService.js.map +1 -1
  13. package/dist/collection/services/EventBusService.js +1 -1
  14. package/dist/collection/services/EventBusService.js.map +1 -1
  15. package/dist/collection/services/ServiceContainer.js +2 -12
  16. package/dist/collection/services/ServiceContainer.js.map +1 -1
  17. package/dist/collection/services/interfaces/ICameraService.js.map +1 -1
  18. package/dist/collection/types/component-types.js.map +1 -1
  19. package/dist/components/jaak-stamps.js +548 -614
  20. package/dist/components/jaak-stamps.js.map +1 -1
  21. package/dist/esm/jaak-stamps-webcomponent.js +1 -1
  22. package/dist/esm/jaak-stamps.entry.js +546 -611
  23. package/dist/esm/jaak-stamps.entry.js.map +1 -1
  24. package/dist/esm/loader.js +1 -1
  25. package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
  26. package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
  27. package/dist/jaak-stamps-webcomponent/p-39560f23.entry.js +2 -0
  28. package/dist/jaak-stamps-webcomponent/p-39560f23.entry.js.map +1 -0
  29. package/dist/types/components/my-component/my-component.d.ts +62 -17
  30. package/dist/types/components.d.ts +6 -8
  31. package/dist/types/services/CameraService.d.ts +12 -14
  32. package/dist/types/services/DetectionService.d.ts +9 -3
  33. package/dist/types/services/ServiceContainer.d.ts +0 -2
  34. package/dist/types/services/interfaces/ICameraService.d.ts +3 -12
  35. package/dist/types/types/component-types.d.ts +3 -0
  36. package/package.json +2 -2
  37. package/dist/collection/services/LoggerService.js +0 -40
  38. package/dist/collection/services/LoggerService.js.map +0 -1
  39. package/dist/collection/services/interfaces/ILogger.js +0 -2
  40. package/dist/collection/services/interfaces/ILogger.js.map +0 -1
  41. package/dist/jaak-stamps-webcomponent/p-2264b5b4.entry.js +0 -2
  42. package/dist/jaak-stamps-webcomponent/p-2264b5b4.entry.js.map +0 -1
  43. package/dist/types/services/LoggerService.d.ts +0 -12
  44. package/dist/types/services/interfaces/ILogger.d.ts +0 -8
@@ -1,45 +1,5 @@
1
1
  import { r as registerInstance, c as createEvent, a as getElement, h } from './index-BP1Q4KOg.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();
421
337
  }
422
338
  }
423
- selectFrontCamera() {
339
+ async checkCameraAutofocus(deviceId) {
340
+ try {
341
+ // Check cache first for optimization
342
+ if (CameraService.cameraCapabilitiesCache.has(deviceId)) {
343
+ const cached = CameraService.cameraCapabilitiesCache.get(deviceId);
344
+ return typeof cached === 'boolean' ? cached : false;
345
+ }
346
+ const stream = await navigator.mediaDevices.getUserMedia({
347
+ video: { deviceId: { exact: deviceId } }
348
+ });
349
+ const videoTrack = stream.getVideoTracks()[0];
350
+ const capabilities = videoTrack.getCapabilities();
351
+ stream.getTracks().forEach(track => track.stop());
352
+ const hasFocusMode = capabilities.focusMode && Array.isArray(capabilities.focusMode) && capabilities.focusMode.length > 0;
353
+ const hasAutofocus = hasFocusMode && (capabilities.focusMode.includes('continuous') ||
354
+ capabilities.focusMode.includes('auto'));
355
+ // Cache the result for future use
356
+ CameraService.cameraCapabilitiesCache.set(deviceId, hasAutofocus);
357
+ return hasAutofocus;
358
+ }
359
+ catch (error) {
360
+ // Cache negative result to avoid repeated failures
361
+ CameraService.cameraCapabilitiesCache.set(deviceId, false);
362
+ return false;
363
+ }
364
+ }
365
+ async selectFrontCamera() {
424
366
  this.preferredCameraFacing = 'user';
425
- this.logger.state('BUSCANDO_CAMARA_FRONTAL', {
426
- availableCameras: this.availableCameras.map(cam => ({
427
- id: cam.deviceId,
428
- label: cam.label
429
- }))
430
- });
431
- const frontCamera = this.availableCameras.find(camera => {
367
+ const frontCameras = this.availableCameras.filter(camera => {
432
368
  const label = camera.label.toLowerCase();
433
369
  return label.includes('front') ||
434
370
  label.includes('user') ||
@@ -436,24 +372,24 @@ class CameraService {
436
372
  label.includes('frontal') ||
437
373
  (!label.includes('back') && !label.includes('rear') && !label.includes('environment'));
438
374
  });
439
- // If not found, use the first camera (usually front on mobile)
440
- this.selectedCameraId = frontCamera ? frontCamera.deviceId : this.availableCameras[0].deviceId;
441
- this.logger.state('CAMARA_FRONTAL_SELECCIONADA', {
442
- found: !!frontCamera,
443
- label: frontCamera?.label || this.availableCameras[0].label,
444
- deviceId: this.selectedCameraId
445
- });
375
+ let selectedCamera = null;
376
+ if (frontCameras.length > 0) {
377
+ for (const camera of frontCameras) {
378
+ const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
379
+ if (hasAutofocus) {
380
+ selectedCamera = camera;
381
+ break;
382
+ }
383
+ }
384
+ if (!selectedCamera) {
385
+ selectedCamera = frontCameras[0];
386
+ }
387
+ }
388
+ this.selectedCameraId = selectedCamera ? selectedCamera.deviceId : this.availableCameras[0].deviceId;
446
389
  }
447
- selectBackCamera() {
390
+ async selectBackCamera() {
448
391
  this.preferredCameraFacing = 'environment';
449
- this.logger.state('BUSCANDO_CAMARA_TRASERA', {
450
- availableCameras: this.availableCameras.map(cam => ({
451
- id: cam.deviceId,
452
- label: cam.label
453
- }))
454
- });
455
- // Try to find rear camera with multiple detection methods
456
- let backCamera = this.availableCameras.find(camera => {
392
+ const backCameras = this.availableCameras.filter(camera => {
457
393
  const label = camera.label.toLowerCase();
458
394
  return label.includes('back') ||
459
395
  label.includes('rear') ||
@@ -461,34 +397,33 @@ class CameraService {
461
397
  label.includes('trasera') ||
462
398
  label.includes('posterior');
463
399
  });
464
- // If not found by label, try to use the last camera (usually rear on mobile)
465
- if (!backCamera && this.availableCameras.length > 1) {
466
- backCamera = this.availableCameras[this.availableCameras.length - 1];
467
- this.logger.state('USANDO_ULTIMA_CAMARA_COMO_TRASERA', {
468
- label: backCamera.label,
469
- deviceId: backCamera.deviceId
470
- });
400
+ let selectedCamera = null;
401
+ if (backCameras.length > 0) {
402
+ for (const camera of backCameras) {
403
+ const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
404
+ if (hasAutofocus) {
405
+ selectedCamera = camera;
406
+ break;
407
+ }
408
+ }
409
+ if (!selectedCamera) {
410
+ selectedCamera = backCameras[0];
411
+ }
471
412
  }
472
- this.selectedCameraId = backCamera ? backCamera.deviceId : this.availableCameras[0].deviceId;
473
- this.logger.state('CAMARA_TRASERA_SELECCIONADA', {
474
- found: !!backCamera,
475
- label: backCamera?.label || this.availableCameras[0].label,
476
- deviceId: this.selectedCameraId
477
- });
413
+ else if (this.availableCameras.length > 1) {
414
+ const lastCamera = this.availableCameras[this.availableCameras.length - 1];
415
+ const hasAutofocus = await this.checkCameraAutofocus(lastCamera.deviceId);
416
+ selectedCamera = hasAutofocus ? lastCamera : this.availableCameras[this.availableCameras.length - 1];
417
+ }
418
+ this.selectedCameraId = selectedCamera ? selectedCamera.deviceId : this.availableCameras[0].deviceId;
478
419
  }
479
- selectAutoCamera() {
420
+ async selectAutoCamera() {
480
421
  if (this.deviceType === 'mobile' || this.deviceType === 'tablet') {
481
- this.selectBackCamera();
482
- this.logger.state('CAMARA_AUTO_SELECCIONADA_MOBILE', { type: 'rear' });
422
+ await this.selectBackCamera();
483
423
  }
484
424
  else {
485
425
  // For desktop, prefer front camera (usually built-in webcam)
486
- this.selectFrontCamera();
487
- this.logger.state('CAMARA_AUTO_SELECCIONADA_DESKTOP', {
488
- type: 'front',
489
- label: this.availableCameras[0].label,
490
- deviceId: this.selectedCameraId
491
- });
426
+ await this.selectFrontCamera();
492
427
  }
493
428
  }
494
429
  updatePreferredFacing(camera) {
@@ -504,40 +439,19 @@ class CameraService {
504
439
  await this.detectDeviceType();
505
440
  }
506
441
  const videoConstraints = {};
507
- this.logger.state('CONFIGURANDO_CONSTRAINS_CAMARA', {
508
- selectedCameraId: this.selectedCameraId,
509
- preferredCameraFacing: this.preferredCameraFacing,
510
- preferredCamera: this.preferredCamera,
511
- deviceType: this.deviceType,
512
- isManuallySelected: this.isManuallySelected
513
- });
514
- if (this.isManuallySelected && this.selectedCameraId) {
515
- // When manually selected, use deviceId for exact camera selection
442
+ // Priority 1: Always use deviceId if available (ensures specific camera with autofocus)
443
+ if (this.selectedCameraId) {
516
444
  videoConstraints.deviceId = { exact: this.selectedCameraId };
517
- this.logger.state('USANDO_CAMARA_MANUAL', {
518
- deviceId: this.selectedCameraId
519
- });
520
445
  }
446
+ // Priority 2: Use facingMode as fallback when no specific camera is selected
521
447
  else if (this.preferredCameraFacing) {
522
- // Use facingMode for initial preference-based selection
523
- // Use 'ideal' instead of 'exact' to avoid OverconstrainedError on desktop
524
448
  const facingConstraint = this.deviceType === 'desktop'
525
449
  ? { ideal: this.preferredCameraFacing }
526
450
  : { exact: this.preferredCameraFacing };
527
451
  videoConstraints.facingMode = facingConstraint;
528
- this.logger.state('USANDO_FACING_CONSTRAINT', {
529
- facingMode: this.preferredCameraFacing,
530
- constraintType: this.deviceType === 'desktop' ? 'ideal' : 'exact'
531
- });
532
- }
533
- else if (this.selectedCameraId) {
534
- videoConstraints.deviceId = { exact: this.selectedCameraId };
535
- this.logger.state('USANDO_CAMARA_SELECCIONADA', {
536
- deviceId: this.selectedCameraId
537
- });
538
452
  }
453
+ // Priority 3: Fallback to preferredCamera setting
539
454
  else {
540
- // Use preferredCamera setting to determine default facing mode
541
455
  if (this.preferredCamera === 'front') {
542
456
  videoConstraints.facingMode = "user";
543
457
  }
@@ -545,24 +459,25 @@ class CameraService {
545
459
  videoConstraints.facingMode = "environment";
546
460
  }
547
461
  else {
548
- // Auto mode: use environment for mobile/tablet, user for desktop
549
462
  videoConstraints.facingMode = (this.deviceType === 'mobile' || this.deviceType === 'tablet') ? "environment" : "user";
550
463
  }
551
- this.logger.state('USANDO_PREFERENCIA_CONFIGURADA', {
552
- facingMode: videoConstraints.facingMode,
553
- preferredCamera: this.preferredCamera,
554
- deviceType: this.deviceType
555
- });
556
464
  }
557
- const tempStream = await navigator.mediaDevices.getUserMedia({
558
- video: videoConstraints
559
- });
560
- const videoTrack = tempStream.getVideoTracks()[0];
561
- const capabilities = videoTrack.getCapabilities();
562
- tempStream.getTracks().forEach(track => track.stop());
465
+ // Optimization: Use cached capabilities if available
466
+ let capabilities;
467
+ if (this.selectedCameraId && CameraService.cameraCapabilitiesCache.has(this.selectedCameraId + '_caps')) {
468
+ capabilities = JSON.parse(CameraService.cameraCapabilitiesCache.get(this.selectedCameraId + '_caps'));
469
+ }
470
+ else {
471
+ const tempStream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints });
472
+ const videoTrack = tempStream.getVideoTracks()[0];
473
+ capabilities = videoTrack.getCapabilities();
474
+ tempStream.getTracks().forEach(track => track.stop());
475
+ // Cache capabilities for future use
476
+ if (this.selectedCameraId) {
477
+ CameraService.cameraCapabilitiesCache.set(this.selectedCameraId + '_caps', JSON.stringify(capabilities));
478
+ }
479
+ }
563
480
  const constraints = { ...videoConstraints };
564
- // Enhanced focus and image quality settings
565
- this.applyAdvancedCameraSettings(constraints, capabilities);
566
481
  if (capabilities.width && capabilities.height) {
567
482
  const maxWidth = Math.min(capabilities.width.max, 1920);
568
483
  const maxHeight = Math.min(capabilities.height.max, 1080);
@@ -576,15 +491,20 @@ class CameraService {
576
491
  constraints.height = { ideal: maxHeight };
577
492
  }
578
493
  }
579
- this.logger.state('CONFIGURACION_CAMARA_OPTIMIZADA', {
580
- constraints,
581
- capabilities: this.getCapabilitiesSummary(capabilities)
582
- });
494
+ // Configure autofocus if camera has the capability
495
+ const capabilitiesAny = capabilities;
496
+ if (capabilitiesAny.focusMode && Array.isArray(capabilitiesAny.focusMode)) {
497
+ if (capabilitiesAny.focusMode.includes('continuous')) {
498
+ constraints.focusMode = 'continuous';
499
+ }
500
+ else if (capabilitiesAny.focusMode.includes('auto')) {
501
+ constraints.focusMode = 'auto';
502
+ }
503
+ }
583
504
  return constraints;
584
505
  }
585
506
  catch (err) {
586
- this.logger.warn('No se pudieron obtener capacidades de cámara, usando configuración de respaldo');
587
- // Ensure device type is detected before determining constraints
507
+ // Fallback logic with minimal temporary streams
588
508
  if (!this.deviceType || this.deviceType === 'desktop') {
589
509
  await this.detectDeviceType();
590
510
  }
@@ -593,25 +513,16 @@ class CameraService {
593
513
  width: { ideal: isTablet ? 1280 : 1920 },
594
514
  height: { ideal: isTablet ? 720 : 1080 }
595
515
  };
596
- // Apply basic focus settings even for fallback
597
- this.applyBasicFocusSettings(fallbackConstraints);
598
- if (this.isManuallySelected && this.selectedCameraId) {
599
- // When manually selected, use deviceId for exact camera selection
516
+ if (this.selectedCameraId) {
600
517
  fallbackConstraints.deviceId = { exact: this.selectedCameraId };
601
518
  }
602
519
  else if (this.preferredCameraFacing) {
603
- // Use facingMode for initial preference-based selection
604
- // Use 'ideal' instead of 'exact' to avoid OverconstrainedError on desktop
605
520
  const facingConstraint = this.deviceType === 'desktop'
606
521
  ? { ideal: this.preferredCameraFacing }
607
522
  : { exact: this.preferredCameraFacing };
608
523
  fallbackConstraints.facingMode = facingConstraint;
609
524
  }
610
- else if (this.selectedCameraId) {
611
- fallbackConstraints.deviceId = { exact: this.selectedCameraId };
612
- }
613
525
  else {
614
- // Use preferredCamera setting to determine default facing mode
615
526
  if (this.preferredCamera === 'front') {
616
527
  fallbackConstraints.facingMode = "user";
617
528
  }
@@ -619,165 +530,26 @@ class CameraService {
619
530
  fallbackConstraints.facingMode = "environment";
620
531
  }
621
532
  else {
622
- // Auto mode: use environment for mobile/tablet, user for desktop
623
533
  fallbackConstraints.facingMode = (this.deviceType === 'mobile' || this.deviceType === 'tablet') ? "environment" : "user";
624
534
  }
625
535
  }
626
536
  return fallbackConstraints;
627
537
  }
628
538
  }
629
- applyAdvancedCameraSettings(constraints, capabilities) {
630
- // Apply standard camera settings that are widely supported
631
- // Frame rate optimization
632
- if (capabilities.frameRate) {
633
- constraints.frameRate = {
634
- ideal: 30,
635
- min: 15,
636
- max: 60
637
- };
638
- }
639
- // Width and height constraints for optimal resolution
640
- if (capabilities.width && capabilities.height) {
641
- constraints.width = {
642
- ideal: Math.min(1920, capabilities.width.max || 1920),
643
- min: 640
644
- };
645
- constraints.height = {
646
- ideal: Math.min(1080, capabilities.height.max || 1080),
647
- min: 480
648
- };
649
- }
650
- // Apply enhanced constraints through advanced array for better browser compatibility
651
- const advancedConstraints = [];
652
- // Try to set optimal settings through advanced constraints
653
- const advanced = {};
654
- // Focus mode - use 'any' type to bypass TypeScript limitations
655
- if (capabilities.focusMode) {
656
- if (capabilities.focusMode.includes('continuous')) {
657
- advanced.focusMode = 'continuous';
658
- }
659
- else if (capabilities.focusMode.includes('single-shot')) {
660
- advanced.focusMode = 'single-shot';
661
- }
662
- }
663
- // Focus distance for close objects
664
- if (capabilities.focusDistance) {
665
- advanced.focusDistance = {
666
- ideal: 0.4, // 40cm - optimal for document capture
667
- min: 0.2,
668
- max: 1.0
669
- };
670
- }
671
- // Zoom control
672
- if (capabilities.zoom) {
673
- advanced.zoom = {
674
- ideal: 1.0,
675
- min: capabilities.zoom.min,
676
- max: Math.min(capabilities.zoom.max, 3.0)
677
- };
678
- }
679
- // Add advanced constraints if any were set
680
- if (Object.keys(advanced).length > 0) {
681
- advancedConstraints.push(advanced);
682
- }
683
- if (advancedConstraints.length > 0) {
684
- constraints.advanced = advancedConstraints;
685
- }
686
- }
687
- applyBasicFocusSettings(constraints) {
688
- // Apply basic focus settings when capabilities are not available
689
- // Use advanced constraints for non-standard properties
690
- const advanced = {
691
- focusMode: 'continuous',
692
- exposureMode: 'continuous',
693
- whiteBalanceMode: 'continuous'
694
- };
695
- if (!constraints.advanced) {
696
- constraints.advanced = [];
697
- }
698
- constraints.advanced.push(advanced);
699
- constraints.frameRate = { ideal: 30 };
700
- }
701
- getCapabilitiesSummary(capabilities) {
702
- return {
703
- hasAutoFocus: !!capabilities.focusMode,
704
- hasFocusDistance: !!capabilities.focusDistance,
705
- hasExposureControl: !!capabilities.exposureMode,
706
- hasWhiteBalance: !!capabilities.whiteBalanceMode,
707
- hasZoom: !!capabilities.zoom,
708
- hasTorch: capabilities.torch !== undefined,
709
- hasImageControls: !!(capabilities.brightness || capabilities.contrast || capabilities.saturation || capabilities.sharpness),
710
- resolutionRange: capabilities.width && capabilities.height ?
711
- `${capabilities.width.min}x${capabilities.height.min} - ${capabilities.width.max}x${capabilities.height.max}` : 'unknown'
712
- };
713
- }
714
- // New method to enable torch/flash for better illumination
715
- async setTorchEnabled(enabled, stream) {
716
- try {
717
- if (!stream) {
718
- this.logger.warn('No hay stream disponible para controlar el flash');
719
- return false;
720
- }
721
- const videoTrack = stream.getVideoTracks()[0];
722
- if (!videoTrack) {
723
- this.logger.warn('No hay track de video disponible');
724
- return false;
725
- }
726
- const capabilities = videoTrack.getCapabilities();
727
- if (capabilities.torch === undefined) {
728
- this.logger.warn('El dispositivo no soporta control de flash');
729
- return false;
730
- }
731
- await videoTrack.applyConstraints({
732
- advanced: [{ torch: enabled }]
733
- });
734
- this.logger.state('FLASH_CONTROLADO', { enabled });
735
- return true;
736
- }
737
- catch (error) {
738
- this.logger.error('Error al controlar el flash:', error);
539
+ // Optimization helper methods
540
+ constraintsEqual(a, b) {
541
+ if (!a || !b)
739
542
  return false;
740
- }
543
+ return JSON.stringify(a) === JSON.stringify(b);
741
544
  }
742
- // Method to manually trigger focus on a specific point
743
- async focusAtPoint(x, y, stream) {
744
- try {
745
- if (!stream) {
746
- this.logger.warn('No hay stream disponible para enfocar');
747
- return false;
748
- }
749
- const videoTrack = stream.getVideoTracks()[0];
750
- if (!videoTrack) {
751
- this.logger.warn('No hay track de video disponible');
752
- return false;
753
- }
754
- const capabilities = videoTrack.getCapabilities();
755
- if (!capabilities.focusMode || !capabilities.focusMode.includes('single-shot')) {
756
- this.logger.warn('El dispositivo no soporta enfoque manual');
757
- return false;
758
- }
759
- // Trigger single-shot focus
760
- await videoTrack.applyConstraints({
761
- advanced: [{ focusMode: 'single-shot' }]
762
- });
763
- // Return to continuous focus after a short delay
764
- setTimeout(async () => {
765
- try {
766
- await videoTrack.applyConstraints({
767
- advanced: [{ focusMode: 'continuous' }]
768
- });
769
- }
770
- catch (error) {
771
- this.logger.warn('Error al volver a enfoque continuo:', error);
772
- }
773
- }, 1000);
774
- this.logger.state('ENFOQUE_MANUAL_ACTIVADO', { x, y });
775
- return true;
776
- }
777
- catch (error) {
778
- this.logger.error('Error al enfocar manualmente:', error);
779
- return false;
780
- }
545
+ // Cleanup method for optimization
546
+ static clearCaches() {
547
+ CameraService.cameraCapabilitiesCache.clear();
548
+ CameraService.deviceEnumerationCache = null;
549
+ }
550
+ // Method to invalidate device cache when needed
551
+ invalidateDeviceCache() {
552
+ CameraService.deviceEnumerationCache = null;
781
553
  }
782
554
  }
783
555
 
@@ -856,7 +628,6 @@ class DeviceStrategyFactory {
856
628
  }
857
629
 
858
630
  class DetectionService {
859
- logger;
860
631
  debug;
861
632
  useDocumentClassification;
862
633
  session;
@@ -873,8 +644,10 @@ class DetectionService {
873
644
  preprocessCanvas;
874
645
  preprocessCtx;
875
646
  captureCanvas;
876
- constructor(logger, debug = false, useDocumentClassification = false) {
877
- this.logger = logger;
647
+ // Static canvas pool for reuse across instances
648
+ static canvasPool = new Map();
649
+ static MAX_POOL_SIZE = 5;
650
+ constructor(debug = false, useDocumentClassification = false) {
878
651
  this.debug = debug;
879
652
  this.useDocumentClassification = useDocumentClassification;
880
653
  this.deviceStrategy = DeviceStrategyFactory.createStrategy();
@@ -882,21 +655,15 @@ class DetectionService {
882
655
  }
883
656
  async loadModel() {
884
657
  if (this.modelLoaded || this.session) {
885
- this.logger.state('MODELO_YA_PRECARGADO', {
886
- sessionExists: !!this.session,
887
- modelPreloaded: this.modelLoaded
888
- });
889
658
  return;
890
659
  }
891
660
  try {
892
- this.logger.state('PRECARGANDO_MODELO_DETECCION', { modelPath: this.MODEL_PATH });
893
661
  const sessionOptions = this.deviceStrategy.getSessionOptions(this.debug);
894
662
  try {
895
663
  this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, sessionOptions);
896
664
  }
897
665
  catch (error) {
898
666
  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
667
  const fallbackOptions = {
901
668
  executionProviders: ['wasm'],
902
669
  graphOptimizationLevel: 'disabled',
@@ -914,10 +681,8 @@ class DetectionService {
914
681
  }
915
682
  }
916
683
  this.modelLoaded = true;
917
- this.logger.state('MODELO_DETECCION_CARGADO_EXITOSAMENTE', { sessionCreated: !!this.session });
918
684
  }
919
685
  catch (error) {
920
- this.logger.error('Error al precargar modelo de detección:', error);
921
686
  throw error;
922
687
  }
923
688
  }
@@ -926,19 +691,14 @@ class DetectionService {
926
691
  return;
927
692
  }
928
693
  try {
929
- this.logger.state('CARGANDO_MODELO_MOBILENET', { path: this.MOBILENET_MODEL_PATH });
930
694
  // Try to load class map (optional - SqueezeNet may not need it for basic classification)
931
695
  try {
932
696
  const classResponse = await fetch(this.MOBILENET_CLASSES_PATH);
933
697
  if (classResponse.ok) {
934
698
  this.mobileNetClassMap = await classResponse.json();
935
- this.logger.state('CLASES_MOBILENET_CARGADAS', {
936
- classCount: Object.keys(this.mobileNetClassMap).length
937
- });
938
699
  }
939
700
  }
940
701
  catch (error) {
941
- this.logger.warn('No se pudo cargar el mapa de clases de MobileNet, usando clasificación básica');
942
702
  // Create a basic class map for document types
943
703
  this.mobileNetClassMap = {
944
704
  "0": "document",
@@ -955,7 +715,6 @@ class DetectionService {
955
715
  }
956
716
  catch (error) {
957
717
  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
718
  const fallbackOptions = {
960
719
  executionProviders: ['wasm'],
961
720
  graphOptimizationLevel: 'disabled',
@@ -972,12 +731,8 @@ class DetectionService {
972
731
  throw error;
973
732
  }
974
733
  }
975
- this.logger.state('MODELO_MOBILENET_CARGADO_EXITOSAMENTE', {
976
- sessionCreated: !!this.mobileNetSession
977
- });
978
734
  }
979
735
  catch (error) {
980
- this.logger.error('Error al cargar modelo MobileNet:', error);
981
736
  throw error;
982
737
  }
983
738
  }
@@ -1030,11 +785,9 @@ class DetectionService {
1030
785
  }
1031
786
  async classifyDocument(canvas) {
1032
787
  if (!this.mobileNetSession || !this.mobileNetClassMap) {
1033
- this.logger.warn('Modelo MobileNet no está cargado, saltando clasificación');
1034
788
  return null;
1035
789
  }
1036
790
  try {
1037
- this.logger.state('CLASIFICANDO_DOCUMENTO', { timestamp: Date.now() });
1038
791
  const inputTensor = this.preprocessMobileNet(canvas);
1039
792
  const feeds = { [this.mobileNetSession.inputNames[0]]: inputTensor };
1040
793
  const results = await this.mobileNetSession.run(feeds);
@@ -1042,13 +795,6 @@ class DetectionService {
1042
795
  const maxIdx = output.reduce((bestIdx, val, idx, arr) => val > arr[bestIdx] ? idx : bestIdx, 0);
1043
796
  const confidence = output[maxIdx];
1044
797
  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
798
  return {
1053
799
  class: className,
1054
800
  confidence: confidence,
@@ -1056,7 +802,6 @@ class DetectionService {
1056
802
  };
1057
803
  }
1058
804
  catch (error) {
1059
- this.logger.error('Error al clasificar documento:', error);
1060
805
  return null;
1061
806
  }
1062
807
  }
@@ -1169,7 +914,6 @@ class DetectionService {
1169
914
  }
1170
915
  this.mobileNetClassMap = undefined;
1171
916
  this.modelLoaded = false;
1172
- this.logger.state('DETECCION_SERVICE_LIMPIADO', { timestamp: Date.now() });
1173
917
  }
1174
918
  initializeCanvasPool() {
1175
919
  this.preprocessCanvas = document.createElement('canvas');
@@ -1180,9 +924,6 @@ class DetectionService {
1180
924
  willReadFrequently: true
1181
925
  });
1182
926
  this.captureCanvas = document.createElement('canvas');
1183
- this.logger.state('CANVAS_POOL_INICIALIZADO', {
1184
- preprocessCanvasSize: this.INPUT_SIZE
1185
- });
1186
927
  }
1187
928
  cleanupCanvasPool() {
1188
929
  if (this.preprocessCanvas) {
@@ -1218,10 +959,11 @@ class DetectionService {
1218
959
  return (sign << 15) | (newExp << 10) | (frac >> 13);
1219
960
  }
1220
961
  preprocessMobileNet(canvas) {
1221
- const tempCanvas = document.createElement('canvas');
1222
- tempCanvas.width = 224;
1223
- tempCanvas.height = 224;
962
+ // Use pooled canvas instead of creating new one
963
+ const tempCanvas = this.getCanvas(224, 224);
1224
964
  const tempCtx = tempCanvas.getContext('2d');
965
+ // Clear canvas before use
966
+ tempCtx.clearRect(0, 0, 224, 224);
1225
967
  // Resize image to 224x224 for SqueezeNet (using MobileNet interface)
1226
968
  tempCtx.drawImage(canvas, 0, 0, 224, 224);
1227
969
  const imageData = tempCtx.getImageData(0, 0, 224, 224);
@@ -1237,8 +979,53 @@ class DetectionService {
1237
979
  // Blue channel
1238
980
  arr[2 * hw + i] = data[i * 4 + 2] / 255.0;
1239
981
  }
982
+ // Return canvas to pool after use
983
+ this.returnCanvas(tempCanvas);
1240
984
  return new window.ort.Tensor('float32', arr, [1, 3, 224, 224]);
1241
985
  }
986
+ // Canvas pool management methods for optimization
987
+ getCanvas(width, height) {
988
+ const key = `${width}x${height}`;
989
+ if (!DetectionService.canvasPool.has(key)) {
990
+ DetectionService.canvasPool.set(key, []);
991
+ }
992
+ const pool = DetectionService.canvasPool.get(key);
993
+ let canvas = pool.pop();
994
+ if (!canvas) {
995
+ canvas = document.createElement('canvas');
996
+ canvas.width = width;
997
+ canvas.height = height;
998
+ }
999
+ else {
1000
+ // Clear the canvas before reuse
1001
+ const ctx = canvas.getContext('2d');
1002
+ ctx.clearRect(0, 0, width, height);
1003
+ }
1004
+ return canvas;
1005
+ }
1006
+ returnCanvas(canvas) {
1007
+ const key = `${canvas.width}x${canvas.height}`;
1008
+ if (!DetectionService.canvasPool.has(key)) {
1009
+ DetectionService.canvasPool.set(key, []);
1010
+ }
1011
+ const pool = DetectionService.canvasPool.get(key);
1012
+ // Only return to pool if not at max capacity
1013
+ if (pool.length < DetectionService.MAX_POOL_SIZE) {
1014
+ pool.push(canvas);
1015
+ }
1016
+ }
1017
+ // Static method to clear all canvas pools (useful for memory management)
1018
+ static clearCanvasPools() {
1019
+ DetectionService.canvasPool.clear();
1020
+ }
1021
+ // Method to get pool statistics for debugging
1022
+ getPoolStats() {
1023
+ const stats = {};
1024
+ DetectionService.canvasPool.forEach((pool, key) => {
1025
+ stats[key] = pool.length;
1026
+ });
1027
+ return stats;
1028
+ }
1242
1029
  }
1243
1030
 
1244
1031
  class ServiceContainer {
@@ -1249,13 +1036,11 @@ class ServiceContainer {
1249
1036
  initializeServices(config) {
1250
1037
  // Initialize services in dependency order
1251
1038
  const eventBus = new EventBusService();
1252
- const logger = new LoggerService(config.debug);
1253
1039
  const stateManager = new StateManagerService(eventBus);
1254
- const cameraService = new CameraService(logger, eventBus, config.preferredCamera);
1255
- const detectionService = new DetectionService(logger, config.debug, config.useDocumentClassification);
1040
+ const cameraService = new CameraService(eventBus, config.preferredCamera);
1041
+ const detectionService = new DetectionService(config.debug, config.useDocumentClassification);
1256
1042
  // Register services
1257
1043
  this.services.set('eventBus', eventBus);
1258
- this.services.set('logger', logger);
1259
1044
  this.services.set('stateManager', stateManager);
1260
1045
  this.services.set('cameraService', cameraService);
1261
1046
  this.services.set('detectionService', detectionService);
@@ -1267,9 +1052,6 @@ class ServiceContainer {
1267
1052
  }
1268
1053
  return service;
1269
1054
  }
1270
- getLogger() {
1271
- return this.get('logger');
1272
- }
1273
1055
  getEventBus() {
1274
1056
  return this.get('eventBus');
1275
1057
  }
@@ -1283,10 +1065,6 @@ class ServiceContainer {
1283
1065
  return this.get('detectionService');
1284
1066
  }
1285
1067
  updateConfig(config) {
1286
- if (config.debug !== undefined) {
1287
- const logger = this.services.get('logger');
1288
- logger.setDebugMode(config.debug);
1289
- }
1290
1068
  }
1291
1069
  cleanup() {
1292
1070
  // Cleanup services in reverse order
@@ -1296,7 +1074,7 @@ class ServiceContainer {
1296
1074
  }
1297
1075
  }
1298
1076
 
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}}";
1077
+ 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: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}}.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}}";
1300
1078
 
1301
1079
  const JaakStamps = class {
1302
1080
  constructor(hostRef) {
@@ -1326,9 +1104,16 @@ const JaakStamps = class {
1326
1104
  showCameraSelector = false;
1327
1105
  isSwitchingCamera = false;
1328
1106
  hasDocumentDetected = false;
1107
+ cameraInfoWithAutofocus = {
1108
+ availableCameras: [],
1109
+ selectedCameraId: null,
1110
+ deviceType: 'desktop',
1111
+ isMultipleCamerasAvailable: false,
1112
+ preferredFacing: null
1113
+ };
1329
1114
  currentStatus = {
1330
- message: 'Inicializando componente...',
1331
- description: 'Configurando servicios y cargando recursos',
1115
+ message: 'Preparando capturador...',
1116
+ description: 'Configurando herramientas de captura',
1332
1117
  type: 'initializing',
1333
1118
  isInitialized: false
1334
1119
  };
@@ -1345,7 +1130,6 @@ const JaakStamps = class {
1345
1130
  backDocumentTimerRemaining = 0;
1346
1131
  // Services
1347
1132
  serviceContainer;
1348
- logger;
1349
1133
  eventBus;
1350
1134
  stateManager;
1351
1135
  cameraService;
@@ -1376,19 +1160,25 @@ const JaakStamps = class {
1376
1160
  lastUpdateTime: 0
1377
1161
  };
1378
1162
  performanceUpdateInterval;
1379
- // Performance optimization
1163
+ // Performance optimization with adaptive frame skipping
1380
1164
  frameSkipCounter = 0;
1381
- FRAME_SKIP = 2;
1165
+ BASE_FRAME_SKIP = 2;
1166
+ MAX_FRAME_SKIP = 8;
1382
1167
  consecutiveFailures = 0;
1383
1168
  MAX_FAILURES = 30;
1384
1169
  lastInferenceTime = 0;
1385
1170
  MIN_INFERENCE_INTERVAL = 50;
1171
+ performanceHistory = [];
1172
+ PERFORMANCE_HISTORY_SIZE = 10;
1173
+ // Canvas pool for optimized screenshot capture
1174
+ canvasPool = [];
1175
+ MAX_CANVAS_POOL_SIZE = 3;
1386
1176
  async componentDidLoad() {
1387
- this.updateStatus('Iniciando servicios...', 'Configurando módulos internos', 'initializing');
1177
+ this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
1388
1178
  await this.initializeServices();
1389
- this.updateStatus('Configurando eventos...', 'Preparando comunicación entre servicios', 'initializing');
1179
+ this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
1390
1180
  await this.setupEventListeners();
1391
- this.updateStatus('Inicializando cámara...', 'Detectando dispositivos disponibles', 'initializing');
1181
+ this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
1392
1182
  await this.initializeComponent();
1393
1183
  if (this.debug) {
1394
1184
  this.initializePerformanceMonitor();
@@ -1405,33 +1195,23 @@ const JaakStamps = class {
1405
1195
  captureDelay: this.captureDelay
1406
1196
  };
1407
1197
  this.serviceContainer = new ServiceContainer(config);
1408
- this.logger = this.serviceContainer.getLogger();
1409
1198
  this.eventBus = this.serviceContainer.getEventBus();
1410
1199
  this.stateManager = this.serviceContainer.getStateManager();
1411
1200
  this.cameraService = this.serviceContainer.getCameraService();
1412
1201
  this.detectionService = this.serviceContainer.getDetectionService();
1413
- this.logger.state('SERVICIOS_INICIALIZADOS', { timestamp: Date.now() });
1414
1202
  }
1415
1203
  async setupEventListeners() {
1416
1204
  this.eventBus.on('state-changed', (data) => {
1417
1205
  this.handleStateChange(data);
1418
1206
  });
1419
- this.eventBus.on('camera-changed', (cameraId) => {
1420
- this.logger.state('CAMARA_CAMBIADA_EVENT', { cameraId });
1207
+ this.eventBus.on('camera-changed', () => {
1208
+ // Camera changed event
1421
1209
  });
1422
- this.eventBus.on('error', (error) => {
1423
- this.logger.error('Error en servicio:', error);
1210
+ this.eventBus.on('error', () => {
1211
+ // Handle service errors
1424
1212
  });
1425
1213
  }
1426
1214
  async initializeComponent() {
1427
- this.logger.state('COMPONENTE_INICIALIZANDO', {
1428
- debug: this.debug,
1429
- maskSize: this.maskSize,
1430
- cropMargin: this.cropMargin,
1431
- useDocumentClassification: this.useDocumentClassification,
1432
- preferredCamera: this.preferredCamera,
1433
- captureDelay: this.captureDelay
1434
- });
1435
1215
  this.validateProps();
1436
1216
  if (this.debug) {
1437
1217
  this.stateManager.updateCaptureState({ isLoading: true });
@@ -1440,32 +1220,29 @@ const JaakStamps = class {
1440
1220
  this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura', 'initializing');
1441
1221
  await this.cameraService.detectDeviceType();
1442
1222
  await this.cameraService.enumerateDevices();
1443
- this.updateStatus('Cargando modelo IA...', 'Preparando reconocimiento de documentos', 'initializing');
1223
+ await this.updateCameraInfoWithAutofocus();
1224
+ this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'initializing');
1444
1225
  await this.loadOnnxRuntime();
1445
1226
  this.initializeResizeObserver();
1446
1227
  }
1447
1228
  validateProps() {
1448
1229
  if (this.maskSize < 50 || this.maskSize > 100) {
1449
- this.logger.warn(`Propiedad maskSize inválida. Valor: ${this.maskSize}, esperado: 50-100. Usando valor por defecto: 90`);
1450
1230
  this.maskSize = 90;
1451
1231
  }
1452
1232
  if (this.cropMargin < 0 || this.cropMargin > 100) {
1453
- this.logger.warn(`Propiedad cropMargin inválida. Valor: ${this.cropMargin}, esperado: 0-100. Usando valor por defecto: 0`);
1454
1233
  this.cropMargin = 0;
1455
1234
  }
1456
1235
  const validOptions = ['auto', 'front', 'back'];
1457
1236
  if (!validOptions.includes(this.preferredCamera)) {
1458
- this.logger.warn(`Propiedad preferredCamera inválida. Valor: ${this.preferredCamera}, esperado: ${validOptions.join(', ')}. Usando valor por defecto: 'auto'`);
1459
1237
  this.preferredCamera = 'auto';
1460
1238
  }
1461
1239
  if (this.captureDelay < 0 || this.captureDelay > 10000) {
1462
- this.logger.warn(`Propiedad captureDelay inválida. Valor: ${this.captureDelay}, esperado: 0-10000. Usando valor por defecto: 1500`);
1463
1240
  this.captureDelay = 1500;
1464
1241
  }
1465
1242
  }
1466
1243
  async loadOnnxRuntime() {
1467
1244
  if (!window.ort) {
1468
- this.updateStatus('Descargando librerías...', 'Obteniendo recursos de reconocimiento', 'initializing');
1245
+ this.updateStatus('Preparando herramientas...', 'Configurando funciones de captura', 'initializing');
1469
1246
  const script = document.createElement('script');
1470
1247
  script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
1471
1248
  document.head.appendChild(script);
@@ -1505,11 +1282,6 @@ const JaakStamps = class {
1505
1282
  emitReadyEvent() {
1506
1283
  const isDocumentReady = !!window.ort && this.detectionService.isModelLoaded();
1507
1284
  this.isReady.emit(isDocumentReady);
1508
- this.logger.state('COMPONENTE_LISTO', {
1509
- ortLibraryLoaded: !!window.ort,
1510
- modelPreloaded: this.detectionService.isModelLoaded(),
1511
- isReady: isDocumentReady
1512
- });
1513
1285
  }
1514
1286
  handleStateChange(data) {
1515
1287
  }
@@ -1526,7 +1298,6 @@ const JaakStamps = class {
1526
1298
  const container = this.detectionContainer.parentElement;
1527
1299
  const rect = container.getBoundingClientRect();
1528
1300
  this.updateMaskDimensions(rect);
1529
- this.logger.debug('Container redimensionado:', { width: rect.width, height: rect.height });
1530
1301
  }
1531
1302
  }
1532
1303
  updateMaskDimensions(containerRect) {
@@ -1580,16 +1351,38 @@ const JaakStamps = class {
1580
1351
  this.el.style.setProperty('--mask-center-x', `${videoCenterXPercent}%`);
1581
1352
  this.el.style.setProperty('--mask-center-y', `${videoCenterYPercent}%`);
1582
1353
  this.isMaskReady = true;
1583
- this.logger.state('DIMENSIONES_MASCARA_ACTUALIZADAS', {
1584
- video: { width: videoWidth, height: videoHeight },
1585
- displayed: { width: displayedVideoWidth, height: displayedVideoHeight },
1586
- mask: { widthPercent: maskWidthPercent, heightPercent: maskHeightPercent },
1587
- center: { x: videoCenterXPercent, y: videoCenterYPercent },
1588
- offset: { x: videoOffsetX, y: videoOffsetY }
1589
- });
1590
1354
  }
1591
1355
  // PUBLIC METHODS
1356
+ // Helper method to check if component is ready for operations
1357
+ isComponentReady() {
1358
+ if (!this.currentStatus.isInitialized) {
1359
+ return {
1360
+ ready: false,
1361
+ message: 'El componente aún se está inicializando',
1362
+ status: 'initializing'
1363
+ };
1364
+ }
1365
+ if (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') {
1366
+ return {
1367
+ ready: false,
1368
+ message: 'El componente está cargando recursos',
1369
+ status: 'loading'
1370
+ };
1371
+ }
1372
+ if (!this.serviceContainer || !this.stateManager || !this.cameraService || !this.detectionService) {
1373
+ return {
1374
+ ready: false,
1375
+ message: 'Los servicios del componente no están disponibles',
1376
+ status: 'error'
1377
+ };
1378
+ }
1379
+ return { ready: true };
1380
+ }
1592
1381
  async getCapturedImages() {
1382
+ const readyCheck = this.isComponentReady();
1383
+ if (!readyCheck.ready) {
1384
+ throw new Error(readyCheck.message);
1385
+ }
1593
1386
  const state = this.stateManager.getCaptureState();
1594
1387
  if (state.step !== 'completed') {
1595
1388
  throw new Error('El proceso de captura no ha sido completado');
@@ -1597,47 +1390,128 @@ const JaakStamps = class {
1597
1390
  return this.stateManager.getCapturedImages();
1598
1391
  }
1599
1392
  async isProcessCompleted() {
1393
+ const readyCheck = this.isComponentReady();
1394
+ if (!readyCheck.ready) {
1395
+ return false; // Return false instead of error for this method
1396
+ }
1600
1397
  return this.stateManager.isProcessCompleted();
1601
1398
  }
1602
1399
  async startCapture() {
1603
- await this.startDetection();
1400
+ const readyCheck = this.isComponentReady();
1401
+ if (!readyCheck.ready) {
1402
+ this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
1403
+ return { success: false, error: readyCheck.message };
1404
+ }
1405
+ try {
1406
+ await this.startDetection();
1407
+ return { success: true };
1408
+ }
1409
+ catch (error) {
1410
+ this.updateStatus('Error al iniciar captura', error.message, 'error');
1411
+ return { success: false, error: error.message };
1412
+ }
1604
1413
  }
1605
1414
  async stopCapture() {
1606
- this.exitSession();
1415
+ const readyCheck = this.isComponentReady();
1416
+ if (!readyCheck.ready) {
1417
+ return { success: false, error: readyCheck.message };
1418
+ }
1419
+ try {
1420
+ this.exitSession();
1421
+ return { success: true };
1422
+ }
1423
+ catch (error) {
1424
+ return { success: false, error: error.message };
1425
+ }
1607
1426
  }
1608
1427
  async resetCapture() {
1609
- this.resetDetection();
1428
+ const readyCheck = this.isComponentReady();
1429
+ if (!readyCheck.ready) {
1430
+ return { success: false, error: readyCheck.message };
1431
+ }
1432
+ try {
1433
+ this.resetDetection();
1434
+ return { success: true };
1435
+ }
1436
+ catch (error) {
1437
+ return { success: false, error: error.message };
1438
+ }
1610
1439
  }
1611
1440
  async skipBackCapture() {
1612
- const captureState = this.stateManager.getCaptureState();
1613
- const capturedImages = this.stateManager.getCapturedImages();
1614
- if (captureState.step === 'back' && capturedImages.front.fullFrame) {
1615
- this.completeProcess(true);
1441
+ const readyCheck = this.isComponentReady();
1442
+ if (!readyCheck.ready) {
1443
+ return { success: false, error: readyCheck.message };
1444
+ }
1445
+ try {
1446
+ const captureState = this.stateManager.getCaptureState();
1447
+ const capturedImages = this.stateManager.getCapturedImages();
1448
+ if (captureState.step === 'back' && capturedImages.front.fullFrame) {
1449
+ this.completeProcess(true);
1450
+ return { success: true };
1451
+ }
1452
+ else {
1453
+ return { success: false, error: 'No se puede saltar el reverso en el estado actual' };
1454
+ }
1455
+ }
1456
+ catch (error) {
1457
+ return { success: false, error: error.message };
1616
1458
  }
1617
1459
  }
1618
1460
  async getStatus() {
1619
- const captureState = this.stateManager.getCaptureState();
1620
- const capturedImages = this.stateManager.getCapturedImages();
1621
- return {
1622
- isVideoActive: captureState.isVideoActive,
1623
- captureStep: captureState.step,
1624
- hasImages: !!(capturedImages.front.fullFrame || capturedImages.back.fullFrame),
1625
- isProcessCompleted: this.stateManager.isProcessCompleted(),
1626
- isModelPreloaded: this.detectionService.isModelLoaded()
1627
- };
1461
+ const readyCheck = this.isComponentReady();
1462
+ if (!readyCheck.ready) {
1463
+ return {
1464
+ isVideoActive: false,
1465
+ captureStep: 'front',
1466
+ hasImages: false,
1467
+ isProcessCompleted: false,
1468
+ isModelPreloaded: false,
1469
+ componentStatus: readyCheck.status,
1470
+ componentMessage: readyCheck.message
1471
+ };
1472
+ }
1473
+ try {
1474
+ const captureState = this.stateManager.getCaptureState();
1475
+ const capturedImages = this.stateManager.getCapturedImages();
1476
+ return {
1477
+ isVideoActive: captureState.isVideoActive,
1478
+ captureStep: captureState.step,
1479
+ hasImages: !!(capturedImages.front.fullFrame || capturedImages.back.fullFrame),
1480
+ isProcessCompleted: this.stateManager.isProcessCompleted(),
1481
+ isModelPreloaded: this.detectionService.isModelLoaded(),
1482
+ componentStatus: 'ready'
1483
+ };
1484
+ }
1485
+ catch (error) {
1486
+ return {
1487
+ isVideoActive: false,
1488
+ captureStep: 'front',
1489
+ hasImages: false,
1490
+ isProcessCompleted: false,
1491
+ isModelPreloaded: false,
1492
+ componentStatus: 'error',
1493
+ componentMessage: error.message
1494
+ };
1495
+ }
1628
1496
  }
1629
1497
  async preloadModel() {
1498
+ const readyCheck = this.isComponentReady();
1499
+ if (!readyCheck.ready) {
1500
+ this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
1501
+ return { success: false, error: readyCheck.message };
1502
+ }
1630
1503
  if (this.detectionService.isModelLoaded()) {
1631
- this.logger.state('MODELO_YA_PRECARGADO');
1632
- this.updateStatus('Modelos ya cargados', '', 'ready');
1504
+ this.updateStatus('Reconocimiento listo', '', 'ready');
1633
1505
  return { success: true, message: 'Model already loaded' };
1634
1506
  }
1635
1507
  try {
1636
1508
  const loadStartTime = performance.now();
1637
- this.updateStatus('Descargando modelo de detección...', 'Obteniendo red neuronal para reconocimiento de documentos', 'loading');
1638
- this.stateManager.updateCaptureState({ isLoading: true });
1509
+ this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
1510
+ if (this.stateManager) {
1511
+ this.stateManager.updateCaptureState({ isLoading: true });
1512
+ }
1639
1513
  await this.detectionService.loadModel();
1640
- this.updateStatus('Cargando clasificador...', 'Preparando modelo de clasificación de tipos de documento', 'loading');
1514
+ this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
1641
1515
  await this.detectionService.loadClassificationModel();
1642
1516
  const loadEndTime = performance.now();
1643
1517
  const loadTime = loadEndTime - loadStartTime;
@@ -1645,80 +1519,118 @@ const JaakStamps = class {
1645
1519
  if (this.debug) {
1646
1520
  this.recordOnnxPerformance(loadTime, 0);
1647
1521
  }
1648
- this.updateStatus('Optimizando modelos...', 'Configurando parámetros de rendimiento', 'loading');
1522
+ this.updateStatus('Finalizando configuración...', 'Preparando herramientas para captura', 'loading');
1649
1523
  await new Promise(resolve => setTimeout(resolve, 300));
1650
- this.updateStatus('Modelos precargados', '', 'ready');
1651
- this.stateManager.updateCaptureState({ isLoading: false });
1524
+ this.updateStatus('Reconocimiento preparado', '', 'ready');
1525
+ if (this.stateManager) {
1526
+ this.stateManager.updateCaptureState({ isLoading: false });
1527
+ }
1652
1528
  this.emitReadyEvent();
1653
- this.logger.state('MODELOS_PRECARGADOS_EXITOSAMENTE', { loadTime: Math.round(loadTime) });
1654
1529
  return { success: true, message: 'Models preloaded successfully' };
1655
1530
  }
1656
1531
  catch (error) {
1657
- this.logger.error('Error al precargar modelos:', error);
1658
- this.updateStatus('Error al cargar modelos', 'No se pudieron descargar los recursos necesarios', 'error');
1659
- this.stateManager.updateCaptureState({ isLoading: false });
1532
+ this.updateStatus('Error de configuración', 'No se pudieron preparar las herramientas necesarias', 'error');
1533
+ if (this.stateManager) {
1534
+ this.stateManager.updateCaptureState({ isLoading: false });
1535
+ }
1660
1536
  return { success: false, error: error.message };
1661
1537
  }
1662
1538
  }
1663
1539
  async getCameraInfo() {
1664
- return this.cameraService.getCameraInfo();
1540
+ const readyCheck = this.isComponentReady();
1541
+ if (!readyCheck.ready) {
1542
+ return {
1543
+ availableCameras: [],
1544
+ selectedCameraId: null,
1545
+ deviceType: 'desktop',
1546
+ isMultipleCamerasAvailable: false,
1547
+ preferredFacing: null,
1548
+ error: readyCheck.message
1549
+ };
1550
+ }
1551
+ try {
1552
+ return await this.cameraService.getCameraInfo();
1553
+ }
1554
+ catch (error) {
1555
+ return {
1556
+ availableCameras: [],
1557
+ selectedCameraId: null,
1558
+ deviceType: 'desktop',
1559
+ isMultipleCamerasAvailable: false,
1560
+ preferredFacing: null,
1561
+ error: error.message
1562
+ };
1563
+ }
1665
1564
  }
1666
1565
  async setPreferredCamera(camera) {
1667
- this.preferredCamera = camera;
1668
- this.serviceContainer.updateConfig({ preferredCamera: camera });
1669
- await this.cameraService.enumerateDevices();
1670
- const captureState = this.stateManager.getCaptureState();
1671
- if (captureState.isVideoActive) {
1672
- const selectedCameraId = this.cameraService.getSelectedCameraId();
1673
- if (selectedCameraId) {
1674
- await this.cameraService.switchCamera(selectedCameraId);
1566
+ const readyCheck = this.isComponentReady();
1567
+ if (!readyCheck.ready) {
1568
+ return {
1569
+ success: false,
1570
+ error: readyCheck.message,
1571
+ selectedCamera: null,
1572
+ availableCameras: 0
1573
+ };
1574
+ }
1575
+ try {
1576
+ this.preferredCamera = camera;
1577
+ this.serviceContainer.updateConfig({ preferredCamera: camera });
1578
+ await this.cameraService.enumerateDevices();
1579
+ await this.updateCameraInfoWithAutofocus();
1580
+ const captureState = this.stateManager.getCaptureState();
1581
+ if (captureState.isVideoActive) {
1582
+ const selectedCameraId = this.cameraService.getSelectedCameraId();
1583
+ if (selectedCameraId) {
1584
+ await this.cameraService.switchCamera(selectedCameraId);
1585
+ }
1675
1586
  }
1587
+ return {
1588
+ success: true,
1589
+ selectedCamera: this.cameraService.getSelectedCameraId(),
1590
+ availableCameras: this.cameraService.getAvailableCameras().length
1591
+ };
1592
+ }
1593
+ catch (error) {
1594
+ return {
1595
+ success: false,
1596
+ error: error.message,
1597
+ selectedCamera: null,
1598
+ availableCameras: 0
1599
+ };
1676
1600
  }
1677
- return {
1678
- success: true,
1679
- selectedCamera: this.cameraService.getSelectedCameraId(),
1680
- availableCameras: this.cameraService.getAvailableCameras().length
1681
- };
1682
1601
  }
1683
1602
  async setCaptureDelay(delay) {
1684
1603
  if (delay < 0 || delay > 10000) {
1685
- throw new Error('Capture delay must be between 0 and 10000 milliseconds');
1604
+ return {
1605
+ success: false,
1606
+ error: 'Capture delay must be between 0 and 10000 milliseconds',
1607
+ captureDelay: this.captureDelay
1608
+ };
1686
1609
  }
1610
+ // Allow setting capture delay even during initialization
1687
1611
  this.captureDelay = delay;
1688
- this.serviceContainer.updateConfig({ captureDelay: delay });
1612
+ if (this.serviceContainer) {
1613
+ this.serviceContainer.updateConfig({ captureDelay: delay });
1614
+ }
1689
1615
  return {
1690
1616
  success: true,
1691
1617
  captureDelay: this.captureDelay
1692
1618
  };
1693
1619
  }
1694
1620
  async getCaptureDelay() {
1621
+ // Always allow getting capture delay, even during initialization
1695
1622
  return this.captureDelay;
1696
1623
  }
1697
- async setTorchEnabled(enabled) {
1698
- const torchEnabled = await this.cameraService.setTorchEnabled(enabled, this.videoStream);
1699
- return {
1700
- success: torchEnabled,
1701
- enabled: torchEnabled ? enabled : false
1702
- };
1703
- }
1704
- async focusAtPoint(x, y) {
1705
- const focused = await this.cameraService.focusAtPoint(x, y, this.videoStream);
1706
- return {
1707
- success: focused,
1708
- coordinates: { x, y }
1709
- };
1710
- }
1711
1624
  // DETECTION METHODS
1712
1625
  async startDetection() {
1713
- this.logger.state('INICIANDO_DETECCION');
1714
1626
  try {
1715
1627
  // Paso 1: Verificar y cargar modelos si es necesario
1716
1628
  if (!this.detectionService.isModelLoaded()) {
1717
1629
  const loadStartTime = performance.now();
1718
- this.updateStatus('Cargando modelo de detección...', 'Descargando red neuronal para reconocimiento', 'loading');
1630
+ this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
1719
1631
  this.stateManager.updateCaptureState({ isLoading: true });
1720
1632
  await this.detectionService.loadModel();
1721
- this.updateStatus('Cargando clasificador...', 'Preparando modelo de clasificación de documentos', 'loading');
1633
+ this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
1722
1634
  await this.detectionService.loadClassificationModel();
1723
1635
  const loadEndTime = performance.now();
1724
1636
  const loadTime = loadEndTime - loadStartTime;
@@ -1726,13 +1638,13 @@ const JaakStamps = class {
1726
1638
  if (this.debug) {
1727
1639
  this.recordOnnxPerformance(loadTime, 0);
1728
1640
  }
1729
- this.logger.state('MODELOS_CARGADOS_EN_DETECCION', { loadTime: Math.round(loadTime) });
1730
1641
  }
1731
1642
  // Paso 2: Detectar y configurar dispositivos
1732
- this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura disponibles', 'loading');
1643
+ this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
1733
1644
  await this.cameraService.enumerateDevices();
1645
+ await this.updateCameraInfoWithAutofocus();
1734
1646
  // Paso 3: Configurar cámara seleccionada
1735
- this.updateStatus('Configurando cámara...', 'Estableciendo resolución y parámetros óptimos', 'loading');
1647
+ this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
1736
1648
  const stream = await this.cameraService.setupCamera();
1737
1649
  // Paso 4: Inicializar video y ocultar estado inmediatamente
1738
1650
  this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
@@ -1749,8 +1661,7 @@ const JaakStamps = class {
1749
1661
  this.detectFrame();
1750
1662
  }
1751
1663
  catch (err) {
1752
- this.logger.error('Error al inicializar detección:', err);
1753
- this.updateStatus('Error al iniciar captura', 'No se pudo completar la inicialización', 'error');
1664
+ this.updateStatus('Error al iniciar', 'No se pudo preparar la captura', 'error');
1754
1665
  this.stateManager.updateCaptureState({ isLoading: false });
1755
1666
  }
1756
1667
  }
@@ -1760,10 +1671,6 @@ const JaakStamps = class {
1760
1671
  this.videoStream = stream;
1761
1672
  const isRear = this.cameraService.isRearCamera(stream);
1762
1673
  this.shouldMirrorVideo = !isRear;
1763
- this.logger.state('CAMARA_CONFIGURADA', {
1764
- isRearCamera: isRear,
1765
- shouldMirrorVideo: this.shouldMirrorVideo
1766
- });
1767
1674
  return new Promise((resolve) => {
1768
1675
  this.videoRef.onloadedmetadata = async () => {
1769
1676
  await this.videoRef.play();
@@ -1788,9 +1695,10 @@ const JaakStamps = class {
1788
1695
  }
1789
1696
  return;
1790
1697
  }
1791
- // Frame skipping for performance
1698
+ // Adaptive frame skipping for performance
1792
1699
  this.frameSkipCounter++;
1793
- if (this.frameSkipCounter <= this.FRAME_SKIP) {
1700
+ const currentFrameSkip = this.getAdaptiveFrameSkip();
1701
+ if (this.frameSkipCounter <= currentFrameSkip) {
1794
1702
  if (captureState.step !== 'completed') {
1795
1703
  this.animationId = requestAnimationFrame(() => this.detectFrame());
1796
1704
  }
@@ -1866,7 +1774,6 @@ const JaakStamps = class {
1866
1774
  }
1867
1775
  }
1868
1776
  catch (e) {
1869
- this.logger.error('Error en inferencia de modelo:', e);
1870
1777
  const captureState = this.stateManager.getCaptureState();
1871
1778
  if (captureState.step !== 'completed') {
1872
1779
  setTimeout(() => this.detectFrame(), 100);
@@ -1893,6 +1800,8 @@ const JaakStamps = class {
1893
1800
  clearInterval(this.performanceUpdateInterval);
1894
1801
  this.performanceUpdateInterval = undefined;
1895
1802
  }
1803
+ // Clear canvas pool
1804
+ this.canvasPool = [];
1896
1805
  this.detectionBoxes = [];
1897
1806
  this.alignmentStartTime = undefined;
1898
1807
  this.hasDocumentDetected = false;
@@ -1906,11 +1815,8 @@ const JaakStamps = class {
1906
1815
  step: 'front',
1907
1816
  isCapturing: false
1908
1817
  };
1909
- const cameraInfo = this.cameraService?.getCameraInfo() || {
1910
- availableCameras: [],
1911
- selectedCameraId: null,
1912
- deviceType: 'desktop'};
1913
- return (h("div", { key: '9d4d77042218ab3d0bc4fb1ce879db09dc3e78b4', class: "detector-container" }, h("div", { key: '51ac8ccb1275637282b038ad8364d19f14ef61b1', class: "video-container" }, h("video", { key: 'a34f0481da20ec5b7b62b21aa275ec5f6dfec5d1', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: '43b71dfe3eaf20ffa66a4139e24b1af6a2074631', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index) => (h("div", { key: index, class: "detection-box", style: {
1818
+ const cameraInfo = this.cameraInfoWithAutofocus;
1819
+ return (h("div", { key: '69a8df7969288e19088e8809f1fefcc89b237cda', class: "detector-container" }, h("div", { key: '79e59a315d266be9d4fb5eed5740e5ad825d5890', class: "video-container" }, h("video", { key: 'a3312788c739748b774a25acb30c2dcea7f01695', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: 'ddab107500c2dab24c140049bef0b44aafc35dc7', 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
1820
  position: 'absolute',
1915
1821
  left: `${box.x}px`,
1916
1822
  top: `${box.y}px`,
@@ -1919,9 +1825,9 @@ const JaakStamps = class {
1919
1825
  border: '2px solid #32406C',
1920
1826
  pointerEvents: 'none',
1921
1827
  boxSizing: 'border-box'
1922
- } })))), this.isMaskReady && (h("div", { key: 'c2596f8cabb227e3ea3b082495f52b141cb1690f', class: "overlay-mask" }, h("div", { key: 'ba1ea39b5df1cd312a9bd9410b7225a29cfb8a29', class: "card-outline" }, h("div", { key: '6ce4d4d969d5f3fbcf75a4eb285f61d434c5633b', class: "side side-top" }), h("div", { key: '36653ecc4fcda25dfc84bc4d1f4c4c5dfb71ced7', class: "side side-right" }), h("div", { key: '8b108fa8fb370b229a0d1a77cb1ff044319a8f75', class: "side side-bottom" }), h("div", { key: 'a7aa3b45c065a50445aafd5c3ccefb57b0c9c769', class: "side side-left" }), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: 'b94b92273e12a019238e11ae57f21e140f13c3f1', class: "guide-text" }, "Alinee su identificaci\u00F3n con el marco"))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '144c3741003ec0cc148bb6ce92012d7b3cd110c8', class: "skip-section" }, h("div", { key: 'f0a984a6b75afa374c5d47da9d53e39f98071da0', class: "skip-explanation" }, "Si tu documento no tiene lado trasero, da clic en el bot\u00F3n para continuar con el proceso"), h("button", { key: '57ac30d776e30709aab6e0a621cef889da3cc677', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
1828
+ } })))), this.isMaskReady && (h("div", { key: '370337089a3de949b8cb4ae84dc0037e6044b2d5', class: "overlay-mask" }, h("div", { key: 'cb35c2e42bf1729ad02a8fe7e7abaf27cf1a8d7a', class: "card-outline" }, h("div", { key: '637d72ada63805e7f25051cd1ad9093030cb025a', class: "side side-top" }), h("div", { key: 'c7a465aa686afa41f80433d949715b40f6f349dc', class: "side side-right" }), h("div", { key: '384aade0610b5bc7bdd7db350a70d82f9bebd8ab', class: "side side-bottom" }), h("div", { key: '5506cc346f3fd718e3fda4365f69ea780304b6d6', class: "side side-left" }), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: 'f5b9b62937eabab7c096b7417a13bbf6f822ee8a', class: "guide-text" }, "Alinee su identificaci\u00F3n con el marco"))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '74b122125a42839d2600cf99fb855140042ab761', class: "skip-section" }, h("div", { key: '0b4a39d1599854d6c445e2c35dcc89a0e4970846', class: "skip-explanation" }, "Si tu documento no tiene lado trasero, da clic en el bot\u00F3n para continuar con el proceso"), h("button", { key: 'b52b47df390cc099c8847ca8a676270bb154294d', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
1923
1829
  ? `Saltar reverso (${this.backDocumentTimerRemaining}s)`
1924
- : 'Saltar reverso'))), captureState.isVideoActive && (h("div", { key: 'bd45628707e0169317ed3dd4247e1056d7046104', class: "camera-controls" }, h("button", { key: 'e750fbf4d5d76d9e9596823bb2b86801ef19e485', class: `camera-selector-button ${this.isSwitchingCamera ? 'loading' : ''}`, onClick: () => this.toggleCameraSelector(), type: "button", title: "Seleccionar c\u00E1mara", disabled: this.isSwitchingCamera }, this.isSwitchingCamera ? (h("div", { class: "button-spinner" })) : ('Cámaras')))), this.showCameraSelector && cameraInfo.availableCameras.length > 0 && (h("div", { key: '40ada780a8ea13aeeaee9cb39bc6496f69f2679d', class: "camera-selector-dropdown" }, h("div", { key: '3a920bb02ba024f0359513f10544bf0528ecee6d', class: "camera-selector-header" }, h("span", { key: 'e5efd478a602b13821ba53cb9a7a59d1b4a71178' }, "Seleccionar C\u00E1mara"), h("button", { key: '2df6cc11e162be74862029e33815927e3fa8f2f8', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: '06934642dff134c9e1a6872f935a5892a9c7b835', class: "camera-list" }, cameraInfo.availableCameras.map((camera) => (h("button", { key: camera.id, class: `camera-option ${cameraInfo.selectedCameraId === camera.id ? 'selected' : ''}`, onClick: () => this.handleCameraSwitch(camera.id), type: "button" }, h("span", { class: "camera-label" }, camera.label || `Cámara ${cameraInfo.availableCameras.indexOf(camera) + 1}`), cameraInfo.selectedCameraId === camera.id && (h("span", { class: "selected-indicator" }, "\u2713")))))), h("div", { key: '8d5ede287c2efb0ebe418bee8dc7fb310443c991', class: "device-info" }, h("small", { key: 'b7dd10565113694b09f01ab28ffa29262ae1d2bc' }, "Dispositivo: ", cameraInfo.deviceType)))))), captureState.isCapturing && (h("div", { key: '2f9e549e6c0c56d4db5835e58361fb17bba6cf90', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: 'a6ef0ed127f23e9d764896ed743ca8d8454851a6', class: "flip-animation" }, h("div", { key: '0b04411f88a634bb3243b00ca2d5a45fd6962dea', class: "id-card-icon" }), h("div", { key: '9327be58879b407cea87f455989406e58302e9d1', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: 'bd268578b5003e99256e3340b4a9a6fb7656be64', class: "success-animation" }, h("div", { key: '064e0a5b9851de14bc6829f028e7ea7161fd1633', class: "check-icon" }), h("div", { key: 'bdcd94a8806399e396e218a359c10b11d05f38d6', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '3d9af617376c16e44e065873919c3d90ff46110a', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: 'be7b56decfd6f12dcca46b2a9456480e3d5ac00e', class: "status-spinner" })), h("div", { key: '5fe9850883cb749cebaa18c3e9c13ca21325e373', class: "status-content" }, h("div", { key: '43fdb50691e4b98e56126af1a129f625061128bf', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: 'b76cbfa4a0349421fcd535d3fd8bf40c89ca3aef', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: 'eddefb464af0487ab9ab2a0f76dccbb6a2092e51', class: "performance-monitor" }, h("div", { key: '7b7d93b56cb4cd4fbb1bdde89c6150b282fa805e', class: "performance-expanded" }, h("div", { key: 'f302e8b9d985fb8cad53c4c4e0d37a3bf5a63261', class: "metrics-row" }, h("div", { key: 'e8d8f2420ee8643a7f454d38065c75b5be598f77', class: "metric-compact" }, h("span", { key: '6e52eb5bb06e0df57e61774de38c31349f45e22b', class: "metric-label" }, "FPS"), h("span", { key: '6636f9cea20c49b49c17db86188e2f6b214c45c3', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: 'afa1d9dec05b14256a1b1fcec8638b8c21a56e37', class: "metric-compact" }, h("span", { key: '9c55e30f5d01582648bacc1fb38ca1990485a591', class: "metric-label" }, "MEM"), h("span", { key: 'ca263601e8715027f95ffb2d5413a643dc570cf2', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: '03caf19961fd01b7781484d49d8121b1e78c161f', class: "metrics-row" }, h("div", { key: 'bffb08f01f703411f7c8605e4f8d95b1c37b5c0f', class: "metric-compact" }, h("span", { key: '9b92279b3253cc669412733facb3751f4344d69f', class: "metric-label" }, "INF"), h("span", { key: '3234d4150e2e1479e3b552767af8123e69911d6c', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: '77082d2669db53f19fb50b3bbfa1ebefb0b86ff4', class: "metric-compact" }, h("span", { key: 'cc6ef96c72c800ba36dab2a388f1de863b2dacd5', class: "metric-label" }, "FRAME"), h("span", { key: 'e2bb4b7351869a76e810f8bbf20d805154495e35', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: '5700d7ef839fa77686299d79445203090dc8079f', class: "metrics-row" }, h("div", { key: 'a2999a035fccb7f250c01b084075490678513c87', class: "metric-compact" }, h("span", { key: '3677d58665996fa5969507dbaff9e956b2ea54ee', class: "metric-label" }, "DET"), h("span", { key: 'c584312438b339c82a3bdc33f00fb985ff2a6f55', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: '58866219f213bf8607083f30be2d5ca5d0515cea', class: "metric-compact" }, h("span", { key: 'd90b450051331347f219b6c0f197536d0dcee9b7', class: "metric-label" }, "RATE"), h("span", { key: 'f6e6cd7948e06014e5a0416e4e81394fa36ec61d', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: 'c1d0194f181a0082cc1aa5452de382908da61b65', class: "watermark" }, h("img", { key: 'e04d35f3c79a2b4a3922ab5b0f102220171fa7c0', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
1830
+ : 'Saltar reverso'))), captureState.isVideoActive && (h("div", { key: 'f04e47749a25197c434b849844c67eadca5c6af3', class: "camera-controls" }, h("button", { key: '82625eef057f59b6dac11c4443ccb315231eac7f', 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: '61cfb89cd9bb549cfff2cb70293db06ee6cdf06b', class: "camera-selector-dropdown" }, h("div", { key: '60f751d8db7b305f2bdfdc089d2110bcff940150', class: "camera-selector-header" }, h("span", { key: '9dae045a34cc06f323ff4850921b495fc845be47' }, "Seleccionar C\u00E1mara"), h("button", { key: '4584d7964d91ddd7f3d980c3e5e90493530d33bd', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: '9881f2b4fc6ffbdc76db06a7b49d060cefbe6277', 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: '3dfe9ffc7b4cc9afb199a78026cef6d4b469805e', class: "device-info" }, h("small", { key: 'bddc865a30469bd2724624b2f7c8f641f9d97a07' }, "Dispositivo: ", cameraInfo.deviceType)))))), captureState.isCapturing && (h("div", { key: '2fd963e44f69f80d17c6bbbfa3c9b5e11bc816be', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: 'ba4872ea5388a0ac3134be61feff5412373465ab', class: "flip-animation" }, h("div", { key: 'e83a92cea79f3e85aefc6964c5d9de7b495d511e', class: "id-card-icon" }), h("div", { key: '1d06b51fd6ad14d9b37aa6bca72bd13c951a386a', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: '503c377896d5fdc3ae8476b09f511d571f23ae72', class: "success-animation" }, h("div", { key: 'e1929c245b0d1479a6fb1513b0b37c53070c4291', class: "check-icon" }), h("div", { key: '024c897c70427b205390770fc3dfb999ffcc84d3', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '54e350707628e5c88ae2900e412e6bda2eace943', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: '82ea10569ed46285686cf3e63885ca1941bb559c', class: "status-spinner" })), h("div", { key: '1fecf179c10965cccf1be3dc0a54afd377ba38ad', class: "status-content" }, h("div", { key: '1b301c1bbb02e5cac2e6babcb20c190662d45c68', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: '5b6c61308d17176daade02c951a436d114a74100', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: 'b8104db64208dc87a617a072957c007f28055194', class: "performance-monitor" }, h("div", { key: 'e66aec071f10821be2713858dcc251c04f480e63', class: "performance-expanded" }, h("div", { key: '1ae09a91d0704ab21b9eb51097cf452fd5f44359', class: "metrics-row" }, h("div", { key: '340d4d16d048a5217fe13d5de63618c9ea221165', class: "metric-compact" }, h("span", { key: '20cfda14969ef70d232edd68f2e9902bd44b5d9a', class: "metric-label" }, "FPS"), h("span", { key: '3a089c2814a71ed6513bf05b0120a66aa159daee', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: '23f700def988c3220fc5df37aeb57a0a20ff56b3', class: "metric-compact" }, h("span", { key: '441e8490a871469813a6d832114e03583d375e91', class: "metric-label" }, "MEM"), h("span", { key: '51f7c49164c9d5e699141c92c88375fc99b312ed', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: '6547fee80e2a1db7c9ff889a1c478fc8248159e6', class: "metrics-row" }, h("div", { key: '7b2d1642fd122ee0fbed36387afb23152afe10c2', class: "metric-compact" }, h("span", { key: 'a9137119bf7da366f18feaf8f28807ee40ad7a25', class: "metric-label" }, "INF"), h("span", { key: '5cbc0c040102688f1039cdcd4bb58020248cf700', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: 'cb4a86f5da46e4081d997c8b8bb6a899f06dfcd2', class: "metric-compact" }, h("span", { key: '88ad5e1d842eb156cf6813ecaa9d2bec76a5999e', class: "metric-label" }, "FRAME"), h("span", { key: 'a9b14cd98b64dcfc058d3b68a3c168a9e092fb3c', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: '86e01f3596682cb8ad2ff5464748e6ecb7db374a', class: "metrics-row" }, h("div", { key: 'e73b9d39e9bcbc5d46fa0ba0a737700e3e5566d9', class: "metric-compact" }, h("span", { key: '8ecfe969dfe77c87d9da76d7390706014b4872f3', class: "metric-label" }, "DET"), h("span", { key: 'bd1a6a52e0bd1fb4d377413d7515e4e0f00da10f', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: '36539ae98787fd60642082db1bf89031003c4bef', class: "metric-compact" }, h("span", { key: '7f3a81629c09053e3beeca97f485da0f60412052', class: "metric-label" }, "RATE"), h("span", { key: '72399e0ac39270c82b265ce64e402f84cda9fb2c', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: '4e3a914ba2f5993f85de7b9dd6dc93d76b41c96d', class: "watermark" }, h("img", { key: 'aa634c2ba2090f64ee9b41aa1d75a9114d46fd63', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
1925
1831
  }
1926
1832
  // Utility methods
1927
1833
  updateDetectionBoxes(boxes) {
@@ -2002,35 +1908,18 @@ const JaakStamps = class {
2002
1908
  if (allSidesAligned && bestBox) {
2003
1909
  cardOutline?.classList.add('perfect-match');
2004
1910
  corners?.forEach(corner => corner.classList.add('perfect-match'));
2005
- // Debug logging
2006
- this.logger.state('CAPTURE_EVALUATION', {
2007
- allSidesAligned: true,
2008
- hasScreenshotTaken: this.hasScreenshotTaken,
2009
- captureDelay: this.captureDelay,
2010
- alignmentStartTime: this.alignmentStartTime
2011
- });
2012
1911
  if (!this.hasScreenshotTaken) {
2013
1912
  const currentTime = Date.now();
2014
1913
  // Initialize alignment start time if not set
2015
1914
  if (!this.alignmentStartTime) {
2016
1915
  this.alignmentStartTime = currentTime;
2017
- this.logger.state('ALIGNMENT_TIMER_STARTED', { startTime: currentTime });
2018
1916
  }
2019
1917
  // Check if document has been aligned for the configured delay
2020
1918
  const alignmentDuration = currentTime - this.alignmentStartTime;
2021
- this.logger.state('ALIGNMENT_DURATION_CHECK', {
2022
- alignmentDuration,
2023
- captureDelay: this.captureDelay,
2024
- readyToCapture: alignmentDuration >= this.captureDelay
2025
- });
2026
1919
  if (alignmentDuration >= this.captureDelay) {
2027
- this.logger.state('TRIGGERING_CAPTURE', {
2028
- alignmentDuration,
2029
- captureDelay: this.captureDelay
2030
- });
2031
1920
  this.lastDetectedBox = bestBox;
2032
- this.takeScreenshot().catch(error => {
2033
- this.logger.error('Error al tomar captura de pantalla:', error);
1921
+ this.takeScreenshot().catch(() => {
1922
+ // Handle screenshot error silently
2034
1923
  });
2035
1924
  this.hasScreenshotTaken = true;
2036
1925
  this.alignmentStartTime = undefined;
@@ -2056,21 +1945,12 @@ const JaakStamps = class {
2056
1945
  async takeScreenshot() {
2057
1946
  if (!this.videoRef || !this.lastDetectedBox)
2058
1947
  return;
2059
- this.logger.state('INICIANDO_CAPTURA', {
2060
- captureStep: this.stateManager.getCaptureState().step,
2061
- detectedBox: this.lastDetectedBox,
2062
- videoResolution: {
2063
- width: this.videoRef.videoWidth,
2064
- height: this.videoRef.videoHeight
2065
- }
2066
- });
2067
1948
  this.stateManager.updateCaptureState({ isCapturing: true });
2068
1949
  this.triggerCaptureAnimation();
2069
- // Create capture canvas
2070
- const captureCanvas = document.createElement('canvas');
2071
- captureCanvas.width = this.videoRef.videoWidth;
2072
- captureCanvas.height = this.videoRef.videoHeight;
1950
+ // Use pooled canvas for optimization
1951
+ const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
2073
1952
  const captureCtx = captureCanvas.getContext('2d', { alpha: false });
1953
+ captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
2074
1954
  captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
2075
1955
  // Calculate crop coordinates
2076
1956
  const INPUT_SIZE = 320;
@@ -2080,11 +1960,10 @@ const JaakStamps = class {
2080
1960
  const cropY = Math.max(0, (this.lastDetectedBox.y * scaleY) - this.cropMargin);
2081
1961
  const cropWidth = Math.min((this.lastDetectedBox.w * scaleX) + (2 * this.cropMargin), this.videoRef.videoWidth - cropX);
2082
1962
  const cropHeight = Math.min((this.lastDetectedBox.h * scaleY) + (2 * this.cropMargin), this.videoRef.videoHeight - cropY);
2083
- // Create cropped version
2084
- const croppedCanvas = document.createElement('canvas');
2085
- croppedCanvas.width = cropWidth;
2086
- croppedCanvas.height = cropHeight;
1963
+ // Use pooled canvas for cropped version
1964
+ const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
2087
1965
  const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
1966
+ croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
2088
1967
  croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
2089
1968
  const captureState = this.stateManager.getCaptureState();
2090
1969
  if (captureState.step === 'front') {
@@ -2098,7 +1977,6 @@ const JaakStamps = class {
2098
1977
  if (this.useDocumentClassification) {
2099
1978
  const classification = await this.detectionService.classifyDocument(croppedCanvas);
2100
1979
  if (classification && classification.class === 'passport') {
2101
- this.logger.state('PASAPORTE_DETECTADO_SALTANDO_REVERSO', { classification: classification?.class });
2102
1980
  this.completeProcess(true);
2103
1981
  return;
2104
1982
  }
@@ -2125,6 +2003,9 @@ const JaakStamps = class {
2125
2003
  });
2126
2004
  this.completeProcess(false);
2127
2005
  }
2006
+ // Return canvases to pool after use
2007
+ this.returnCanvasToPool(captureCanvas);
2008
+ this.returnCanvasToPool(croppedCanvas);
2128
2009
  }
2129
2010
  triggerCaptureAnimation() {
2130
2011
  const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
@@ -2153,11 +2034,6 @@ const JaakStamps = class {
2153
2034
  setTimeout(() => {
2154
2035
  this.stateManager.updateCaptureState({ showSuccessAnimation: false });
2155
2036
  }, 3000);
2156
- this.logger.state('PROCESO_COMPLETADO', {
2157
- skippedBack,
2158
- totalImages: capturedImages.metadata.totalImages,
2159
- timestamp: new Date().toISOString()
2160
- });
2161
2037
  }
2162
2038
  stopDetection() {
2163
2039
  if (this.animationId) {
@@ -2166,7 +2042,6 @@ const JaakStamps = class {
2166
2042
  }
2167
2043
  this.clearBackDocumentTimer();
2168
2044
  this.detectionBoxes = [];
2169
- this.logger.state('DETECTOR_DETENIDO', { timestamp: Date.now() });
2170
2045
  }
2171
2046
  startBackDocumentTimer() {
2172
2047
  if (!this.enableBackDocumentTimer)
@@ -2195,6 +2070,15 @@ const JaakStamps = class {
2195
2070
  return; // Don't toggle if switching camera
2196
2071
  this.showCameraSelector = !this.showCameraSelector;
2197
2072
  }
2073
+ async updateCameraInfoWithAutofocus() {
2074
+ try {
2075
+ const cameraInfo = await this.cameraService.getCameraInfo();
2076
+ this.cameraInfoWithAutofocus = cameraInfo;
2077
+ }
2078
+ catch (error) {
2079
+ // Keep existing state if update fails
2080
+ }
2081
+ }
2198
2082
  async handleCameraSwitch(cameraId) {
2199
2083
  if (this.isSwitchingCamera)
2200
2084
  return; // Prevent multiple simultaneous switches
@@ -2202,10 +2086,6 @@ const JaakStamps = class {
2202
2086
  // Close the selector immediately when user selects a camera
2203
2087
  this.showCameraSelector = false;
2204
2088
  this.isSwitchingCamera = true;
2205
- this.logger.state('INICIANDO_CAMBIO_CAMARA', {
2206
- from: this.cameraService.getSelectedCameraId(),
2207
- to: cameraId
2208
- });
2209
2089
  // Stop current video stream
2210
2090
  if (this.videoStream) {
2211
2091
  this.videoStream.getTracks().forEach(track => track.stop());
@@ -2216,21 +2096,17 @@ const JaakStamps = class {
2216
2096
  const newStream = await this.cameraService.setupCamera();
2217
2097
  // Update video element
2218
2098
  await this.initializeVideoStream(newStream);
2219
- this.logger.state('CAMBIO_CAMARA_EXITOSO', {
2220
- newCameraId: cameraId,
2221
- isRearCamera: this.cameraService.isRearCamera(newStream)
2222
- });
2099
+ // Update camera info with autofocus data
2100
+ await this.updateCameraInfoWithAutofocus();
2223
2101
  }
2224
2102
  catch (error) {
2225
- this.logger.error('Error al cambiar cámara:', error);
2226
2103
  // Try to restore previous camera if switch failed
2227
2104
  try {
2228
2105
  const fallbackStream = await this.cameraService.setupCamera();
2229
2106
  await this.initializeVideoStream(fallbackStream);
2230
2107
  }
2231
2108
  catch (fallbackError) {
2232
- this.logger.error('Error al restaurar cámara anterior:', fallbackError);
2233
- this.updateStatus('Error al cambiar cámara', 'No se pudo completar el cambio de dispositivo', 'error');
2109
+ this.updateStatus('Error al cambiar cámara', 'No se pudo cambiar el dispositivo', 'error');
2234
2110
  }
2235
2111
  }
2236
2112
  finally {
@@ -2271,7 +2147,7 @@ const JaakStamps = class {
2271
2147
  this.stateManager.updateCaptureState({ isVideoActive: false, isLoading: false });
2272
2148
  }
2273
2149
  this.isMaskReady = false;
2274
- this.updateStatus('Sesión finalizada', '', 'ready');
2150
+ this.updateStatus('Captura finalizada', '', 'ready');
2275
2151
  this.detectionBoxes = [];
2276
2152
  this.cleanup();
2277
2153
  }
@@ -2282,7 +2158,6 @@ const JaakStamps = class {
2282
2158
  this.performanceUpdateInterval = window.setInterval(() => {
2283
2159
  this.updatePerformanceMetrics();
2284
2160
  }, 500);
2285
- this.logger.debug('Monitor de performance inicializado');
2286
2161
  }
2287
2162
  updatePerformanceMetrics() {
2288
2163
  const currentTime = performance.now();
@@ -2325,6 +2200,66 @@ const JaakStamps = class {
2325
2200
  if (detectionsFound > 0) {
2326
2201
  this.performanceMetrics.successfulDetections++;
2327
2202
  }
2203
+ // Update performance history for adaptive frame skipping
2204
+ this.performanceHistory.push(processingTime);
2205
+ if (this.performanceHistory.length > this.PERFORMANCE_HISTORY_SIZE) {
2206
+ this.performanceHistory.shift();
2207
+ }
2208
+ }
2209
+ // Adaptive frame skipping based on performance
2210
+ getAdaptiveFrameSkip() {
2211
+ // Base conditions for high frame skip
2212
+ if (this.consecutiveFailures > 20)
2213
+ return this.MAX_FRAME_SKIP;
2214
+ if (this.performanceMetrics.inferenceTime > 200)
2215
+ return 6;
2216
+ if (this.performanceMetrics.memoryUsage > 200)
2217
+ return 5;
2218
+ // Calculate average processing time
2219
+ if (this.performanceHistory.length > 0) {
2220
+ const avgProcessingTime = this.performanceHistory.reduce((a, b) => a + b, 0) / this.performanceHistory.length;
2221
+ if (avgProcessingTime > 100)
2222
+ return 4;
2223
+ if (avgProcessingTime > 80)
2224
+ return 3;
2225
+ if (avgProcessingTime > 60)
2226
+ return this.BASE_FRAME_SKIP + 1;
2227
+ }
2228
+ // Low memory devices get higher frame skip
2229
+ if (this.performanceMetrics.memoryUsage > 150)
2230
+ return 4;
2231
+ // Performance is good, use base frame skip
2232
+ return this.BASE_FRAME_SKIP;
2233
+ }
2234
+ // Canvas pool management for screenshots
2235
+ getPooledCanvas(width, height) {
2236
+ // Try to find a canvas with matching dimensions
2237
+ const matchingIndex = this.canvasPool.findIndex(canvas => canvas.width === width && canvas.height === height);
2238
+ if (matchingIndex !== -1) {
2239
+ return this.canvasPool.splice(matchingIndex, 1)[0];
2240
+ }
2241
+ // If no matching canvas, try to reuse any canvas and resize
2242
+ if (this.canvasPool.length > 0) {
2243
+ const canvas = this.canvasPool.pop();
2244
+ canvas.width = width;
2245
+ canvas.height = height;
2246
+ return canvas;
2247
+ }
2248
+ // Create new canvas if pool is empty
2249
+ const canvas = document.createElement('canvas');
2250
+ canvas.width = width;
2251
+ canvas.height = height;
2252
+ return canvas;
2253
+ }
2254
+ returnCanvasToPool(canvas) {
2255
+ // Only return to pool if not at max capacity
2256
+ if (this.canvasPool.length < this.MAX_CANVAS_POOL_SIZE) {
2257
+ // Clear the canvas before returning to pool
2258
+ const ctx = canvas.getContext('2d');
2259
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
2260
+ this.canvasPool.push(canvas);
2261
+ }
2262
+ // If pool is full, let the canvas be garbage collected
2328
2263
  }
2329
2264
  };
2330
2265
  JaakStamps.style = myComponentCss;