@jaak.ai/stamps 2.0.0-dev.58 → 2.0.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 +611 -546
  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 +2 -14
  7. package/dist/collection/components/my-component/my-component.js +237 -346
  8. package/dist/collection/components/my-component/my-component.js.map +1 -1
  9. package/dist/collection/services/CameraService.js +344 -156
  10. package/dist/collection/services/CameraService.js.map +1 -1
  11. package/dist/collection/services/DetectionService.js +38 -53
  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/LoggerService.js +40 -0
  16. package/dist/collection/services/LoggerService.js.map +1 -0
  17. package/dist/collection/services/ServiceContainer.js +12 -2
  18. package/dist/collection/services/ServiceContainer.js.map +1 -1
  19. package/dist/collection/services/interfaces/ICameraService.js.map +1 -1
  20. package/dist/collection/services/interfaces/ILogger.js +2 -0
  21. package/dist/collection/services/interfaces/ILogger.js.map +1 -0
  22. package/dist/collection/types/component-types.js.map +1 -1
  23. package/dist/components/jaak-stamps.js +614 -548
  24. package/dist/components/jaak-stamps.js.map +1 -1
  25. package/dist/esm/jaak-stamps-webcomponent.js +1 -1
  26. package/dist/esm/jaak-stamps.entry.js +611 -546
  27. package/dist/esm/jaak-stamps.entry.js.map +1 -1
  28. package/dist/esm/loader.js +1 -1
  29. package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
  30. package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
  31. package/dist/jaak-stamps-webcomponent/p-2264b5b4.entry.js +2 -0
  32. package/dist/jaak-stamps-webcomponent/p-2264b5b4.entry.js.map +1 -0
  33. package/dist/types/components/my-component/my-component.d.ts +17 -62
  34. package/dist/types/components.d.ts +8 -6
  35. package/dist/types/services/CameraService.d.ts +14 -12
  36. package/dist/types/services/DetectionService.d.ts +3 -9
  37. package/dist/types/services/LoggerService.d.ts +12 -0
  38. package/dist/types/services/ServiceContainer.d.ts +2 -0
  39. package/dist/types/services/interfaces/ICameraService.d.ts +12 -3
  40. package/dist/types/services/interfaces/ILogger.d.ts +8 -0
  41. package/dist/types/types/component-types.d.ts +0 -3
  42. package/package.json +4 -4
  43. package/dist/jaak-stamps-webcomponent/p-39560f23.entry.js +0 -2
  44. package/dist/jaak-stamps-webcomponent/p-39560f23.entry.js.map +0 -1
@@ -2,6 +2,46 @@
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
+
5
45
  class EventBusService {
6
46
  events = new Map();
7
47
  on(event, callback) {
@@ -27,7 +67,7 @@ class EventBusService {
27
67
  callback(data);
28
68
  }
29
69
  catch (error) {
30
- // Silently handle callback errors
70
+ console.error(`Error in event callback for ${event}:`, error);
31
71
  }
32
72
  });
33
73
  }
@@ -154,20 +194,16 @@ class StateManagerService {
154
194
  }
155
195
 
156
196
  class CameraService {
197
+ logger;
157
198
  eventBus;
158
199
  availableCameras = [];
159
200
  selectedCameraId = null;
160
201
  deviceType = 'desktop';
161
202
  preferredCameraFacing = null;
162
203
  preferredCamera = 'auto';
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') {
204
+ isManuallySelected = false;
205
+ constructor(logger, eventBus, preferredCamera = 'auto') {
206
+ this.logger = logger;
171
207
  this.eventBus = eventBus;
172
208
  this.preferredCamera = preferredCamera;
173
209
  }
@@ -184,21 +220,18 @@ class CameraService {
184
220
  else {
185
221
  this.deviceType = 'desktop';
186
222
  }
223
+ this.logger.state('DISPOSITIVO_DETECTADO', {
224
+ deviceType: this.deviceType,
225
+ userAgent: navigator.userAgent,
226
+ screenDimensions: { width: window.innerWidth, height: window.innerHeight }
227
+ });
187
228
  return this.deviceType;
188
229
  }
189
230
  async enumerateDevices() {
190
231
  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
- }
200
232
  const permissionStatus = await this.checkCameraPermission();
201
233
  if (permissionStatus === 'denied') {
234
+ this.logger.error('Permiso de cámara denegado por el usuario');
202
235
  return [];
203
236
  }
204
237
  if (permissionStatus === 'prompt') {
@@ -207,16 +240,24 @@ class CameraService {
207
240
  }
208
241
  const devices = await navigator.mediaDevices.enumerateDevices();
209
242
  this.availableCameras = devices.filter(device => device.kind === 'videoinput');
210
- // Cache the results for optimization
211
- CameraService.deviceEnumerationCache = {
212
- devices: this.availableCameras,
213
- timestamp: now
214
- };
215
- await this.setInitialCameraPreference();
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
+ });
216
256
  this.eventBus.emit('camera-changed', this.selectedCameraId);
217
257
  return this.availableCameras;
218
258
  }
219
259
  catch (error) {
260
+ this.logger.error('Error al enumerar cámaras disponibles:', error);
220
261
  this.handleCameraPermissionError(error);
221
262
  return [];
222
263
  }
@@ -237,6 +278,8 @@ class CameraService {
237
278
  }
238
279
  this.selectedCameraId = cameraId;
239
280
  this.updatePreferredFacing(camera);
281
+ this.isManuallySelected = true; // Mark as manually selected
282
+ this.savePreference();
240
283
  this.eventBus.emit('camera-changed', cameraId);
241
284
  }
