@jaak.ai/stamps 2.0.0 → 2.1.0-dev.1

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