242
285
  getPreferredFacing() {
@@ -245,20 +288,14 @@ class CameraService {
245
288
  async setupCamera(constraints) {
246
289
  try {
247
290
  const finalConstraints = constraints || await this.getMaxResolution();
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({
291
+ const stream = await navigator.mediaDevices.getUserMedia({
254
292
  video: finalConstraints,
255
293
  audio: false
256
294
  });
257
- this.lastStreamConstraints = finalConstraints;
258
- const stream = await this.currentStreamPromise;
259
295
  return stream;
260
296
  }
261
297
  catch (error) {
298
+ this.logger.error('Error en setupCamera, intentando con restricciones básicas:', error);
262
299
  // Fallback to basic constraints if getMaxResolution fails
263
300
  const basicConstraints = {
264
301
  width: { ideal: 1280 },
@@ -268,21 +305,65 @@ class CameraService {
268
305
  if (this.deviceType !== 'desktop' && this.preferredCameraFacing) {
269
306
  basicConstraints.facingMode = this.preferredCameraFacing;
270
307
  }
271
- this.currentStreamPromise = navigator.mediaDevices.getUserMedia({
308
+ this.logger.state('USANDO_RESTRICCIONES_BASICAS', { constraints: basicConstraints });
309
+ return await navigator.mediaDevices.getUserMedia({
272
310
  video: basicConstraints,
273
311
  audio: false
274
312
  });
275
- this.lastStreamConstraints = basicConstraints;
276
- return await this.currentStreamPromise;
277
313
  }
278
314
  }
279
315
  async switchCamera(cameraId) {
280
316
  const selectedCamera = this.availableCameras.find(cam => cam.deviceId === cameraId);
281
317
  if (!selectedCamera) {
318
+ this.logger.warn('Cámara seleccionada no encontrada, re-enumerando dispositivos...');
282
319
  await this.enumerateDevices();
283
320
  return;
284
321
  }
285
322
  await this.setSelectedCamera(cameraId);
323
+ this.logger.state('CAMARA_CAMBIADA', {
324
+ label: selectedCamera.label,
325
+ deviceId: selectedCamera.deviceId,
326
+ isManuallySelected: this.isManuallySelected
327
+ });
328
+ }
329
+ async flipToNextCamera() {
330
+ if (!this.isMultipleCamerasAvailable())
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
+ }
286
367
  }
287
368
  isRearCamera(stream) {
288
369
  const videoTrack = stream.getVideoTracks()[0];
@@ -291,18 +372,13 @@ class CameraService {
291
372
  const settings = videoTrack.getSettings();
292
373
  return settings.facingMode === 'environment';
293
374
  }
294
- async getCameraInfo() {
295
- const availableCamerasWithAutofocus = await Promise.all(this.availableCameras.map(async (camera) => {
296
- const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
297
- return {
375
+ getCameraInfo() {
376
+ return {
377
+ availableCameras: this.availableCameras.map(camera => ({
298
378
  id: camera.deviceId,
299
379
  label: camera.label || 'Unknown Camera',
300
- selected: camera.deviceId === this.selectedCameraId,
301
- hasAutofocus
302
- };
303
- }));
304
- return {
305
- availableCameras: availableCamerasWithAutofocus,
380
+ selected: camera.deviceId === this.selectedCameraId
381
+ })),
306
382
  selectedCameraId: this.selectedCameraId,
307
383
  deviceType: this.deviceType,
308
384
  isMultipleCamerasAvailable: this.isMultipleCamerasAvailable(),
@@ -318,6 +394,7 @@ class CameraService {
318
394
  return permission.state;
319
395
  }
320
396
  catch (error) {
397
+ this.logger.warn('No se pudo verificar permisos de cámara:', error);
321
398
  return 'prompt';
322
399
  }
323
400
  }
@@ -325,48 +402,35 @@ class CameraService {
325
402
  this.availableCameras = [];
326
403
  this.eventBus.emit('error', new Error(`Camera permission error: ${error.message}`));
327
404
  }
328
- async setInitialCameraPreference() {
405
+ setInitialCameraPreference() {
329
406
  if (this.availableCameras.length === 0)
330
407
  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
331
415
  if (this.preferredCamera === 'front') {
332
- await this.selectFrontCamera();
416
+ this.selectFrontCamera();
333
417
  }
334
418
  else if (this.preferredCamera === 'back') {
335
- await this.selectBackCamera();
419
+ this.selectBackCamera();
336
420
  }
337
421
  else {
338
- await this.selectAutoCamera();
422
+ this.selectAutoCamera();
339
423
  }
340
424
  }
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() {
425
+ selectFrontCamera() {
368
426
  this.preferredCameraFacing = 'user';
369
- const frontCameras = this.availableCameras.filter(camera => {
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 => {
370
434
  const label = camera.label.toLowerCase();
371
435
  return label.includes('front') ||
372
436
  label.includes('user') ||
@@ -374,24 +438,24 @@ class CameraService {
374
438
  label.includes('frontal') ||
375
439
  (!label.includes('back') && !label.includes('rear') && !label.includes('environment'));
376
440
  });
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;
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
+ });
391
448
  }
392
- async selectBackCamera() {
449
+ selectBackCamera() {
393
450
  this.preferredCameraFacing = 'environment';
394
- const backCameras = this.availableCameras.filter(camera => {
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 => {
395
459
  const label = camera.label.toLowerCase();
396
460
  return label.includes('back') ||
397
461
  label.includes('rear') ||
@@ -399,33 +463,34 @@ class CameraService {
399
463
  label.includes('trasera') ||
400
464
  label.includes('posterior');
401
465
  });
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
- }
414
- }
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];
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
+ });
419
473
  }
420
- this.selectedCameraId = selectedCamera ? selectedCamera.deviceId : this.availableCameras[0].deviceId;
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
+ });
421
480
  }
422
- async selectAutoCamera() {
481
+ selectAutoCamera() {
423
482
  if (this.deviceType === 'mobile' || this.deviceType === 'tablet') {
424
- await this.selectBackCamera();
483
+ this.selectBackCamera();
484
+ this.logger.state('CAMARA_AUTO_SELECCIONADA_MOBILE', { type: 'rear' });
425
485
  }
426
486
  else {
427
487
  // For desktop, prefer front camera (usually built-in webcam)
428
- await this.selectFrontCamera();
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
+ });
429
494
  }
430
495
  }
431
496
  updatePreferredFacing(camera) {
@@ -441,19 +506,40 @@ class CameraService {
441
506
  await this.detectDeviceType();
442
507
  }
443
508
  const videoConstraints = {};
444
- // Priority 1: Always use deviceId if available (ensures specific camera with autofocus)
445
- if (this.selectedCameraId) {
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
446
518
  videoConstraints.deviceId = { exact: this.selectedCameraId };
519
+ this.logger.state('USANDO_CAMARA_MANUAL', {
520
+ deviceId: this.selectedCameraId
521
+ });
447
522
  }
448
- // Priority 2: Use facingMode as fallback when no specific camera is selected
449
523
  else if (this.preferredCameraFacing) {
524
+ // Use facingMode for initial preference-based selection
525
+ // Use 'ideal' instead of 'exact' to avoid OverconstrainedError on desktop
450
526
  const facingConstraint = this.deviceType === 'desktop'
451
527
  ? { ideal: this.preferredCameraFacing }
452
528
  : { exact: this.preferredCameraFacing };
453
529
  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
+ });
454
540
  }
455
- // Priority 3: Fallback to preferredCamera setting
456
541
  else {
542
+ // Use preferredCamera setting to determine default facing mode
457
543
  if (this.preferredCamera === 'front') {
458
544
  videoConstraints.facingMode = "user";
459
545
  }
@@ -461,25 +547,24 @@ class CameraService {
461
547
  videoConstraints.facingMode = "environment";
462
548
  }
463
549
  else {
550
+ // Auto mode: use environment for mobile/tablet, user for desktop
464
551
  videoConstraints.facingMode = (this.deviceType === 'mobile' || this.deviceType === 'tablet') ? "environment" : "user";
465
552
  }
553
+ this.logger.state('USANDO_PREFERENCIA_CONFIGURADA', {
554
+ facingMode: videoConstraints.facingMode,
555
+ preferredCamera: this.preferredCamera,
556
+ deviceType: this.deviceType
557
+ });
466
558
  }
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
- }
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());
482
565
  const constraints = { ...videoConstraints };
566
+ // Enhanced focus and image quality settings
567
+ this.applyAdvancedCameraSettings(constraints, capabilities);
483
568
  if (capabilities.width && capabilities.height) {
484
569
  const maxWidth = Math.min(capabilities.width.max, 1920);
485
570
  const maxHeight = Math.min(capabilities.height.max, 1080);
@@ -493,20 +578,15 @@ class CameraService {
493
578
  constraints.height = { ideal: maxHeight };
494
579
  }
495
580
  }
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
- }
581
+ this.logger.state('CONFIGURACION_CAMARA_OPTIMIZADA', {
582
+ constraints,
583
+ capabilities: this.getCapabilitiesSummary(capabilities)
584
+ });
506
585
  return constraints;
507
586
  }
508
587
  catch (err) {
509
- // Fallback logic with minimal temporary streams
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
510
590
  if (!this.deviceType || this.deviceType === 'desktop') {
511
591
  await this.detectDeviceType();
512
592
  }
@@ -515,16 +595,25 @@ class CameraService {
515
595
  width: { ideal: isTablet ? 1280 : 1920 },
516
596
  height: { ideal: isTablet ? 720 : 1080 }
517
597
  };
518
- if (this.selectedCameraId) {
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
519
602
  fallbackConstraints.deviceId = { exact: this.selectedCameraId };
520
603
  }
521
604
  else if (this.preferredCameraFacing) {
605
+ // Use facingMode for initial preference-based selection
606
+ // Use 'ideal' instead of 'exact' to avoid OverconstrainedError on desktop
522
607
  const facingConstraint = this.deviceType === 'desktop'
523
608
  ? { ideal: this.preferredCameraFacing }
524
609
  : { exact: this.preferredCameraFacing };
525
610
  fallbackConstraints.facingMode = facingConstraint;
526
611
  }
612
+ else if (this.selectedCameraId) {
613
+ fallbackConstraints.deviceId = { exact: this.selectedCameraId };
614
+ }
527
615
  else {
616
+ // Use preferredCamera setting to determine default facing mode
528
617
  if (this.preferredCamera === 'front') {
529
618
  fallbackConstraints.facingMode = "user";
530
619
  }
@@ -532,26 +621,165 @@ class CameraService {
532
621
  fallbackConstraints.facingMode = "environment";
533
622
  }
534
623
  else {
624
+ // Auto mode: use environment for mobile/tablet, user for desktop
535
625
  fallbackConstraints.facingMode = (this.deviceType === 'mobile' || this.deviceType === 'tablet') ? "environment" : "user";
536
626
  }
537
627
  }
538
628
  return fallbackConstraints;
539
629
  }
540
630
  }
541
- // Optimization helper methods
542
- constraintsEqual(a, b) {
543
- if (!a || !b)
544
- return false;
545
- return JSON.stringify(a) === JSON.stringify(b);
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
+ }
546
688
  }
547
- // Cleanup method for optimization
548
- static clearCaches() {
549
- CameraService.cameraCapabilitiesCache.clear();
550
- CameraService.deviceEnumerationCache = null;
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 };
551
702
  }
552
- // Method to invalidate device cache when needed
553
- invalidateDeviceCache() {
554
- CameraService.deviceEnumerationCache = null;
703
+ getCapabilitiesSummary(capabilities) {
704
+ return {
705
+ hasAutoFocus: !!capabilities.focusMode,
706
+ hasFocusDistance: !!capabilities.focusDistance,
707
+ hasExposureControl: !!capabilities.exposureMode,
708
+ hasWhiteBalance: !!capabilities.whiteBalanceMode,
709
+ hasZoom: !!capabilities.zoom,
710
+ hasTorch: capabilities.torch !== undefined,
711
+ hasImageControls: !!(capabilities.brightness || capabilities.contrast || capabilities.saturation || capabilities.sharpness),
712
+ resolutionRange: capabilities.width && capabilities.height ?
713
+ `${capabilities.width.min}x${capabilities.height.min} - ${capabilities.width.max}x${capabilities.height.max}` : 'unknown'
714
+ };
715
+ }
716
+ // New method to enable torch/flash for better illumination
717
+ async setTorchEnabled(enabled, stream) {
718
+ try {
719
+ if (!stream) {
720
+ this.logger.warn('No hay stream disponible para controlar el flash');
721
+ return false;
722
+ }
723
+ const videoTrack = stream.getVideoTracks()[0];
724
+ if (!videoTrack) {
725
+ this.logger.warn('No hay track de video disponible');
726
+ return false;
727
+ }
728
+ const capabilities = videoTrack.getCapabilities();
729
+ if (capabilities.torch === undefined) {
730
+ this.logger.warn('El dispositivo no soporta control de flash');
731
+ return false;
732
+ }
733
+ await videoTrack.applyConstraints({
734
+ advanced: [{ torch: enabled }]
735
+ });
736
+ this.logger.state('FLASH_CONTROLADO', { enabled });
737
+ return true;
738
+ }
739
+ catch (error) {
740
+ this.logger.error('Error al controlar el flash:', error);
741
+ return false;
742
+ }
743
+ }
744
+ // Method to manually trigger focus on a specific point
745
+ async focusAtPoint(x, y, stream) {
746
+ try {
747
+ if (!stream) {
748
+ this.logger.warn('No hay stream disponible para enfocar');
749
+ return false;
750
+ }
751
+ const videoTrack = stream.getVideoTracks()[0];
752
+ if (!videoTrack) {
753
+ this.logger.warn('No hay track de video disponible');
754
+ return false;
755
+ }
756
+ const capabilities = videoTrack.getCapabilities();
757
+ if (!capabilities.focusMode || !capabilities.focusMode.includes('single-shot')) {
758
+ this.logger.warn('El dispositivo no soporta enfoque manual');
759
+ return false;
760
+ }
761
+ // Trigger single-shot focus
762
+ await videoTrack.applyConstraints({
763
+ advanced: [{ focusMode: 'single-shot' }]
764
+ });
765
+ // Return to continuous focus after a short delay
766
+ setTimeout(async () => {
767
+ try {
768
+ await videoTrack.applyConstraints({
769
+ advanced: [{ focusMode: 'continuous' }]
770
+ });
771
+ }
772
+ catch (error) {
773
+ this.logger.warn('Error al volver a enfoque continuo:', error);
774
+ }
775
+ }, 1000);
776
+ this.logger.state('ENFOQUE_MANUAL_ACTIVADO', { x, y });
777
+ return true;
778
+ }
779
+ catch (error) {
780
+ this.logger.error('Error al enfocar manualmente:', error);
781
+ return false;
782
+ }
555
783
  }
556
784
  }
557
785
 
@@ -630,6 +858,7 @@ class DeviceStrategyFactory {
630
858
  }
631
859
 
632
860
  class DetectionService {
861
+ logger;
633
862
  debug;
634
863
  useDocumentClassification;
635
864
  session;
@@ -646,10 +875,8 @@ class DetectionService {
646
875
  preprocessCanvas;
647
876
  preprocessCtx;
648
877
  captureCanvas;
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) {
878
+ constructor(logger, debug = false, useDocumentClassification = false) {
879
+ this.logger = logger;
653
880
  this.debug = debug;
654
881
  this.useDocumentClassification = useDocumentClassification;
655
882
  this.deviceStrategy = DeviceStrategyFactory.createStrategy();
@@ -657,15 +884,21 @@ class DetectionService {
657
884
  }
658
885
  async loadModel() {
659
886
  if (this.modelLoaded || this.session) {
887
+ this.logger.state('MODELO_YA_PRECARGADO', {
888
+ sessionExists: !!this.session,
889
+ modelPreloaded: this.modelLoaded
890
+ });
660
891
  return;
661
892
  }
662
893
  try {
894
+ this.logger.state('PRECARGANDO_MODELO_DETECCION', { modelPath: this.MODEL_PATH });
663
895
  const sessionOptions = this.deviceStrategy.getSessionOptions(this.debug);
664
896
  try {
665
897
  this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, sessionOptions);
666
898
  }
667
899
  catch (error) {
668
900
  if (error.message.includes('failed to allocate a buffer')) {
901
+ this.logger.warn('Fallo en asignación de buffer durante precarga, intentando con configuración mínima');
669
902
  const fallbackOptions = {
670
903
  executionProviders: ['wasm'],
671
904
  graphOptimizationLevel: 'disabled',
@@ -683,8 +916,10 @@ class DetectionService {
683
916
  }
684
917
  }
685
918
  this.modelLoaded = true;
919
+ this.logger.state('MODELO_DETECCION_CARGADO_EXITOSAMENTE', { sessionCreated: !!this.session });
686
920
  }
687
921
  catch (error) {
922
+ this.logger.error('Error al precargar modelo de detección:', error);
688
923
  throw error;
689
924
  }
690
925
  }
@@ -693,14 +928,19 @@ class DetectionService {
693
928
  return;
694
929
  }
695
930
  try {
931
+ this.logger.state('CARGANDO_MODELO_MOBILENET', { path: this.MOBILENET_MODEL_PATH });
696
932
  // Try to load class map (optional - SqueezeNet may not need it for basic classification)
697
933
  try {
698
934
  const classResponse = await fetch(this.MOBILENET_CLASSES_PATH);
699
935
  if (classResponse.ok) {
700
936
  this.mobileNetClassMap = await classResponse.json();
937
+ this.logger.state('CLASES_MOBILENET_CARGADAS', {
938
+ classCount: Object.keys(this.mobileNetClassMap).length
939
+ });
701
940
  }
702
941
  }
703
942
  catch (error) {
943
+ this.logger.warn('No se pudo cargar el mapa de clases de MobileNet, usando clasificación básica');
704
944
  // Create a basic class map for document types
705
945
  this.mobileNetClassMap = {
706
946
  "0": "document",
@@ -717,6 +957,7 @@ class DetectionService {
717
957
  }
718
958
  catch (error) {
719
959
  if (error.message.includes('failed to allocate a buffer')) {
960
+ this.logger.warn('Fallo en asignación de buffer de MobileNet, intentando con configuración mínima');
720
961
  const fallbackOptions = {
721
962
  executionProviders: ['wasm'],
722
963
  graphOptimizationLevel: 'disabled',
@@ -733,8 +974,12 @@ class DetectionService {
733
974
  throw error;
734
975
  }
735
976
  }
977
+ this.logger.state('MODELO_MOBILENET_CARGADO_EXITOSAMENTE', {
978
+ sessionCreated: !!this.mobileNetSession
979
+ });
736
980
  }
737
981
  catch (error) {
982
+ this.logger.error('Error al cargar modelo MobileNet:', error);
738
983
  throw error;
739
984
  }
740
985
  }
@@ -787,9 +1032,11 @@ class DetectionService {
787
1032
  }
788
1033
  async classifyDocument(canvas) {
789
1034
  if (!this.mobileNetSession || !this.mobileNetClassMap) {
1035
+ this.logger.warn('Modelo MobileNet no está cargado, saltando clasificación');
790
1036
  return null;
791
1037
  }
792
1038
  try {
1039
+ this.logger.state('CLASIFICANDO_DOCUMENTO', { timestamp: Date.now() });
793
1040
  const inputTensor = this.preprocessMobileNet(canvas);
794
1041
  const feeds = { [this.mobileNetSession.inputNames[0]]: inputTensor };
795
1042
  const results = await this.mobileNetSession.run(feeds);
@@ -797,6 +1044,13 @@ class DetectionService {
797
1044
  const maxIdx = output.reduce((bestIdx, val, idx, arr) => val > arr[bestIdx] ? idx : bestIdx, 0);
798
1045
  const confidence = output[maxIdx];
799
1046
  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
+ });
800
1054
  return {
801
1055
  class: className,
802
1056
  confidence: confidence,
@@ -804,6 +1058,7 @@ class DetectionService {
804
1058
  };
805
1059
  }
806
1060
  catch (error) {
1061
+ this.logger.error('Error al clasificar documento:', error);
807
1062
  return null;
808
1063
  }
809
1064
  }
@@ -916,6 +1171,7 @@ class DetectionService {
916
1171
  }
917
1172
  this.mobileNetClassMap = undefined;
918
1173
  this.modelLoaded = false;
1174
+ this.logger.state('DETECCION_SERVICE_LIMPIADO', { timestamp: Date.now() });
919
1175
  }
920
1176
  initializeCanvasPool() {
921
1177
  this.preprocessCanvas = document.createElement('canvas');
@@ -926,6 +1182,9 @@ class DetectionService {
926
1182
  willReadFrequently: true
927
1183
  });
928
1184
  this.captureCanvas = document.createElement('canvas');
1185
+ this.logger.state('CANVAS_POOL_INICIALIZADO', {
1186
+ preprocessCanvasSize: this.INPUT_SIZE
1187
+ });
929
1188
  }
930
1189
  cleanupCanvasPool() {
931
1190
  if (this.preprocessCanvas) {
@@ -961,11 +1220,10 @@ class DetectionService {
961
1220
  return (sign << 15) | (newExp << 10) | (frac >> 13);
962
1221
  }
963
1222
  preprocessMobileNet(canvas) {
964
- // Use pooled canvas instead of creating new one
965
- const tempCanvas = this.getCanvas(224, 224);
1223
+ const tempCanvas = document.createElement('canvas');
1224
+ tempCanvas.width = 224;
1225
+ tempCanvas.height = 224;
966
1226
  const tempCtx = tempCanvas.getContext('2d');
967
- // Clear canvas before use
968
- tempCtx.clearRect(0, 0, 224, 224);
969
1227
  // Resize image to 224x224 for SqueezeNet (using MobileNet interface)
970
1228
  tempCtx.drawImage(canvas, 0, 0, 224, 224);
971
1229
  const imageData = tempCtx.getImageData(0, 0, 224, 224);
@@ -981,53 +1239,8 @@ class DetectionService {
981
1239
  // Blue channel
982
1240
  arr[2 * hw + i] = data[i * 4 + 2] / 255.0;
983
1241
  }
984
- // Return canvas to pool after use
985
- this.returnCanvas(tempCanvas);
986
1242
  return new window.ort.Tensor('float32', arr, [1, 3, 224, 224]);
987
1243
  }
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
- }
1031
1244
  }
1032
1245
 
1033
1246
  class ServiceContainer {
@@ -1038,11 +1251,13 @@ class ServiceContainer {
1038
1251
  initializeServices(config) {
1039
1252
  // Initialize services in dependency order
1040
1253
  const eventBus = new EventBusService();
1254
+ const logger = new LoggerService(config.debug);
1041
1255
  const stateManager = new StateManagerService(eventBus);
1042
- const cameraService = new CameraService(eventBus, config.preferredCamera);
1043
- const detectionService = new DetectionService(config.debug, config.useDocumentClassification);
1256
+ const cameraService = new CameraService(logger, eventBus, config.preferredCamera);
1257
+ const detectionService = new DetectionService(logger, config.debug, config.useDocumentClassification);
1044
1258
  // Register services
1045
1259
  this.services.set('eventBus', eventBus);
1260
+ this.services.set('logger', logger);
1046
1261
  this.services.set('stateManager', stateManager);
1047
1262
  this.services.set('cameraService', cameraService);
1048
1263
  this.services.set('detectionService', detectionService);
@@ -1054,6 +1269,9 @@ class ServiceContainer {
1054
1269
  }
1055
1270
  return service;
1056
1271
  }
1272
+ getLogger() {
1273
+ return this.get('logger');
1274
+ }
1057
1275
  getEventBus() {
1058
1276
  return this.get('eventBus');
1059
1277
  }
@@ -1067,6 +1285,10 @@ class ServiceContainer {
1067
1285
  return this.get('detectionService');
1068
1286
  }
1069
1287
  updateConfig(config) {
1288
+ if (config.debug !== undefined) {
1289
+ const logger = this.services.get('logger');
1290
+ logger.setDebugMode(config.debug);
1291
+ }
1070
1292
  }
1071
1293
  cleanup() {
1072
1294
  // Cleanup services in reverse order
@@ -1076,7 +1298,7 @@ class ServiceContainer {
1076
1298
  }
1077
1299
  }
1078
1300
 
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}}";
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}}";
1080
1302
 
1081
1303
  const JaakStamps = class {
1082
1304
  constructor(hostRef) {
@@ -1106,16 +1328,9 @@ const JaakStamps = class {
1106
1328
  showCameraSelector = false;
1107
1329
  isSwitchingCamera = false;
1108
1330
  hasDocumentDetected = false;
1109
- cameraInfoWithAutofocus = {
1110
- availableCameras: [],
1111
- selectedCameraId: null,
1112
- deviceType: 'desktop',
1113
- isMultipleCamerasAvailable: false,
1114
- preferredFacing: null
1115
- };
1116
1331
  currentStatus = {
1117
- message: 'Preparando capturador...',
1118
- description: 'Configurando herramientas de captura',
1332
+ message: 'Inicializando componente...',
1333
+ description: 'Configurando servicios y cargando recursos',
1119
1334
  type: 'initializing',
1120
1335
  isInitialized: false
1121
1336
  };
@@ -1132,6 +1347,7 @@ const JaakStamps = class {
1132
1347
  backDocumentTimerRemaining = 0;
1133
1348
  // Services
1134
1349
  serviceContainer;
1350
+ logger;
1135
1351
  eventBus;
1136
1352
  stateManager;
1137
1353
  cameraService;
@@ -1162,25 +1378,19 @@ const JaakStamps = class {
1162
1378
  lastUpdateTime: 0
1163
1379
  };
1164
1380
  performanceUpdateInterval;
1165
- // Performance optimization with adaptive frame skipping
1381
+ // Performance optimization
1166
1382
  frameSkipCounter = 0;
1167
- BASE_FRAME_SKIP = 2;
1168
- MAX_FRAME_SKIP = 8;
1383
+ FRAME_SKIP = 2;
1169
1384
  consecutiveFailures = 0;
1170
1385
  MAX_FAILURES = 30;
1171
1386
  lastInferenceTime = 0;
1172
1387
  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;
1178
1388
  async componentDidLoad() {
1179
- this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
1389
+ this.updateStatus('Iniciando servicios...', 'Configurando módulos internos', 'initializing');
1180
1390
  await this.initializeServices();
1181
- this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
1391
+ this.updateStatus('Configurando eventos...', 'Preparando comunicación entre servicios', 'initializing');
1182
1392
  await this.setupEventListeners();
1183
- this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
1393
+ this.updateStatus('Inicializando cámara...', 'Detectando dispositivos disponibles', 'initializing');
1184
1394
  await this.initializeComponent();
1185
1395
  if (this.debug) {
1186
1396
  this.initializePerformanceMonitor();
@@ -1197,23 +1407,33 @@ const JaakStamps = class {
1197
1407
  captureDelay: this.captureDelay
1198
1408
  };
1199
1409
  this.serviceContainer = new ServiceContainer(config);
1410
+ this.logger = this.serviceContainer.getLogger();
1200
1411
  this.eventBus = this.serviceContainer.getEventBus();
1201
1412
  this.stateManager = this.serviceContainer.getStateManager();
1202
1413
  this.cameraService = this.serviceContainer.getCameraService();
1203
1414
  this.detectionService = this.serviceContainer.getDetectionService();
1415
+ this.logger.state('SERVICIOS_INICIALIZADOS', { timestamp: Date.now() });
1204
1416
  }
1205
1417
  async setupEventListeners() {
1206
1418
  this.eventBus.on('state-changed', (data) => {
1207
1419
  this.handleStateChange(data);
1208
1420
  });
1209
- this.eventBus.on('camera-changed', () => {
1210
- // Camera changed event
1421
+ this.eventBus.on('camera-changed', (cameraId) => {
1422
+ this.logger.state('CAMARA_CAMBIADA_EVENT', { cameraId });
1211
1423
  });
1212
- this.eventBus.on('error', () => {
1213
- // Handle service errors
1424
+ this.eventBus.on('error', (error) => {
1425
+ this.logger.error('Error en servicio:', error);
1214
1426
  });
1215
1427
  }
1216
1428
  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
+ });
1217
1437
  this.validateProps();
1218
1438
  if (this.debug) {
1219
1439
  this.stateManager.updateCaptureState({ isLoading: true });
@@ -1222,29 +1442,32 @@ const JaakStamps = class {
1222
1442
  this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura', 'initializing');
1223
1443
  await this.cameraService.detectDeviceType();
1224
1444
  await this.cameraService.enumerateDevices();
1225
- await this.updateCameraInfoWithAutofocus();
1226
- this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'initializing');
1445
+ this.updateStatus('Cargando modelo IA...', 'Preparando reconocimiento de documentos', 'initializing');
1227
1446
  await this.loadOnnxRuntime();
1228
1447
  this.initializeResizeObserver();
1229
1448
  }
1230
1449
  validateProps() {
1231
1450
  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`);
1232
1452
  this.maskSize = 90;
1233
1453
  }
1234
1454
  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`);
1235
1456
  this.cropMargin = 0;
1236
1457
  }
1237
1458
  const validOptions = ['auto', 'front', 'back'];
1238
1459
  if (!validOptions.includes(this.preferredCamera)) {
1460
+ this.logger.warn(`Propiedad preferredCamera inválida. Valor: ${this.preferredCamera}, esperado: ${validOptions.join(', ')}. Usando valor por defecto: 'auto'`);
1239
1461
  this.preferredCamera = 'auto';
1240
1462
  }
1241
1463
  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`);
1242
1465
  this.captureDelay = 1500;
1243
1466
  }
1244
1467
  }
1245
1468
  async loadOnnxRuntime() {
1246
1469
  if (!window.ort) {
1247
- this.updateStatus('Preparando herramientas...', 'Configurando funciones de captura', 'initializing');
1470
+ this.updateStatus('Descargando librerías...', 'Obteniendo recursos de reconocimiento', 'initializing');
1248
1471
  const script = document.createElement('script');
1249
1472
  script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
1250
1473
  document.head.appendChild(script);
@@ -1284,6 +1507,11 @@ const JaakStamps = class {
1284
1507
  emitReadyEvent() {
1285
1508
  const isDocumentReady = !!window.ort && this.detectionService.isModelLoaded();
1286
1509
  this.isReady.emit(isDocumentReady);
1510
+ this.logger.state('COMPONENTE_LISTO', {
1511
+ ortLibraryLoaded: !!window.ort,
1512
+ modelPreloaded: this.detectionService.isModelLoaded(),
1513
+ isReady: isDocumentReady
1514
+ });
1287
1515
  }
1288
1516
  handleStateChange(data) {
1289
1517
  }
@@ -1300,6 +1528,7 @@ const JaakStamps = class {
1300
1528
  const container = this.detectionContainer.parentElement;
1301
1529
  const rect = container.getBoundingClientRect();
1302
1530
  this.updateMaskDimensions(rect);
1531
+ this.logger.debug('Container redimensionado:', { width: rect.width, height: rect.height });
1303
1532
  }
1304
1533
  }
1305
1534
  updateMaskDimensions(containerRect) {
@@ -1353,38 +1582,16 @@ const JaakStamps = class {
1353
1582
  this.el.style.setProperty('--mask-center-x', `${videoCenterXPercent}%`);
1354
1583
  this.el.style.setProperty('--mask-center-y', `${videoCenterYPercent}%`);
1355
1584
  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
+ });
1356
1592
  }
1357
1593
  // 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
- }
1383
1594
  async getCapturedImages() {
1384
- const readyCheck = this.isComponentReady();
1385
- if (!readyCheck.ready) {
1386
- throw new Error(readyCheck.message);
1387
- }
1388
1595
  const state = this.stateManager.getCaptureState();
1389
1596
  if (state.step !== 'completed') {
1390
1597
  throw new Error('El proceso de captura no ha sido completado');
@@ -1392,128 +1599,47 @@ const JaakStamps = class {
1392
1599
  return this.stateManager.getCapturedImages();
1393
1600
  }
1394
1601
  async isProcessCompleted() {
1395
- const readyCheck = this.isComponentReady();
1396
- if (!readyCheck.ready) {
1397
- return false; // Return false instead of error for this method
1398
- }
1399
1602
  return this.stateManager.isProcessCompleted();
1400
1603
  }
1401
1604
  async startCapture() {
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
- }
1605
+ await this.startDetection();
1415
1606
  }
1416
1607
  async stopCapture() {
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
- }
1608
+ this.exitSession();
1428
1609
  }
1429
1610
  async resetCapture() {
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
- }
1611
+ this.resetDetection();
1441
1612
  }
1442
1613
  async skipBackCapture() {
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 };
1614
+ const captureState = this.stateManager.getCaptureState();
1615
+ const capturedImages = this.stateManager.getCapturedImages();
1616
+ if (captureState.step === 'back' && capturedImages.front.fullFrame) {
1617
+ this.completeProcess(true);
1460
1618
  }
1461
1619
  }
1462
1620
  async getStatus() {
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
- }
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
+ };
1498
1630
  }
1499
1631
  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
- }
1505
1632
  if (this.detectionService.isModelLoaded()) {
1506
- this.updateStatus('Reconocimiento listo', '', 'ready');
1633
+ this.logger.state('MODELO_YA_PRECARGADO');
1634
+ this.updateStatus('Modelos ya cargados', '', 'ready');
1507
1635
  return { success: true, message: 'Model already loaded' };
1508
1636
  }
1509
1637
  try {
1510
1638
  const loadStartTime = performance.now();
1511
- this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
1512
- if (this.stateManager) {
1513
- this.stateManager.updateCaptureState({ isLoading: true });
1514
- }
1639
+ this.updateStatus('Descargando modelo de detección...', 'Obteniendo red neuronal para reconocimiento de documentos', 'loading');
1640
+ this.stateManager.updateCaptureState({ isLoading: true });
1515
1641
  await this.detectionService.loadModel();
1516
- this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
1642
+ this.updateStatus('Cargando clasificador...', 'Preparando modelo de clasificación de tipos de documento', 'loading');
1517
1643
  await this.detectionService.loadClassificationModel();
1518
1644
  const loadEndTime = performance.now();
1519
1645
  const loadTime = loadEndTime - loadStartTime;
@@ -1521,118 +1647,80 @@ const JaakStamps = class {
1521
1647
  if (this.debug) {
1522
1648
  this.recordOnnxPerformance(loadTime, 0);
1523
1649
  }
1524
- this.updateStatus('Finalizando configuración...', 'Preparando herramientas para captura', 'loading');
1650
+ this.updateStatus('Optimizando modelos...', 'Configurando parámetros de rendimiento', 'loading');
1525
1651
  await new Promise(resolve => setTimeout(resolve, 300));
1526
- this.updateStatus('Reconocimiento preparado', '', 'ready');
1527
- if (this.stateManager) {
1528
- this.stateManager.updateCaptureState({ isLoading: false });
1529
- }
1652
+ this.updateStatus('Modelos precargados', '', 'ready');
1653
+ this.stateManager.updateCaptureState({ isLoading: false });
1530
1654
  this.emitReadyEvent();
1655
+ this.logger.state('MODELOS_PRECARGADOS_EXITOSAMENTE', { loadTime: Math.round(loadTime) });
1531
1656
  return { success: true, message: 'Models preloaded successfully' };
1532
1657
  }
1533
1658
  catch (error) {
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
- }
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 });
1538
1662
  return { success: false, error: error.message };
1539
1663
  }
1540
1664
  }
1541
1665
  async 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
- }
1666
+ return this.cameraService.getCameraInfo();
1566
1667
  }
1567
1668
  async setPreferredCamera(camera) {
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
- }
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);
1588
1677
  }
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
- };
1602
1678
  }
1679
+ return {
1680
+ success: true,
1681
+ selectedCamera: this.cameraService.getSelectedCameraId(),
1682
+ availableCameras: this.cameraService.getAvailableCameras().length
1683
+ };
1603
1684
  }
1604
1685
  async setCaptureDelay(delay) {
1605
1686
  if (delay < 0 || delay > 10000) {
1606
- return {
1607
- success: false,
1608
- error: 'Capture delay must be between 0 and 10000 milliseconds',
1609
- captureDelay: this.captureDelay
1610
- };
1687
+ throw new Error('Capture delay must be between 0 and 10000 milliseconds');
1611
1688
  }
1612
- // Allow setting capture delay even during initialization
1613
1689
  this.captureDelay = delay;
1614
- if (this.serviceContainer) {
1615
- this.serviceContainer.updateConfig({ captureDelay: delay });
1616
- }
1690
+ this.serviceContainer.updateConfig({ captureDelay: delay });
1617
1691
  return {
1618
1692
  success: true,
1619
1693
  captureDelay: this.captureDelay
1620
1694
  };
1621
1695
  }
1622
1696
  async getCaptureDelay() {
1623
- // Always allow getting capture delay, even during initialization
1624
1697
  return this.captureDelay;
1625
1698
  }
1699
+ async setTorchEnabled(enabled) {
1700
+ const torchEnabled = await this.cameraService.setTorchEnabled(enabled, this.videoStream);
1701
+ return {
1702
+ success: torchEnabled,
1703
+ enabled: torchEnabled ? enabled : false
1704
+ };
1705
+ }
1706
+ async focusAtPoint(x, y) {
1707
+ const focused = await this.cameraService.focusAtPoint(x, y, this.videoStream);
1708
+ return {
1709
+ success: focused,
1710
+ coordinates: { x, y }
1711
+ };
1712
+ }
1626
1713
  // DETECTION METHODS
1627
1714
  async startDetection() {
1715
+ this.logger.state('INICIANDO_DETECCION');
1628
1716
  try {
1629
1717
  // Paso 1: Verificar y cargar modelos si es necesario
1630
1718
  if (!this.detectionService.isModelLoaded()) {
1631
1719
  const loadStartTime = performance.now();
1632
- this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
1720
+ this.updateStatus('Cargando modelo de detección...', 'Descargando red neuronal para reconocimiento', 'loading');
1633
1721
  this.stateManager.updateCaptureState({ isLoading: true });
1634
1722
  await this.detectionService.loadModel();
1635
- this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
1723
+ this.updateStatus('Cargando clasificador...', 'Preparando modelo de clasificación de documentos', 'loading');
1636
1724
  await this.detectionService.loadClassificationModel();
1637
1725
  const loadEndTime = performance.now();
1638
1726
  const loadTime = loadEndTime - loadStartTime;
@@ -1640,13 +1728,13 @@ const JaakStamps = class {
1640
1728
  if (this.debug) {
1641
1729
  this.recordOnnxPerformance(loadTime, 0);
1642
1730
  }
1731
+ this.logger.state('MODELOS_CARGADOS_EN_DETECCION', { loadTime: Math.round(loadTime) });
1643
1732
  }
1644
1733
  // Paso 2: Detectar y configurar dispositivos
1645
- this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
1734
+ this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura disponibles', 'loading');
1646
1735
  await this.cameraService.enumerateDevices();
1647
- await this.updateCameraInfoWithAutofocus();
1648
1736
  // Paso 3: Configurar cámara seleccionada
1649
- this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
1737
+ this.updateStatus('Configurando cámara...', 'Estableciendo resolución y parámetros óptimos', 'loading');
1650
1738
  const stream = await this.cameraService.setupCamera();
1651
1739
  // Paso 4: Inicializar video y ocultar estado inmediatamente
1652
1740
  this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
@@ -1663,7 +1751,8 @@ const JaakStamps = class {
1663
1751
  this.detectFrame();
1664
1752
  }
1665
1753
  catch (err) {
1666
- this.updateStatus('Error al iniciar', 'No se pudo preparar la captura', 'error');
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');
1667
1756
  this.stateManager.updateCaptureState({ isLoading: false });
1668
1757
  }
1669
1758
  }
@@ -1673,6 +1762,10 @@ const JaakStamps = class {
1673
1762
  this.videoStream = stream;
1674
1763
  const isRear = this.cameraService.isRearCamera(stream);
1675
1764
  this.shouldMirrorVideo = !isRear;
1765
+ this.logger.state('CAMARA_CONFIGURADA', {
1766
+ isRearCamera: isRear,
1767
+ shouldMirrorVideo: this.shouldMirrorVideo
1768
+ });
1676
1769
  return new Promise((resolve) => {
1677
1770
  this.videoRef.onloadedmetadata = async () => {
1678
1771
  await this.videoRef.play();
@@ -1697,10 +1790,9 @@ const JaakStamps = class {
1697
1790
  }
1698
1791
  return;
1699
1792
  }
1700
- // Adaptive frame skipping for performance
1793
+ // Frame skipping for performance
1701
1794
  this.frameSkipCounter++;
1702
- const currentFrameSkip = this.getAdaptiveFrameSkip();
1703
- if (this.frameSkipCounter <= currentFrameSkip) {
1795
+ if (this.frameSkipCounter <= this.FRAME_SKIP) {
1704
1796
  if (captureState.step !== 'completed') {
1705
1797
  this.animationId = requestAnimationFrame(() => this.detectFrame());
1706
1798
  }
@@ -1776,6 +1868,7 @@ const JaakStamps = class {
1776
1868
  }
1777
1869
  }
1778
1870
  catch (e) {
1871
+ this.logger.error('Error en inferencia de modelo:', e);
1779
1872
  const captureState = this.stateManager.getCaptureState();
1780
1873
  if (captureState.step !== 'completed') {
1781
1874
  setTimeout(() => this.detectFrame(), 100);
@@ -1802,8 +1895,6 @@ const JaakStamps = class {
1802
1895
  clearInterval(this.performanceUpdateInterval);
1803
1896
  this.performanceUpdateInterval = undefined;
1804
1897
  }
1805
- // Clear canvas pool
1806
- this.canvasPool = [];
1807
1898
  this.detectionBoxes = [];
1808
1899
  this.alignmentStartTime = undefined;
1809
1900
  this.hasDocumentDetected = false;
@@ -1817,8 +1908,11 @@ const JaakStamps = class {
1817
1908
  step: 'front',
1818
1909
  isCapturing: false
1819
1910
  };
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: {
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: {
1822
1916
  position: 'absolute',
1823
1917
  left: `${box.x}px`,
1824
1918
  top: `${box.y}px`,
@@ -1827,9 +1921,9 @@ const JaakStamps = class {
1827
1921
  border: '2px solid #32406C',
1828
1922
  pointerEvents: 'none',
1829
1923
  boxSizing: 'border-box'
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
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
1831
1925
  ? `Saltar reverso (${this.backDocumentTimerRemaining}s)`
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" })))));
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" })))));
1833
1927
  }
1834
1928
  // Utility methods
1835
1929
  updateDetectionBoxes(boxes) {
@@ -1910,18 +2004,35 @@ const JaakStamps = class {
1910
2004
  if (allSidesAligned && bestBox) {
1911
2005
  cardOutline?.classList.add('perfect-match');
1912
2006
  corners?.forEach(corner => corner.classList.add('perfect-match'));
2007
+ // Debug logging
2008
+ this.logger.state('CAPTURE_EVALUATION', {
2009
+ allSidesAligned: true,
2010
+ hasScreenshotTaken: this.hasScreenshotTaken,
2011
+ captureDelay: this.captureDelay,
2012
+ alignmentStartTime: this.alignmentStartTime
2013
+ });
1913
2014
  if (!this.hasScreenshotTaken) {
1914
2015
  const currentTime = Date.now();
1915
2016
  // Initialize alignment start time if not set
1916
2017
  if (!this.alignmentStartTime) {
1917
2018
  this.alignmentStartTime = currentTime;
2019
+ this.logger.state('ALIGNMENT_TIMER_STARTED', { startTime: currentTime });
1918
2020
  }
1919
2021
  // Check if document has been aligned for the configured delay
1920
2022
  const alignmentDuration = currentTime - this.alignmentStartTime;
2023
+ this.logger.state('ALIGNMENT_DURATION_CHECK', {
2024
+ alignmentDuration,
2025
+ captureDelay: this.captureDelay,
2026
+ readyToCapture: alignmentDuration >= this.captureDelay
2027
+ });
1921
2028
  if (alignmentDuration >= this.captureDelay) {
2029
+ this.logger.state('TRIGGERING_CAPTURE', {
2030
+ alignmentDuration,
2031
+ captureDelay: this.captureDelay
2032
+ });
1922
2033
  this.lastDetectedBox = bestBox;
1923
- this.takeScreenshot().catch(() => {
1924
- // Handle screenshot error silently
2034
+ this.takeScreenshot().catch(error => {
2035
+ this.logger.error('Error al tomar captura de pantalla:', error);
1925
2036
  });
1926
2037
  this.hasScreenshotTaken = true;
1927
2038
  this.alignmentStartTime = undefined;
@@ -1947,12 +2058,21 @@ const JaakStamps = class {
1947
2058
  async takeScreenshot() {
1948
2059
  if (!this.videoRef || !this.lastDetectedBox)
1949
2060
  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
+ });
1950
2069
  this.stateManager.updateCaptureState({ isCapturing: true });
1951
2070
  this.triggerCaptureAnimation();
1952
- // Use pooled canvas for optimization
1953
- const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
2071
+ // Create capture canvas
2072
+ const captureCanvas = document.createElement('canvas');
2073
+ captureCanvas.width = this.videoRef.videoWidth;
2074
+ captureCanvas.height = this.videoRef.videoHeight;
1954
2075
  const captureCtx = captureCanvas.getContext('2d', { alpha: false });
1955
- captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
1956
2076
  captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
1957
2077
  // Calculate crop coordinates
1958
2078
  const INPUT_SIZE = 320;
@@ -1962,10 +2082,11 @@ const JaakStamps = class {
1962
2082
  const cropY = Math.max(0, (this.lastDetectedBox.y * scaleY) - this.cropMargin);
1963
2083
  const cropWidth = Math.min((this.lastDetectedBox.w * scaleX) + (2 * this.cropMargin), this.videoRef.videoWidth - cropX);
1964
2084
  const cropHeight = Math.min((this.lastDetectedBox.h * scaleY) + (2 * this.cropMargin), this.videoRef.videoHeight - cropY);
1965
- // Use pooled canvas for cropped version
1966
- const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
2085
+ // Create cropped version
2086
+ const croppedCanvas = document.createElement('canvas');
2087
+ croppedCanvas.width = cropWidth;
2088
+ croppedCanvas.height = cropHeight;
1967
2089
  const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
1968
- croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
1969
2090
  croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
1970
2091
  const captureState = this.stateManager.getCaptureState();
1971
2092
  if (captureState.step === 'front') {
@@ -1979,6 +2100,7 @@ const JaakStamps = class {
1979
2100
  if (this.useDocumentClassification) {
1980
2101
  const classification = await this.detectionService.classifyDocument(croppedCanvas);
1981
2102
  if (classification && classification.class === 'passport') {
2103
+ this.logger.state('PASAPORTE_DETECTADO_SALTANDO_REVERSO', { classification: classification?.class });
1982
2104
  this.completeProcess(true);
1983
2105
  return;
1984
2106
  }
@@ -2005,9 +2127,6 @@ const JaakStamps = class {
2005
2127
  });
2006
2128
  this.completeProcess(false);
2007
2129
  }
2008
- // Return canvases to pool after use
2009
- this.returnCanvasToPool(captureCanvas);
2010
- this.returnCanvasToPool(croppedCanvas);
2011
2130
  }
2012
2131
  triggerCaptureAnimation() {
2013
2132
  const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
@@ -2036,6 +2155,11 @@ const JaakStamps = class {
2036
2155
  setTimeout(() => {
2037
2156
  this.stateManager.updateCaptureState({ showSuccessAnimation: false });
2038
2157
  }, 3000);
2158
+ this.logger.state('PROCESO_COMPLETADO', {
2159
+ skippedBack,
2160
+ totalImages: capturedImages.metadata.totalImages,
2161
+ timestamp: new Date().toISOString()
2162
+ });
2039
2163
  }
2040
2164
  stopDetection() {
2041
2165
  if (this.animationId) {
@@ -2044,6 +2168,7 @@ const JaakStamps = class {
2044
2168
  }
2045
2169
  this.clearBackDocumentTimer();
2046
2170
  this.detectionBoxes = [];
2171
+ this.logger.state('DETECTOR_DETENIDO', { timestamp: Date.now() });
2047
2172
  }
2048
2173
  startBackDocumentTimer() {
2049
2174
  if (!this.enableBackDocumentTimer)
@@ -2072,15 +2197,6 @@ const JaakStamps = class {
2072
2197
  return; // Don't toggle if switching camera
2073
2198
  this.showCameraSelector = !this.showCameraSelector;
2074
2199
  }
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
- }
2084
2200
  async handleCameraSwitch(cameraId) {
2085
2201
  if (this.isSwitchingCamera)
2086
2202
  return; // Prevent multiple simultaneous switches
@@ -2088,6 +2204,10 @@ const JaakStamps = class {
2088
2204
  // Close the selector immediately when user selects a camera
2089
2205
  this.showCameraSelector = false;
2090
2206
  this.isSwitchingCamera = true;
2207
+ this.logger.state('INICIANDO_CAMBIO_CAMARA', {
2208
+ from: this.cameraService.getSelectedCameraId(),
2209
+ to: cameraId
2210
+ });
2091
2211
  // Stop current video stream
2092
2212
  if (this.videoStream) {
2093
2213
  this.videoStream.getTracks().forEach(track => track.stop());
@@ -2098,17 +2218,21 @@ const JaakStamps = class {
2098
2218
  const newStream = await this.cameraService.setupCamera();
2099
2219
  // Update video element
2100
2220
  await this.initializeVideoStream(newStream);
2101
- // Update camera info with autofocus data
2102
- await this.updateCameraInfoWithAutofocus();
2221
+ this.logger.state('CAMBIO_CAMARA_EXITOSO', {
2222
+ newCameraId: cameraId,
2223
+ isRearCamera: this.cameraService.isRearCamera(newStream)
2224
+ });
2103
2225
  }
2104
2226
  catch (error) {
2227
+ this.logger.error('Error al cambiar cámara:', error);
2105
2228
  // Try to restore previous camera if switch failed
2106
2229
  try {
2107
2230
  const fallbackStream = await this.cameraService.setupCamera();
2108
2231
  await this.initializeVideoStream(fallbackStream);
2109
2232
  }
2110
2233
  catch (fallbackError) {
2111
- this.updateStatus('Error al cambiar cámara', 'No se pudo cambiar el dispositivo', 'error');
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');
2112
2236
  }
2113
2237
  }
2114
2238
  finally {
@@ -2149,7 +2273,7 @@ const JaakStamps = class {
2149
2273
  this.stateManager.updateCaptureState({ isVideoActive: false, isLoading: false });
2150
2274
  }
2151
2275
  this.isMaskReady = false;
2152
- this.updateStatus('Captura finalizada', '', 'ready');
2276
+ this.updateStatus('Sesión finalizada', '', 'ready');
2153
2277
  this.detectionBoxes = [];
2154
2278
  this.cleanup();
2155
2279
  }
@@ -2160,6 +2284,7 @@ const JaakStamps = class {
2160
2284
  this.performanceUpdateInterval = window.setInterval(() => {
2161
2285
  this.updatePerformanceMetrics();
2162
2286
  }, 500);
2287
+ this.logger.debug('Monitor de performance inicializado');
2163
2288
  }
2164
2289
  updatePerformanceMetrics() {
2165
2290
  const currentTime = performance.now();
@@ -2202,66 +2327,6 @@ const JaakStamps = class {
2202
2327
  if (detectionsFound > 0) {
2203
2328
  this.performanceMetrics.successfulDetections++;
2204
2329
  }
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
2265
2330
  }
2266
2331
  };
2267
2332
  JaakStamps.style = myComponentCss;